From 10f8a16a0fd5c1583a1d9f6aabd9e4b5a18dd486 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 7 May 2024 18:04:12 -0500 Subject: [PATCH 01/21] noop functions and messages for now --- src/qos_core/src/protocol/msg.rs | 43 +++++++++++++++++++++++++++++- src/qos_core/src/protocol/state.rs | 42 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/qos_core/src/protocol/msg.rs b/src/qos_core/src/protocol/msg.rs index 855bb66e..1834526e 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -10,6 +10,13 @@ use crate::protocol::{ ProtocolError, }; +/// Type to represent UDP or TCP sockets +#[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] +pub enum SocketType { + TcpSocket, + UdpSocket, +} + /// Message types for communicating with protocol executor. #[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] pub enum ProtocolMsg { @@ -66,7 +73,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, }, @@ -138,6 +145,40 @@ pub enum ProtocolMsg { /// if the manifest envelope does not exist. manifest_envelope: Box>, }, + + /// Request from the enclave app to open a TCP connection to a remote host + /// TODO: can this be done automatically? + OpenRemoteConnectionRequest { + /// UDP or TCP + socket_type: SocketType, + /// The hostname to connect to, e.g. "www.googleapis.com" + hostname: String, + /// e.g. 443 + port: u8, + /// e.g. "8.8.8.8" + dns_resolver: String, + }, + /// Response for `OpenTcpConnectionRequest` + OpenRemoteConnectionResponse { + /// Connection ID to reference the opened connection when used with `RemoteRequest` and `RemoteResponse`. + /// TODO: maybe we reply with a fd name directly? + /// Not sure what this ID will map to. + connection_id: u8, + }, + /// Proxy bytes to a remote host + RemoteRequest { + /// A connection ID from `OpenRemoteConnectionResponse` + connection_id: u8, + /// Data to be sent + data: Vec, + }, + /// Response to the proxy request. + RemoteResponse { + /// Connection ID from `OpenRemoteConnectionResponse` + connection_id: u8, + /// Data received on the connection + data: Vec, + }, } #[cfg(test)] diff --git a/src/qos_core/src/protocol/state.rs b/src/qos_core/src/protocol/state.rs index 6df38852..966c0dac 100644 --- a/src/qos_core/src/protocol/state.rs +++ b/src/qos_core/src/protocol/state.rs @@ -146,6 +146,22 @@ impl ProtocolRoute { ) } + pub fn open_remote_connection(current_phase: ProtocolPhase) -> Self { + ProtocolRoute::new( + Box::new(handlers::open_remote_connection), + current_phase, + current_phase, + ) + } + + pub fn remote_send(current_phase: ProtocolPhase) -> Self { + ProtocolRoute::new( + Box::new(handlers::remote_send), + current_phase, + current_phase, + ) + } + pub fn export_key(current_phase: ProtocolPhase) -> Self { ProtocolRoute::new( Box::new(handlers::export_key), @@ -275,6 +291,8 @@ impl ProtocolState { // phase specific routes ProtocolRoute::proxy(self.phase), ProtocolRoute::export_key(self.phase), + ProtocolRoute::open_remote_connection(self.phase), + ProtocolRoute::remote_send(self.phase), ] } ProtocolPhase::WaitingForForwardedKey => { @@ -376,6 +394,30 @@ mod handlers { } } + pub(super) fn open_remote_connection( + req: &ProtocolMsg, + state: &mut ProtocolState, + ) -> ProtocolRouteResponse { + if let ProtocolMsg::OpenRemoteConnectionRequest { socket_type, hostname, port, dns_resolver } = req { + // TODO: open a TCP / UDP socket, generate a connection ID, save it to the state + Some(Ok(ProtocolMsg::OpenRemoteConnectionResponse { connection_id: 42 })) + } else { + None + } + } + + pub(super) fn remote_send( + req: &ProtocolMsg, + state: &mut ProtocolState, + ) -> ProtocolRouteResponse { + if let ProtocolMsg::RemoteRequest { connection_id, data } = req { + // TODO: need to call state.connections.get(id).send(data) to actually send data + Some(Ok(ProtocolMsg::RemoteResponse { connection_id: 1, data: vec![] })) + } else { + None + } + } + pub(super) fn proxy( req: &ProtocolMsg, state: &mut ProtocolState, From 1d0571c407c6717d63d25863cf75bec4ca8180f6 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 20 May 2024 09:30:31 -0500 Subject: [PATCH 02/21] Implement Read/Write traits with Stream, write a test to confirm it works --- src/Cargo.lock | 381 ++++++++++++++++++++++++++--- src/qos_core/Cargo.toml | 2 + src/qos_core/src/io/stream.rs | 86 ++++++- src/qos_core/src/protocol/msg.rs | 6 +- src/qos_core/src/protocol/state.rs | 29 ++- src/qos_nsm/Cargo.toml | 3 +- src/rust-toolchain.toml | 4 + 7 files changed, 463 insertions(+), 48 deletions(-) create mode 100644 src/rust-toolchain.toml diff --git a/src/Cargo.lock b/src/Cargo.lock index 0ed5424c..9d0fa47e 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" @@ -572,6 +683,16 @@ 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 +724,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 +786,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -729,6 +856,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" @@ -794,6 +927,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" @@ -942,12 +1084,30 @@ dependencies = [ "ureq", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" 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,18 +1126,40 @@ 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 = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.21" @@ -1037,13 +1219,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 +1398,12 @@ dependencies = [ "sha2", ] +[[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 +1419,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 +1464,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1337,6 +1531,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 +1628,10 @@ dependencies = [ "qos_nsm", "qos_p256", "qos_test_primitives", + "rustls", "serde", "serde_bytes", + "webpki-roots", ] [[package]] @@ -1466,7 +1672,6 @@ dependencies = [ "borsh", "hex-literal", "p384 0.12.0", - "qos_core", "qos_hex", "rand", "serde_bytes", @@ -1537,6 +1742,35 @@ dependencies = [ "getrandom", ] +[[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 +1845,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 +1860,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" @@ -1666,9 +1952,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 +1977,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 +2009,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1753,7 +2039,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1778,6 +2064,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 +2139,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 +2156,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 +2174,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1920,7 +2212,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -1956,9 +2248,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", ] @@ -1993,7 +2285,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] [[package]] @@ -2141,9 +2433,9 @@ dependencies = [ [[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 +2482,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -2212,7 +2504,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2233,6 +2525,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 +2794,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.67", + "syn 2.0.68", ] diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index dc2984fc..624f2867 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -24,6 +24,8 @@ serde = { version = "1", features = ["derive"], default-features = false } 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/stream.rs b/src/qos_core/src/io/stream.rs index f4e85cfe..de16c02c 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; @@ -245,6 +249,37 @@ impl Stream { } } +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> Result { + match recv(self.fd, buf, MsgFlags::empty()) { + Ok(size) if size == 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(size) if size == 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 @@ -328,6 +363,10 @@ fn socket_fd(addr: &SocketAddress) -> Result { #[cfg(test)] mod test { + use std::{io::ErrorKind, sync::Arc}; + + use rustls::RootCertStore; + use super::*; fn timeval() -> TimeVal { @@ -354,6 +393,51 @@ mod test { assert_eq!(data, resp); } + #[test] + fn stream_implement_reader_writer_interfaces() { + let host = "www.turnkey.com"; + let path = "/health"; + + let unix_addr = + nix::sys::socket::UnixAddr::new("/tmp/host.sock").unwrap(); + let addr: SocketAddress = SocketAddress::Unix(unix_addr); + let timeout = TimeVal::new(1, 0); + let mut stream = Stream::connect(&addr, timeout).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 stream); + + let http_request = format!( + "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + 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)) + ); + println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + } + #[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 1834526e..09d08705 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -160,9 +160,9 @@ pub enum ProtocolMsg { }, /// Response for `OpenTcpConnectionRequest` OpenRemoteConnectionResponse { - /// Connection ID to reference the opened connection when used with `RemoteRequest` and `RemoteResponse`. - /// TODO: maybe we reply with a fd name directly? - /// Not sure what this ID will map to. + /// Connection ID to reference the opened connection when used with + /// `RemoteRequest` and `RemoteResponse`. TODO: maybe we reply with a + /// fd name directly? Not sure what this ID will map to. connection_id: u8, }, /// Proxy bytes to a remote host diff --git a/src/qos_core/src/protocol/state.rs b/src/qos_core/src/protocol/state.rs index 966c0dac..06be1d89 100644 --- a/src/qos_core/src/protocol/state.rs +++ b/src/qos_core/src/protocol/state.rs @@ -396,11 +396,20 @@ mod handlers { pub(super) fn open_remote_connection( req: &ProtocolMsg, - state: &mut ProtocolState, + _state: &mut ProtocolState, ) -> ProtocolRouteResponse { - if let ProtocolMsg::OpenRemoteConnectionRequest { socket_type, hostname, port, dns_resolver } = req { - // TODO: open a TCP / UDP socket, generate a connection ID, save it to the state - Some(Ok(ProtocolMsg::OpenRemoteConnectionResponse { connection_id: 42 })) + if let ProtocolMsg::OpenRemoteConnectionRequest { + socket_type: _, + hostname: _, + port: _, + dns_resolver: _, + } = req + { + // TODO: open a TCP / UDP socket, generate a connection ID, save it + // to the state + Some(Ok(ProtocolMsg::OpenRemoteConnectionResponse { + connection_id: 42, + })) } else { None } @@ -408,11 +417,15 @@ mod handlers { pub(super) fn remote_send( req: &ProtocolMsg, - state: &mut ProtocolState, + _state: &mut ProtocolState, ) -> ProtocolRouteResponse { - if let ProtocolMsg::RemoteRequest { connection_id, data } = req { - // TODO: need to call state.connections.get(id).send(data) to actually send data - Some(Ok(ProtocolMsg::RemoteResponse { connection_id: 1, data: vec![] })) + if let ProtocolMsg::RemoteRequest { connection_id: _, data: _ } = req { + // TODO: need to call state.connections.get(id).send(data) to + // actually send data + Some(Ok(ProtocolMsg::RemoteResponse { + connection_id: 1, + data: vec![], + })) } else { None } 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" From 923cc19f9e6f563b7f38ddbaaa70f3224244879d Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 21 May 2024 11:14:39 -0500 Subject: [PATCH 03/21] Put all interfaces in place for remote read/write, and feature gate remote_connection code everywhere --- src/Cargo.lock | 157 +++++++++++++++- src/qos_core/Cargo.toml | 7 +- src/qos_core/src/protocol/error.rs | 34 ++++ src/qos_core/src/protocol/msg.rs | 60 ++++--- src/qos_core/src/protocol/services/mod.rs | 2 + .../protocol/services/remote_connection.rs | 67 +++++++ src/qos_core/src/protocol/state.rs | 170 +++++++++++++++--- 7 files changed, 442 insertions(+), 55 deletions(-) create mode 100644 src/qos_core/src/protocol/services/remote_connection.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 9d0fa47e..6feb322c 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -677,6 +677,18 @@ 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" @@ -891,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" @@ -909,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" @@ -1022,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" @@ -1084,6 +1155,12 @@ dependencies = [ "ureq", ] +[[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" @@ -1154,18 +1231,43 @@ 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" @@ -1398,6 +1500,29 @@ 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" @@ -1621,6 +1746,7 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", "borsh", + "hickory-resolver", "libc", "nix", "qos_crypto", @@ -1628,6 +1754,7 @@ dependencies = [ "qos_nsm", "qos_p256", "qos_test_primitives", + "rand", "rustls", "serde", "serde_bytes", @@ -1742,6 +1869,15 @@ 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" @@ -1918,6 +2054,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" @@ -2268,6 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", + "bytes", "libc", "mio", "num_cpus", @@ -2339,9 +2482,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" @@ -2427,7 +2582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 624f2867..6f39763b 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -16,10 +16,12 @@ 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 } +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" } qos_p256 = { path = "../qos_p256", features = ["mock"] } @@ -28,7 +30,10 @@ rustls = { version = "0.23.5" } webpki-roots = { version = "0.26.1" } [features] +default = ["remote_connection"] # Support for VSOCK vm = [] # Never use in production - support for mock NSM mock = ["qos_nsm/mock"] +# Support for remote connection +remote_connection = ["hickory-resolver", "rand"] diff --git a/src/qos_core/src/protocol/error.rs b/src/qos_core/src/protocol/error.rs index 74e5f4bb..01b04434 100644 --- a/src/qos_core/src/protocol/error.rs +++ b/src/qos_core/src/protocol/error.rs @@ -1,5 +1,9 @@ //! Quorum protocol error +use std::net::AddrParseError; + use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "remote_connection")] +use hickory_resolver::error::ResolveError; use qos_p256::P256Error; use crate::{ @@ -141,6 +145,21 @@ pub enum ProtocolError { /// The new manifest was different from the old manifest when we expected /// them to be the same because they have the same nonce DifferentManifest, + /// Parsing error with a protocol message component + ParseError(String), + /// DNS Resolution error + #[cfg(feature = "remote_connection")] + DNSResolutionError(String), + /// Attempt to save a connection with a duplicate ID + #[cfg(feature = "remote_connection")] + DuplicateConnectionId(u32), + /// Attempt to send a message to a remote connection, but ID isn't found + #[cfg(feature = "remote_connection")] + RemoteConnectionIdNotFound(u32), + /// Attempting to read on a closed remote connection (`.read` returned 0 + /// bytes) + #[cfg(feature = "remote_connection")] + RemoteConnectionClosed, } impl From for ProtocolError { @@ -184,3 +203,18 @@ impl From for ProtocolError { Self::QosAttestError(msg) } } + +impl From for ProtocolError { + fn from(err: AddrParseError) -> Self { + let msg = format!("{err:?}"); + Self::ParseError(msg) + } +} + +#[cfg(feature = "remote_connection")] +impl From for ProtocolError { + fn from(err: ResolveError) -> Self { + let msg = format!("{err:?}"); + Self::ParseError(msg) + } +} diff --git a/src/qos_core/src/protocol/msg.rs b/src/qos_core/src/protocol/msg.rs index 09d08705..d6a13a6f 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -10,13 +10,6 @@ use crate::protocol::{ ProtocolError, }; -/// Type to represent UDP or TCP sockets -#[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] -pub enum SocketType { - TcpSocket, - UdpSocket, -} - /// Message types for communicating with protocol executor. #[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] pub enum ProtocolMsg { @@ -147,37 +140,52 @@ pub enum ProtocolMsg { }, /// Request from the enclave app to open a TCP connection to a remote host - /// TODO: can this be done automatically? - OpenRemoteConnectionRequest { - /// UDP or TCP - socket_type: SocketType, + /// This results in a new remote connection saved in protocol state + RemoteOpenRequest { /// The hostname to connect to, e.g. "www.googleapis.com" hostname: String, /// e.g. 443 - port: u8, - /// e.g. "8.8.8.8" - dns_resolver: String, + 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, }, /// Response for `OpenTcpConnectionRequest` - OpenRemoteConnectionResponse { + RemoteOpenResponse { /// Connection ID to reference the opened connection when used with /// `RemoteRequest` and `RemoteResponse`. TODO: maybe we reply with a /// fd name directly? Not sure what this ID will map to. - connection_id: u8, + connection_id: u32, + }, + /// Read from a remote connection + RemoteReadRequest { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + /// number of bytes to read + size: usize, + }, + /// Response to `RemoteReadRequest` containing read data + RemoteReadResponse { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + /// number of bytes read + data: Vec, }, - /// Proxy bytes to a remote host - RemoteRequest { - /// A connection ID from `OpenRemoteConnectionResponse` - connection_id: u8, + /// Write to a remote connection + RemoteWriteRequest { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, /// Data to be sent data: Vec, }, - /// Response to the proxy request. - RemoteResponse { - /// Connection ID from `OpenRemoteConnectionResponse` - connection_id: u8, - /// Data received on the connection - data: Vec, + /// Response to `RemoteWriteRequest` containing the number of successfully + /// written bytes. + RemoteWriteResponse { + /// Connection ID from `RemoteOpenResponse` + connection_id: u32, + /// Number of bytes written successfully + size: usize, }, } diff --git a/src/qos_core/src/protocol/services/mod.rs b/src/qos_core/src/protocol/services/mod.rs index 5a18d19b..e6538299 100644 --- a/src/qos_core/src/protocol/services/mod.rs +++ b/src/qos_core/src/protocol/services/mod.rs @@ -5,3 +5,5 @@ pub mod boot; pub mod genesis; pub mod key; pub mod provision; +#[cfg(feature = "remote_connection")] +pub(crate) mod remote_connection; diff --git a/src/qos_core/src/protocol/services/remote_connection.rs b/src/qos_core/src/protocol/services/remote_connection.rs new file mode 100644 index 00000000..11c467a3 --- /dev/null +++ b/src/qos_core/src/protocol/services/remote_connection.rs @@ -0,0 +1,67 @@ +use std::net::{AddrParseError, IpAddr, SocketAddr, TcpStream}; + +use hickory_resolver::{ + config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, + Resolver, +}; +use rand::Rng; + +use crate::protocol::ProtocolError; + +pub struct RemoteConnection { + pub id: u32, + pub tcp_stream: TcpStream, +} + +impl RemoteConnection { + pub fn new( + 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(RemoteConnection { id: connection_id, tcp_stream }) + } +} + +pub(in crate::protocol) 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())?; + response.iter().next().ok_or_else(|| { + ProtocolError::DNSResolutionError(format!( + "Empty response when querying for host {hostname}" + )) + }) +} diff --git a/src/qos_core/src/protocol/state.rs b/src/qos_core/src/protocol/state.rs index 06be1d89..b634710c 100644 --- a/src/qos_core/src/protocol/state.rs +++ b/src/qos_core/src/protocol/state.rs @@ -2,6 +2,8 @@ use nix::sys::time::{TimeVal, TimeValLike}; use qos_nsm::NsmProvider; +#[cfg(feature = "remote_connection")] +use super::services::remote_connection::RemoteConnection; use super::{ error::ProtocolError, msg::ProtocolMsg, services::provision::SecretBuilder, }; @@ -146,17 +148,28 @@ impl ProtocolRoute { ) } - pub fn open_remote_connection(current_phase: ProtocolPhase) -> Self { + #[cfg(feature = "remote_connection")] + pub fn remote_open(current_phase: ProtocolPhase) -> Self { ProtocolRoute::new( - Box::new(handlers::open_remote_connection), + Box::new(handlers::remote_open), current_phase, current_phase, ) } - pub fn remote_send(current_phase: ProtocolPhase) -> Self { + #[cfg(feature = "remote_connection")] + pub fn remote_read(current_phase: ProtocolPhase) -> Self { ProtocolRoute::new( - Box::new(handlers::remote_send), + Box::new(handlers::remote_read), + current_phase, + current_phase, + ) + } + + #[cfg(feature = "remote_connection")] + pub fn remote_write(current_phase: ProtocolPhase) -> Self { + ProtocolRoute::new( + Box::new(handlers::remote_write), current_phase, current_phase, ) @@ -193,6 +206,8 @@ pub(crate) struct ProtocolState { pub attestor: Box, pub app_client: Client, pub handles: Handles, + #[cfg(feature = "remote_connection")] + pub remote_connections: Vec, phase: ProtocolPhase, } @@ -223,6 +238,8 @@ impl ProtocolState { app_addr, TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ), + #[cfg(feature = "remote_connection")] + remote_connections: vec![], } } @@ -230,6 +247,27 @@ impl ProtocolState { self.phase } + #[cfg(feature = "remote_connection")] + pub fn save_remote_connection( + &mut self, + connection: RemoteConnection, + ) -> Result<(), ProtocolError> { + if self.remote_connections.iter().any(|c| c.id == connection.id) { + Err(ProtocolError::DuplicateConnectionId(connection.id)) + } else { + self.remote_connections.push(connection); + Ok(()) + } + } + + #[cfg(feature = "remote_connection")] + pub fn get_remote_connection( + &mut self, + id: u32, + ) -> Option<&mut RemoteConnection> { + self.remote_connections.iter_mut().find(|c| c.id == id) + } + pub fn handle_msg(&mut self, msg_req: &ProtocolMsg) -> Vec { for route in &self.routes() { match route.try_msg(msg_req, self) { @@ -291,8 +329,12 @@ impl ProtocolState { // phase specific routes ProtocolRoute::proxy(self.phase), ProtocolRoute::export_key(self.phase), - ProtocolRoute::open_remote_connection(self.phase), - ProtocolRoute::remote_send(self.phase), + #[cfg(feature = "remote_connection")] + ProtocolRoute::remote_open(self.phase), + #[cfg(feature = "remote_connection")] + ProtocolRoute::remote_read(self.phase), + #[cfg(feature = "remote_connection")] + ProtocolRoute::remote_write(self.phase), ] } ProtocolPhase::WaitingForForwardedKey => { @@ -358,6 +400,8 @@ impl ProtocolState { mod handlers { use super::ProtocolRouteResponse; + #[cfg(feature = "remote_connection")] + use crate::protocol::services::remote_connection; use crate::protocol::{ msg::ProtocolMsg, services::{ @@ -394,38 +438,110 @@ mod handlers { } } - pub(super) fn open_remote_connection( + #[cfg(feature = "remote_connection")] + pub(super) fn remote_open( req: &ProtocolMsg, - _state: &mut ProtocolState, + state: &mut ProtocolState, ) -> ProtocolRouteResponse { - if let ProtocolMsg::OpenRemoteConnectionRequest { - socket_type: _, - hostname: _, - port: _, - dns_resolver: _, + if let ProtocolMsg::RemoteOpenRequest { + hostname, + port, + dns_resolvers, + dns_port, } = req { - // TODO: open a TCP / UDP socket, generate a connection ID, save it - // to the state - Some(Ok(ProtocolMsg::OpenRemoteConnectionResponse { - connection_id: 42, - })) + match remote_connection::RemoteConnection::new( + hostname.clone(), + *port, + dns_resolvers.clone(), + *dns_port, + ) { + Ok(remote_connection) => { + let connection_id = remote_connection.id; + match state.save_remote_connection(remote_connection) { + Ok(()) => Some(Ok(ProtocolMsg::RemoteOpenResponse { + connection_id, + })), + Err(e) => { + Some(Err(ProtocolMsg::ProtocolErrorResponse(e))) + } + } + } + Err(e) => Some(Err(ProtocolMsg::ProtocolErrorResponse(e))), + } } else { None } } - pub(super) fn remote_send( + #[cfg(feature = "remote_connection")] + pub(super) fn remote_read( req: &ProtocolMsg, - _state: &mut ProtocolState, + state: &mut ProtocolState, ) -> ProtocolRouteResponse { - if let ProtocolMsg::RemoteRequest { connection_id: _, data: _ } = req { - // TODO: need to call state.connections.get(id).send(data) to - // actually send data - Some(Ok(ProtocolMsg::RemoteResponse { - connection_id: 1, - data: vec![], - })) + use std::io::Read; + + use crate::protocol::ProtocolError; + + if let ProtocolMsg::RemoteReadRequest { connection_id, size } = req { + if let Some(connection) = + state.get_remote_connection(*connection_id) + { + let mut buf: Vec = vec![0; *size]; + match connection.tcp_stream.read(&mut buf) { + Ok(size) => { + if size == 0 { + Some(Err(ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionClosed, + ))) + } else { + Some(Ok(ProtocolMsg::RemoteReadResponse { + connection_id: *connection_id, + data: buf, + })) + } + } + Err(e) => { + Some(Err(ProtocolMsg::ProtocolErrorResponse(e.into()))) + } + } + } else { + Some(Err(ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionIdNotFound(*connection_id), + ))) + } + } else { + None + } + } + + #[cfg(feature = "remote_connection")] + pub(super) fn remote_write( + req: &ProtocolMsg, + state: &mut ProtocolState, + ) -> ProtocolRouteResponse { + use std::io::Write; + + use crate::protocol::ProtocolError; + + if let ProtocolMsg::RemoteWriteRequest { connection_id, data } = req { + if let Some(connection) = + state.get_remote_connection(*connection_id) + { + match connection.tcp_stream.write(data) { + Ok(size) => Some(Ok(ProtocolMsg::RemoteWriteResponse { + connection_id: *connection_id, + size, + })), + Err(e) => { + Some(Err(ProtocolMsg::ProtocolErrorResponse(e.into()))) + } + } + } else { + Some(Err(ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionIdNotFound(*connection_id), + ))) + } } else { None } From cd5f21251f87143a00f40defa3397ae06972371e Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 4 Jun 2024 21:40:24 -0500 Subject: [PATCH 04/21] Break out connection logic into its own crate and protocol --- src/Cargo.lock | 17 + src/Cargo.toml | 3 +- src/integration/src/bin/pivot_remote_http.rs | 42 +++ src/integration/src/lib.rs | 12 + src/integration/tests/remote_http.rs | 35 ++ src/qos_core/Cargo.toml | 3 - src/qos_core/src/protocol/error.rs | 23 -- src/qos_core/src/protocol/services/mod.rs | 2 - src/qos_core/src/protocol/state.rs | 171 --------- src/qos_net/Cargo.toml | 30 ++ src/qos_net/README.MD | 8 + src/qos_net/src/cli.rs | 174 +++++++++ src/qos_net/src/error.rs | 53 +++ src/qos_net/src/lib.rs | 8 + src/qos_net/src/main.rs | 5 + src/qos_net/src/processor.rs | 335 ++++++++++++++++++ .../src}/remote_connection.rs | 41 ++- 17 files changed, 758 insertions(+), 204 deletions(-) create mode 100644 src/integration/src/bin/pivot_remote_http.rs create mode 100644 src/integration/tests/remote_http.rs create mode 100644 src/qos_net/Cargo.toml create mode 100644 src/qos_net/README.MD create mode 100644 src/qos_net/src/cli.rs create mode 100644 src/qos_net/src/error.rs create mode 100644 src/qos_net/src/lib.rs create mode 100644 src/qos_net/src/main.rs create mode 100644 src/qos_net/src/processor.rs rename src/{qos_core/src/protocol/services => qos_net/src}/remote_connection.rs (56%) diff --git a/src/Cargo.lock b/src/Cargo.lock index 6feb322c..596f48c6 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1790,6 +1790,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "qos_net" +version = "0.1.0" +dependencies = [ + "aws-nitro-enclaves-nsm-api", + "borsh", + "hickory-resolver", + "libc", + "nix", + "qos_core", + "qos_hex", + "qos_test_primitives", + "rand", + "serde", + "serde_bytes", +] + [[package]] name = "qos_nsm" version = "0.1.0" 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/src/bin/pivot_remote_http.rs b/src/integration/src/bin/pivot_remote_http.rs new file mode 100644 index 00000000..4acc3307 --- /dev/null +++ b/src/integration/src/bin/pivot_remote_http.rs @@ -0,0 +1,42 @@ +use core::panic; + +use borsh::{BorshDeserialize, BorshSerialize}; +use integration::PivotRemoteHttpMsg; +use qos_core::{ + io::SocketAddress, + server::{RequestProcessor, SocketServer}, +}; + +struct Processor; + +impl RequestProcessor for Processor { + fn process(&mut self, request: Vec) -> Vec { + let msg = PivotRemoteHttpMsg::try_from_slice(&request) + .expect("Received invalid message - test is broken"); + + match msg { + PivotRemoteHttpMsg::RemoteHttpRequest(url) => { + // TODO: + // implement Read/Write traits with + // ProtocolMsg::RemoteReadRequest and + // ProtocolMsg::RemoteWriteRequest + + PivotRemoteHttpMsg::RemoteHttpResponse(format!( + "hello! I am a response to {url}" + )) + .try_to_vec() + .expect("RemoteHttpResponse is valid borsh") + } + PivotRemoteHttpMsg::RemoteHttpResponse(_) => { + panic!("Unexpected RemoteHttpResponse - test is broken") + } + } + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + let socket_path = &args[1]; + SocketServer::listen(SocketAddress::new_unix(socket_path), Processor) + .unwrap(); +} diff --git a/src/integration/src/lib.rs b/src/integration/src/lib.rs index 4250b56f..9af2ab65 100644 --- a/src/integration/src/lib.rs +++ b/src/integration/src/lib.rs @@ -25,6 +25,8 @@ 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_HTTP_PATH: &str = "../target/debug/pivot_remote_http"; /// 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 +55,16 @@ pub enum PivotSocketStressMsg { SlowResponse, } +/// Request/Response messages for "socket stress" pivot app. +#[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq)] +pub enum PivotRemoteHttpMsg { + /// Request a remote URL to be fetched over the socket. + RemoteHttpRequest(String), + /// A successful response to [`Self::RemoteHttpRequest`] with the contents + /// of the response. + RemoteHttpResponse(String), +} + struct PivotParser; impl GetParserForOptions for PivotParser { fn parser() -> Parser { diff --git a/src/integration/tests/remote_http.rs b/src/integration/tests/remote_http.rs new file mode 100644 index 00000000..7f6b805f --- /dev/null +++ b/src/integration/tests/remote_http.rs @@ -0,0 +1,35 @@ +use std::{process::Command, str}; + +use borsh::BorshSerialize; +use integration::{PivotRemoteHttpMsg, PIVOT_REMOTE_HTTP_PATH}; +use qos_core::{ + client::Client, + io::{SocketAddress, TimeVal, TimeValLike}, + protocol::ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS, +}; +use qos_test_primitives::ChildWrapper; + +const REMOTE_HTTP_TEST_SOCKET: &str = "/tmp/remote_http_test.sock"; + +#[test] +fn remote_http() { + let _enclave_app: ChildWrapper = Command::new(PIVOT_REMOTE_HTTP_PATH) + .arg(REMOTE_HTTP_TEST_SOCKET) + .spawn() + .unwrap() + .into(); + + let enclave_client = Client::new( + SocketAddress::new_unix(REMOTE_HTTP_TEST_SOCKET), + TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), + ); + + let app_request = PivotRemoteHttpMsg::RemoteHttpRequest( + "https://api.turnkey.com/health".to_string(), + ) + .try_to_vec() + .unwrap(); + let response = enclave_client.send(&app_request).unwrap(); + let response_text = str::from_utf8(&response).unwrap(); + assert_eq!(response_text, "something"); +} diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 6f39763b..16f17d07 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -30,10 +30,7 @@ rustls = { version = "0.23.5" } webpki-roots = { version = "0.26.1" } [features] -default = ["remote_connection"] # Support for VSOCK vm = [] # Never use in production - support for mock NSM mock = ["qos_nsm/mock"] -# Support for remote connection -remote_connection = ["hickory-resolver", "rand"] diff --git a/src/qos_core/src/protocol/error.rs b/src/qos_core/src/protocol/error.rs index 01b04434..4b959c4a 100644 --- a/src/qos_core/src/protocol/error.rs +++ b/src/qos_core/src/protocol/error.rs @@ -2,8 +2,6 @@ use std::net::AddrParseError; use borsh::{BorshDeserialize, BorshSerialize}; -#[cfg(feature = "remote_connection")] -use hickory_resolver::error::ResolveError; use qos_p256::P256Error; use crate::{ @@ -147,19 +145,6 @@ pub enum ProtocolError { DifferentManifest, /// Parsing error with a protocol message component ParseError(String), - /// DNS Resolution error - #[cfg(feature = "remote_connection")] - DNSResolutionError(String), - /// Attempt to save a connection with a duplicate ID - #[cfg(feature = "remote_connection")] - DuplicateConnectionId(u32), - /// Attempt to send a message to a remote connection, but ID isn't found - #[cfg(feature = "remote_connection")] - RemoteConnectionIdNotFound(u32), - /// Attempting to read on a closed remote connection (`.read` returned 0 - /// bytes) - #[cfg(feature = "remote_connection")] - RemoteConnectionClosed, } impl From for ProtocolError { @@ -210,11 +195,3 @@ impl From for ProtocolError { Self::ParseError(msg) } } - -#[cfg(feature = "remote_connection")] -impl From for ProtocolError { - fn from(err: ResolveError) -> Self { - let msg = format!("{err:?}"); - Self::ParseError(msg) - } -} diff --git a/src/qos_core/src/protocol/services/mod.rs b/src/qos_core/src/protocol/services/mod.rs index e6538299..5a18d19b 100644 --- a/src/qos_core/src/protocol/services/mod.rs +++ b/src/qos_core/src/protocol/services/mod.rs @@ -5,5 +5,3 @@ pub mod boot; pub mod genesis; pub mod key; pub mod provision; -#[cfg(feature = "remote_connection")] -pub(crate) mod remote_connection; diff --git a/src/qos_core/src/protocol/state.rs b/src/qos_core/src/protocol/state.rs index b634710c..6df38852 100644 --- a/src/qos_core/src/protocol/state.rs +++ b/src/qos_core/src/protocol/state.rs @@ -2,8 +2,6 @@ use nix::sys::time::{TimeVal, TimeValLike}; use qos_nsm::NsmProvider; -#[cfg(feature = "remote_connection")] -use super::services::remote_connection::RemoteConnection; use super::{ error::ProtocolError, msg::ProtocolMsg, services::provision::SecretBuilder, }; @@ -148,33 +146,6 @@ impl ProtocolRoute { ) } - #[cfg(feature = "remote_connection")] - pub fn remote_open(current_phase: ProtocolPhase) -> Self { - ProtocolRoute::new( - Box::new(handlers::remote_open), - current_phase, - current_phase, - ) - } - - #[cfg(feature = "remote_connection")] - pub fn remote_read(current_phase: ProtocolPhase) -> Self { - ProtocolRoute::new( - Box::new(handlers::remote_read), - current_phase, - current_phase, - ) - } - - #[cfg(feature = "remote_connection")] - pub fn remote_write(current_phase: ProtocolPhase) -> Self { - ProtocolRoute::new( - Box::new(handlers::remote_write), - current_phase, - current_phase, - ) - } - pub fn export_key(current_phase: ProtocolPhase) -> Self { ProtocolRoute::new( Box::new(handlers::export_key), @@ -206,8 +177,6 @@ pub(crate) struct ProtocolState { pub attestor: Box, pub app_client: Client, pub handles: Handles, - #[cfg(feature = "remote_connection")] - pub remote_connections: Vec, phase: ProtocolPhase, } @@ -238,8 +207,6 @@ impl ProtocolState { app_addr, TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ), - #[cfg(feature = "remote_connection")] - remote_connections: vec![], } } @@ -247,27 +214,6 @@ impl ProtocolState { self.phase } - #[cfg(feature = "remote_connection")] - pub fn save_remote_connection( - &mut self, - connection: RemoteConnection, - ) -> Result<(), ProtocolError> { - if self.remote_connections.iter().any(|c| c.id == connection.id) { - Err(ProtocolError::DuplicateConnectionId(connection.id)) - } else { - self.remote_connections.push(connection); - Ok(()) - } - } - - #[cfg(feature = "remote_connection")] - pub fn get_remote_connection( - &mut self, - id: u32, - ) -> Option<&mut RemoteConnection> { - self.remote_connections.iter_mut().find(|c| c.id == id) - } - pub fn handle_msg(&mut self, msg_req: &ProtocolMsg) -> Vec { for route in &self.routes() { match route.try_msg(msg_req, self) { @@ -329,12 +275,6 @@ impl ProtocolState { // phase specific routes ProtocolRoute::proxy(self.phase), ProtocolRoute::export_key(self.phase), - #[cfg(feature = "remote_connection")] - ProtocolRoute::remote_open(self.phase), - #[cfg(feature = "remote_connection")] - ProtocolRoute::remote_read(self.phase), - #[cfg(feature = "remote_connection")] - ProtocolRoute::remote_write(self.phase), ] } ProtocolPhase::WaitingForForwardedKey => { @@ -400,8 +340,6 @@ impl ProtocolState { mod handlers { use super::ProtocolRouteResponse; - #[cfg(feature = "remote_connection")] - use crate::protocol::services::remote_connection; use crate::protocol::{ msg::ProtocolMsg, services::{ @@ -438,115 +376,6 @@ mod handlers { } } - #[cfg(feature = "remote_connection")] - pub(super) fn remote_open( - req: &ProtocolMsg, - state: &mut ProtocolState, - ) -> ProtocolRouteResponse { - if let ProtocolMsg::RemoteOpenRequest { - hostname, - port, - dns_resolvers, - dns_port, - } = req - { - match remote_connection::RemoteConnection::new( - hostname.clone(), - *port, - dns_resolvers.clone(), - *dns_port, - ) { - Ok(remote_connection) => { - let connection_id = remote_connection.id; - match state.save_remote_connection(remote_connection) { - Ok(()) => Some(Ok(ProtocolMsg::RemoteOpenResponse { - connection_id, - })), - Err(e) => { - Some(Err(ProtocolMsg::ProtocolErrorResponse(e))) - } - } - } - Err(e) => Some(Err(ProtocolMsg::ProtocolErrorResponse(e))), - } - } else { - None - } - } - - #[cfg(feature = "remote_connection")] - pub(super) fn remote_read( - req: &ProtocolMsg, - state: &mut ProtocolState, - ) -> ProtocolRouteResponse { - use std::io::Read; - - use crate::protocol::ProtocolError; - - if let ProtocolMsg::RemoteReadRequest { connection_id, size } = req { - if let Some(connection) = - state.get_remote_connection(*connection_id) - { - let mut buf: Vec = vec![0; *size]; - match connection.tcp_stream.read(&mut buf) { - Ok(size) => { - if size == 0 { - Some(Err(ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionClosed, - ))) - } else { - Some(Ok(ProtocolMsg::RemoteReadResponse { - connection_id: *connection_id, - data: buf, - })) - } - } - Err(e) => { - Some(Err(ProtocolMsg::ProtocolErrorResponse(e.into()))) - } - } - } else { - Some(Err(ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionIdNotFound(*connection_id), - ))) - } - } else { - None - } - } - - #[cfg(feature = "remote_connection")] - pub(super) fn remote_write( - req: &ProtocolMsg, - state: &mut ProtocolState, - ) -> ProtocolRouteResponse { - use std::io::Write; - - use crate::protocol::ProtocolError; - - if let ProtocolMsg::RemoteWriteRequest { connection_id, data } = req { - if let Some(connection) = - state.get_remote_connection(*connection_id) - { - match connection.tcp_stream.write(data) { - Ok(size) => Some(Ok(ProtocolMsg::RemoteWriteResponse { - connection_id: *connection_id, - size, - })), - Err(e) => { - Some(Err(ProtocolMsg::ProtocolErrorResponse(e.into()))) - } - } - } else { - Some(Err(ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionIdNotFound(*connection_id), - ))) - } - } else { - None - } - } - pub(super) fn proxy( req: &ProtocolMsg, state: &mut ProtocolState, diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml new file mode 100644 index 00000000..d8b8fbd7 --- /dev/null +++ b/src/qos_net/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "qos_net" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +qos_hex = { path = "../qos_hex", features = ["serde"] } +qos_core = { path = "../qos_core", default-features = false } + +nix = { version = "0.26", features = ["socket"], default-features = false } +libc = "=0.2.148" +borsh = { version = "0.10" } + +# For AWS Nitro +aws-nitro-enclaves-nsm-api = { version = "0.3", default-features = false } + +serde_bytes = { version = "0.11", default-features = false } +serde = { version = "1", features = ["derive"], default-features = false } + +hickory-resolver = { version = "0.24.1", features = ["tokio-runtime"], default-features = false} +rand = { version = "0.8.5", default-features = false } + +[dev-dependencies] +qos_test_primitives = { path = "../qos_test_primitives" } + +[features] +# Support for VSOCK +vm = [] + diff --git a/src/qos_net/README.MD b/src/qos_net/README.MD new file mode 100644 index 00000000..c5f624c6 --- /dev/null +++ b/src/qos_net/README.MD @@ -0,0 +1,8 @@ +# QOS Net + +This crate contains a proxy server which implements the protocol messages to implement remote connections: +* `ProtocolMsg::RemoteOpenConnection` +* `ProtocolMsg::RemoteRead` +* `ProtocolMsg::RemoteWrite` + +It also contains a protocol and libraries to interact with the protocol diff --git a/src/qos_net/src/cli.rs b/src/qos_net/src/cli.rs new file mode 100644 index 00000000..643dba25 --- /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::processor::Processor; + +/// "cid" +pub const CID: &str = "cid"; +/// "port" +pub const PORT: &str = "port"; +/// "usock" +pub const USOCK: &str = "usock"; + +/// CLI options for starting up the enclave server. +#[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"), + } + } +} + +/// Enclave server CLI. +pub struct CLI; +impl CLI { + /// Execute the enclave server 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(), Processor::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..501f95db --- /dev/null +++ b/src/qos_net/src/error.rs @@ -0,0 +1,53 @@ +//! Remote protocol error +use std::net::AddrParseError; + +use borsh::{BorshDeserialize, BorshSerialize}; +use hickory_resolver::error::ResolveError; + +/// Errors during protocol execution. +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +pub enum ProtocolError { + /// Error variant encapsulating OS IO errors + IOError, + /// Hash of the Pivot binary does not match the pivot configuration in the + /// manifest. + InvalidPivotHash, + /// 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 + RemoteConnectionIdNotFound(u32), + /// Attempting to read on a closed remote connection (`.read` returned 0 + /// bytes) + RemoteConnectionClosed, +} + +impl From for ProtocolError { + fn from(_err: std::io::Error) -> Self { + Self::IOError + } +} + +impl From for ProtocolError { + fn from(err: AddrParseError) -> Self { + let msg = format!("{err:?}"); + Self::ParseError(msg) + } +} + +impl From for ProtocolError { + fn from(err: ResolveError) -> Self { + let msg = format!("{err:?}"); + Self::ParseError(msg) + } +} diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs new file mode 100644 index 00000000..47e3c10f --- /dev/null +++ b/src/qos_net/src/lib.rs @@ -0,0 +1,8 @@ +//! This crate contains a simple proxy server to implement QOS protocol messages +//! related to establishing and using remote connections. + +#![deny(clippy::all, unsafe_code)] +pub mod cli; +pub mod error; +pub mod processor; +pub mod remote_connection; 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/processor.rs b/src/qos_net/src/processor.rs new file mode 100644 index 00000000..6103b2b0 --- /dev/null +++ b/src/qos_net/src/processor.rs @@ -0,0 +1,335 @@ +//! Quorum protocol processor +use std::io::{Read, Write}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use qos_core::server; + +use crate::{ + error::ProtocolError, + remote_connection::{self, RemoteConnection}, +}; + +const MEGABYTE: usize = 1024 * 1024; +const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; + +/// Enclave state machine that executes when given a `ProtocolMsg`. +pub struct Processor { + remote_connections: Vec, +} + +impl Default for Processor { + fn default() -> Self { + Self::new() + } +} + +impl Processor { + /// Create a new `Self`. + #[must_use] + pub fn new() -> Self { + Self { remote_connections: vec![] } + } + + fn save_remote_connection( + &mut self, + connection: RemoteConnection, + ) -> Result<(), ProtocolError> { + if self.remote_connections.iter().any(|c| c.id == connection.id) { + Err(ProtocolError::DuplicateConnectionId(connection.id)) + } else { + self.remote_connections.push(connection); + Ok(()) + } + } + + fn get_remote_connection( + &mut self, + id: u32, + ) -> Option<&mut RemoteConnection> { + self.remote_connections.iter_mut().find(|c| c.id == id) + } + + /// Open and save a new remote connection by resolving a name into an IP + /// address, then opening a new TCP connection + pub fn remote_open_by_name( + &mut self, + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> ProtocolMsg { + match remote_connection::RemoteConnection::new_from_name( + hostname.clone(), + port, + dns_resolvers.clone(), + dns_port, + ) { + Ok(remote_connection) => { + let connection_id = remote_connection.id; + let remote_host = remote_connection.ip.clone(); + match self.save_remote_connection(remote_connection) { + Ok(()) => ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_host, + }, + Err(e) => ProtocolMsg::ProtocolErrorResponse(e), + } + } + Err(e) => ProtocolMsg::ProtocolErrorResponse(e), + } + } + + /// Open a new remote connection by connecting to an IP address directly + pub fn remote_open_by_ip(&mut self, ip: String, port: u16) -> ProtocolMsg { + match remote_connection::RemoteConnection::new_from_ip(ip, port) { + Ok(remote_connection) => { + let connection_id = remote_connection.id; + let remote_host = remote_connection.ip.clone(); + match self.save_remote_connection(remote_connection) { + Ok(()) => ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_host, + }, + Err(e) => ProtocolMsg::ProtocolErrorResponse(e), + } + } + Err(e) => ProtocolMsg::ProtocolErrorResponse(e), + } + } + + /// Performs a Read on a remote connection + pub fn remote_read( + &mut self, + connection_id: u32, + size: usize, + ) -> ProtocolMsg { + if let Some(connection) = self.get_remote_connection(connection_id) { + let mut buf: Vec = vec![0; size]; + match connection.tcp_stream.read(&mut buf) { + Ok(size) => { + if size == 0 { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionClosed, + ) + } else { + ProtocolMsg::RemoteReadResponse { + connection_id, + data: buf, + } + } + } + Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), + } + } else { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionIdNotFound(connection_id), + ) + } + } + + /// Performs a Write on a remote connection + pub fn remote_write( + &mut self, + connection_id: u32, + data: Vec, + ) -> ProtocolMsg { + if let Some(connection) = self.get_remote_connection(connection_id) { + match connection.tcp_stream.write(&data) { + Ok(size) => { + ProtocolMsg::RemoteWriteResponse { connection_id, size } + } + Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), + } + } else { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionIdNotFound(connection_id), + ) + } + } +} + +impl server::RequestProcessor for Processor { + fn process(&mut self, req_bytes: Vec) -> Vec { + if req_bytes.len() > MAX_ENCODED_MSG_LEN { + return ProtocolMsg::ProtocolErrorResponse( + ProtocolError::OversizedPayload, + ) + .try_to_vec() + .expect("ProtocolMsg can always be serialized. qed."); + } + + let resp = match ProtocolMsg::try_from_slice(&req_bytes) { + // TODO: match on all variants? + Ok(req) => match req { + ProtocolMsg::StatusRequest => { + ProtocolMsg::StatusResponse(self.remote_connections.len()) + } + ProtocolMsg::RemoteOpenByNameRequest { + hostname, + port, + dns_resolvers, + dns_port, + } => self.remote_open_by_name( + hostname, + port, + dns_resolvers, + dns_port, + ), + ProtocolMsg::RemoteOpenByIpRequest { ip, port } => { + self.remote_open_by_ip(ip, port) + } + ProtocolMsg::RemoteReadRequest { connection_id, size } => { + self.remote_read(connection_id, size) + } + ProtocolMsg::RemoteWriteRequest { connection_id, data } => { + self.remote_write(connection_id, data) + } + ProtocolMsg::ProtocolErrorResponse(_) => { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ) + } + ProtocolMsg::StatusResponse(_) => { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ) + } + ProtocolMsg::RemoteOpenResponse { + connection_id: _, + remote_host: _, + } => ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ), + ProtocolMsg::RemoteWriteResponse { + connection_id: _, + size: _, + } => ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ), + ProtocolMsg::RemoteReadResponse { + connection_id: _, + data: _, + } => ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ), + }, + Err(_) => { + ProtocolMsg::ProtocolErrorResponse(ProtocolError::InvalidMsg) + } + }; + + resp.try_to_vec() + .expect("Protocol message can always be serialized. qed!") + } +} + +/// Message types to use with the remote proxy. +#[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] +pub enum ProtocolMsg { + /// A error from executing the protocol. + ProtocolErrorResponse(ProtocolError), + + /// 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 + RemoteOpenByNameRequest { + /// 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 + RemoteOpenByIpRequest { + /// The IP to connect to, e.g. "1.2.3.4" + ip: String, + /// e.g. 443 + port: u16, + }, + /// Response for `RemoteOpenByNameRequest` and `RemoteOpenByIpRequest` + RemoteOpenResponse { + /// Connection ID to reference the opened connection when used with + /// `RemoteRequest` and `RemoteResponse`. TODO: maybe we reply with a + /// fd name directly? Not sure what this ID will map to. + connection_id: u32, + /// The remote host IP, e.g. "1.2.3.4" + remote_host: String, + }, + /// Read from a remote connection + RemoteReadRequest { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + /// number of bytes to read + size: usize, + }, + /// Response to `RemoteReadRequest` containing read data + RemoteReadResponse { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + /// number of bytes read + data: Vec, + }, + /// Write to a remote connection + RemoteWriteRequest { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + /// Data to be sent + data: Vec, + }, + /// Response to `RemoteWriteRequest` containing the number of successfully + /// written bytes. + RemoteWriteResponse { + /// Connection ID from `RemoteOpenResponse` + connection_id: u32, + /// Number of bytes written successfully + size: usize, + }, +} + +#[cfg(test)] +mod test { + use server::RequestProcessor; + + use super::*; + + #[test] + fn simple_status_request() { + let mut processor = Processor::new(); + let request = ProtocolMsg::StatusRequest.try_to_vec().unwrap(); + let response = processor.process(request.try_into().unwrap()); + let msg = ProtocolMsg::try_from_slice(&response).unwrap(); + assert_eq!(msg, ProtocolMsg::StatusResponse(0)); + } + + #[test] + fn open_remote_to_turnkey_com() { + let mut processor = Processor::new(); + let request = ProtocolMsg::RemoteOpenByNameRequest { + hostname: "api.turnkey.com".to_string(), + port: 443, + dns_resolvers: vec!["8.8.8.8".to_string()], + dns_port: 53, + } + .try_to_vec() + .unwrap(); + let response = processor.process(request.try_into().unwrap()); + let msg = ProtocolMsg::try_from_slice(&response).unwrap(); + assert!(matches!( + msg, + ProtocolMsg::RemoteOpenResponse { + connection_id: _, + remote_host: _ + } + )); + } +} diff --git a/src/qos_core/src/protocol/services/remote_connection.rs b/src/qos_net/src/remote_connection.rs similarity index 56% rename from src/qos_core/src/protocol/services/remote_connection.rs rename to src/qos_net/src/remote_connection.rs index 11c467a3..e841f375 100644 --- a/src/qos_core/src/protocol/services/remote_connection.rs +++ b/src/qos_net/src/remote_connection.rs @@ -1,3 +1,5 @@ +//! Contains logic for remote connection establishment: DNS resolution and TCP +//! connection. use std::net::{AddrParseError, IpAddr, SocketAddr, TcpStream}; use hickory_resolver::{ @@ -6,15 +8,23 @@ use hickory_resolver::{ }; use rand::Rng; -use crate::protocol::ProtocolError; +use crate::error::ProtocolError; +/// Struct representing a remote connection pub struct RemoteConnection { + /// Unsigned integer with the connection ID. This is a random positive + /// integer pub id: u32, + /// IP address for the remote host + pub ip: String, + /// TCP stream object pub tcp_stream: TcpStream, } impl RemoteConnection { - pub fn new( + /// Create a new `RemoteConnection` from a name. This results in a DNS + /// request + TCP connection + pub fn new_from_name( hostname: String, port: u16, dns_resolvers: Vec, @@ -30,11 +40,34 @@ impl RemoteConnection { let tcp_addr = SocketAddr::new(ip, port); let tcp_stream = TcpStream::connect(tcp_addr)?; - Ok(RemoteConnection { id: connection_id, tcp_stream }) + Ok(RemoteConnection { + id: connection_id, + ip: ip.to_string(), + tcp_stream, + }) + } + + /// Create a new `RemoteConnection` 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(RemoteConnection { id: connection_id, ip, tcp_stream }) } } -pub(in crate::protocol) fn resolve_hostname( +// Resolve a name into an IP address +fn resolve_hostname( hostname: String, resolver_addrs: Vec, port: u16, From 40a04d8c2193163703e698b37bf533d0ed7cfbe8 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Fri, 7 Jun 2024 09:57:23 -0500 Subject: [PATCH 05/21] Remote read/write interface working. Let the cleanup begin... --- src/Cargo.lock | 5 + src/integration/Cargo.toml | 3 + src/integration/src/bin/pivot_remote_http.rs | 78 +++- src/integration/src/lib.rs | 12 +- src/integration/tests/remote_http.rs | 20 +- src/qos_core/src/io/mod.rs | 2 +- src/qos_core/src/io/stream.rs | 23 +- src/qos_net/Cargo.toml | 2 + src/qos_net/src/error.rs | 16 +- src/qos_net/src/lib.rs | 1 + src/qos_net/src/processor.rs | 216 ++++++++- src/qos_net/src/remote_connection.rs | 76 +++- src/qos_net/src/remote_stream.rs | 449 +++++++++++++++++++ 13 files changed, 849 insertions(+), 54 deletions(-) create mode 100644 src/qos_net/src/remote_stream.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 596f48c6..6e2364f3 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1146,13 +1146,16 @@ dependencies = [ "qos_crypto", "qos_hex", "qos_host", + "qos_net", "qos_nsm", "qos_p256", "qos_test_primitives", "rand", + "rustls", "serde", "tokio", "ureq", + "webpki-roots", ] [[package]] @@ -1803,8 +1806,10 @@ dependencies = [ "qos_hex", "qos_test_primitives", "rand", + "rustls", "serde", "serde_bytes", + "webpki-roots", ] [[package]] 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_http.rs b/src/integration/src/bin/pivot_remote_http.rs index 4acc3307..64caad02 100644 --- a/src/integration/src/bin/pivot_remote_http.rs +++ b/src/integration/src/bin/pivot_remote_http.rs @@ -1,28 +1,74 @@ use core::panic; +use std::{io::{Read, Write}, sync::Arc}; use borsh::{BorshDeserialize, BorshSerialize}; use integration::PivotRemoteHttpMsg; use qos_core::{ - io::SocketAddress, + io::{SocketAddress, TimeVal}, server::{RequestProcessor, SocketServer}, }; +use qos_net::remote_stream::RemoteStream; +use rustls::RootCertStore; -struct Processor; +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 = PivotRemoteHttpMsg::try_from_slice(&request) - .expect("Received invalid message - test is broken"); + .expect("Received invalid message - test is broken!"); match msg { - PivotRemoteHttpMsg::RemoteHttpRequest(url) => { - // TODO: - // implement Read/Write traits with - // ProtocolMsg::RemoteReadRequest and - // ProtocolMsg::RemoteWriteRequest + PivotRemoteHttpMsg::RemoteHttpRequest{ host, path } => { + let timeout = TimeVal::new(1, 0); + let mut stream = RemoteStream::new_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" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + let mut response_bytes = Vec::new(); + let read_to_end_result: usize = tls.read_to_end(&mut response_bytes).unwrap(); + + // Ignore eof errors: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof + let fetched_content = std::str::from_utf8(&response_bytes).unwrap(); PivotRemoteHttpMsg::RemoteHttpResponse(format!( - "hello! I am a response to {url}" + "Content fetched successfully ({read_to_end_result} bytes): {fetched_content}" )) .try_to_vec() .expect("RemoteHttpResponse is valid borsh") @@ -35,8 +81,16 @@ impl RequestProcessor for Processor { } fn main() { + // Parse args: + // - first argument is the socket to bind to (server) + // - second argument is the socket to query (net proxy) let args: Vec = std::env::args().collect(); - let socket_path = &args[1]; - SocketServer::listen(SocketAddress::new_unix(socket_path), Processor) - .unwrap(); + + 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 9af2ab65..86c430a9 100644 --- a/src/integration/src/lib.rs +++ b/src/integration/src/lib.rs @@ -27,6 +27,8 @@ pub const PIVOT_ABORT_PATH: &str = "../target/debug/pivot_abort"; 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_HTTP_PATH: &str = "../target/debug/pivot_remote_http"; +/// 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"; @@ -58,8 +60,14 @@ pub enum PivotSocketStressMsg { /// Request/Response messages for "socket stress" pivot app. #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq)] pub enum PivotRemoteHttpMsg { - /// Request a remote URL to be fetched over the socket. - RemoteHttpRequest(String), + /// 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) + RemoteHttpRequest { + /// Hostname (e.g. "api.turnkey.com") + host: String, + /// Path to fetch (e.g. "/health") + path: String, + }, /// A successful response to [`Self::RemoteHttpRequest`] with the contents /// of the response. RemoteHttpResponse(String), diff --git a/src/integration/tests/remote_http.rs b/src/integration/tests/remote_http.rs index 7f6b805f..3aa35187 100644 --- a/src/integration/tests/remote_http.rs +++ b/src/integration/tests/remote_http.rs @@ -1,7 +1,7 @@ use std::{process::Command, str}; use borsh::BorshSerialize; -use integration::{PivotRemoteHttpMsg, PIVOT_REMOTE_HTTP_PATH}; +use integration::{PivotRemoteHttpMsg, PIVOT_REMOTE_HTTP_PATH, QOS_NET_PATH}; use qos_core::{ client::Client, io::{SocketAddress, TimeVal, TimeValLike}, @@ -9,26 +9,30 @@ use qos_core::{ }; use qos_test_primitives::ChildWrapper; -const REMOTE_HTTP_TEST_SOCKET: &str = "/tmp/remote_http_test.sock"; +const REMOTE_HTTP_TEST_NET_PROXY_SOCKET: &str = "/tmp/remote_http_test.net.sock"; +const REMOTE_HTTP_TEST_ENCLAVE_SOCKET: &str = "/tmp/remote_http_test.enclave.sock"; #[test] fn remote_http() { + let _net_proxy: ChildWrapper = Command::new(QOS_NET_PATH).arg("--usock").arg(REMOTE_HTTP_TEST_NET_PROXY_SOCKET).spawn().unwrap().into(); let _enclave_app: ChildWrapper = Command::new(PIVOT_REMOTE_HTTP_PATH) - .arg(REMOTE_HTTP_TEST_SOCKET) + .arg(REMOTE_HTTP_TEST_ENCLAVE_SOCKET) + .arg(REMOTE_HTTP_TEST_NET_PROXY_SOCKET) .spawn() .unwrap() .into(); let enclave_client = Client::new( - SocketAddress::new_unix(REMOTE_HTTP_TEST_SOCKET), + SocketAddress::new_unix(REMOTE_HTTP_TEST_ENCLAVE_SOCKET), TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ); - let app_request = PivotRemoteHttpMsg::RemoteHttpRequest( - "https://api.turnkey.com/health".to_string(), - ) - .try_to_vec() + let app_request = PivotRemoteHttpMsg::RemoteHttpRequest{ + host: "api.turnkey.com".to_string(), + path: "/health".to_string(), + }.try_to_vec() .unwrap(); + let response = enclave_client.send(&app_request).unwrap(); let response_text = str::from_utf8(&response).unwrap(); assert_eq!(response_text, "something"); diff --git a/src/qos_core/src/io/mod.rs b/src/qos_core/src/io/mod.rs index 61e03d85..54eeb3ba 100644 --- a/src/qos_core/src/io/mod.rs +++ b/src/qos_core/src/io/mod.rs @@ -5,8 +5,8 @@ mod stream; -pub(crate) use stream::{Listener, Stream}; pub use stream::{ + Listener, Stream, SocketAddress, TimeVal, TimeValLike, VMADDR_FLAG_TO_HOST, VMADDR_NO_FLAGS, }; diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index de16c02c..726998a7 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -90,7 +90,7 @@ impl SocketAddress { } /// Get the `AddressFamily` of the socket. - fn family(&self) -> AddressFamily { + pub fn family(&self) -> AddressFamily { match *self { #[cfg(feature = "vm")] Self::Vsock(_) => AddressFamily::Vsock, @@ -98,8 +98,8 @@ impl SocketAddress { } } - // Convenience method for accessing the wrapped address - fn addr(&self) -> Box { + /// Convenience method for accessing the wrapped address + pub fn addr(&self) -> Box { match *self { #[cfg(feature = "vm")] Self::Vsock(vsa) => Box::new(vsa), @@ -109,12 +109,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 { @@ -144,13 +145,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( @@ -182,7 +184,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::()]; @@ -290,7 +293,7 @@ impl Drop for Stream { } /// Abstraction to listen for incoming stream connections. -pub(crate) struct Listener { +pub struct Listener { fd: RawFd, addr: SocketAddress, } @@ -395,7 +398,7 @@ mod test { #[test] fn stream_implement_reader_writer_interfaces() { - let host = "www.turnkey.com"; + let host = "api.turnkey.com"; let path = "/health"; let unix_addr = diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml index d8b8fbd7..735e908a 100644 --- a/src/qos_net/Cargo.toml +++ b/src/qos_net/Cargo.toml @@ -23,6 +23,8 @@ rand = { version = "0.8.5", default-features = false } [dev-dependencies] qos_test_primitives = { path = "../qos_test_primitives" } +rustls = { version = "0.23.5" } +webpki-roots = { version = "0.26.1" } [features] # Support for VSOCK diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index 501f95db..7b50b111 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -9,9 +9,8 @@ use hickory_resolver::error::ResolveError; pub enum ProtocolError { /// Error variant encapsulating OS IO errors IOError, - /// Hash of the Pivot binary does not match the pivot configuration in the - /// manifest. - InvalidPivotHash, + /// Error variant encapsulating OS IO errors + QOSIOError, /// The message is too large. OversizeMsg, /// Payload is too big. See `MAX_ENCODED_MSG_LEN` for the upper bound on @@ -30,6 +29,11 @@ pub enum ProtocolError { /// Attempting to read on a closed remote connection (`.read` returned 0 /// bytes) RemoteConnectionClosed, + /// Happens if a RemoteRead response has empty data + RemoteReadEmpty, + /// Happens if a RemoteRead 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. + RemoteReadOverflow(usize, usize), } impl From for ProtocolError { @@ -38,6 +42,12 @@ impl From for ProtocolError { } } +impl From for ProtocolError { + fn from(_err: qos_core::io::IOError) -> Self { + Self::QOSIOError + } +} + impl From for ProtocolError { fn from(err: AddrParseError) -> Self { let msg = format!("{err:?}"); diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs index 47e3c10f..3eda6942 100644 --- a/src/qos_net/src/lib.rs +++ b/src/qos_net/src/lib.rs @@ -6,3 +6,4 @@ pub mod cli; pub mod error; pub mod processor; pub mod remote_connection; +pub mod remote_stream; diff --git a/src/qos_net/src/processor.rs b/src/qos_net/src/processor.rs index 6103b2b0..95aa4bcb 100644 --- a/src/qos_net/src/processor.rs +++ b/src/qos_net/src/processor.rs @@ -1,5 +1,5 @@ //! Quorum protocol processor -use std::io::{Read, Write}; +use std::io::{ErrorKind, Read, Write}; use borsh::{BorshDeserialize, BorshSerialize}; use qos_core::server; @@ -58,6 +58,8 @@ impl Processor { dns_resolvers: Vec, dns_port: u16, ) -> ProtocolMsg { + println!("opening a new remote connection by hostname for {hostname}"); + match remote_connection::RemoteConnection::new_from_name( hostname.clone(), port, @@ -66,11 +68,11 @@ impl Processor { ) { Ok(remote_connection) => { let connection_id = remote_connection.id; - let remote_host = remote_connection.ip.clone(); + let remote_ip = remote_connection.ip.clone(); match self.save_remote_connection(remote_connection) { Ok(()) => ProtocolMsg::RemoteOpenResponse { connection_id, - remote_host, + remote_ip, }, Err(e) => ProtocolMsg::ProtocolErrorResponse(e), } @@ -84,11 +86,11 @@ impl Processor { match remote_connection::RemoteConnection::new_from_ip(ip, port) { Ok(remote_connection) => { let connection_id = remote_connection.id; - let remote_host = remote_connection.ip.clone(); + let remote_ip = remote_connection.ip.clone(); match self.save_remote_connection(remote_connection) { Ok(()) => ProtocolMsg::RemoteOpenResponse { connection_id, - remote_host, + remote_ip, }, Err(e) => ProtocolMsg::ProtocolErrorResponse(e), } @@ -103,9 +105,10 @@ impl Processor { connection_id: u32, size: usize, ) -> ProtocolMsg { + println!("reading {size} bytes from connection {connection_id}"); if let Some(connection) = self.get_remote_connection(connection_id) { let mut buf: Vec = vec![0; size]; - match connection.tcp_stream.read(&mut buf) { + match connection.read(&mut buf) { Ok(size) => { if size == 0 { ProtocolMsg::ProtocolErrorResponse( @@ -115,6 +118,7 @@ impl Processor { ProtocolMsg::RemoteReadResponse { connection_id, data: buf, + size, } } } @@ -134,7 +138,7 @@ impl Processor { data: Vec, ) -> ProtocolMsg { if let Some(connection) = self.get_remote_connection(connection_id) { - match connection.tcp_stream.write(&data) { + match connection.write(&data) { Ok(size) => { ProtocolMsg::RemoteWriteResponse { connection_id, size } } @@ -146,6 +150,78 @@ impl Processor { ) } } + pub fn remote_flush( + &mut self, + connection_id: u32, + ) -> ProtocolMsg { + println!("Flushing connection {connection_id}"); + if let Some(connection) = self.get_remote_connection(connection_id) { + match connection.flush() { + Ok(_) => { + ProtocolMsg::RemoteFlushResponse { connection_id } + } + Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), + } + } else { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::RemoteConnectionIdNotFound(connection_id), + ) + } + } +} + +impl Read for Processor { + // fn read(&mut self, buf: &mut [u8]) -> Result { + // let size = self.remote_connections.first_mut().unwrap().read(buf)?; + // println!("READ {}: read {} bytes: |{}|", buf.len(), size, qos_hex::encode(&buf)); + // Ok(size) + // } + fn read(&mut self, buf: &mut [u8]) -> Result { + let connection_id = self.remote_connections.first().unwrap().id; + + match self.remote_read(connection_id, buf.len()) { + ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { + if data.len() == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); + } + 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 + } + println!("READ {}: read {} bytes: |{}|", buf.len(), data.len(), qos_hex::encode(&data)); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + } + } +} + +impl Write for Processor { + fn write(&mut self, buf: &[u8]) -> Result { + let connection_id = self.remote_connections.first().unwrap().id; + let resp = self.remote_write(connection_id, buf.to_vec()); + match resp { + ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { + if size == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); + } + println!("WRITE {}: sent buf of {} bytes: |{}|", buf.len(), size, qos_hex::encode(buf)); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + } + } + fn flush(&mut self) -> std::io::Result<()> { + self.remote_connections.first_mut().unwrap().flush() + } } impl server::RequestProcessor for Processor { @@ -159,7 +235,6 @@ impl server::RequestProcessor for Processor { } let resp = match ProtocolMsg::try_from_slice(&req_bytes) { - // TODO: match on all variants? Ok(req) => match req { ProtocolMsg::StatusRequest => { ProtocolMsg::StatusResponse(self.remote_connections.len()) @@ -179,11 +254,17 @@ impl server::RequestProcessor for Processor { self.remote_open_by_ip(ip, port) } ProtocolMsg::RemoteReadRequest { connection_id, size } => { + println!("processing RemoteReadRequest"); self.remote_read(connection_id, size) } ProtocolMsg::RemoteWriteRequest { connection_id, data } => { + println!("processing RemoteWriteRequest"); self.remote_write(connection_id, data) } + ProtocolMsg::RemoteFlushRequest { connection_id } => { + println!("processing RemoteWriteRequest"); + self.remote_flush(connection_id) + } ProtocolMsg::ProtocolErrorResponse(_) => { ProtocolMsg::ProtocolErrorResponse( ProtocolError::InvalidMsg, @@ -196,7 +277,7 @@ impl server::RequestProcessor for Processor { } ProtocolMsg::RemoteOpenResponse { connection_id: _, - remote_host: _, + remote_ip: _, } => ProtocolMsg::ProtocolErrorResponse( ProtocolError::InvalidMsg, ), @@ -206,8 +287,14 @@ impl server::RequestProcessor for Processor { } => ProtocolMsg::ProtocolErrorResponse( ProtocolError::InvalidMsg, ), + ProtocolMsg::RemoteFlushResponse { + connection_id: _, + } => ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ), ProtocolMsg::RemoteReadResponse { connection_id: _, + size: _, data: _, } => ProtocolMsg::ProtocolErrorResponse( ProtocolError::InvalidMsg, @@ -263,7 +350,7 @@ pub enum ProtocolMsg { /// fd name directly? Not sure what this ID will map to. connection_id: u32, /// The remote host IP, e.g. "1.2.3.4" - remote_host: String, + remote_ip: String, }, /// Read from a remote connection RemoteReadRequest { @@ -278,6 +365,8 @@ pub enum ProtocolMsg { 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 RemoteWriteRequest { @@ -294,11 +383,24 @@ pub enum ProtocolMsg { /// Number of bytes written successfully size: usize, }, + /// Write to a remote connection + RemoteFlushRequest { + /// A connection ID from `RemoteOpenResponse` + connection_id: u32, + }, + /// Response to `RemoteFlushRequest` + /// The response only contains the connection ID. Success is implicit: if the flush response fails, a ProtocolErrorResponse will be sent. + RemoteFlushResponse { + /// Connection ID from `RemoteOpenResponse` + connection_id: u32, + }, } #[cfg(test)] mod test { - use server::RequestProcessor; + use std::{io::ErrorKind, str::from_utf8, sync::Arc}; + use rustls::RootCertStore; +use server::RequestProcessor; use super::*; @@ -312,7 +414,7 @@ mod test { } #[test] - fn open_remote_to_turnkey_com() { + fn fetch_plain_http_from_api_turnkey_com() { let mut processor = Processor::new(); let request = ProtocolMsg::RemoteOpenByNameRequest { hostname: "api.turnkey.com".to_string(), @@ -324,12 +426,96 @@ mod test { .unwrap(); let response = processor.process(request.try_into().unwrap()); let msg = ProtocolMsg::try_from_slice(&response).unwrap(); + let connection_id = match msg { + ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_ip: _ + } => { connection_id }, + _ => { panic!("test failure: ProtocolMsg is not RemoteOpenResponse")} + }; + let http_request = format!( + "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n" + ); + + let request = ProtocolMsg::RemoteWriteRequest { connection_id: connection_id, data: http_request.as_bytes().to_vec() } + .try_to_vec() + .unwrap(); + let response = processor.process(request.try_into().unwrap()); + let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); assert!(matches!( msg, - ProtocolMsg::RemoteOpenResponse { + ProtocolMsg::RemoteWriteResponse {connection_id:_, size } + )); + + let request = ProtocolMsg::RemoteReadRequest { connection_id: connection_id, size: 512 } + .try_to_vec() + .unwrap(); + let response = processor.process(request.try_into().unwrap()); + let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); + let data = match msg { + ProtocolMsg::RemoteReadResponse { connection_id: _, - remote_host: _ - } + size: _, + data + } => { data }, + _ => { panic!("test failure: ProtocolMsg is not RemoteReadResponse")} + }; + + 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 fetch_tls_content_from_api_turnkey_com() { + let host = "api.turnkey.com"; + let path = "/health"; + + let mut processor = Processor::new(); + let resp = processor.remote_open_by_name( + host.to_string(), + 443, + vec!["8.8.8.8".to_string()], + 53 + ); + + assert!(matches!( + resp, + ProtocolMsg::RemoteOpenResponse { connection_id:_, remote_ip: _ } )); + + 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 processor); + + let http_request = format!( + "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + 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)) + ); + println!("{}", std::str::from_utf8(&response_bytes).unwrap()); } } diff --git a/src/qos_net/src/remote_connection.rs b/src/qos_net/src/remote_connection.rs index e841f375..ad4c4f29 100644 --- a/src/qos_net/src/remote_connection.rs +++ b/src/qos_net/src/remote_connection.rs @@ -1,6 +1,6 @@ //! Contains logic for remote connection establishment: DNS resolution and TCP //! connection. -use std::net::{AddrParseError, IpAddr, SocketAddr, TcpStream}; +use std::{io::{Read, Write}, net::{AddrParseError, IpAddr, SocketAddr, TcpStream}}; use hickory_resolver::{ config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, @@ -18,7 +18,7 @@ pub struct RemoteConnection { /// IP address for the remote host pub ip: String, /// TCP stream object - pub tcp_stream: TcpStream, + tcp_stream: TcpStream, } impl RemoteConnection { @@ -39,7 +39,7 @@ impl RemoteConnection { let tcp_addr = SocketAddr::new(ip, port); let tcp_stream = TcpStream::connect(tcp_addr)?; - + println!("done. Now persisting TcpStream with connection ID {}", connection_id); Ok(RemoteConnection { id: connection_id, ip: ip.to_string(), @@ -66,6 +66,21 @@ impl RemoteConnection { } } +impl Read for RemoteConnection { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.tcp_stream.read(buf) + } +} + +impl Write for RemoteConnection { + 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, @@ -98,3 +113,58 @@ fn resolve_hostname( )) }) } + +#[cfg(test)] +mod test { + + use std::{io::{ErrorKind, Read, Write}, sync::Arc}; + use rustls::RootCertStore; + + use super::*; + + #[test] + fn can_fetch_tls_content_with_remote_connection_struct() { + let host = "api.turnkey.com"; + let path = "/health"; + + let mut remote_connection = RemoteConnection::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" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + 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)) + ); + println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + } +} diff --git a/src/qos_net/src/remote_stream.rs b/src/qos_net/src/remote_stream.rs new file mode 100644 index 00000000..57d8495e --- /dev/null +++ b/src/qos_net/src/remote_stream.rs @@ -0,0 +1,449 @@ +//! Contains a RemoteStream abstraction to use qos_net's RemoteRead/RemoteWrite under standard Read/Write traits +use std::io::{ErrorKind, Read, Write}; +use borsh::{BorshDeserialize, BorshSerialize}; +use qos_core::io::{SocketAddress, Stream, TimeVal}; + +use crate::{error::ProtocolError, processor::ProtocolMsg}; + + +/// Struct representing a remote connection +/// This is going to be used by enclaves, on the other side of a socket +pub struct RemoteStream { + /// socket address and timeout to create the underlying Stream. + /// Because `Stream` implements `drop` it can't be persisted here unfortunately... + /// TODO: figure out if this can work? + /// stream: Box, + addr: SocketAddress, + timeout: TimeVal, + /// Tracks the state of the remote stream + /// After initialization the connection is is `None`. + /// Once a remote connection is established (successful RemoteOpenByName or RemoteOpenByIp request), this connection ID is set the u32 in RemoteOpenResponse. + 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 RemoteStream { + /// Create a new RemoteStream by name + /// `addr` is 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 + /// `hostname` is the hostname to connect to (the remote qos_net proxy will resolve DNS) + /// `port` is the port the remote qos_net proxy should connect to + /// `dns_resolvers` and `dns_port` are the resolvers to use. + pub fn new_by_name( + addr: &SocketAddress, + timeout: TimeVal, + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> Result { + println!("creating new RemoteStream by name"); + let stream = Stream::connect(addr, timeout)?; + let req = ProtocolMsg::RemoteOpenByNameRequest{ hostname: hostname.clone(), port, dns_resolvers, dns_port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + stream.send(&req)?; + let resp_bytes = stream.recv()?; + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { + Ok(Self { + addr: addr.clone(), + timeout, + connection_id, + remote_ip, + remote_hostname: Some(hostname), + }) + }, + _ => { + Err(ProtocolError::InvalidMsg) + } + }, + Err(_) => { + Err(ProtocolError::InvalidMsg) + } + } + } + + /// Create a new RemoteStream by IP + /// `addr` is 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 + /// `ip` and `port` are the IP and port to connect to (on the outside of the enclave) + pub fn new_by_ip( + addr: &SocketAddress, + timeout: TimeVal, + ip: String, + port: u16, + ) -> Result { + let stream: Stream = Stream::connect(addr, timeout)?; + let req = ProtocolMsg::RemoteOpenByIpRequest { ip, port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + stream.send(&req)?; + let resp_bytes = stream.recv()?; + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { + Ok(Self { + addr: addr.clone(), + timeout, + connection_id, + remote_ip, + remote_hostname: None, + }) + }, + _ => { + Err(ProtocolError::InvalidMsg) + } + }, + Err(_) => { + Err(ProtocolError::InvalidMsg) + } + } + } +} + +impl Read for RemoteStream { + 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 = ProtocolMsg::RemoteReadRequest { connection_id: self.connection_id, size: buf.len() }.try_to_vec().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 ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { + if data.len() == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); + } + 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 + } + println!("READ {}: read {} bytes", buf.len(), size); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + }, + Err(_) => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); + } + } + } +} + +impl Write for RemoteStream { + 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 = ProtocolMsg::RemoteWriteRequest { connection_id: self.connection_id, data: buf.to_vec() }.try_to_vec().expect("ProtocolMsg can always be serialized."); + stream.send(&req).map_err(|e| std::io::Error::new( + ErrorKind::Other, + format!("QOS IOError sending RemoteWriteRequest: {:?}", e), + ))?; + + let resp_bytes = stream.recv().map_err(|e| std::io::Error::new( + ErrorKind::Other, + format!("QOS IOError receiving bytes from stream after RemoteWriteRequest: {:?}", e), + ))?; + + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { + if size == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); + } + println!("WRITE {}: sent buf of {} bytes", buf.len(), size); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + }, + Err(_) => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); + } + } + } + + // No-op because we can't flush a socket. We're not keeping any sort of client-side buffer here. + fn flush(&mut self) -> Result<(), std::io::Error> { + Ok(()) + } +} + + +#[cfg(test)] +mod test { + + use std::{io::ErrorKind, sync::Arc}; + + use qos_core::server::RequestProcessor; +use rustls::RootCertStore; + + use crate::processor::Processor; + +use super::*; + + #[test] + fn can_fetch_tls_content_with_local_stream() { + let host = "api.turnkey.com"; + let path = "/health"; + + let mut stream = LocalStream::new_by_name( + host.to_string(), + 443, + vec!["8.8.8.8".to_string()], + 53, + ).unwrap(); + + assert_eq!(stream.remote_hostname, Some("api.turnkey.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" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + 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)) + ); + println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + } + + #[test] + fn can_fetch_tls_content_with_remote_stream() { + let host = "api.turnkey.com"; + let path = "/health"; + + let proxy_addr = + nix::sys::socket::UnixAddr::new("/tmp/proxy.sock").unwrap(); + let addr: SocketAddress = SocketAddress::Unix(proxy_addr); + let timeout = TimeVal::new(1, 0); + + let mut stream = RemoteStream::new_by_name( + &addr, + timeout, + host.to_string(), + 443, + vec!["8.8.8.8".to_string()], + 53, + ).unwrap(); + + assert_eq!(stream.remote_hostname, Some("api.turnkey.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" + ); + println!("=== making HTTP request: \n{http_request}"); + + tls.write_all(http_request.as_bytes()).unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + println!("=== current ciphersuite: {:?}", ciphersuite.suite()); + 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)) + ); + println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + } + + + /// Struct representing a connection, with direct access to the processor. + /// Useful in tests. + struct LocalStream { + /// socket address and timeout to create the underlying Stream. + /// Because `Stream` implements `drop` it can't be persisted here unfortunately... + /// TODO: figure out if this can work? + /// stream: Box, + processor: Box, + /// Tracks the state of the remote stream + /// After initialization the connection is is `None`. + /// Once a remote connection is established (successful RemoteOpenByName or RemoteOpenByIp request), this connection ID is set the u32 in RemoteOpenResponse. + 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 LocalStream { + pub fn new_by_name( + hostname: String, + port: u16, + dns_resolvers: Vec, + dns_port: u16, + ) -> Result { + println!("creating new RemoteStream by name"); + let req = ProtocolMsg::RemoteOpenByNameRequest{ hostname: hostname.clone(), port, dns_resolvers, dns_port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let mut processor = Box::new(Processor::new()); + let resp_bytes = processor.process(req); + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { + #[allow(unsafe_code)] + unsafe { + Ok(Self { + processor, + connection_id, + remote_ip, + remote_hostname: Some(hostname), + }) + } + }, + _ => { + Err(ProtocolError::InvalidMsg) + } + }, + Err(_) => { + Err(ProtocolError::InvalidMsg) + } + } + } + } + + impl Read for LocalStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + let req = ProtocolMsg::RemoteReadRequest { connection_id: self.connection_id, size: buf.len() }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let resp_bytes = self.processor.process(req); + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { + if data.len() == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); + } + 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 + } + println!("READ {}: read {} bytes: |{}|", buf.len(), data.len(), qos_hex::encode(&data)); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + }, + Err(_) => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); + } + } + } + } + + impl Write for LocalStream { + fn write(&mut self, buf: &[u8]) -> Result { + let req = ProtocolMsg::RemoteWriteRequest { connection_id: self.connection_id, data: buf.to_vec() }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let resp_bytes = self.processor.process(req); + + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { + if size == 0 { + return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); + } + println!("WRITE {}: sent buf of {} bytes: |{}|", buf.len(), size, qos_hex::encode(buf)); + Ok(size) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + }, + Err(_) => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); + } + } + } + + // No-op because we can't flush a socket. We're not keeping any sort of client-side buffer here. + fn flush(&mut self) -> Result<(), std::io::Error> { + let req = ProtocolMsg::RemoteFlushRequest { connection_id: self.connection_id }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let resp_bytes = self.processor.process(req); + + + match ProtocolMsg::try_from_slice(&resp_bytes) { + Ok(resp) => match resp { + ProtocolMsg::RemoteFlushResponse { connection_id: _ } => { + println!("FLUSH OK"); + Ok(()) + }, + _ => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); + } + }, + Err(_) => { + return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); + } + } + } + } + +} From a8613f64cc5885a6486db23eaf02c997dbbceb06 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Fri, 7 Jun 2024 10:12:25 -0500 Subject: [PATCH 06/21] Cleanup uneeded read/write interfaces, rename remote http to remote TLS --- ...vot_remote_http.rs => pivot_remote_tls.rs} | 59 +-- src/integration/src/lib.rs | 13 +- src/integration/tests/remote_http.rs | 39 -- src/integration/tests/remote_tls.rs | 50 +++ src/qos_core/src/io/mod.rs | 4 +- src/qos_core/src/io/stream.rs | 4 +- src/qos_net/src/error.rs | 5 +- src/qos_net/src/processor.rs | 183 ++------- src/qos_net/src/remote_connection.rs | 19 +- src/qos_net/src/remote_stream.rs | 368 +++++++++++------- 10 files changed, 376 insertions(+), 368 deletions(-) rename src/integration/src/bin/{pivot_remote_http.rs => pivot_remote_tls.rs} (64%) delete mode 100644 src/integration/tests/remote_http.rs create mode 100644 src/integration/tests/remote_tls.rs diff --git a/src/integration/src/bin/pivot_remote_http.rs b/src/integration/src/bin/pivot_remote_tls.rs similarity index 64% rename from src/integration/src/bin/pivot_remote_http.rs rename to src/integration/src/bin/pivot_remote_tls.rs index 64caad02..9768acf6 100644 --- a/src/integration/src/bin/pivot_remote_http.rs +++ b/src/integration/src/bin/pivot_remote_tls.rs @@ -1,8 +1,11 @@ use core::panic; -use std::{io::{Read, Write}, sync::Arc}; +use std::{ + io::{Read, Write}, + sync::Arc, +}; use borsh::{BorshDeserialize, BorshSerialize}; -use integration::PivotRemoteHttpMsg; +use integration::PivotRemoteTlsMsg; use qos_core::{ io::{SocketAddress, TimeVal}, server::{RequestProcessor, SocketServer}, @@ -16,19 +19,17 @@ struct Processor { impl Processor { fn new(proxy_address: String) -> Self { - Processor { - net_proxy: SocketAddress::new_unix(&proxy_address) - } + Processor { net_proxy: SocketAddress::new_unix(&proxy_address) } } } impl RequestProcessor for Processor { fn process(&mut self, request: Vec) -> Vec { - let msg = PivotRemoteHttpMsg::try_from_slice(&request) + let msg = PivotRemoteTlsMsg::try_from_slice(&request) .expect("Received invalid message - test is broken!"); match msg { - PivotRemoteHttpMsg::RemoteHttpRequest{ host, path } => { + PivotRemoteTlsMsg::RemoteTlsRequest { host, path } => { let timeout = TimeVal::new(1, 0); let mut stream = RemoteStream::new_by_name( &self.net_proxy, @@ -37,19 +38,24 @@ impl RequestProcessor for Processor { 443, vec!["8.8.8.8".to_string()], 53, - ).unwrap(); + ) + .unwrap(); - let root_store = - RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into() }; + 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 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!( @@ -62,19 +68,21 @@ impl RequestProcessor for Processor { println!("=== current ciphersuite: {:?}", ciphersuite.suite()); let mut response_bytes = Vec::new(); - let read_to_end_result: usize = tls.read_to_end(&mut response_bytes).unwrap(); + let read_to_end_result: usize = + tls.read_to_end(&mut response_bytes).unwrap(); // Ignore eof errors: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof - let fetched_content = std::str::from_utf8(&response_bytes).unwrap(); - PivotRemoteHttpMsg::RemoteHttpResponse(format!( + let fetched_content = + std::str::from_utf8(&response_bytes).unwrap(); + PivotRemoteTlsMsg::RemoteTlsResponse(format!( "Content fetched successfully ({read_to_end_result} bytes): {fetched_content}" )) .try_to_vec() - .expect("RemoteHttpResponse is valid borsh") + .expect("RemoteTlsResponse is valid borsh") } - PivotRemoteHttpMsg::RemoteHttpResponse(_) => { - panic!("Unexpected RemoteHttpResponse - test is broken") + PivotRemoteTlsMsg::RemoteTlsResponse(_) => { + panic!("Unexpected RemoteTlsResponse - test is broken") } } } @@ -85,12 +93,13 @@ fn main() { // - first argument is the socket to bind to (server) // - second argument is the socket to query (net proxy) 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(); + ) + .unwrap(); } diff --git a/src/integration/src/lib.rs b/src/integration/src/lib.rs index 86c430a9..3d074171 100644 --- a/src/integration/src/lib.rs +++ b/src/integration/src/lib.rs @@ -26,7 +26,7 @@ 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_HTTP_PATH: &str = "../target/debug/pivot_remote_http"; +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. @@ -59,18 +59,19 @@ pub enum PivotSocketStressMsg { /// Request/Response messages for "socket stress" pivot app. #[derive(BorshDeserialize, BorshSerialize, Debug, PartialEq, Eq)] -pub enum PivotRemoteHttpMsg { +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) - RemoteHttpRequest { + /// 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::RemoteHttpRequest`] with the contents + /// A successful response to [`Self::RemoteTlsRequest`] with the contents /// of the response. - RemoteHttpResponse(String), + RemoteTlsResponse(String), } struct PivotParser; diff --git a/src/integration/tests/remote_http.rs b/src/integration/tests/remote_http.rs deleted file mode 100644 index 3aa35187..00000000 --- a/src/integration/tests/remote_http.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::{process::Command, str}; - -use borsh::BorshSerialize; -use integration::{PivotRemoteHttpMsg, PIVOT_REMOTE_HTTP_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_HTTP_TEST_NET_PROXY_SOCKET: &str = "/tmp/remote_http_test.net.sock"; -const REMOTE_HTTP_TEST_ENCLAVE_SOCKET: &str = "/tmp/remote_http_test.enclave.sock"; - -#[test] -fn remote_http() { - let _net_proxy: ChildWrapper = Command::new(QOS_NET_PATH).arg("--usock").arg(REMOTE_HTTP_TEST_NET_PROXY_SOCKET).spawn().unwrap().into(); - let _enclave_app: ChildWrapper = Command::new(PIVOT_REMOTE_HTTP_PATH) - .arg(REMOTE_HTTP_TEST_ENCLAVE_SOCKET) - .arg(REMOTE_HTTP_TEST_NET_PROXY_SOCKET) - .spawn() - .unwrap() - .into(); - - let enclave_client = Client::new( - SocketAddress::new_unix(REMOTE_HTTP_TEST_ENCLAVE_SOCKET), - TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), - ); - - let app_request = PivotRemoteHttpMsg::RemoteHttpRequest{ - host: "api.turnkey.com".to_string(), - path: "/health".to_string(), - }.try_to_vec() - .unwrap(); - - let response = enclave_client.send(&app_request).unwrap(); - let response_text = str::from_utf8(&response).unwrap(); - assert_eq!(response_text, "something"); -} diff --git a/src/integration/tests/remote_tls.rs b/src/integration/tests/remote_tls.rs new file mode 100644 index 00000000..ec6e6c82 --- /dev/null +++ b/src/integration/tests/remote_tls.rs @@ -0,0 +1,50 @@ +use std::{process::Command, str}; + +use borsh::BorshSerialize; +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 = PivotRemoteTlsMsg::RemoteTlsRequest { + host: "api.turnkey.com".to_string(), + path: "/health".to_string(), + } + .try_to_vec() + .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")); +} diff --git a/src/qos_core/src/io/mod.rs b/src/qos_core/src/io/mod.rs index 54eeb3ba..323a75be 100644 --- a/src/qos_core/src/io/mod.rs +++ b/src/qos_core/src/io/mod.rs @@ -6,8 +6,8 @@ mod stream; pub use stream::{ - Listener, 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 726998a7..8527000e 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -90,6 +90,7 @@ impl SocketAddress { } /// Get the `AddressFamily` of the socket. + #[must_use] pub fn family(&self) -> AddressFamily { match *self { #[cfg(feature = "vm")] @@ -99,6 +100,7 @@ impl SocketAddress { } /// Convenience method for accessing the wrapped address + #[must_use] pub fn addr(&self) -> Box { match *self { #[cfg(feature = "vm")] @@ -114,7 +116,7 @@ pub struct Stream { } impl Stream { - /// Create a new `Stream` from a SocketAddress and a timeout + /// Create a new `Stream` from a `SocketAddress` and a timeout pub fn connect( addr: &SocketAddress, timeout: TimeVal, diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index 7b50b111..1bfca3f0 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -31,8 +31,9 @@ pub enum ProtocolError { RemoteConnectionClosed, /// Happens if a RemoteRead response has empty data RemoteReadEmpty, - /// Happens if a RemoteRead 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. + /// Happens if a RemoteRead 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. RemoteReadOverflow(usize, usize), } diff --git a/src/qos_net/src/processor.rs b/src/qos_net/src/processor.rs index 95aa4bcb..d71ebd83 100644 --- a/src/qos_net/src/processor.rs +++ b/src/qos_net/src/processor.rs @@ -1,5 +1,5 @@ -//! Quorum protocol processor -use std::io::{ErrorKind, Read, Write}; +//! Protocol processor for our remote QOS net proxy +use std::io::{Read, Write}; use borsh::{BorshDeserialize, BorshSerialize}; use qos_core::server; @@ -105,7 +105,6 @@ impl Processor { connection_id: u32, size: usize, ) -> ProtocolMsg { - println!("reading {size} bytes from connection {connection_id}"); if let Some(connection) = self.get_remote_connection(connection_id) { let mut buf: Vec = vec![0; size]; match connection.read(&mut buf) { @@ -150,16 +149,10 @@ impl Processor { ) } } - pub fn remote_flush( - &mut self, - connection_id: u32, - ) -> ProtocolMsg { - println!("Flushing connection {connection_id}"); + pub fn remote_flush(&mut self, connection_id: u32) -> ProtocolMsg { if let Some(connection) = self.get_remote_connection(connection_id) { match connection.flush() { - Ok(_) => { - ProtocolMsg::RemoteFlushResponse { connection_id } - } + Ok(_) => ProtocolMsg::RemoteFlushResponse { connection_id }, Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), } } else { @@ -170,60 +163,6 @@ impl Processor { } } -impl Read for Processor { - // fn read(&mut self, buf: &mut [u8]) -> Result { - // let size = self.remote_connections.first_mut().unwrap().read(buf)?; - // println!("READ {}: read {} bytes: |{}|", buf.len(), size, qos_hex::encode(&buf)); - // Ok(size) - // } - fn read(&mut self, buf: &mut [u8]) -> Result { - let connection_id = self.remote_connections.first().unwrap().id; - - match self.remote_read(connection_id, buf.len()) { - ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { - if data.len() == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); - } - 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 - } - println!("READ {}: read {} bytes: |{}|", buf.len(), data.len(), qos_hex::encode(&data)); - Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); - } - } - } -} - -impl Write for Processor { - fn write(&mut self, buf: &[u8]) -> Result { - let connection_id = self.remote_connections.first().unwrap().id; - let resp = self.remote_write(connection_id, buf.to_vec()); - match resp { - ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { - if size == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); - } - println!("WRITE {}: sent buf of {} bytes: |{}|", buf.len(), size, qos_hex::encode(buf)); - Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); - } - } - } - fn flush(&mut self) -> std::io::Result<()> { - self.remote_connections.first_mut().unwrap().flush() - } -} - impl server::RequestProcessor for Processor { fn process(&mut self, req_bytes: Vec) -> Vec { if req_bytes.len() > MAX_ENCODED_MSG_LEN { @@ -287,11 +226,11 @@ impl server::RequestProcessor for Processor { } => ProtocolMsg::ProtocolErrorResponse( ProtocolError::InvalidMsg, ), - ProtocolMsg::RemoteFlushResponse { - connection_id: _, - } => ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ), + ProtocolMsg::RemoteFlushResponse { connection_id: _ } => { + ProtocolMsg::ProtocolErrorResponse( + ProtocolError::InvalidMsg, + ) + } ProtocolMsg::RemoteReadResponse { connection_id: _, size: _, @@ -365,7 +304,8 @@ pub enum ProtocolMsg { 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 + /// buffer after mutation from `read`. The first `size` bytes contain + /// the result of the `read` call size: usize, }, /// Write to a remote connection @@ -389,7 +329,8 @@ pub enum ProtocolMsg { connection_id: u32, }, /// Response to `RemoteFlushRequest` - /// The response only contains the connection ID. Success is implicit: if the flush response fails, a ProtocolErrorResponse will be sent. + /// The response only contains the connection ID. Success is implicit: if + /// the flush response fails, a ProtocolErrorResponse will be sent. RemoteFlushResponse { /// Connection ID from `RemoteOpenResponse` connection_id: u32, @@ -398,9 +339,9 @@ pub enum ProtocolMsg { #[cfg(test)] mod test { - use std::{io::ErrorKind, str::from_utf8, sync::Arc}; - use rustls::RootCertStore; -use server::RequestProcessor; + use std::str::from_utf8; + + use server::RequestProcessor; use super::*; @@ -414,7 +355,7 @@ use server::RequestProcessor; } #[test] - fn fetch_plain_http_from_api_turnkey_com() { + fn fetch_plaintext_http_from_api_turnkey_com() { let mut processor = Processor::new(); let request = ProtocolMsg::RemoteOpenByNameRequest { hostname: "api.turnkey.com".to_string(), @@ -427,95 +368,47 @@ use server::RequestProcessor; let response = processor.process(request.try_into().unwrap()); let msg = ProtocolMsg::try_from_slice(&response).unwrap(); let connection_id = match msg { - ProtocolMsg::RemoteOpenResponse { - connection_id, - remote_ip: _ - } => { connection_id }, - _ => { panic!("test failure: ProtocolMsg is not RemoteOpenResponse")} + ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip: _ } => { + connection_id + } + _ => { + panic!("test failure: ProtocolMsg is not RemoteOpenResponse") + } }; - let http_request = format!( - "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n" - ); + let http_request = "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n".to_string(); - let request = ProtocolMsg::RemoteWriteRequest { connection_id: connection_id, data: http_request.as_bytes().to_vec() } + let request = ProtocolMsg::RemoteWriteRequest { + connection_id, + data: http_request.as_bytes().to_vec(), + } .try_to_vec() .unwrap(); let response = processor.process(request.try_into().unwrap()); let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); assert!(matches!( msg, - ProtocolMsg::RemoteWriteResponse {connection_id:_, size } + ProtocolMsg::RemoteWriteResponse { connection_id: _, size: _ } )); - let request = ProtocolMsg::RemoteReadRequest { connection_id: connection_id, size: 512 } - .try_to_vec() - .unwrap(); + let request = + ProtocolMsg::RemoteReadRequest { connection_id, size: 512 } + .try_to_vec() + .unwrap(); let response = processor.process(request.try_into().unwrap()); let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); let data = match msg { ProtocolMsg::RemoteReadResponse { connection_id: _, size: _, - data - } => { data }, - _ => { panic!("test failure: ProtocolMsg is not RemoteReadResponse")} + data, + } => data, + _ => { + panic!("test failure: ProtocolMsg is not RemoteReadResponse") + } }; 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 fetch_tls_content_from_api_turnkey_com() { - let host = "api.turnkey.com"; - let path = "/health"; - - let mut processor = Processor::new(); - let resp = processor.remote_open_by_name( - host.to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53 - ); - - assert!(matches!( - resp, - ProtocolMsg::RemoteOpenResponse { connection_id:_, remote_ip: _ } - )); - - 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 processor); - - let http_request = format!( - "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" - ); - println!("=== making HTTP request: \n{http_request}"); - - tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); - 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)) - ); - println!("{}", std::str::from_utf8(&response_bytes).unwrap()); - } } diff --git a/src/qos_net/src/remote_connection.rs b/src/qos_net/src/remote_connection.rs index ad4c4f29..35bb4dc8 100644 --- a/src/qos_net/src/remote_connection.rs +++ b/src/qos_net/src/remote_connection.rs @@ -1,6 +1,9 @@ //! Contains logic for remote connection establishment: DNS resolution and TCP //! connection. -use std::{io::{Read, Write}, net::{AddrParseError, IpAddr, SocketAddr, TcpStream}}; +use std::{ + io::{Read, Write}, + net::{AddrParseError, IpAddr, SocketAddr, TcpStream}, +}; use hickory_resolver::{ config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, @@ -39,7 +42,10 @@ impl RemoteConnection { let tcp_addr = SocketAddr::new(ip, port); let tcp_stream = TcpStream::connect(tcp_addr)?; - println!("done. Now persisting TcpStream with connection ID {}", connection_id); + println!( + "done. Now persisting TcpStream with connection ID {}", + connection_id + ); Ok(RemoteConnection { id: connection_id, ip: ip.to_string(), @@ -117,7 +123,11 @@ fn resolve_hostname( #[cfg(test)] mod test { - use std::{io::{ErrorKind, Read, Write}, sync::Arc}; + use std::{ + io::{ErrorKind, Read, Write}, + sync::Arc, + }; + use rustls::RootCertStore; use super::*; @@ -132,7 +142,8 @@ mod test { 443, vec!["8.8.8.8".to_string()], 53, - ).unwrap(); + ) + .unwrap(); let root_store = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into() }; diff --git a/src/qos_net/src/remote_stream.rs b/src/qos_net/src/remote_stream.rs index 57d8495e..846d0809 100644 --- a/src/qos_net/src/remote_stream.rs +++ b/src/qos_net/src/remote_stream.rs @@ -1,23 +1,26 @@ -//! Contains a RemoteStream abstraction to use qos_net's RemoteRead/RemoteWrite under standard Read/Write traits +//! Contains a RemoteStream abstraction to use qos_net's RemoteRead/RemoteWrite +//! under standard Read/Write traits use std::io::{ErrorKind, Read, Write}; + use borsh::{BorshDeserialize, BorshSerialize}; use qos_core::io::{SocketAddress, Stream, TimeVal}; use crate::{error::ProtocolError, processor::ProtocolMsg}; - /// Struct representing a remote connection /// This is going to be used by enclaves, on the other side of a socket pub struct RemoteStream { /// socket address and timeout to create the underlying Stream. - /// Because `Stream` implements `drop` it can't be persisted here unfortunately... - /// TODO: figure out if this can work? + /// Because `Stream` implements `drop` it can't be persisted here + /// unfortunately... TODO: figure out if this can work? /// stream: Box, addr: SocketAddress, timeout: TimeVal, /// Tracks the state of the remote stream /// After initialization the connection is is `None`. - /// Once a remote connection is established (successful RemoteOpenByName or RemoteOpenByIp request), this connection ID is set the u32 in RemoteOpenResponse. + /// Once a remote connection is established (successful RemoteOpenByName or + /// RemoteOpenByIp request), this connection ID is set the u32 in + /// RemoteOpenResponse. pub connection_id: u32, /// The remote host this connection points to pub remote_hostname: Option, @@ -27,11 +30,11 @@ pub struct RemoteStream { impl RemoteStream { /// Create a new RemoteStream by name - /// `addr` is 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 - /// `hostname` is the hostname to connect to (the remote qos_net proxy will resolve DNS) - /// `port` is the port the remote qos_net proxy should connect to - /// `dns_resolvers` and `dns_port` are the resolvers to use. + /// `addr` is 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 + /// `hostname` is the hostname to connect to (the remote qos_net proxy will + /// resolve DNS) `port` is the port the remote qos_net proxy should connect + /// to `dns_resolvers` and `dns_port` are the resolvers to use. pub fn new_by_name( addr: &SocketAddress, timeout: TimeVal, @@ -42,35 +45,40 @@ impl RemoteStream { ) -> Result { println!("creating new RemoteStream by name"); let stream = Stream::connect(addr, timeout)?; - let req = ProtocolMsg::RemoteOpenByNameRequest{ hostname: hostname.clone(), port, dns_resolvers, dns_port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteOpenByNameRequest { + hostname: hostname.clone(), + port, + dns_resolvers, + dns_port, + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); stream.send(&req)?; let resp_bytes = stream.recv()?; match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { - Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: Some(hostname), - }) - }, - _ => { - Err(ProtocolError::InvalidMsg) - } + ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_ip, + } => Ok(Self { + addr: addr.clone(), + timeout, + connection_id, + remote_ip, + remote_hostname: Some(hostname), + }), + _ => Err(ProtocolError::InvalidMsg), }, - Err(_) => { - Err(ProtocolError::InvalidMsg) - } + Err(_) => Err(ProtocolError::InvalidMsg), } } /// Create a new RemoteStream by IP - /// `addr` is 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 - /// `ip` and `port` are the IP and port to connect to (on the outside of the enclave) + /// `addr` is 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 + /// `ip` and `port` are the IP and port to connect to (on the outside of the + /// enclave) pub fn new_by_ip( addr: &SocketAddress, timeout: TimeVal, @@ -78,55 +86,72 @@ impl RemoteStream { port: u16, ) -> Result { let stream: Stream = Stream::connect(addr, timeout)?; - let req = ProtocolMsg::RemoteOpenByIpRequest { ip, port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteOpenByIpRequest { ip, port } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); stream.send(&req)?; let resp_bytes = stream.recv()?; match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { - Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: None, - }) - }, - _ => { - Err(ProtocolError::InvalidMsg) - } + ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_ip, + } => Ok(Self { + addr: addr.clone(), + timeout, + connection_id, + remote_ip, + remote_hostname: None, + }), + _ => Err(ProtocolError::InvalidMsg), }, - Err(_) => { - Err(ProtocolError::InvalidMsg) - } + Err(_) => Err(ProtocolError::InvalidMsg), } } } impl Read for RemoteStream { fn read(&mut self, buf: &mut [u8]) -> Result { - let stream: Stream = Stream::connect(&self.addr, self.timeout).map_err(|e| std::io::Error::new( + 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 = ProtocolMsg::RemoteReadRequest { connection_id: self.connection_id, size: buf.len() }.try_to_vec().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), - ))?; + ) + })?; + let req = ProtocolMsg::RemoteReadRequest { + connection_id: self.connection_id, + size: buf.len(), + } + .try_to_vec() + .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 ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { - if data.len() == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); + ProtocolMsg::RemoteReadResponse { + connection_id: _, + size, + data, + } => { + if data.is_empty() { + return Err(std::io::Error::new( + ErrorKind::Interrupted, + "empty RemoteRead", + )); } 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()))); @@ -138,75 +163,90 @@ impl Read for RemoteStream { } println!("READ {}: read {} bytes", buf.len(), size); Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), }, - Err(_) => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); - } + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), } } } impl Write for RemoteStream { fn write(&mut self, buf: &[u8]) -> Result { - let stream: Stream = Stream::connect(&self.addr, self.timeout).map_err(|e| std::io::Error::new( + 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 = ProtocolMsg::RemoteWriteRequest { connection_id: self.connection_id, data: buf.to_vec() }.try_to_vec().expect("ProtocolMsg can always be serialized."); - stream.send(&req).map_err(|e| std::io::Error::new( - ErrorKind::Other, - format!("QOS IOError sending RemoteWriteRequest: {:?}", e), - ))?; + ) + })?; + + let req = ProtocolMsg::RemoteWriteRequest { + connection_id: self.connection_id, + data: buf.to_vec(), + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); + stream.send(&req).map_err(|e| { + std::io::Error::new( + ErrorKind::Other, + format!("QOS IOError sending RemoteWriteRequest: {:?}", e), + ) + })?; let resp_bytes = stream.recv().map_err(|e| std::io::Error::new( ErrorKind::Other, format!("QOS IOError receiving bytes from stream after RemoteWriteRequest: {:?}", e), ))?; - match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { if size == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); + return Err(std::io::Error::new( + ErrorKind::Interrupted, + "failed RemoteWrite", + )); } println!("WRITE {}: sent buf of {} bytes", buf.len(), size); Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), }, - Err(_) => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); - } + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), } } - // No-op because we can't flush a socket. We're not keeping any sort of client-side buffer here. + // No-op because we can't flush a socket. We're not keeping any sort of + // client-side buffer here. fn flush(&mut self) -> Result<(), std::io::Error> { Ok(()) } } - #[cfg(test)] mod test { use std::{io::ErrorKind, sync::Arc}; use qos_core::server::RequestProcessor; -use rustls::RootCertStore; + use rustls::RootCertStore; + use super::*; use crate::processor::Processor; -use super::*; - #[test] fn can_fetch_tls_content_with_local_stream() { let host = "api.turnkey.com"; @@ -217,7 +257,8 @@ use super::*; 443, vec!["8.8.8.8".to_string()], 53, - ).unwrap(); + ) + .unwrap(); assert_eq!(stream.remote_hostname, Some("api.turnkey.com".to_string())); @@ -264,7 +305,7 @@ use super::*; nix::sys::socket::UnixAddr::new("/tmp/proxy.sock").unwrap(); let addr: SocketAddress = SocketAddress::Unix(proxy_addr); let timeout = TimeVal::new(1, 0); - + let mut stream = RemoteStream::new_by_name( &addr, timeout, @@ -272,7 +313,8 @@ use super::*; 443, vec!["8.8.8.8".to_string()], 53, - ).unwrap(); + ) + .unwrap(); assert_eq!(stream.remote_hostname, Some("api.turnkey.com".to_string())); @@ -310,18 +352,19 @@ use super::*; println!("{}", std::str::from_utf8(&response_bytes).unwrap()); } - /// Struct representing a connection, with direct access to the processor. /// Useful in tests. struct LocalStream { /// socket address and timeout to create the underlying Stream. - /// Because `Stream` implements `drop` it can't be persisted here unfortunately... - /// TODO: figure out if this can work? + /// Because `Stream` implements `drop` it can't be persisted here + /// unfortunately... TODO: figure out if this can work? /// stream: Box, processor: Box, /// Tracks the state of the remote stream /// After initialization the connection is is `None`. - /// Once a remote connection is established (successful RemoteOpenByName or RemoteOpenByIp request), this connection ID is set the u32 in RemoteOpenResponse. + /// Once a remote connection is established (successful + /// RemoteOpenByName or RemoteOpenByIp request), this connection ID is + /// set the u32 in RemoteOpenResponse. pub connection_id: u32, /// The remote host this connection points to pub remote_hostname: Option, @@ -337,44 +380,57 @@ use super::*; dns_port: u16, ) -> Result { println!("creating new RemoteStream by name"); - let req = ProtocolMsg::RemoteOpenByNameRequest{ hostname: hostname.clone(), port, dns_resolvers, dns_port }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteOpenByNameRequest { + hostname: hostname.clone(), + port, + dns_resolvers, + dns_port, + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); let mut processor = Box::new(Processor::new()); let resp_bytes = processor.process(req); match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip } => { - #[allow(unsafe_code)] - unsafe { - Ok(Self { - processor, - connection_id, - remote_ip, - remote_hostname: Some(hostname), - }) - } - }, - _ => { - Err(ProtocolError::InvalidMsg) - } + ProtocolMsg::RemoteOpenResponse { + connection_id, + remote_ip, + } => Ok(Self { + processor, + connection_id, + remote_ip, + remote_hostname: Some(hostname), + }), + _ => Err(ProtocolError::InvalidMsg), }, - Err(_) => { - Err(ProtocolError::InvalidMsg) - } + Err(_) => Err(ProtocolError::InvalidMsg), } } } impl Read for LocalStream { fn read(&mut self, buf: &mut [u8]) -> Result { - let req = ProtocolMsg::RemoteReadRequest { connection_id: self.connection_id, size: buf.len() }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteReadRequest { + connection_id: self.connection_id, + size: buf.len(), + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); let resp_bytes = self.processor.process(req); match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteReadResponse { connection_id: _, size, data } => { - if data.len() == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "empty RemoteRead")); + ProtocolMsg::RemoteReadResponse { + connection_id: _, + size, + data, + } => { + if data.is_empty() { + return Err(std::io::Error::new( + ErrorKind::Interrupted, + "empty RemoteRead", + )); } 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()))); @@ -384,66 +440,90 @@ use super::*; for (i, b) in data.iter().enumerate() { buf[i] = *b } - println!("READ {}: read {} bytes: |{}|", buf.len(), data.len(), qos_hex::encode(&data)); + println!("READ {}: read {} bytes", buf.len(), size); Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), }, - Err(_) => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); - } + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), } } } impl Write for LocalStream { fn write(&mut self, buf: &[u8]) -> Result { - let req = ProtocolMsg::RemoteWriteRequest { connection_id: self.connection_id, data: buf.to_vec() }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteWriteRequest { + connection_id: self.connection_id, + data: buf.to_vec(), + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); let resp_bytes = self.processor.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { + ProtocolMsg::RemoteWriteResponse { + connection_id: _, + size, + } => { if size == 0 { - return Err(std::io::Error::new(ErrorKind::Interrupted, "failed RemoteWrite")); + return Err(std::io::Error::new( + ErrorKind::Interrupted, + "failed RemoteWrite", + )); } - println!("WRITE {}: sent buf of {} bytes: |{}|", buf.len(), size, qos_hex::encode(buf)); + println!( + "WRITE {}: sent buf of {} bytes: |{}|", + buf.len(), + size, + qos_hex::encode(buf) + ); Ok(size) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), }, - Err(_) => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); - } + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), } } - // No-op because we can't flush a socket. We're not keeping any sort of client-side buffer here. + // No-op because we can't flush a socket. We're not keeping any sort of + // client-side buffer here. fn flush(&mut self) -> Result<(), std::io::Error> { - let req = ProtocolMsg::RemoteFlushRequest { connection_id: self.connection_id }.try_to_vec().expect("ProtocolMsg can always be serialized."); + let req = ProtocolMsg::RemoteFlushRequest { + connection_id: self.connection_id, + } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); let resp_bytes = self.processor.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { ProtocolMsg::RemoteFlushResponse { connection_id: _ } => { println!("FLUSH OK"); Ok(()) - }, - _ => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "unexpected response")); } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), }, - Err(_) => { - return Err(std::io::Error::new(ErrorKind::InvalidData, "cannot deserialize message")); - } + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), } } } - } From 456df93c3fe0fc74129cbb547997caa6d937580e Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Fri, 7 Jun 2024 11:54:29 -0500 Subject: [PATCH 07/21] Bunch of renaming and comment updates --- src/Cargo.lock | 2 - src/integration/src/bin/pivot_remote_tls.rs | 12 +- src/qos_core/Cargo.toml | 2 - src/qos_core/src/io/stream.rs | 12 +- src/qos_core/src/protocol/error.rs | 11 - src/qos_core/src/protocol/msg.rs | 49 --- src/qos_net/README.MD | 15 +- src/qos_net/src/cli.rs | 4 +- src/qos_net/src/error.rs | 30 +- src/qos_net/src/lib.rs | 11 +- src/qos_net/src/processor.rs | 414 ------------------ src/qos_net/src/proxy.rs | 372 ++++++++++++++++ ...mote_connection.rs => proxy_connection.rs} | 51 +-- .../src/{remote_stream.rs => proxy_stream.rs} | 322 ++++++-------- 14 files changed, 576 insertions(+), 731 deletions(-) delete mode 100644 src/qos_net/src/processor.rs create mode 100644 src/qos_net/src/proxy.rs rename src/qos_net/src/{remote_connection.rs => proxy_connection.rs} (76%) rename src/qos_net/src/{remote_stream.rs => proxy_stream.rs} (56%) diff --git a/src/Cargo.lock b/src/Cargo.lock index 6e2364f3..2803144e 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1749,7 +1749,6 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", "borsh", - "hickory-resolver", "libc", "nix", "qos_crypto", @@ -1757,7 +1756,6 @@ dependencies = [ "qos_nsm", "qos_p256", "qos_test_primitives", - "rand", "rustls", "serde", "serde_bytes", diff --git a/src/integration/src/bin/pivot_remote_tls.rs b/src/integration/src/bin/pivot_remote_tls.rs index 9768acf6..e3c5ece2 100644 --- a/src/integration/src/bin/pivot_remote_tls.rs +++ b/src/integration/src/bin/pivot_remote_tls.rs @@ -10,7 +10,7 @@ use qos_core::{ io::{SocketAddress, TimeVal}, server::{RequestProcessor, SocketServer}, }; -use qos_net::remote_stream::RemoteStream; +use qos_net::proxy_stream::ProxyStream; use rustls::RootCertStore; struct Processor { @@ -31,7 +31,7 @@ impl RequestProcessor for Processor { match msg { PivotRemoteTlsMsg::RemoteTlsRequest { host, path } => { let timeout = TimeVal::new(1, 0); - let mut stream = RemoteStream::new_by_name( + let mut stream = ProxyStream::connect_by_name( &self.net_proxy, timeout, host.clone(), @@ -61,12 +61,10 @@ impl RequestProcessor for Processor { let http_request = format!( "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" ); - println!("=== making HTTP request: \n{http_request}"); tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + let _ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); let mut response_bytes = Vec::new(); let read_to_end_result: usize = tls.read_to_end(&mut response_bytes).unwrap(); @@ -90,8 +88,8 @@ impl RequestProcessor for Processor { fn main() { // Parse args: - // - first argument is the socket to bind to (server) - // - second argument is the socket to query (net proxy) + // - 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]; diff --git a/src/qos_core/Cargo.toml b/src/qos_core/Cargo.toml index 16f17d07..5ecb72ef 100644 --- a/src/qos_core/Cargo.toml +++ b/src/qos_core/Cargo.toml @@ -20,8 +20,6 @@ aws-nitro-enclaves-nsm-api = { version = "0.3", default-features = false } serde_bytes = { version = "0.11", 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" } qos_p256 = { path = "../qos_p256", features = ["mock"] } diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 8527000e..31a4e008 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -201,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, @@ -234,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, @@ -257,7 +257,7 @@ impl Stream { impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> Result { match recv(self.fd, buf, MsgFlags::empty()) { - Ok(size) if size == 0 => Err(std::io::Error::new( + Ok(0) => Err(std::io::Error::new( ErrorKind::ConnectionAborted, "read 0 bytes", )), @@ -270,7 +270,7 @@ impl Read for Stream { impl Write for Stream { fn write(&mut self, buf: &[u8]) -> Result { match send(self.fd, buf, MsgFlags::empty()) { - Ok(size) if size == 0 => Err(std::io::Error::new( + Ok(0) => Err(std::io::Error::new( ErrorKind::ConnectionAborted, "wrote 0 bytes", )), @@ -398,8 +398,10 @@ mod test { assert_eq!(data, resp); } + // TODO: replace this test with something simpler. Local socket which does a + // simple echo? #[test] - fn stream_implement_reader_writer_interfaces() { + fn stream_implement_read_write_traits() { let host = "api.turnkey.com"; let path = "/health"; diff --git a/src/qos_core/src/protocol/error.rs b/src/qos_core/src/protocol/error.rs index 4b959c4a..74e5f4bb 100644 --- a/src/qos_core/src/protocol/error.rs +++ b/src/qos_core/src/protocol/error.rs @@ -1,6 +1,4 @@ //! Quorum protocol error -use std::net::AddrParseError; - use borsh::{BorshDeserialize, BorshSerialize}; use qos_p256::P256Error; @@ -143,8 +141,6 @@ pub enum ProtocolError { /// The new manifest was different from the old manifest when we expected /// them to be the same because they have the same nonce DifferentManifest, - /// Parsing error with a protocol message component - ParseError(String), } impl From for ProtocolError { @@ -188,10 +184,3 @@ impl From for ProtocolError { Self::QosAttestError(msg) } } - -impl From for ProtocolError { - fn from(err: AddrParseError) -> Self { - let msg = format!("{err:?}"); - Self::ParseError(msg) - } -} diff --git a/src/qos_core/src/protocol/msg.rs b/src/qos_core/src/protocol/msg.rs index d6a13a6f..9b1a3e98 100644 --- a/src/qos_core/src/protocol/msg.rs +++ b/src/qos_core/src/protocol/msg.rs @@ -138,55 +138,6 @@ pub enum ProtocolMsg { /// if the manifest envelope does not exist. manifest_envelope: Box>, }, - - /// Request from the enclave app to open a TCP connection to a remote host - /// This results in a new remote connection saved in protocol state - RemoteOpenRequest { - /// 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, - }, - /// Response for `OpenTcpConnectionRequest` - RemoteOpenResponse { - /// Connection ID to reference the opened connection when used with - /// `RemoteRequest` and `RemoteResponse`. TODO: maybe we reply with a - /// fd name directly? Not sure what this ID will map to. - connection_id: u32, - }, - /// Read from a remote connection - RemoteReadRequest { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - /// number of bytes to read - size: usize, - }, - /// Response to `RemoteReadRequest` containing read data - RemoteReadResponse { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - /// number of bytes read - data: Vec, - }, - /// Write to a remote connection - RemoteWriteRequest { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - /// Data to be sent - data: Vec, - }, - /// Response to `RemoteWriteRequest` containing the number of successfully - /// written bytes. - RemoteWriteResponse { - /// Connection ID from `RemoteOpenResponse` - connection_id: u32, - /// Number of bytes written successfully - size: usize, - }, } #[cfg(test)] diff --git a/src/qos_net/README.MD b/src/qos_net/README.MD index c5f624c6..229e7554 100644 --- a/src/qos_net/README.MD +++ b/src/qos_net/README.MD @@ -1,8 +1,13 @@ # QOS Net -This crate contains a proxy server which implements the protocol messages to implement remote connections: -* `ProtocolMsg::RemoteOpenConnection` -* `ProtocolMsg::RemoteRead` -* `ProtocolMsg::RemoteWrite` +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. -It also contains a protocol and libraries to interact with the protocol +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 index 643dba25..f5345880 100644 --- a/src/qos_net/src/cli.rs +++ b/src/qos_net/src/cli.rs @@ -8,7 +8,7 @@ use qos_core::{ server::SocketServer, }; -use crate::processor::Processor; +use crate::proxy::Proxy; /// "cid" pub const CID: &str = "cid"; @@ -68,7 +68,7 @@ impl CLI { } else if opts.parsed.help() { println!("{}", opts.parsed.info()); } else { - SocketServer::listen(opts.addr(), Processor::new()).unwrap(); + SocketServer::listen(opts.addr(), Proxy::new()).unwrap(); } } } diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index 1bfca3f0..cae08e42 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -1,12 +1,12 @@ -//! Remote protocol error +//! qos_net errors related to creating and using proxy connections. use std::net::AddrParseError; use borsh::{BorshDeserialize, BorshSerialize}; use hickory_resolver::error::ResolveError; -/// Errors during protocol execution. +/// Errors related to creating and using proxy connections #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] -pub enum ProtocolError { +pub enum QosNetError { /// Error variant encapsulating OS IO errors IOError, /// Error variant encapsulating OS IO errors @@ -25,38 +25,38 @@ pub enum ProtocolError { /// 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 - RemoteConnectionIdNotFound(u32), + ConnectionIdNotFound(u32), /// Attempting to read on a closed remote connection (`.read` returned 0 /// bytes) - RemoteConnectionClosed, - /// Happens if a RemoteRead response has empty data - RemoteReadEmpty, - /// Happens if a RemoteRead 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. - RemoteReadOverflow(usize, usize), + ConnectionClosed, + /// Happens when a socket `read` results in no data + EmptyRead, + /// 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), } -impl From for ProtocolError { +impl From for QosNetError { fn from(_err: std::io::Error) -> Self { Self::IOError } } -impl From for ProtocolError { +impl From for QosNetError { fn from(_err: qos_core::io::IOError) -> Self { Self::QOSIOError } } -impl From for ProtocolError { +impl From for QosNetError { fn from(err: AddrParseError) -> Self { let msg = format!("{err:?}"); Self::ParseError(msg) } } -impl From for ProtocolError { +impl From for QosNetError { fn from(err: ResolveError) -> Self { let msg = format!("{err:?}"); Self::ParseError(msg) diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs index 3eda6942..bab3b9d5 100644 --- a/src/qos_net/src/lib.rs +++ b/src/qos_net/src/lib.rs @@ -1,9 +1,10 @@ -//! This crate contains a simple proxy server to implement QOS protocol messages -//! related to establishing and using remote connections. +//! 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)] pub mod cli; pub mod error; -pub mod processor; -pub mod remote_connection; -pub mod remote_stream; +pub mod proxy; +pub mod proxy_connection; +pub mod proxy_stream; diff --git a/src/qos_net/src/processor.rs b/src/qos_net/src/processor.rs deleted file mode 100644 index d71ebd83..00000000 --- a/src/qos_net/src/processor.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! Protocol processor for our remote QOS net proxy -use std::io::{Read, Write}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use qos_core::server; - -use crate::{ - error::ProtocolError, - remote_connection::{self, RemoteConnection}, -}; - -const MEGABYTE: usize = 1024 * 1024; -const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; - -/// Enclave state machine that executes when given a `ProtocolMsg`. -pub struct Processor { - remote_connections: Vec, -} - -impl Default for Processor { - fn default() -> Self { - Self::new() - } -} - -impl Processor { - /// Create a new `Self`. - #[must_use] - pub fn new() -> Self { - Self { remote_connections: vec![] } - } - - fn save_remote_connection( - &mut self, - connection: RemoteConnection, - ) -> Result<(), ProtocolError> { - if self.remote_connections.iter().any(|c| c.id == connection.id) { - Err(ProtocolError::DuplicateConnectionId(connection.id)) - } else { - self.remote_connections.push(connection); - Ok(()) - } - } - - fn get_remote_connection( - &mut self, - id: u32, - ) -> Option<&mut RemoteConnection> { - self.remote_connections.iter_mut().find(|c| c.id == id) - } - - /// Open and save a new remote connection by resolving a name into an IP - /// address, then opening a new TCP connection - pub fn remote_open_by_name( - &mut self, - hostname: String, - port: u16, - dns_resolvers: Vec, - dns_port: u16, - ) -> ProtocolMsg { - println!("opening a new remote connection by hostname for {hostname}"); - - match remote_connection::RemoteConnection::new_from_name( - hostname.clone(), - port, - dns_resolvers.clone(), - dns_port, - ) { - Ok(remote_connection) => { - let connection_id = remote_connection.id; - let remote_ip = remote_connection.ip.clone(); - match self.save_remote_connection(remote_connection) { - Ok(()) => ProtocolMsg::RemoteOpenResponse { - connection_id, - remote_ip, - }, - Err(e) => ProtocolMsg::ProtocolErrorResponse(e), - } - } - Err(e) => ProtocolMsg::ProtocolErrorResponse(e), - } - } - - /// Open a new remote connection by connecting to an IP address directly - pub fn remote_open_by_ip(&mut self, ip: String, port: u16) -> ProtocolMsg { - match remote_connection::RemoteConnection::new_from_ip(ip, port) { - Ok(remote_connection) => { - let connection_id = remote_connection.id; - let remote_ip = remote_connection.ip.clone(); - match self.save_remote_connection(remote_connection) { - Ok(()) => ProtocolMsg::RemoteOpenResponse { - connection_id, - remote_ip, - }, - Err(e) => ProtocolMsg::ProtocolErrorResponse(e), - } - } - Err(e) => ProtocolMsg::ProtocolErrorResponse(e), - } - } - - /// Performs a Read on a remote connection - pub fn remote_read( - &mut self, - connection_id: u32, - size: usize, - ) -> ProtocolMsg { - if let Some(connection) = self.get_remote_connection(connection_id) { - let mut buf: Vec = vec![0; size]; - match connection.read(&mut buf) { - Ok(size) => { - if size == 0 { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionClosed, - ) - } else { - ProtocolMsg::RemoteReadResponse { - connection_id, - data: buf, - size, - } - } - } - Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), - } - } else { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionIdNotFound(connection_id), - ) - } - } - - /// Performs a Write on a remote connection - pub fn remote_write( - &mut self, - connection_id: u32, - data: Vec, - ) -> ProtocolMsg { - if let Some(connection) = self.get_remote_connection(connection_id) { - match connection.write(&data) { - Ok(size) => { - ProtocolMsg::RemoteWriteResponse { connection_id, size } - } - Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), - } - } else { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionIdNotFound(connection_id), - ) - } - } - pub fn remote_flush(&mut self, connection_id: u32) -> ProtocolMsg { - if let Some(connection) = self.get_remote_connection(connection_id) { - match connection.flush() { - Ok(_) => ProtocolMsg::RemoteFlushResponse { connection_id }, - Err(e) => ProtocolMsg::ProtocolErrorResponse(e.into()), - } - } else { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::RemoteConnectionIdNotFound(connection_id), - ) - } - } -} - -impl server::RequestProcessor for Processor { - fn process(&mut self, req_bytes: Vec) -> Vec { - if req_bytes.len() > MAX_ENCODED_MSG_LEN { - return ProtocolMsg::ProtocolErrorResponse( - ProtocolError::OversizedPayload, - ) - .try_to_vec() - .expect("ProtocolMsg can always be serialized. qed."); - } - - let resp = match ProtocolMsg::try_from_slice(&req_bytes) { - Ok(req) => match req { - ProtocolMsg::StatusRequest => { - ProtocolMsg::StatusResponse(self.remote_connections.len()) - } - ProtocolMsg::RemoteOpenByNameRequest { - hostname, - port, - dns_resolvers, - dns_port, - } => self.remote_open_by_name( - hostname, - port, - dns_resolvers, - dns_port, - ), - ProtocolMsg::RemoteOpenByIpRequest { ip, port } => { - self.remote_open_by_ip(ip, port) - } - ProtocolMsg::RemoteReadRequest { connection_id, size } => { - println!("processing RemoteReadRequest"); - self.remote_read(connection_id, size) - } - ProtocolMsg::RemoteWriteRequest { connection_id, data } => { - println!("processing RemoteWriteRequest"); - self.remote_write(connection_id, data) - } - ProtocolMsg::RemoteFlushRequest { connection_id } => { - println!("processing RemoteWriteRequest"); - self.remote_flush(connection_id) - } - ProtocolMsg::ProtocolErrorResponse(_) => { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ) - } - ProtocolMsg::StatusResponse(_) => { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ) - } - ProtocolMsg::RemoteOpenResponse { - connection_id: _, - remote_ip: _, - } => ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ), - ProtocolMsg::RemoteWriteResponse { - connection_id: _, - size: _, - } => ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ), - ProtocolMsg::RemoteFlushResponse { connection_id: _ } => { - ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ) - } - ProtocolMsg::RemoteReadResponse { - connection_id: _, - size: _, - data: _, - } => ProtocolMsg::ProtocolErrorResponse( - ProtocolError::InvalidMsg, - ), - }, - Err(_) => { - ProtocolMsg::ProtocolErrorResponse(ProtocolError::InvalidMsg) - } - }; - - resp.try_to_vec() - .expect("Protocol message can always be serialized. qed!") - } -} - -/// Message types to use with the remote proxy. -#[derive(Debug, PartialEq, borsh::BorshSerialize, borsh::BorshDeserialize)] -pub enum ProtocolMsg { - /// A error from executing the protocol. - ProtocolErrorResponse(ProtocolError), - - /// 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 - RemoteOpenByNameRequest { - /// 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 - RemoteOpenByIpRequest { - /// The IP to connect to, e.g. "1.2.3.4" - ip: String, - /// e.g. 443 - port: u16, - }, - /// Response for `RemoteOpenByNameRequest` and `RemoteOpenByIpRequest` - RemoteOpenResponse { - /// Connection ID to reference the opened connection when used with - /// `RemoteRequest` and `RemoteResponse`. TODO: maybe we reply with a - /// fd name directly? Not sure what this ID will map to. - connection_id: u32, - /// The remote host IP, e.g. "1.2.3.4" - remote_ip: String, - }, - /// Read from a remote connection - RemoteReadRequest { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - /// number of bytes to read - size: usize, - }, - /// Response to `RemoteReadRequest` containing read data - RemoteReadResponse { - /// A connection ID from `RemoteOpenResponse` - 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 - RemoteWriteRequest { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - /// Data to be sent - data: Vec, - }, - /// Response to `RemoteWriteRequest` containing the number of successfully - /// written bytes. - RemoteWriteResponse { - /// Connection ID from `RemoteOpenResponse` - connection_id: u32, - /// Number of bytes written successfully - size: usize, - }, - /// Write to a remote connection - RemoteFlushRequest { - /// A connection ID from `RemoteOpenResponse` - connection_id: u32, - }, - /// Response to `RemoteFlushRequest` - /// The response only contains the connection ID. Success is implicit: if - /// the flush response fails, a ProtocolErrorResponse will be sent. - RemoteFlushResponse { - /// Connection ID from `RemoteOpenResponse` - connection_id: u32, - }, -} - -#[cfg(test)] -mod test { - use std::str::from_utf8; - - use server::RequestProcessor; - - use super::*; - - #[test] - fn simple_status_request() { - let mut processor = Processor::new(); - let request = ProtocolMsg::StatusRequest.try_to_vec().unwrap(); - let response = processor.process(request.try_into().unwrap()); - let msg = ProtocolMsg::try_from_slice(&response).unwrap(); - assert_eq!(msg, ProtocolMsg::StatusResponse(0)); - } - - #[test] - fn fetch_plaintext_http_from_api_turnkey_com() { - let mut processor = Processor::new(); - let request = ProtocolMsg::RemoteOpenByNameRequest { - hostname: "api.turnkey.com".to_string(), - port: 443, - dns_resolvers: vec!["8.8.8.8".to_string()], - dns_port: 53, - } - .try_to_vec() - .unwrap(); - let response = processor.process(request.try_into().unwrap()); - let msg = ProtocolMsg::try_from_slice(&response).unwrap(); - let connection_id = match msg { - ProtocolMsg::RemoteOpenResponse { connection_id, remote_ip: _ } => { - connection_id - } - _ => { - panic!("test failure: ProtocolMsg is not RemoteOpenResponse") - } - }; - let http_request = "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n".to_string(); - - let request = ProtocolMsg::RemoteWriteRequest { - connection_id, - data: http_request.as_bytes().to_vec(), - } - .try_to_vec() - .unwrap(); - let response = processor.process(request.try_into().unwrap()); - let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); - assert!(matches!( - msg, - ProtocolMsg::RemoteWriteResponse { connection_id: _, size: _ } - )); - - let request = - ProtocolMsg::RemoteReadRequest { connection_id, size: 512 } - .try_to_vec() - .unwrap(); - let response = processor.process(request.try_into().unwrap()); - let msg: ProtocolMsg = ProtocolMsg::try_from_slice(&response).unwrap(); - let data = match msg { - ProtocolMsg::RemoteReadResponse { - connection_id: _, - size: _, - data, - } => data, - _ => { - panic!("test failure: ProtocolMsg is not RemoteReadResponse") - } - }; - - 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")); - } -} diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs new file mode 100644 index 00000000..31798908 --- /dev/null +++ b/src/qos_net/src/proxy.rs @@ -0,0 +1,372 @@ +//! Protocol proxy for our remote QOS net proxy +use std::io::{Read, Write}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use qos_core::server; + +use crate::{ + error::QosNetError, + proxy_connection::{self, ProxyConnection}, +}; + +const MEGABYTE: usize = 1024 * 1024; +const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; + +/// Enclave state machine that executes when given a `ProtocolMsg`. +pub struct Proxy { + connections: Vec, +} + +impl Default for Proxy { + fn default() -> Self { + Self::new() + } +} + +impl Proxy { + /// Create a new `Self`. + #[must_use] + pub fn new() -> Self { + Self { connections: vec![] } + } + + 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 { + self.connections.push(connection); + Ok(()) + } + } + + fn get_connection(&mut self, id: u32) -> Option<&mut ProxyConnection> { + self.connections.iter_mut().find(|c| c.id == id) + } + + /// 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(()) => { + ProxyMsg::ConnectResponse { connection_id, remote_ip } + } + Err(e) => ProxyMsg::ProxyError(e), + } + } + Err(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, port) { + Ok(conn) => { + let connection_id = conn.id; + let remote_ip = conn.ip.clone(); + match self.save_connection(conn) { + Ok(()) => { + ProxyMsg::ConnectResponse { connection_id, remote_ip } + } + Err(e) => ProxyMsg::ProxyError(e), + } + } + Err(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(size) => { + if size == 0 { + ProxyMsg::ProxyError(QosNetError::ConnectionClosed) + } else { + ProxyMsg::ReadResponse { + connection_id, + data: buf, + size, + } + } + } + Err(e) => ProxyMsg::ProxyError(e.into()), + } + } 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 ProxyMsg::ProxyError(QosNetError::OversizedPayload) + .try_to_vec() + .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, + port, + dns_resolvers, + dns_port, + ), + ProxyMsg::ConnectByIpRequest { ip, port } => { + self.connect_by_ip(ip, port) + } + 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::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), + }; + + resp.try_to_vec() + .expect("Protocol message can always be serialized. qed!") + } +} + +/// 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, + }, + /// 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, + }, +} + +#[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 = ProxyMsg::StatusRequest.try_to_vec().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(); + let request = ProxyMsg::ConnectByNameRequest { + hostname: "api.turnkey.com".to_string(), + port: 443, + dns_resolvers: vec!["8.8.8.8".to_string()], + dns_port: 53, + } + .try_to_vec() + .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 = ProxyMsg::WriteRequest { + connection_id, + data: http_request.as_bytes().to_vec(), + } + .try_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: _ } + )); + + let request = ProxyMsg::ReadRequest { connection_id, size: 512 } + .try_to_vec() + .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")); + } +} diff --git a/src/qos_net/src/remote_connection.rs b/src/qos_net/src/proxy_connection.rs similarity index 76% rename from src/qos_net/src/remote_connection.rs rename to src/qos_net/src/proxy_connection.rs index 35bb4dc8..52e85201 100644 --- a/src/qos_net/src/remote_connection.rs +++ b/src/qos_net/src/proxy_connection.rs @@ -11,28 +11,27 @@ use hickory_resolver::{ }; use rand::Rng; -use crate::error::ProtocolError; +use crate::error::QosNetError; -/// Struct representing a remote connection -pub struct RemoteConnection { - /// Unsigned integer with the connection ID. This is a random positive - /// integer +/// 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 for the remote host + /// IP address of the remote host pub ip: String, /// TCP stream object tcp_stream: TcpStream, } -impl RemoteConnection { - /// Create a new `RemoteConnection` from a name. This results in a DNS +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 { + ) -> 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 @@ -42,23 +41,19 @@ impl RemoteConnection { let tcp_addr = SocketAddr::new(ip, port); let tcp_stream = TcpStream::connect(tcp_addr)?; - println!( - "done. Now persisting TcpStream with connection ID {}", - connection_id - ); - Ok(RemoteConnection { + Ok(ProxyConnection { id: connection_id, ip: ip.to_string(), tcp_stream, }) } - /// Create a new `RemoteConnection` from an IP address. This results in a + /// 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 { + ) -> 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(); @@ -68,17 +63,17 @@ impl RemoteConnection { let tcp_addr = SocketAddr::new(ip_addr, port); let tcp_stream = TcpStream::connect(tcp_addr)?; - Ok(RemoteConnection { id: connection_id, ip, tcp_stream }) + Ok(ProxyConnection { id: connection_id, ip, tcp_stream }) } } -impl Read for RemoteConnection { +impl Read for ProxyConnection { fn read(&mut self, buf: &mut [u8]) -> Result { self.tcp_stream.read(buf) } } -impl Write for RemoteConnection { +impl Write for ProxyConnection { fn write(&mut self, buf: &[u8]) -> Result { self.tcp_stream.write(buf) } @@ -92,7 +87,7 @@ fn resolve_hostname( hostname: String, resolver_addrs: Vec, port: u16, -) -> Result { +) -> Result { let resolver_parsed_addrs = resolver_addrs .iter() .map(|resolver_address| { @@ -114,7 +109,7 @@ fn resolve_hostname( let resolver = Resolver::new(resolver_config, ResolverOpts::default())?; let response = resolver.lookup_ip(hostname.clone())?; response.iter().next().ok_or_else(|| { - ProtocolError::DNSResolutionError(format!( + QosNetError::DNSResolutionError(format!( "Empty response when querying for host {hostname}" )) }) @@ -128,16 +123,16 @@ mod test { sync::Arc, }; - use rustls::RootCertStore; + use rustls::{RootCertStore, SupportedCipherSuite}; use super::*; #[test] - fn can_fetch_tls_content_with_remote_connection_struct() { + fn can_fetch_tls_content_with_proxy_connection() { let host = "api.turnkey.com"; let path = "/health"; - let mut remote_connection = RemoteConnection::new_from_name( + let mut remote_connection = ProxyConnection::new_from_name( host.to_string(), 443, vec!["8.8.8.8".to_string()], @@ -161,12 +156,11 @@ mod test { let http_request = format!( "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" ); - println!("=== making HTTP request: \n{http_request}"); tls.write_all(http_request.as_bytes()).unwrap(); let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + assert!(matches!(ciphersuite, SupportedCipherSuite::Tls13(_))); - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); let mut response_bytes = Vec::new(); let read_to_end_result = tls.read_to_end(&mut response_bytes); @@ -176,6 +170,9 @@ mod test { || (read_to_end_result .is_err_and(|e| e.kind() == ErrorKind::UnexpectedEof)) ); - println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + + 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/remote_stream.rs b/src/qos_net/src/proxy_stream.rs similarity index 56% rename from src/qos_net/src/remote_stream.rs rename to src/qos_net/src/proxy_stream.rs index 846d0809..c7040490 100644 --- a/src/qos_net/src/remote_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -1,26 +1,24 @@ -//! Contains a RemoteStream abstraction to use qos_net's RemoteRead/RemoteWrite -//! under standard Read/Write traits +//! Contains an abstraction to implement the standard library's Read/Write +//! traits with `ProxyMsg`s. use std::io::{ErrorKind, Read, Write}; use borsh::{BorshDeserialize, BorshSerialize}; use qos_core::io::{SocketAddress, Stream, TimeVal}; -use crate::{error::ProtocolError, processor::ProtocolMsg}; +use crate::{error::QosNetError, proxy::ProxyMsg}; /// Struct representing a remote connection /// This is going to be used by enclaves, on the other side of a socket -pub struct RemoteStream { +pub struct ProxyStream { /// socket address and timeout to create the underlying Stream. /// Because `Stream` implements `drop` it can't be persisted here /// unfortunately... TODO: figure out if this can work? /// stream: Box, addr: SocketAddress, timeout: TimeVal, - /// Tracks the state of the remote stream - /// After initialization the connection is is `None`. - /// Once a remote connection is established (successful RemoteOpenByName or - /// RemoteOpenByIp request), this connection ID is set the u32 in - /// RemoteOpenResponse. + /// 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, @@ -28,24 +26,23 @@ pub struct RemoteStream { pub remote_ip: String, } -impl RemoteStream { - /// Create a new RemoteStream by name +impl ProxyStream { + /// Create a new ProxyStream by targeting a hostname /// `addr` is 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 /// `hostname` is the hostname to connect to (the remote qos_net proxy will /// resolve DNS) `port` is the port the remote qos_net proxy should connect /// to `dns_resolvers` and `dns_port` are the resolvers to use. - pub fn new_by_name( + pub fn connect_by_name( addr: &SocketAddress, timeout: TimeVal, hostname: String, port: u16, dns_resolvers: Vec, dns_port: u16, - ) -> Result { - println!("creating new RemoteStream by name"); + ) -> Result { let stream = Stream::connect(addr, timeout)?; - let req = ProtocolMsg::RemoteOpenByNameRequest { + let req = ProxyMsg::ConnectByNameRequest { hostname: hostname.clone(), port, dns_resolvers, @@ -56,62 +53,60 @@ impl RemoteStream { stream.send(&req)?; let resp_bytes = stream.recv()?; - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { - connection_id, - remote_ip, - } => Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: Some(hostname), - }), - _ => Err(ProtocolError::InvalidMsg), + 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(ProtocolError::InvalidMsg), + Err(_) => Err(QosNetError::InvalidMsg), } } - /// Create a new RemoteStream by IP + /// Create a new ProxyStream by targeting an IP address directly. /// `addr` is 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 /// `ip` and `port` are the IP and port to connect to (on the outside of the /// enclave) - pub fn new_by_ip( + pub fn connect_by_ip( addr: &SocketAddress, timeout: TimeVal, ip: String, port: u16, - ) -> Result { + ) -> Result { let stream: Stream = Stream::connect(addr, timeout)?; - let req = ProtocolMsg::RemoteOpenByIpRequest { ip, port } + let req = ProxyMsg::ConnectByIpRequest { ip, port } .try_to_vec() .expect("ProtocolMsg can always be serialized."); stream.send(&req)?; let resp_bytes = stream.recv()?; - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { - connection_id, - remote_ip, - } => Ok(Self { - addr: addr.clone(), - timeout, - connection_id, - remote_ip, - remote_hostname: None, - }), - _ => Err(ProtocolError::InvalidMsg), + ProxyMsg::ConnectResponse { connection_id, remote_ip } => { + Ok(Self { + addr: addr.clone(), + timeout, + connection_id, + remote_ip, + remote_hostname: None, + }) + } + _ => Err(QosNetError::InvalidMsg), }, - Err(_) => Err(ProtocolError::InvalidMsg), + Err(_) => Err(QosNetError::InvalidMsg), } } } -impl Read for RemoteStream { +impl Read for ProxyStream { fn read(&mut self, buf: &mut [u8]) -> Result { let stream: Stream = Stream::connect(&self.addr, self.timeout) .map_err(|e| { @@ -121,7 +116,7 @@ impl Read for RemoteStream { ) })?; - let req = ProtocolMsg::RemoteReadRequest { + let req = ProxyMsg::ReadRequest { connection_id: self.connection_id, size: buf.len(), } @@ -140,17 +135,13 @@ impl Read for RemoteStream { ) })?; - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteReadResponse { - connection_id: _, - size, - data, - } => { + ProxyMsg::ReadResponse { connection_id: _, size, data } => { if data.is_empty() { return Err(std::io::Error::new( ErrorKind::Interrupted, - "empty RemoteRead", + "empty Read", )); } if data.len() > buf.len() { @@ -177,7 +168,7 @@ impl Read for RemoteStream { } } -impl Write for RemoteStream { +impl Write for ProxyStream { fn write(&mut self, buf: &[u8]) -> Result { let stream: Stream = Stream::connect(&self.addr, self.timeout) .map_err(|e| { @@ -187,7 +178,7 @@ impl Write for RemoteStream { ) })?; - let req = ProtocolMsg::RemoteWriteRequest { + let req = ProxyMsg::WriteRequest { connection_id: self.connection_id, data: buf.to_vec(), } @@ -196,22 +187,24 @@ impl Write for RemoteStream { stream.send(&req).map_err(|e| { std::io::Error::new( ErrorKind::Other, - format!("QOS IOError sending RemoteWriteRequest: {:?}", e), + format!("QOS IOError sending WriteRequest: {:?}", e), ) })?; - let resp_bytes = stream.recv().map_err(|e| std::io::Error::new( + let resp_bytes = stream.recv().map_err(|e| { + std::io::Error::new( ErrorKind::Other, - format!("QOS IOError receiving bytes from stream after RemoteWriteRequest: {:?}", e), - ))?; + format!("QOS IOError receiving bytes from stream after WriteRequest: {:?}", e), + ) + })?; - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteWriteResponse { connection_id: _, size } => { + ProxyMsg::WriteResponse { connection_id: _, size } => { if size == 0 { return Err(std::io::Error::new( ErrorKind::Interrupted, - "failed RemoteWrite", + "Write failed: 0 bytes written", )); } println!("WRITE {}: sent buf of {} bytes", buf.len(), size); @@ -229,10 +222,49 @@ impl Write for RemoteStream { } } - // No-op because we can't flush a socket. We're not keeping any sort of - // client-side buffer here. fn flush(&mut self) -> Result<(), std::io::Error> { - Ok(()) + 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 = ProxyMsg::FlushRequest { connection_id: self.connection_id } + .try_to_vec() + .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: _ } => { + println!("FLUSH OK"); + Ok(()) + } + _ => Err(std::io::Error::new( + ErrorKind::InvalidData, + "unexpected response", + )), + }, + Err(_) => Err(std::io::Error::new( + ErrorKind::InvalidData, + "cannot deserialize message", + )), + } } } @@ -242,10 +274,10 @@ mod test { use std::{io::ErrorKind, sync::Arc}; use qos_core::server::RequestProcessor; - use rustls::RootCertStore; + use rustls::{RootCertStore, SupportedCipherSuite}; use super::*; - use crate::processor::Processor; + use crate::proxy::Proxy; #[test] fn can_fetch_tls_content_with_local_stream() { @@ -278,68 +310,11 @@ mod test { let http_request = format!( "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" ); - println!("=== making HTTP request: \n{http_request}"); - - tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); - 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)) - ); - println!("{}", std::str::from_utf8(&response_bytes).unwrap()); - } - - #[test] - fn can_fetch_tls_content_with_remote_stream() { - let host = "api.turnkey.com"; - let path = "/health"; - - let proxy_addr = - nix::sys::socket::UnixAddr::new("/tmp/proxy.sock").unwrap(); - let addr: SocketAddress = SocketAddress::Unix(proxy_addr); - let timeout = TimeVal::new(1, 0); - - let mut stream = RemoteStream::new_by_name( - &addr, - timeout, - host.to_string(), - 443, - vec!["8.8.8.8".to_string()], - 53, - ) - .unwrap(); - - assert_eq!(stream.remote_hostname, Some("api.turnkey.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" - ); - println!("=== making HTTP request: \n{http_request}"); tls.write_all(http_request.as_bytes()).unwrap(); let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + assert!(matches!(ciphersuite, SupportedCipherSuite::Tls13(_))); - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); let mut response_bytes = Vec::new(); let read_to_end_result = tls.read_to_end(&mut response_bytes); @@ -349,27 +324,17 @@ mod test { || (read_to_end_result .is_err_and(|e| e.kind() == ErrorKind::UnexpectedEof)) ); - println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + let response_text = std::str::from_utf8(&response_bytes).unwrap(); + assert!(response_text.contains("HTTP/1.1 200 OK")); + assert!(response_text.contains("currentTime")); } - /// Struct representing a connection, with direct access to the processor. - /// Useful in tests. + /// Struct representing a stream, with direct access to the proxy. + /// Useful in tests! :) struct LocalStream { - /// socket address and timeout to create the underlying Stream. - /// Because `Stream` implements `drop` it can't be persisted here - /// unfortunately... TODO: figure out if this can work? - /// stream: Box, - processor: Box, - /// Tracks the state of the remote stream - /// After initialization the connection is is `None`. - /// Once a remote connection is established (successful - /// RemoteOpenByName or RemoteOpenByIp request), this connection ID is - /// set the u32 in RemoteOpenResponse. + proxy: Box, 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 LocalStream { @@ -378,9 +343,8 @@ mod test { port: u16, dns_resolvers: Vec, dns_port: u16, - ) -> Result { - println!("creating new RemoteStream by name"); - let req = ProtocolMsg::RemoteOpenByNameRequest { + ) -> Result { + let req = ProxyMsg::ConnectByNameRequest { hostname: hostname.clone(), port, dns_resolvers, @@ -388,48 +352,43 @@ mod test { } .try_to_vec() .expect("ProtocolMsg can always be serialized."); - let mut processor = Box::new(Processor::new()); - let resp_bytes = processor.process(req); + let mut proxy = Box::new(Proxy::new()); + let resp_bytes = proxy.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteOpenResponse { + ProxyMsg::ConnectResponse { connection_id, - remote_ip, + remote_ip: _, } => Ok(Self { - processor, + proxy, connection_id, - remote_ip, remote_hostname: Some(hostname), }), - _ => Err(ProtocolError::InvalidMsg), + _ => Err(QosNetError::InvalidMsg), }, - Err(_) => Err(ProtocolError::InvalidMsg), + Err(_) => Err(QosNetError::InvalidMsg), } } } impl Read for LocalStream { fn read(&mut self, buf: &mut [u8]) -> Result { - let req = ProtocolMsg::RemoteReadRequest { + let req = ProxyMsg::ReadRequest { connection_id: self.connection_id, size: buf.len(), } .try_to_vec() .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.processor.process(req); + let resp_bytes = self.proxy.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteReadResponse { - connection_id: _, - size, - data, - } => { + ProxyMsg::ReadResponse { connection_id: _, size, data } => { if data.is_empty() { return Err(std::io::Error::new( ErrorKind::Interrupted, - "empty RemoteRead", + "empty Read", )); } if data.len() > buf.len() { @@ -458,32 +417,24 @@ mod test { impl Write for LocalStream { fn write(&mut self, buf: &[u8]) -> Result { - let req = ProtocolMsg::RemoteWriteRequest { + let req = ProxyMsg::WriteRequest { connection_id: self.connection_id, data: buf.to_vec(), } .try_to_vec() .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.processor.process(req); + let resp_bytes = self.proxy.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteWriteResponse { - connection_id: _, - size, - } => { + ProxyMsg::WriteResponse { connection_id: _, size } => { if size == 0 { return Err(std::io::Error::new( ErrorKind::Interrupted, - "failed RemoteWrite", + "failed Write", )); } - println!( - "WRITE {}: sent buf of {} bytes: |{}|", - buf.len(), - size, - qos_hex::encode(buf) - ); + println!("WRITE {}: sent {} bytes", buf.len(), size,); Ok(size) } _ => Err(std::io::Error::new( @@ -498,19 +449,16 @@ mod test { } } - // No-op because we can't flush a socket. We're not keeping any sort of - // client-side buffer here. fn flush(&mut self) -> Result<(), std::io::Error> { - let req = ProtocolMsg::RemoteFlushRequest { - connection_id: self.connection_id, - } - .try_to_vec() - .expect("ProtocolMsg can always be serialized."); - let resp_bytes = self.processor.process(req); + let req = + ProxyMsg::FlushRequest { connection_id: self.connection_id } + .try_to_vec() + .expect("ProtocolMsg can always be serialized."); + let resp_bytes = self.proxy.process(req); - match ProtocolMsg::try_from_slice(&resp_bytes) { + match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProtocolMsg::RemoteFlushResponse { connection_id: _ } => { + ProxyMsg::FlushResponse { connection_id: _ } => { println!("FLUSH OK"); Ok(()) } From eb6646eeb89213f731b5d4b5110b45a2cb1786bd Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 11 Jun 2024 16:41:30 -0500 Subject: [PATCH 08/21] Simplify stream read/write trait test --- src/qos_core/src/io/stream.rs | 114 ++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 31a4e008..78b05e93 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -368,9 +368,12 @@ fn socket_fd(addr: &SocketAddress) -> Result { #[cfg(test)] mod test { - use std::{io::ErrorKind, sync::Arc}; - - use rustls::RootCertStore; + use std::{ + os::{fd::AsRawFd, unix::net::UnixListener}, + path::Path, + str::from_utf8, + thread, + }; use super::*; @@ -378,6 +381,41 @@ mod test { 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(); + if from_utf8(&buf).unwrap() == "PING" { + stream.write(b"PONG").unwrap(); + } + + // And 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 @@ -385,8 +423,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(); @@ -398,51 +436,33 @@ mod test { assert_eq!(data, resp); } - // TODO: replace this test with something simpler. Local socket which does a - // simple echo? #[test] - fn stream_implement_read_write_traits() { - let host = "api.turnkey.com"; - let path = "/health"; + fn stream_implements_read_write_traits() { + let socket_server_path = "./stream_implements_read_write_traits.sock"; + + // Start a barebone socket server which replies "Roger that." 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("/tmp/host.sock").unwrap(); - let addr: SocketAddress = SocketAddress::Unix(unix_addr); - let timeout = TimeVal::new(1, 0); - let mut stream = Stream::connect(&addr, timeout).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 stream); - - let http_request = format!( - "GET {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n" - ); - println!("=== making HTTP request: \n{http_request}"); - - tls.write_all(http_request.as_bytes()).unwrap(); - let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); - - println!("=== current ciphersuite: {:?}", ciphersuite.suite()); - 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)) - ); - println!("{}", std::str::from_utf8(&response_bytes).unwrap()); + 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] From 9082021ea20e249b1f51a4e0fb404ce8423e9829 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 11 Jun 2024 17:56:54 -0500 Subject: [PATCH 09/21] Proxy feature in qos_net --- src/Cargo.lock | 5 -- src/qos_core/src/io/stream.rs | 8 +-- src/qos_net/Cargo.toml | 18 ++----- src/qos_net/src/error.rs | 2 + src/qos_net/src/lib.rs | 5 ++ src/qos_net/src/proxy.rs | 88 +-------------------------------- src/qos_net/src/proxy_msg.rs | 88 +++++++++++++++++++++++++++++++++ src/qos_net/src/proxy_stream.rs | 34 ++++++++----- 8 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 src/qos_net/src/proxy_msg.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 2803144e..9e5d548b 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1795,18 +1795,13 @@ dependencies = [ name = "qos_net" version = "0.1.0" dependencies = [ - "aws-nitro-enclaves-nsm-api", "borsh", "hickory-resolver", - "libc", - "nix", "qos_core", - "qos_hex", "qos_test_primitives", "rand", "rustls", "serde", - "serde_bytes", "webpki-roots", ] diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 78b05e93..01a5cca4 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -381,8 +381,8 @@ mod test { TimeVal::seconds(1) } - // A simple test socket server which says "PONG" when you send "PING". Then - // it kills itself. + // A simple test socket server which says "PONG" when you send "PING". + // Then it kills itself. pub struct HarakiriPongServer { path: String, } @@ -400,11 +400,13 @@ mod test { // 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" { stream.write(b"PONG").unwrap(); } - // And shutdown the server + // Then shutdown the server let _ = shutdown(listener.as_raw_fd(), Shutdown::Both); let _ = close(listener.as_raw_fd()); diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml index 735e908a..8643ede2 100644 --- a/src/qos_net/Cargo.toml +++ b/src/qos_net/Cargo.toml @@ -5,21 +5,12 @@ edition = "2021" publish = false [dependencies] -qos_hex = { path = "../qos_hex", features = ["serde"] } qos_core = { path = "../qos_core", default-features = false } -nix = { version = "0.26", features = ["socket"], default-features = false } -libc = "=0.2.148" borsh = { version = "0.10" } - -# For AWS Nitro -aws-nitro-enclaves-nsm-api = { version = "0.3", default-features = false } - -serde_bytes = { version = "0.11", default-features = false } serde = { version = "1", features = ["derive"], default-features = false } - -hickory-resolver = { version = "0.24.1", features = ["tokio-runtime"], default-features = false} -rand = { version = "0.8.5", 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" } @@ -27,6 +18,5 @@ rustls = { version = "0.23.5" } webpki-roots = { version = "0.26.1" } [features] -# Support for VSOCK -vm = [] - +default = ["proxy"] # keep this as a default feature ensures we lint by default +proxy = ["rand", "hickory-resolver"] diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index cae08e42..37152d0e 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -2,6 +2,7 @@ use std::net::AddrParseError; use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "proxy")] use hickory_resolver::error::ResolveError; /// Errors related to creating and using proxy connections @@ -56,6 +57,7 @@ impl From for QosNetError { } } +#[cfg(feature = "proxy")] impl From for QosNetError { fn from(err: ResolveError) -> Self { let msg = format!("{err:?}"); diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs index bab3b9d5..46ced348 100644 --- a/src/qos_net/src/lib.rs +++ b/src/qos_net/src/lib.rs @@ -3,8 +3,13 @@ //! 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/proxy.rs b/src/qos_net/src/proxy.rs index 31798908..385d9439 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -7,6 +7,7 @@ use qos_core::server; use crate::{ error::QosNetError, proxy_connection::{self, ProxyConnection}, + proxy_msg::ProxyMsg, }; const MEGABYTE: usize = 1024 * 1024; @@ -213,93 +214,6 @@ impl server::RequestProcessor for Proxy { } } -/// 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, - }, - /// 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, - }, -} - #[cfg(test)] mod test { use std::str::from_utf8; diff --git a/src/qos_net/src/proxy_msg.rs b/src/qos_net/src/proxy_msg.rs new file mode 100644 index 00000000..cd14429d --- /dev/null +++ b/src/qos_net/src/proxy_msg.rs @@ -0,0 +1,88 @@ +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, + }, + /// 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 index c7040490..39f9978f 100644 --- a/src/qos_net/src/proxy_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -5,16 +5,15 @@ use std::io::{ErrorKind, Read, Write}; use borsh::{BorshDeserialize, BorshSerialize}; use qos_core::io::{SocketAddress, Stream, TimeVal}; -use crate::{error::QosNetError, proxy::ProxyMsg}; +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 and timeout to create the underlying Stream. - /// Because `Stream` implements `drop` it can't be persisted here - /// unfortunately... TODO: figure out if this can work? - /// stream: Box, + /// 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 @@ -28,11 +27,18 @@ pub struct ProxyStream { impl ProxyStream { /// Create a new ProxyStream by targeting a hostname - /// `addr` is the USOCK or VSOCK to connect to (this socket should be bound + /// + /// # 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 - /// `hostname` is the hostname to connect to (the remote qos_net proxy will - /// resolve DNS) `port` is the port the remote qos_net proxy should connect - /// to `dns_resolvers` and `dns_port` are the resolvers to use. + /// * `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, @@ -71,10 +77,14 @@ impl ProxyStream { } /// Create a new ProxyStream by targeting an IP address directly. - /// `addr` is the USOCK or VSOCK to connect to (this socket should be bound + /// + /// # 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 - /// `ip` and `port` are the IP and port to connect to (on the outside of the - /// enclave) + /// * `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, From 8c6a675a311210a01ecc40aaf9f20bc35c99c44f Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Fri, 14 Jun 2024 12:54:34 -0500 Subject: [PATCH 10/21] Introduce limit for connection pool and CloseRequest/CloseResponse --- src/qos_net/src/error.rs | 2 + src/qos_net/src/lib.rs | 3 +- src/qos_net/src/proxy.rs | 123 +++++++++++++++++++++++++++++--- src/qos_net/src/proxy_msg.rs | 10 +++ src/qos_net/src/proxy_stream.rs | 35 +++++++++ 5 files changed, 161 insertions(+), 12 deletions(-) diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index 37152d0e..9b7952e5 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -36,6 +36,8 @@ pub enum QosNetError { /// 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 { diff --git a/src/qos_net/src/lib.rs b/src/qos_net/src/lib.rs index 46ced348..f903c091 100644 --- a/src/qos_net/src/lib.rs +++ b/src/qos_net/src/lib.rs @@ -1,5 +1,6 @@ //! 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 +//! 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)] diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index 385d9439..809ae2a1 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -13,9 +13,12 @@ use crate::{ const MEGABYTE: usize = 1024 * 1024; const MAX_ENCODED_MSG_LEN: usize = 128 * MEGABYTE; -/// Enclave state machine that executes when given a `ProtocolMsg`. +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 { @@ -28,7 +31,15 @@ impl Proxy { /// Create a new `Self`. #[must_use] pub fn new() -> Self { - Self { connections: vec![] } + 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( @@ -38,15 +49,43 @@ impl Proxy { 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( @@ -99,18 +138,23 @@ impl Proxy { if let Some(conn) = self.get_connection(connection_id) { let mut buf: Vec = vec![0; size]; match conn.read(&mut buf) { - Ok(size) => { - if size == 0 { - ProxyMsg::ProxyError(QosNetError::ConnectionClosed) - } else { - ProxyMsg::ReadResponse { - connection_id, - data: buf, - size, + 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(_) => { + ProxyMsg::ProxyError(QosNetError::ConnectionClosed) } + Err(e) => ProxyMsg::ProxyError(e), } } - Err(e) => ProxyMsg::ProxyError(e.into()), + 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( @@ -175,6 +219,9 @@ impl server::RequestProcessor for Proxy { 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) } @@ -194,6 +241,9 @@ impl server::RequestProcessor for Proxy { 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) } @@ -234,6 +284,8 @@ mod test { #[test] fn fetch_plaintext_http_from_api_turnkey_com() { let mut proxy = Proxy::new(); + assert_eq!(proxy.num_connections(), 0); + let request = ProxyMsg::ConnectByNameRequest { hostname: "api.turnkey.com".to_string(), port: 443, @@ -267,6 +319,9 @@ mod test { ProxyMsg::WriteResponse { connection_id: _, size: _ } )); + // Check that we now have an active connection + assert_eq!(proxy.num_connections(), 1); + let request = ProxyMsg::ReadRequest { connection_id, size: 512 } .try_to_vec() .unwrap(); @@ -283,4 +338,50 @@ mod test { 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_msg.rs b/src/qos_net/src/proxy_msg.rs index cd14429d..23090cc7 100644 --- a/src/qos_net/src/proxy_msg.rs +++ b/src/qos_net/src/proxy_msg.rs @@ -41,6 +41,16 @@ pub enum ProxyMsg { /// 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` diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs index 39f9978f..5db878d0 100644 --- a/src/qos_net/src/proxy_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -29,6 +29,7 @@ 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 @@ -114,6 +115,24 @@ impl ProxyStream { 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 = ProxyMsg::CloseRequest { connection_id: self.connection_id } + .try_to_vec() + .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 { @@ -301,6 +320,7 @@ mod test { 53, ) .unwrap(); + assert_eq!(stream.num_connections(), 1); assert_eq!(stream.remote_hostname, Some("api.turnkey.com".to_string())); @@ -337,6 +357,10 @@ mod test { let response_text = std::str::from_utf8(&response_bytes).unwrap(); assert!(response_text.contains("HTTP/1.1 200 OK")); assert!(response_text.contains("currentTime")); + + let closed = stream.close(); + assert!(closed.is_ok()); + assert_eq!(stream.num_connections(), 0); } /// Struct representing a stream, with direct access to the proxy. @@ -380,6 +404,17 @@ mod test { 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 { From 95653dff227c6b5e1d27c48a06b4ae4f1f46666a Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Sun, 16 Jun 2024 22:23:22 -0500 Subject: [PATCH 11/21] Bubble up underlying QOS IO error / OS IO error --- src/qos_net/src/error.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index 9b7952e5..b7bbc266 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -9,9 +9,9 @@ use hickory_resolver::error::ResolveError; #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum QosNetError { /// Error variant encapsulating OS IO errors - IOError, + IOError(String), /// Error variant encapsulating OS IO errors - QOSIOError, + QOSIOError(String), /// The message is too large. OversizeMsg, /// Payload is too big. See `MAX_ENCODED_MSG_LEN` for the upper bound on @@ -41,14 +41,16 @@ pub enum QosNetError { } impl From for QosNetError { - fn from(_err: std::io::Error) -> Self { - Self::IOError + 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 { - Self::QOSIOError + fn from(err: qos_core::io::IOError) -> Self { + let msg = format!("{err:?}"); + Self::QOSIOError(msg) } } From fb4fab32201374a37bf5603aa394ef51ac15818d Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 18 Jun 2024 21:45:37 -0500 Subject: [PATCH 12/21] Add CHANGELOG entry --- CHANGELOG.MD | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8c33f23d..8dd6e849 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: `mono` references `qos` with a submodule (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 From 80538aeb7def9924abcbab52d76692ea8662ec21 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 24 Jun 2024 12:36:43 -0500 Subject: [PATCH 13/21] Add logging to proxy server --- src/qos_net/src/proxy.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index 809ae2a1..d5cde7d5 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -194,6 +194,7 @@ impl Proxy { impl server::RequestProcessor for Proxy { fn process(&mut self, req_bytes: Vec) -> Vec { + println!("Proxy processing request"); if req_bytes.len() > MAX_ENCODED_MSG_LEN { return ProxyMsg::ProxyError(QosNetError::OversizedPayload) .try_to_vec() @@ -203,6 +204,7 @@ impl server::RequestProcessor for Proxy { let resp = match ProxyMsg::try_from_slice(&req_bytes) { Ok(req) => match req { ProxyMsg::StatusRequest => { + println!("Proxy processing StatusRequest"); ProxyMsg::StatusResponse(self.connections.len()) } ProxyMsg::ConnectByNameRequest { @@ -210,25 +212,33 @@ impl server::RequestProcessor for Proxy { port, dns_resolvers, dns_port, - } => self.connect_by_name( - hostname, - port, - dns_resolvers, - dns_port, - ), + } => { + println!("Proxy connecting to {hostname}:{port}"); + self.connect_by_name( + hostname, + port, + dns_resolvers, + dns_port, + ) + }, ProxyMsg::ConnectByIpRequest { ip, port } => { + println!("Proxy connecting to {ip}:{port}"); self.connect_by_ip(ip, port) } ProxyMsg::CloseRequest { connection_id } => { + println!("Proxy closing connection {connection_id}"); self.close(connection_id) } ProxyMsg::ReadRequest { connection_id, size } => { + println!("Proxy reading {size} bytes from connection {connection_id}"); self.read(connection_id, size) } ProxyMsg::WriteRequest { connection_id, data } => { + println!("Proxy writing to connection {connection_id}"); self.write(connection_id, data) } ProxyMsg::FlushRequest { connection_id } => { + println!("Proxy flushing connection {connection_id}"); self.flush(connection_id) } ProxyMsg::ProxyError(_) => { From a000ba08958264fa48536dbbf27b219ed8a1e3f8 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 24 Jun 2024 13:18:26 -0500 Subject: [PATCH 14/21] lint --- src/qos_net/src/proxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index d5cde7d5..59332a56 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -220,7 +220,7 @@ impl server::RequestProcessor for Proxy { dns_resolvers, dns_port, ) - }, + } ProxyMsg::ConnectByIpRequest { ip, port } => { println!("Proxy connecting to {ip}:{port}"); self.connect_by_ip(ip, port) From 99089066803d478dadf218b5963898a516f01248 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 24 Jun 2024 15:47:19 -0500 Subject: [PATCH 15/21] More logging --- src/qos_net/src/proxy.rs | 20 +++++++++++++++----- src/qos_net/src/proxy_connection.rs | 8 ++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index 59332a56..edba0561 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -104,14 +104,22 @@ impl Proxy { Ok(conn) => { let connection_id = conn.id; let remote_ip = conn.ip.clone(); + println!("called new_from_name successfully. Saving connection ID {connection_id}..."); match self.save_connection(conn) { Ok(()) => { + println!("Connection established and saved. Returning ConnectResponse to client"); ProxyMsg::ConnectResponse { connection_id, remote_ip } } - Err(e) => ProxyMsg::ProxyError(e), + Err(e) => { + println!("error saving connection."); + ProxyMsg::ProxyError(e) + } } } - Err(e) => ProxyMsg::ProxyError(e), + Err(e) => { + println!("error calling new_from_name"); + ProxyMsg::ProxyError(e) + } } } @@ -214,12 +222,14 @@ impl server::RequestProcessor for Proxy { dns_port, } => { println!("Proxy connecting to {hostname}:{port}"); - self.connect_by_name( - hostname, + let resp = self.connect_by_name( + hostname.clone(), port, dns_resolvers, dns_port, - ) + ); + println!("Proxy connected to {hostname}:{port}"); + resp } ProxyMsg::ConnectByIpRequest { ip, port } => { println!("Proxy connecting to {ip}:{port}"); diff --git a/src/qos_net/src/proxy_connection.rs b/src/qos_net/src/proxy_connection.rs index 52e85201..3c70a88b 100644 --- a/src/qos_net/src/proxy_connection.rs +++ b/src/qos_net/src/proxy_connection.rs @@ -32,15 +32,20 @@ impl ProxyConnection { dns_resolvers: Vec, dns_port: u16, ) -> Result { + println!("new_from_name invoked"); let ip = resolve_hostname(hostname, dns_resolvers, dns_port)?; + println!("resolved name into an IP"); // 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::(); + println!("Random connection ID generated"); let tcp_addr = SocketAddr::new(ip, port); + println!("TCP addr created. Connecting..."); let tcp_stream = TcpStream::connect(tcp_addr)?; + println!("Connected"); Ok(ProxyConnection { id: connection_id, ip: ip.to_string(), @@ -88,6 +93,7 @@ fn resolve_hostname( resolver_addrs: Vec, port: u16, ) -> Result { + println!("resolving hostname..."); let resolver_parsed_addrs = resolver_addrs .iter() .map(|resolver_address| { @@ -107,7 +113,9 @@ fn resolve_hostname( ), ); let resolver = Resolver::new(resolver_config, ResolverOpts::default())?; + println!("resolver ready"); let response = resolver.lookup_ip(hostname.clone())?; + println!("resolver successfully invoked"); response.iter().next().ok_or_else(|| { QosNetError::DNSResolutionError(format!( "Empty response when querying for host {hostname}" From 109044b3a0648d4472e3ab1fb915a09acbe3e731 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Mon, 24 Jun 2024 16:52:55 -0500 Subject: [PATCH 16/21] Specific log for DNS resolution errors --- src/qos_net/src/error.rs | 2 +- src/qos_net/src/proxy_connection.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qos_net/src/error.rs b/src/qos_net/src/error.rs index b7bbc266..ec2326b0 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -65,6 +65,6 @@ impl From for QosNetError { impl From for QosNetError { fn from(err: ResolveError) -> Self { let msg = format!("{err:?}"); - Self::ParseError(msg) + Self::DNSResolutionError(msg) } } diff --git a/src/qos_net/src/proxy_connection.rs b/src/qos_net/src/proxy_connection.rs index 3c70a88b..ef6b390e 100644 --- a/src/qos_net/src/proxy_connection.rs +++ b/src/qos_net/src/proxy_connection.rs @@ -114,7 +114,10 @@ fn resolve_hostname( ); let resolver = Resolver::new(resolver_config, ResolverOpts::default())?; println!("resolver ready"); - let response = resolver.lookup_ip(hostname.clone())?; + let response = resolver.lookup_ip(hostname.clone()).map_err(|e| { + println!("error invoking resolver: {:?}", e); + QosNetError::from(e) + })?; println!("resolver successfully invoked"); response.iter().next().ok_or_else(|| { QosNetError::DNSResolutionError(format!( From 61fbca61c6c2012ec75a41016a00a0406b8a0b04 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Wed, 26 Jun 2024 10:31:44 -0500 Subject: [PATCH 17/21] Remove ConnectionClosed/EmptyRead errors: these are not errors, they should be propagated as empty, successful reads --- src/integration/src/bin/pivot_remote_tls.rs | 27 +++++++++--- src/integration/tests/remote_tls.rs | 14 ++++++ src/qos_net/src/error.rs | 5 --- src/qos_net/src/proxy.rs | 7 ++- src/qos_net/src/proxy_stream.rs | 47 +++++++++++++++------ 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/integration/src/bin/pivot_remote_tls.rs b/src/integration/src/bin/pivot_remote_tls.rs index e3c5ece2..a44af7e4 100644 --- a/src/integration/src/bin/pivot_remote_tls.rs +++ b/src/integration/src/bin/pivot_remote_tls.rs @@ -1,6 +1,6 @@ use core::panic; use std::{ - io::{Read, Write}, + io::{ErrorKind, Read, Write}, sync::Arc, }; @@ -63,18 +63,31 @@ impl RequestProcessor for Processor { ); tls.write_all(http_request.as_bytes()).unwrap(); - let _ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); let mut response_bytes = Vec::new(); - let read_to_end_result: usize = - tls.read_to_end(&mut response_bytes).unwrap(); - - // Ignore eof errors: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof + 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(); PivotRemoteTlsMsg::RemoteTlsResponse(format!( - "Content fetched successfully ({read_to_end_result} bytes): {fetched_content}" + "Content fetched successfully: {fetched_content}" )) .try_to_vec() .expect("RemoteTlsResponse is valid borsh") diff --git a/src/integration/tests/remote_tls.rs b/src/integration/tests/remote_tls.rs index ec6e6c82..9513c0bb 100644 --- a/src/integration/tests/remote_tls.rs +++ b/src/integration/tests/remote_tls.rs @@ -47,4 +47,18 @@ fn fetch_remote_tls_content() { assert!(response_text.contains("Content fetched successfully")); assert!(response_text.contains("HTTP/1.1 200 OK")); assert!(response_text.contains("currentTime")); + + let app_request = PivotRemoteTlsMsg::RemoteTlsRequest { + host: "www.googleapis.com".to_string(), + path: "/oauth2/v3/certs".to_string(), + } + .try_to_vec() + .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_net/src/error.rs b/src/qos_net/src/error.rs index ec2326b0..326437cc 100644 --- a/src/qos_net/src/error.rs +++ b/src/qos_net/src/error.rs @@ -27,11 +27,6 @@ pub enum QosNetError { DuplicateConnectionId(u32), /// Attempt to send a message to a remote connection, but ID isn't found ConnectionIdNotFound(u32), - /// Attempting to read on a closed remote connection (`.read` returned 0 - /// bytes) - ConnectionClosed, - /// Happens when a socket `read` results in no data - EmptyRead, /// 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. diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index edba0561..a9d65f74 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -151,7 +151,12 @@ impl Proxy { // connection close. So we can safely remove it. match self.remove_connection(connection_id) { Ok(_) => { - ProxyMsg::ProxyError(QosNetError::ConnectionClosed) + // Connection was successfully removed / closed + ProxyMsg::ReadResponse { + connection_id, + data: buf, + size: 0, + } } Err(e) => ProxyMsg::ProxyError(e), } diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs index 5db878d0..a9f63a46 100644 --- a/src/qos_net/src/proxy_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -184,6 +184,10 @@ impl Read for ProxyStream { println!("READ {}: read {} bytes", buf.len(), size); 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", @@ -310,8 +314,8 @@ mod test { #[test] fn can_fetch_tls_content_with_local_stream() { - let host = "api.turnkey.com"; - let path = "/health"; + let host = "www.googleapis.com"; + let path = "/oauth2/v3/certs"; let mut stream = LocalStream::new_by_name( host.to_string(), @@ -322,7 +326,10 @@ mod test { .unwrap(); assert_eq!(stream.num_connections(), 1); - assert_eq!(stream.remote_hostname, Some("api.turnkey.com".to_string())); + assert_eq!( + stream.remote_hostname, + Some("www.googleapis.com".to_string()) + ); let root_store = RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.into() }; @@ -348,19 +355,27 @@ mod test { 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")); + 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) + } + } - let closed = stream.close(); - assert!(closed.is_ok()); + // 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. @@ -447,6 +462,10 @@ mod test { println!("READ {}: read {} bytes", buf.len(), size); 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", From 89e3535b99f2be6c12c351f908b21a8b6affed3d Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Thu, 27 Jun 2024 08:49:20 -0500 Subject: [PATCH 18/21] Adjust logging to be less verbose --- src/qos_net/src/proxy.rs | 44 +++++++++++++---------------- src/qos_net/src/proxy_connection.rs | 14 ++------- src/qos_net/src/proxy_stream.rs | 14 ++------- 3 files changed, 23 insertions(+), 49 deletions(-) diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index a9d65f74..437d5287 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -104,20 +104,19 @@ impl Proxy { Ok(conn) => { let connection_id = conn.id; let remote_ip = conn.ip.clone(); - println!("called new_from_name successfully. Saving connection ID {connection_id}..."); match self.save_connection(conn) { Ok(()) => { - println!("Connection established and saved. Returning ConnectResponse to client"); + println!("Connection to {hostname} established and saved as ID {connection_id}"); ProxyMsg::ConnectResponse { connection_id, remote_ip } } Err(e) => { - println!("error saving connection."); + println!("error saving connection: {e:?}"); ProxyMsg::ProxyError(e) } } } Err(e) => { - println!("error calling new_from_name"); + println!("error while establishing connection: {e:?}"); ProxyMsg::ProxyError(e) } } @@ -126,18 +125,25 @@ impl Proxy { /// 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, port) { + 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) => ProxyMsg::ProxyError(e), + Err(e) => { + println!("error saving connection: {e:?}"); + ProxyMsg::ProxyError(e) + } } } - Err(e) => ProxyMsg::ProxyError(e), + Err(e) => { + println!("error while establishing connection: {e:?}"); + ProxyMsg::ProxyError(e) + } } } @@ -207,7 +213,6 @@ impl Proxy { impl server::RequestProcessor for Proxy { fn process(&mut self, req_bytes: Vec) -> Vec { - println!("Proxy processing request"); if req_bytes.len() > MAX_ENCODED_MSG_LEN { return ProxyMsg::ProxyError(QosNetError::OversizedPayload) .try_to_vec() @@ -217,7 +222,6 @@ impl server::RequestProcessor for Proxy { let resp = match ProxyMsg::try_from_slice(&req_bytes) { Ok(req) => match req { ProxyMsg::StatusRequest => { - println!("Proxy processing StatusRequest"); ProxyMsg::StatusResponse(self.connections.len()) } ProxyMsg::ConnectByNameRequest { @@ -225,35 +229,25 @@ impl server::RequestProcessor for Proxy { port, dns_resolvers, dns_port, - } => { - println!("Proxy connecting to {hostname}:{port}"); - let resp = self.connect_by_name( - hostname.clone(), - port, - dns_resolvers, - dns_port, - ); - println!("Proxy connected to {hostname}:{port}"); - resp - } + } => self.connect_by_name( + hostname.clone(), + port, + dns_resolvers, + dns_port, + ), ProxyMsg::ConnectByIpRequest { ip, port } => { - println!("Proxy connecting to {ip}:{port}"); self.connect_by_ip(ip, port) } ProxyMsg::CloseRequest { connection_id } => { - println!("Proxy closing connection {connection_id}"); self.close(connection_id) } ProxyMsg::ReadRequest { connection_id, size } => { - println!("Proxy reading {size} bytes from connection {connection_id}"); self.read(connection_id, size) } ProxyMsg::WriteRequest { connection_id, data } => { - println!("Proxy writing to connection {connection_id}"); self.write(connection_id, data) } ProxyMsg::FlushRequest { connection_id } => { - println!("Proxy flushing connection {connection_id}"); self.flush(connection_id) } ProxyMsg::ProxyError(_) => { diff --git a/src/qos_net/src/proxy_connection.rs b/src/qos_net/src/proxy_connection.rs index ef6b390e..c44a6e63 100644 --- a/src/qos_net/src/proxy_connection.rs +++ b/src/qos_net/src/proxy_connection.rs @@ -32,20 +32,15 @@ impl ProxyConnection { dns_resolvers: Vec, dns_port: u16, ) -> Result { - println!("new_from_name invoked"); let ip = resolve_hostname(hostname, dns_resolvers, dns_port)?; - println!("resolved name into an IP"); // 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::(); - println!("Random connection ID generated"); let tcp_addr = SocketAddr::new(ip, port); - println!("TCP addr created. Connecting..."); let tcp_stream = TcpStream::connect(tcp_addr)?; - println!("Connected"); Ok(ProxyConnection { id: connection_id, ip: ip.to_string(), @@ -93,7 +88,6 @@ fn resolve_hostname( resolver_addrs: Vec, port: u16, ) -> Result { - println!("resolving hostname..."); let resolver_parsed_addrs = resolver_addrs .iter() .map(|resolver_address| { @@ -113,12 +107,8 @@ fn resolve_hostname( ), ); let resolver = Resolver::new(resolver_config, ResolverOpts::default())?; - println!("resolver ready"); - let response = resolver.lookup_ip(hostname.clone()).map_err(|e| { - println!("error invoking resolver: {:?}", e); - QosNetError::from(e) - })?; - println!("resolver successfully invoked"); + 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}" diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs index a9f63a46..b3e35fe1 100644 --- a/src/qos_net/src/proxy_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -181,7 +181,6 @@ impl Read for ProxyStream { for (i, b) in data.iter().enumerate() { buf[i] = *b } - println!("READ {}: read {} bytes", buf.len(), size); Ok(size) } ProxyMsg::ProxyError(e) => Err(std::io::Error::new( @@ -240,7 +239,6 @@ impl Write for ProxyStream { "Write failed: 0 bytes written", )); } - println!("WRITE {}: sent buf of {} bytes", buf.len(), size); Ok(size) } _ => Err(std::io::Error::new( @@ -284,10 +282,7 @@ impl Write for ProxyStream { match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProxyMsg::FlushResponse { connection_id: _ } => { - println!("FLUSH OK"); - Ok(()) - } + ProxyMsg::FlushResponse { connection_id: _ } => Ok(()), _ => Err(std::io::Error::new( ErrorKind::InvalidData, "unexpected response", @@ -459,7 +454,6 @@ mod test { for (i, b) in data.iter().enumerate() { buf[i] = *b } - println!("READ {}: read {} bytes", buf.len(), size); Ok(size) } ProxyMsg::ProxyError(e) => Err(std::io::Error::new( @@ -498,7 +492,6 @@ mod test { "failed Write", )); } - println!("WRITE {}: sent {} bytes", buf.len(), size,); Ok(size) } _ => Err(std::io::Error::new( @@ -522,10 +515,7 @@ mod test { match ProxyMsg::try_from_slice(&resp_bytes) { Ok(resp) => match resp { - ProxyMsg::FlushResponse { connection_id: _ } => { - println!("FLUSH OK"); - Ok(()) - } + ProxyMsg::FlushResponse { connection_id: _ } => Ok(()), _ => Err(std::io::Error::new( ErrorKind::InvalidData, "unexpected response", From d290dfbd71d506c86ae45a50a6511dbf11fcae3c Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Thu, 27 Jun 2024 08:57:56 -0500 Subject: [PATCH 19/21] Update code to use borsh v1 APIs --- src/Cargo.lock | 95 +++++++++++++++++++-- src/integration/src/bin/pivot_remote_tls.rs | 7 +- src/integration/tests/borsh_serialize.rs | 72 +++++++++------- src/integration/tests/remote_tls.rs | 11 +-- src/qos_core/src/io/stream.rs | 2 +- 5 files changed, 134 insertions(+), 53 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index 9e5d548b..d632434e 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -52,6 +52,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -317,16 +328,39 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.12.3", +] + [[package]] name = "borsh" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ - "borsh-derive", + "borsh-derive 1.5.1", "cfg_aliases", ] +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "borsh-derive" version = "1.5.1" @@ -334,13 +368,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 2.0.68", "syn_derive", ] +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -896,6 +952,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -1139,7 +1198,7 @@ name = "integration" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh", + "borsh 1.5.1", "nix", "qos_client", "qos_core", @@ -1678,6 +1737,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -1725,7 +1793,7 @@ name = "qos_client" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh", + "borsh 1.5.1", "lazy_static", "p256 0.12.0", "qos_core", @@ -1748,7 +1816,7 @@ name = "qos_core" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh", + "borsh 1.5.1", "libc", "nix", "qos_crypto", @@ -1783,7 +1851,7 @@ name = "qos_host" version = "0.1.0" dependencies = [ "axum", - "borsh", + "borsh 1.5.1", "qos_core", "qos_hex", "serde", @@ -1795,7 +1863,7 @@ dependencies = [ name = "qos_net" version = "0.1.0" dependencies = [ - "borsh", + "borsh 0.10.3", "hickory-resolver", "qos_core", "qos_test_primitives", @@ -1811,7 +1879,7 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-cose", "aws-nitro-enclaves-nsm-api", - "borsh", + "borsh 1.5.1", "hex-literal", "p384 0.12.0", "qos_hex", @@ -1827,7 +1895,7 @@ name = "qos_p256" version = "0.1.0" dependencies = [ "aes-gcm", - "borsh", + "borsh 1.5.1", "hkdf", "hmac", "p256 0.12.0", @@ -2446,6 +2514,15 @@ dependencies = [ "syn 2.0.68", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.6.6" diff --git a/src/integration/src/bin/pivot_remote_tls.rs b/src/integration/src/bin/pivot_remote_tls.rs index a44af7e4..3d041bfd 100644 --- a/src/integration/src/bin/pivot_remote_tls.rs +++ b/src/integration/src/bin/pivot_remote_tls.rs @@ -4,7 +4,7 @@ use std::{ sync::Arc, }; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use integration::PivotRemoteTlsMsg; use qos_core::{ io::{SocketAddress, TimeVal}, @@ -86,10 +86,9 @@ impl RequestProcessor for Processor { let fetched_content = std::str::from_utf8(&response_bytes).unwrap(); - PivotRemoteTlsMsg::RemoteTlsResponse(format!( + borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsResponse(format!( "Content fetched successfully: {fetched_content}" - )) - .try_to_vec() + ))) .expect("RemoteTlsResponse is valid borsh") } PivotRemoteTlsMsg::RemoteTlsResponse(_) => { 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 index 9513c0bb..b468d989 100644 --- a/src/integration/tests/remote_tls.rs +++ b/src/integration/tests/remote_tls.rs @@ -1,6 +1,5 @@ use std::{process::Command, str}; -use borsh::BorshSerialize; use integration::{PivotRemoteTlsMsg, PIVOT_REMOTE_TLS_PATH, QOS_NET_PATH}; use qos_core::{ client::Client, @@ -34,11 +33,10 @@ fn fetch_remote_tls_content() { TimeVal::seconds(ENCLAVE_APP_SOCKET_CLIENT_TIMEOUT_SECS), ); - let app_request = PivotRemoteTlsMsg::RemoteTlsRequest { + let app_request = borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsRequest { host: "api.turnkey.com".to_string(), path: "/health".to_string(), - } - .try_to_vec() + }) .unwrap(); let response = enclave_client.send(&app_request).unwrap(); @@ -48,11 +46,10 @@ fn fetch_remote_tls_content() { assert!(response_text.contains("HTTP/1.1 200 OK")); assert!(response_text.contains("currentTime")); - let app_request = PivotRemoteTlsMsg::RemoteTlsRequest { + let app_request = borsh::to_vec(&PivotRemoteTlsMsg::RemoteTlsRequest { host: "www.googleapis.com".to_string(), path: "/oauth2/v3/certs".to_string(), - } - .try_to_vec() + }) .unwrap(); let response = enclave_client.send(&app_request).unwrap(); diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 01a5cca4..058f26ae 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -403,7 +403,7 @@ mod test { // Send "PONG" if "PING" was sent if from_utf8(&buf).unwrap() == "PING" { - stream.write(b"PONG").unwrap(); + let _ = stream.write(b"PONG").unwrap(); } // Then shutdown the server From 310ea4f876576ed3b10f490f455791d8388e093f Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Thu, 27 Jun 2024 09:31:54 -0500 Subject: [PATCH 20/21] Minor comment tweaks, bumps borsh to v1 in qos_net --- src/Cargo.lock | 95 ++++----------------------------- src/qos_core/src/io/stream.rs | 4 +- src/qos_net/Cargo.toml | 2 +- src/qos_net/src/cli.rs | 6 +-- src/qos_net/src/proxy.rs | 29 +++++----- src/qos_net/src/proxy_stream.rs | 57 +++++++++----------- 6 files changed, 55 insertions(+), 138 deletions(-) diff --git a/src/Cargo.lock b/src/Cargo.lock index d632434e..9e5d548b 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -52,17 +52,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -328,39 +317,16 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borsh" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" -dependencies = [ - "borsh-derive 0.10.3", - "hashbrown 0.12.3", -] - [[package]] name = "borsh" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ - "borsh-derive 1.5.1", + "borsh-derive", "cfg_aliases", ] -[[package]] -name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate 0.1.5", - "proc-macro2", - "syn 1.0.109", -] - [[package]] name = "borsh-derive" version = "1.5.1" @@ -368,35 +334,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", - "proc-macro-crate 3.1.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.68", "syn_derive", ] -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -952,9 +896,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -1198,7 +1139,7 @@ name = "integration" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh 1.5.1", + "borsh", "nix", "qos_client", "qos_core", @@ -1737,15 +1678,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -1793,7 +1725,7 @@ name = "qos_client" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh 1.5.1", + "borsh", "lazy_static", "p256 0.12.0", "qos_core", @@ -1816,7 +1748,7 @@ name = "qos_core" version = "0.1.0" dependencies = [ "aws-nitro-enclaves-nsm-api", - "borsh 1.5.1", + "borsh", "libc", "nix", "qos_crypto", @@ -1851,7 +1783,7 @@ name = "qos_host" version = "0.1.0" dependencies = [ "axum", - "borsh 1.5.1", + "borsh", "qos_core", "qos_hex", "serde", @@ -1863,7 +1795,7 @@ dependencies = [ name = "qos_net" version = "0.1.0" dependencies = [ - "borsh 0.10.3", + "borsh", "hickory-resolver", "qos_core", "qos_test_primitives", @@ -1879,7 +1811,7 @@ version = "0.1.0" dependencies = [ "aws-nitro-enclaves-cose", "aws-nitro-enclaves-nsm-api", - "borsh 1.5.1", + "borsh", "hex-literal", "p384 0.12.0", "qos_hex", @@ -1895,7 +1827,7 @@ name = "qos_p256" version = "0.1.0" dependencies = [ "aes-gcm", - "borsh 1.5.1", + "borsh", "hkdf", "hmac", "p256 0.12.0", @@ -2514,15 +2446,6 @@ dependencies = [ "syn 2.0.68", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml_datetime" version = "0.6.6" diff --git a/src/qos_core/src/io/stream.rs b/src/qos_core/src/io/stream.rs index 058f26ae..a695694a 100644 --- a/src/qos_core/src/io/stream.rs +++ b/src/qos_core/src/io/stream.rs @@ -442,8 +442,8 @@ mod test { fn stream_implements_read_write_traits() { let socket_server_path = "./stream_implements_read_write_traits.sock"; - // Start a barebone socket server which replies "Roger that." to any - // incoming request + // 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 || { diff --git a/src/qos_net/Cargo.toml b/src/qos_net/Cargo.toml index 8643ede2..ec09b49d 100644 --- a/src/qos_net/Cargo.toml +++ b/src/qos_net/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] qos_core = { path = "../qos_core", default-features = false } -borsh = { version = "0.10" } +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 } diff --git a/src/qos_net/src/cli.rs b/src/qos_net/src/cli.rs index f5345880..14cc9f99 100644 --- a/src/qos_net/src/cli.rs +++ b/src/qos_net/src/cli.rs @@ -17,7 +17,7 @@ pub const PORT: &str = "port"; /// "usock" pub const USOCK: &str = "usock"; -/// CLI options for starting up the enclave server. +/// CLI options for starting up the proxy. #[derive(Default, Clone, Debug, PartialEq)] struct ProxyOpts { parsed: Parser, @@ -55,10 +55,10 @@ impl ProxyOpts { } } -/// Enclave server CLI. +/// Proxy CLI. pub struct CLI; impl CLI { - /// Execute the enclave server CLI with the environment args. + /// 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); diff --git a/src/qos_net/src/proxy.rs b/src/qos_net/src/proxy.rs index 437d5287..6018ece2 100644 --- a/src/qos_net/src/proxy.rs +++ b/src/qos_net/src/proxy.rs @@ -1,7 +1,7 @@ //! Protocol proxy for our remote QOS net proxy use std::io::{Read, Write}; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use qos_core::server; use crate::{ @@ -214,9 +214,10 @@ impl Proxy { impl server::RequestProcessor for Proxy { fn process(&mut self, req_bytes: Vec) -> Vec { if req_bytes.len() > MAX_ENCODED_MSG_LEN { - return ProxyMsg::ProxyError(QosNetError::OversizedPayload) - .try_to_vec() - .expect("ProtocolMsg can always be serialized. qed."); + return borsh::to_vec(&ProxyMsg::ProxyError( + QosNetError::OversizedPayload, + )) + .expect("ProtocolMsg can always be serialized. qed."); } let resp = match ProxyMsg::try_from_slice(&req_bytes) { @@ -278,7 +279,7 @@ impl server::RequestProcessor for Proxy { Err(_) => ProxyMsg::ProxyError(QosNetError::InvalidMsg), }; - resp.try_to_vec() + borsh::to_vec(&resp) .expect("Protocol message can always be serialized. qed!") } } @@ -294,7 +295,7 @@ mod test { #[test] fn simple_status_request() { let mut proxy = Proxy::new(); - let request = ProxyMsg::StatusRequest.try_to_vec().unwrap(); + 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)); @@ -305,13 +306,12 @@ mod test { let mut proxy = Proxy::new(); assert_eq!(proxy.num_connections(), 0); - let request = ProxyMsg::ConnectByNameRequest { + 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, - } - .try_to_vec() + }) .unwrap(); let response = proxy.process(request); let msg = ProxyMsg::try_from_slice(&response).unwrap(); @@ -325,11 +325,10 @@ mod test { }; let http_request = "GET / HTTP/1.1\r\nHost: api.turnkey.com\r\nConnection: close\r\n\r\n".to_string(); - let request = ProxyMsg::WriteRequest { + let request = borsh::to_vec(&ProxyMsg::WriteRequest { connection_id, data: http_request.as_bytes().to_vec(), - } - .try_to_vec() + }) .unwrap(); let response = proxy.process(request); let msg: ProxyMsg = ProxyMsg::try_from_slice(&response).unwrap(); @@ -341,9 +340,9 @@ mod test { // Check that we now have an active connection assert_eq!(proxy.num_connections(), 1); - let request = ProxyMsg::ReadRequest { connection_id, size: 512 } - .try_to_vec() - .unwrap(); + 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 { diff --git a/src/qos_net/src/proxy_stream.rs b/src/qos_net/src/proxy_stream.rs index b3e35fe1..4d9a158d 100644 --- a/src/qos_net/src/proxy_stream.rs +++ b/src/qos_net/src/proxy_stream.rs @@ -2,7 +2,7 @@ //! traits with `ProxyMsg`s. use std::io::{ErrorKind, Read, Write}; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use qos_core::io::{SocketAddress, Stream, TimeVal}; use crate::{error::QosNetError, proxy_msg::ProxyMsg}; @@ -49,13 +49,12 @@ impl ProxyStream { dns_port: u16, ) -> Result { let stream = Stream::connect(addr, timeout)?; - let req = ProxyMsg::ConnectByNameRequest { + let req = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { hostname: hostname.clone(), port, dns_resolvers, dns_port, - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); stream.send(&req)?; let resp_bytes = stream.recv()?; @@ -93,8 +92,7 @@ impl ProxyStream { port: u16, ) -> Result { let stream: Stream = Stream::connect(addr, timeout)?; - let req = ProxyMsg::ConnectByIpRequest { ip, port } - .try_to_vec() + let req = borsh::to_vec(&ProxyMsg::ConnectByIpRequest { ip, port }) .expect("ProtocolMsg can always be serialized."); stream.send(&req)?; let resp_bytes = stream.recv()?; @@ -119,9 +117,10 @@ impl ProxyStream { /// Close the remote connection pub fn close(&mut self) -> Result<(), QosNetError> { let stream: Stream = Stream::connect(&self.addr, self.timeout)?; - let req = ProxyMsg::CloseRequest { connection_id: self.connection_id } - .try_to_vec() - .expect("ProtocolMsg can always be serialized."); + 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()?; @@ -145,11 +144,10 @@ impl Read for ProxyStream { ) })?; - let req = ProxyMsg::ReadRequest { + let req = borsh::to_vec(&ProxyMsg::ReadRequest { connection_id: self.connection_id, size: buf.len(), - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); stream.send(&req).map_err(|e| { std::io::Error::new( @@ -210,11 +208,10 @@ impl Write for ProxyStream { ) })?; - let req = ProxyMsg::WriteRequest { + let req = borsh::to_vec(&ProxyMsg::WriteRequest { connection_id: self.connection_id, data: buf.to_vec(), - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); stream.send(&req).map_err(|e| { std::io::Error::new( @@ -262,9 +259,10 @@ impl Write for ProxyStream { ) })?; - let req = ProxyMsg::FlushRequest { connection_id: self.connection_id } - .try_to_vec() - .expect("ProtocolMsg can always be serialized."); + 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( @@ -388,13 +386,12 @@ mod test { dns_resolvers: Vec, dns_port: u16, ) -> Result { - let req = ProxyMsg::ConnectByNameRequest { + let req = borsh::to_vec(&ProxyMsg::ConnectByNameRequest { hostname: hostname.clone(), port, dns_resolvers, dns_port, - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); let mut proxy = Box::new(Proxy::new()); let resp_bytes = proxy.process(req); @@ -429,11 +426,10 @@ mod test { impl Read for LocalStream { fn read(&mut self, buf: &mut [u8]) -> Result { - let req = ProxyMsg::ReadRequest { + let req = borsh::to_vec(&ProxyMsg::ReadRequest { connection_id: self.connection_id, size: buf.len(), - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); let resp_bytes = self.proxy.process(req); @@ -475,11 +471,10 @@ mod test { impl Write for LocalStream { fn write(&mut self, buf: &[u8]) -> Result { - let req = ProxyMsg::WriteRequest { + let req = borsh::to_vec(&ProxyMsg::WriteRequest { connection_id: self.connection_id, data: buf.to_vec(), - } - .try_to_vec() + }) .expect("ProtocolMsg can always be serialized."); let resp_bytes = self.proxy.process(req); @@ -507,10 +502,10 @@ mod test { } fn flush(&mut self) -> Result<(), std::io::Error> { - let req = - ProxyMsg::FlushRequest { connection_id: self.connection_id } - .try_to_vec() - .expect("ProtocolMsg can always be serialized."); + 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) { From 3081cfe07ea02b80d2f2118c575fbc1ef8f537e4 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Thu, 27 Jun 2024 11:36:34 -0500 Subject: [PATCH 21/21] Remove mono from CHANGELOG --- CHANGELOG.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 8dd6e849..e3da98f8 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -4,7 +4,7 @@ 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/). This project used to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) but we've since -moved out of versioning: `mono` references `qos` with a submodule (git SHA pointer). +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.