diff --git a/.gitignore b/.gitignore index f102743734..b8df49d1a7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ validator-config *.patch validator-api-config.toml dist -storybook-static \ No newline at end of file +storybook-static +envs/qwerty.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b03ddd8ba..1c9ae78385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - validator-api: can recover from shutdown during DKG process ([#1872]) - clients: deduplicate gateway inititialization, part of work towards a rust-sdk - clients: keep all transmission lanes going at all times by making priority probabilistic +- clients: ability to use multi-reply SURBs to send arbitrarily long messages fully anonymously whilst requesting additional reply blocks whenever they're about to run out ([#1796], [#1801], [#1804], [#1835], [#1858], [#1883])) ### Fixed @@ -16,11 +17,18 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - clients: fix a few panics handling the gateway-client - mixnode, gateway, validator-api: Use mainnet values as defaults for URLs and mixnet contract ([#1884]) - socks5: fixed bug where connections sometimes where closed too early +- clients: improve message logging when received message fails to get reconstructed ([#1803]) +[#1796]: https://github.com/nymtech/nym/pull/1796 +[#1801]: https://github.com/nymtech/nym/pull/1801 +[#1803]: https://github.com/nymtech/nym/pull/1803 +[#1804]: https://github.com/nymtech/nym/pull/1804 +[#1835]: https://github.com/nymtech/nym/pull/1835 +[#1858]: https://github.com/nymtech/nym/pull/1858 [#1872]: https://github.com/nymtech/nym/pull/1872 +[#1883]: https://github.com/nymtech/nym/pull/1883 [#1884]: https://github.com/nymtech/nym/pull/1884 - ## [v1.1.2] ### Changed diff --git a/Cargo.lock b/Cargo.lock index e755f118fc..822fa26726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -588,9 +588,11 @@ dependencies = [ name = "client-core" version = "1.1.2" dependencies = [ + "async-trait", "client-connections", "config", "crypto", + "dashmap 5.4.0", "dirs", "futures", "gateway-client", @@ -604,11 +606,12 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "sled", + "sqlx 0.6.2", "tap", "task", "tempfile", "thiserror", + "time 0.3.17", "tokio", "tokio-stream", "topology", @@ -617,6 +620,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", + "wasm-utils", ] [[package]] @@ -782,7 +786,7 @@ dependencies = [ "rand 0.8.5", "sha2 0.10.2", "subtle 2.4.1", - "time 0.3.14", + "time 0.3.17", "version_check", ] @@ -1350,6 +1354,19 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.4", +] + [[package]] name = "der" version = "0.5.1" @@ -1798,16 +1815,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1920,15 +1927,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gateway-client" version = "0.1.0" @@ -2274,13 +2272,14 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" [[package]] name = "hidapi" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d26e1151deaab68f34fbfd16d491a2a0170cf98d69d3efa23873b567a4199e1" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" dependencies = [ "cc", "libc", "pkg-config", + "winapi", ] [[package]] @@ -2611,9 +2610,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -2759,9 +2758,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2913,7 +2912,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", - "time 0.3.14", + "time 0.3.17", "ts-rs", ] @@ -3090,15 +3089,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" -dependencies = [ - "libc", -] - [[package]] name = "nym-bity-integration" version = "0.1.0" @@ -3161,7 +3151,7 @@ dependencies = [ "serde_json", "tap", "thiserror", - "time 0.3.14", + "time 0.3.17", "toml", "url", "validator-client", @@ -3194,7 +3184,6 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "sled", "tap", "task", "thiserror", @@ -3223,7 +3212,7 @@ dependencies = [ "config", "credentials", "crypto", - "dashmap", + "dashmap 4.0.2", "dirs", "dotenv", "futures", @@ -3464,7 +3453,7 @@ dependencies = [ "tap", "task", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tokio-stream", "topology", @@ -3537,6 +3526,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "rand_distr", + "thiserror", "tokio", "topology", ] @@ -3562,6 +3552,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", ] [[package]] @@ -3575,7 +3566,9 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", "topology", + "wasm-bindgen", ] [[package]] @@ -3587,6 +3580,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", ] [[package]] @@ -3601,6 +3595,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", "topology", ] @@ -3652,9 +3647,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oorandom" @@ -3756,7 +3751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core 0.9.2", + "parking_lot_core 0.9.4", ] [[package]] @@ -3775,15 +3770,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec 1.8.0", - "windows-sys 0.34.0", + "windows-sys 0.42.0", ] [[package]] @@ -4059,11 +4054,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -4622,7 +4617,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.14", + "time 0.3.17", "tokio", "tokio-stream", "tokio-util 0.7.3", @@ -4684,7 +4679,7 @@ dependencies = [ "smallvec 1.8.0", "stable-pattern", "state", - "time 0.3.14", + "time 0.3.17", "tokio", "uncased", ] @@ -5165,22 +5160,6 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "0.6.14" @@ -5198,9 +5177,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "snafu" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5177903bf45656592d9eb5c0e22f408fc023aae51dbe2088889b71633ba451f2" +checksum = "a152ba99b054b22972ee794cf04e5ef572da1229e33b65f3c57abbff0525a454" dependencies = [ "doc-comment", "snafu-derive", @@ -5208,9 +5187,9 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410b26ed97440d90ced3e2488c868d56a86e2064f5d7d6f417909b286afe25e5" +checksum = "d5e79cdebbabaebb06a9bdbaedc7f159b410461f63611d4d0e3fb0fab8fed850" dependencies = [ "heck 0.4.0", "proc-macro2", @@ -5239,7 +5218,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792418eee932859#c494250f2a78bed33a618d470792418eee932859" +source = "git+https://github.com/nymtech/sphinx?rev=e05a1992522ed0afd3c6fcac160313ffc9bb306a#e05a1992522ed0afd3c6fcac160313ffc9bb306a" dependencies = [ "aes 0.7.5", "arrayref", @@ -5597,13 +5576,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.91" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -5692,7 +5671,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.14", + "time 0.3.17", "zeroize", ] @@ -5725,7 +5704,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -5753,7 +5732,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tracing", "url", @@ -5827,22 +5806,31 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.1", - "libc", - "num_threads", + "js-sys", "serde", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tinytemplate" @@ -5856,9 +5844,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg 1.1.0", "bytes", @@ -6070,6 +6058,7 @@ dependencies = [ "nymsphinx-addressing", "nymsphinx-types", "rand 0.7.3", + "thiserror", "version-checker", ] @@ -6329,6 +6318,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + [[package]] name = "unicode-normalization" version = "0.1.9" @@ -6499,7 +6494,7 @@ dependencies = [ "rustc_version 0.4.0", "rustversion", "thiserror", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -6772,19 +6767,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" -dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", -] - [[package]] name = "windows-sys" version = "0.36.1" @@ -6799,10 +6781,25 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" +name = "windows-sys" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" [[package]] name = "windows_aarch64_msvc" @@ -6811,10 +6808,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] -name = "windows_i686_gnu" -version = "0.34.0" +name = "windows_aarch64_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" @@ -6823,10 +6820,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] -name = "windows_i686_msvc" -version = "0.34.0" +name = "windows_i686_gnu" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" @@ -6835,10 +6832,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" +name = "windows_i686_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" @@ -6847,10 +6844,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] -name = "windows_x86_64_msvc" -version = "0.34.0" +name = "windows_x86_64_gnu" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" @@ -6858,6 +6861,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "winreg" version = "0.10.1" diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index 56edca335b..87595d125f 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -7,18 +7,20 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { version = "0.1.58" } dirs = "4.0" +dashmap = "5.4.0" futures = "0.3" humantime-serde = "1.0" log = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.89" -sled = { version = "0.34", optional = true } tap = "1.0.1" thiserror = "1.0.34" -tokio = { version = "1.21.2", features = ["time", "macros"]} url = { version ="2.2", features = ["serde"] } +tokio = { version = "1.21.2", features = ["macros"]} +time = "0.3.17" # internal config = { path = "../../common/config" } @@ -38,6 +40,15 @@ task = { path = "../../common/task" } version = "0.1.9" features = ["time"] +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +version = "1.21.2" +features = ["time"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +version = "0.6.2" +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +optional = true + [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] version = "0.4" @@ -52,15 +63,23 @@ rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" version = "0.2.4" features = ["futures"] -#[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] -#path = "../../common/task" +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils] +path = "../../common/wasm-utils" + +[target."cfg(target_arch = \"wasm32\")".dependencies.time] +version = "0.3.17" +features = ["wasm-bindgen"] [dev-dependencies] tempfile = "3.1.0" +[build-dependencies] +tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + [features] -default = ["reply-surb"] +default = [] +fs-surb-storage = ["sqlx"] wasm = ["gateway-client/wasm"] coconut = ["gateway-client/coconut", "gateway-requests/coconut"] -reply-surb = ["sled"] diff --git a/clients/client-core/build.rs b/clients/client-core/build.rs new file mode 100644 index 0000000000..bac1b20c53 --- /dev/null +++ b/clients/client-core/build.rs @@ -0,0 +1,31 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[tokio::main] +async fn main() { + #[cfg(feature = "fs-surb-storage")] + { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{}/fs-surbs-example.sqlite", out_dir); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./fs_surbs_migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + } +} diff --git a/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql b/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql new file mode 100644 index 0000000000..0143475a37 --- /dev/null +++ b/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql @@ -0,0 +1,40 @@ +CREATE TABLE status +( + flush_in_progress INTEGER NOT NULL, + previous_flush_timestamp INTEGER NOT NULL, + client_in_use INTEGER NOT NULL +); + +CREATE TABLE reply_surb_storage_metadata +( + min_reply_surb_threshold INTEGER NOT NULL, + max_reply_surb_threshold INTEGER NOT NULL +); + +CREATE TABLE sender_tag +( + recipient BLOB NOT NULL UNIQUE, + tag BLOB NOT NULL UNIQUE +); + +CREATE TABLE reply_key +( + key_digest BLOB NOT NULL UNIQUE, + reply_key BLOB NOT NULL UNIQUE, + sent_at_timestamp INTEGER NOT NULL +); + +CREATE TABLE reply_surb_sender +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + last_sent_timestamp INTEGER NOT NULL, + tag BLOB NOT NULL UNIQUE +); + +CREATE TABLE reply_surb +( + reply_surb_sender_id INTEGER NOT NULL, + reply_surb BLOB NOT NULL, + + FOREIGN KEY (reply_surb_sender_id) REFERENCES reply_surb_sender (id) +); \ No newline at end of file diff --git a/clients/client-core/src/client/base_client/mod.rs b/clients/client-core/src/client/base_client/mod.rs index 07a4705bd5..932fb77b31 100644 --- a/clients/client-core/src/client/base_client/mod.rs +++ b/clients/client-core/src/client/base_client/mod.rs @@ -10,13 +10,19 @@ use crate::client::real_messages_control::RealMessagesController; use crate::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, }; +use crate::client::replies::reply_controller; +use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender}; +use crate::client::replies::reply_storage::{ + CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, +}; use crate::client::topology_control::{ TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, }; use crate::config::{Config, DebugConfig, GatewayEndpointConfig}; use crate::error::ClientCoreError; +use crate::spawn_future; use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; -use crypto::asymmetric::identity; +use crypto::asymmetric::{encryption, identity}; use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use gateway_client::{ @@ -24,18 +30,17 @@ use gateway_client::{ MixnetMessageSender, }; use log::{debug, info}; +use nymsphinx::acknowledgements::AckKey; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; -#[cfg(feature = "reply-surb")] -use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; use tap::TapFallible; use task::{ShutdownListener, ShutdownNotifier}; use url::Url; -// it's fine to do this disgusting compilation flag business here as this problem -// is going to go away in 1.2.0 -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +pub mod non_wasm_helpers; pub struct ClientInput { pub shared_lane_queue_lengths: LaneQueueLengths, @@ -75,35 +80,36 @@ impl ClientOutputStatus { } } -pub struct BaseClientBuilder<'a> { +pub struct BaseClientBuilder<'a, B> { // due to wasm limitations I had to split it like this : ( gateway_config: &'a GatewayEndpointConfig, debug_config: &'a DebugConfig, disabled_credentials: bool, validator_api_endpoints: Vec, - - #[cfg(feature = "reply-surb")] - reply_surb_keys_store_path: PathBuf, + reply_storage_backend: B, bandwidth_controller: Option, key_manager: KeyManager, } -impl<'a> BaseClientBuilder<'a> { +impl<'a, B> BaseClientBuilder<'a, B> +where + B: ReplyStorageBackend + Send + Sync + 'static, +{ pub fn new_from_base_config( base_config: &'a Config, key_manager: KeyManager, bandwidth_controller: Option, - ) -> BaseClientBuilder<'a> { + reply_storage_backend: B, + ) -> BaseClientBuilder<'a, B> { BaseClientBuilder { gateway_config: base_config.get_gateway_endpoint_config(), debug_config: base_config.get_debug_config(), disabled_credentials: base_config.get_disabled_credentials_mode(), validator_api_endpoints: base_config.get_validator_api_endpoints(), bandwidth_controller, + reply_storage_backend, key_manager, - #[cfg(feature = "reply-surb")] - reply_surb_keys_store_path: base_config.get_reply_encryption_key_store_path(), } } @@ -112,19 +118,18 @@ impl<'a> BaseClientBuilder<'a> { debug_config: &'a DebugConfig, key_manager: KeyManager, bandwidth_controller: Option, + reply_storage_backend: B, disabled_credentials: bool, validator_api_endpoints: Vec, - #[cfg(feature = "reply-surb")] reply_surb_keys_store_path: PathBuf, - ) -> BaseClientBuilder<'a> { + ) -> BaseClientBuilder<'a, B> { BaseClientBuilder { gateway_config, debug_config, disabled_credentials, validator_api_endpoints, bandwidth_controller, + reply_storage_backend, key_manager, - #[cfg(feature = "reply-surb")] - reply_surb_keys_store_path, } } @@ -141,7 +146,9 @@ impl<'a> BaseClientBuilder<'a> { // future constantly pumping loop cover traffic at some specified average rate // the pumped traffic goes to the MixTrafficController fn start_cover_traffic_stream( - &self, + debug_config: &DebugConfig, + ack_key: Arc, + self_address: Recipient, topology_accessor: TopologyAccessor, mix_tx: BatchMixMessageSender, shutdown: ShutdownListener, @@ -149,16 +156,16 @@ impl<'a> BaseClientBuilder<'a> { info!("Starting loop cover traffic stream..."); let mut stream = LoopCoverTrafficStream::new( - self.key_manager.ack_key(), - self.debug_config.average_ack_delay, - self.debug_config.average_packet_delay, - self.debug_config.loop_cover_traffic_average_delay, + ack_key, + debug_config.average_ack_delay, + debug_config.average_packet_delay, + debug_config.loop_cover_traffic_average_delay, mix_tx, - self.as_mix_recipient(), + self_address, topology_accessor, ); - if let Some(size) = self.debug_config.use_extended_packet_size { + if let Some(size) = debug_config.use_extended_packet_size { log::debug!("Setting extended packet size: {:?}", size); stream.set_custom_packet_size(size.into()); } @@ -168,32 +175,18 @@ impl<'a> BaseClientBuilder<'a> { #[allow(clippy::too_many_arguments)] fn start_real_traffic_controller( - &self, + controller_config: real_messages_control::Config, topology_accessor: TopologyAccessor, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, + reply_storage: CombinedReplyStorage, + reply_controller_sender: ReplyControllerSender, + reply_controller_receiver: ReplyControllerReceiver, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, shutdown: ShutdownListener, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) { - let mut controller_config = real_messages_control::Config::new( - self.key_manager.ack_key(), - self.debug_config.ack_wait_multiplier, - self.debug_config.ack_wait_addition, - self.debug_config.average_ack_delay, - self.debug_config.message_sending_average_delay, - self.debug_config.average_packet_delay, - self.debug_config.disable_main_poisson_packet_distribution, - self.as_mix_recipient(), - ); - - if let Some(size) = self.debug_config.use_extended_packet_size { - log::debug!("Setting extended packet size: {:?}", size); - controller_config.set_custom_packet_size(size.into()); - } - info!("Starting real traffic stream..."); RealMessagesController::new( @@ -202,10 +195,11 @@ impl<'a> BaseClientBuilder<'a> { input_receiver, mix_sender, topology_accessor, + reply_storage, + reply_controller_sender, + reply_controller_receiver, lane_queue_lengths, client_connection_rx, - #[cfg(feature = "reply-surb")] - reply_key_storage, ) .start_with_shutdown(shutdown); } @@ -213,19 +207,20 @@ impl<'a> BaseClientBuilder<'a> { // buffer controlling all messages fetched from provider // required so that other components would be able to use them (say the websocket) fn start_received_messages_buffer_controller( - &self, + local_encryption_keypair: Arc, query_receiver: ReceivedBufferRequestReceiver, mixnet_receiver: MixnetMessageReceiver, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, shutdown: ShutdownListener, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) { info!("Starting received messages buffer controller..."); ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), + local_encryption_keypair, query_receiver, mixnet_receiver, - #[cfg(feature = "reply-surb")] reply_key_storage, + reply_controller_sender, ) .start_with_shutdown(shutdown) } @@ -235,7 +230,7 @@ impl<'a> BaseClientBuilder<'a> { mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, shutdown: ShutdownListener, - ) -> Result { + ) -> Result> { let gateway_id = self.gateway_config.gateway_id.clone(); if gateway_id.is_empty() { return Err(ClientCoreError::GatewayIdUnknown); @@ -286,13 +281,14 @@ impl<'a> BaseClientBuilder<'a> { // future responsible for periodically polling directory server and updating // the current global view of topology async fn start_topology_refresher( - &mut self, + validator_api_urls: Vec, + refresh_rate: Duration, topology_accessor: TopologyAccessor, shutdown: ShutdownListener, - ) -> Result<(), ClientCoreError> { + ) -> Result<(), ClientCoreError> { let topology_refresher_config = TopologyRefresherConfig::new( - self.validator_api_endpoints.clone(), - self.debug_config.topology_refresh_rate, + validator_api_urls, + refresh_rate, env!("CARGO_PKG_VERSION").to_string(), ); let mut topology_refresher = @@ -302,13 +298,12 @@ impl<'a> BaseClientBuilder<'a> { info!("Obtaining initial network topology"); topology_refresher.refresh().await; - // TODO: a slightly more graceful termination here - if !topology_refresher.is_topology_routable().await { + if let Err(err) = topology_refresher.ensure_topology_is_routable().await { log::error!( "The current network topology seem to be insufficient to route any packets through \ - - check if enough nodes and a gateway are online" + - check if enough nodes and a gateway are online - source: {err}" ); - return Err(ClientCoreError::InsufficientNetworkTopology); + return Err(ClientCoreError::InsufficientNetworkTopology(err)); } info!("Starting topology refresher..."); @@ -330,7 +325,27 @@ impl<'a> BaseClientBuilder<'a> { mix_tx } - pub async fn start_base(mut self) -> Result { + async fn setup_persistent_reply_storage( + backend: B, + shutdown: ShutdownListener, + ) -> Result> { + let persistent_storage = PersistentReplyStorage::new(backend); + let mem_store = persistent_storage + .load_state_from_backend() + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + let store_clone = mem_store.clone(); + spawn_future(async move { + persistent_storage + .flush_on_shutdown(store_clone, shutdown) + .await + }); + + Ok(mem_store) + } + + pub async fn start_base(mut self) -> Result> { info!("Starting nym client"); // channels for inter-component communication // TODO: make the channels be internally created by the relevant components @@ -351,32 +366,42 @@ impl<'a> BaseClientBuilder<'a> { let (ack_sender, ack_receiver) = mpsc::unbounded(); let shared_topology_accessor = TopologyAccessor::new(); - #[cfg(feature = "reply-surb")] - let reply_key_storage = - ReplyKeyStorage::load(&self.reply_surb_keys_store_path).tap_err(|err| { - log::error!("Failed to load reply key storage - is it perhaps already in use?"); - log::error!("{:?}", err); - })?; - // Shutdown notifier for signalling tasks to stop let shutdown = ShutdownNotifier::default(); + // channels responsible for dealing with reply-related fun + let (reply_controller_sender, reply_controller_receiver) = + reply_controller::new_control_channels(); + + let self_address = self.as_mix_recipient(); + // the components are started in very specific order. Unless you know what you are doing, // do not change that. - self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe()) - .await?; - self.start_received_messages_buffer_controller( - received_buffer_request_receiver, - mixnet_messages_receiver, - shutdown.subscribe(), - #[cfg(feature = "reply-surb")] - reply_key_storage.clone(), - ); - let gateway_client = self .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) .await?; + let reply_storage = + Self::setup_persistent_reply_storage(self.reply_storage_backend, shutdown.subscribe()) + .await?; + + Self::start_topology_refresher( + self.validator_api_endpoints.clone(), + self.debug_config.topology_refresh_rate, + shared_topology_accessor.clone(), + shutdown.subscribe(), + ) + .await?; + + Self::start_received_messages_buffer_controller( + self.key_manager.encryption_keypair(), + received_buffer_request_receiver, + mixnet_messages_receiver, + reply_storage.key_storage(), + reply_controller_sender.clone(), + shutdown.subscribe(), + ); + // The sphinx_message_sender is the transmitter for any component generating sphinx packets // that are to be sent to the mixnet. They are used by cover traffic stream and real // traffic stream. @@ -392,20 +417,36 @@ impl<'a> BaseClientBuilder<'a> { // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) let shared_lane_queue_lengths = LaneQueueLengths::new(); - self.start_real_traffic_controller( + let mut controller_config = real_messages_control::Config::new( + self.debug_config, + self.key_manager.ack_key(), + self_address, + ); + + if let Some(size) = self.debug_config.use_extended_packet_size { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); + } + + Self::start_real_traffic_controller( + controller_config, shared_topology_accessor.clone(), ack_receiver, input_receiver, sphinx_message_sender.clone(), + reply_storage, + reply_controller_sender, + reply_controller_receiver, shared_lane_queue_lengths.clone(), client_connection_rx, shutdown.subscribe(), - #[cfg(feature = "reply-surb")] - reply_key_storage, ); if !self.debug_config.disable_loop_cover_traffic_stream { - self.start_cover_traffic_stream( + Self::start_cover_traffic_stream( + self.debug_config, + self.key_manager.ack_key(), + self_address, shared_topology_accessor, sphinx_message_sender, shutdown.subscribe(), @@ -413,7 +454,7 @@ impl<'a> BaseClientBuilder<'a> { } debug!("Core client startup finished!"); - debug!("The address of this client is: {}", self.as_mix_recipient()); + debug!("The address of this client is: {self_address}"); Ok(BaseClient { client_input: ClientInputStatus::AwaitingProducer { diff --git a/clients/client-core/src/client/base_client/non_wasm_helpers.rs b/clients/client-core/src/client/base_client/non_wasm_helpers.rs new file mode 100644 index 0000000000..8c51c784c8 --- /dev/null +++ b/clients/client-core/src/client/base_client/non_wasm_helpers.rs @@ -0,0 +1,51 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::{ + fs_backend, CombinedReplyStorage, ReplyStorageBackend, +}; +use crate::config::DebugConfig; +use crate::error::ClientCoreError; +use log::{error, info}; +use std::path::Path; + +pub async fn setup_fs_reply_surb_backend>( + db_path: P, + debug_config: &DebugConfig, +) -> Result> { + // if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one + let db_path = db_path.as_ref(); + if db_path.exists() { + info!("loading existing surb database"); + match fs_backend::Backend::try_load(db_path).await { + Ok(backend) => Ok(backend), + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + Err(ClientCoreError::SurbStorageError { source: err }) + } + } + } else { + info!("creating fresh surb database"); + let mut storage_backend = match fs_backend::Backend::init(db_path).await { + Ok(backend) => backend, + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + return Err(ClientCoreError::SurbStorageError { source: err }); + } + }; + + // while I kinda hate that we're going to be creating `CombinedReplyStorage` twice, + // it will only be happening on the very first run and in practice won't incur huge + // costs since the storage is going to be empty + let mem_store = CombinedReplyStorage::new( + debug_config.minimum_reply_surb_storage_threshold, + debug_config.maximum_reply_surb_storage_threshold, + ); + storage_backend + .init_fresh(&mem_store) + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + Ok(storage_backend) + } +} diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index b862f1bcfe..e3fd0ca197 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -161,15 +161,16 @@ impl LoopCoverTrafficStream { // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) - let topology_ref_option = topology_permit.try_get_valid_topology_ref( + let topology_ref = match topology_permit.try_get_valid_topology_ref( &self.our_full_destination, Some(&self.our_full_destination), - ); - if topology_ref_option.is_none() { - warn!("No valid topology detected - won't send any loop cover message this time"); - return; - } - let topology_ref = topology_ref_option.unwrap(); + ) { + Ok(topology) => topology, + Err(err) => { + warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + return; + } + }; let cover_message = generate_loop_cover_packet( &mut self.rng, @@ -241,19 +242,4 @@ impl LoopCoverTrafficStream { log::debug!("LoopCoverTrafficStream: Exiting"); }) } - - pub fn start(mut self) { - // we should set initial delay only when we actually start the stream - let sampled = - sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); - self.set_next_delay(sampled); - - spawn_future(async move { - debug!("Started LoopCoverTrafficStream without graceful shutdown support"); - - while self.next().await.is_some() { - self.on_new_message().await; - } - }) - } } diff --git a/clients/client-core/src/client/inbound_messages.rs b/clients/client-core/src/client/inbound_messages.rs index 909e3720f9..890787aa60 100644 --- a/clients/client-core/src/client/inbound_messages.rs +++ b/clients/client-core/src/client/inbound_messages.rs @@ -1,40 +1,80 @@ use client_connections::TransmissionLane; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; pub type InputMessageSender = tokio::sync::mpsc::Sender; pub type InputMessageReceiver = tokio::sync::mpsc::Receiver; #[derive(Debug)] pub enum InputMessage { - Fresh { + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant + Regular { recipient: Recipient, data: Vec, - with_reply_surb: bool, lane: TransmissionLane, }, - Reply { - reply_surb: ReplySurb, + + /// Creates a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + Anonymous { + recipient: Recipient, data: Vec, + reply_surbs: u32, + lane: TransmissionLane, + }, + + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + Reply { + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, }, } impl InputMessage { - pub fn new_fresh( - recipient: Recipient, - data: Vec, - with_reply_surb: bool, - lane: TransmissionLane, - ) -> Self { - InputMessage::Fresh { + pub fn new_regular(recipient: Recipient, data: Vec, lane: TransmissionLane) -> Self { + InputMessage::Regular { recipient, data, - with_reply_surb, lane, } } - pub fn new_reply(reply_surb: ReplySurb, data: Vec) -> Self { - InputMessage::Reply { reply_surb, data } + pub fn new_anonymous( + recipient: Recipient, + data: Vec, + reply_surbs: u32, + lane: TransmissionLane, + ) -> Self { + InputMessage::Anonymous { + recipient, + data, + reply_surbs, + lane, + } + } + + pub fn new_reply( + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) -> Self { + InputMessage::Reply { + recipient_tag, + data, + lane, + } } } diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index 0d3aab9513..f61a83af95 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -91,14 +91,4 @@ impl MixTrafficController { log::debug!("MixTrafficController: Exiting"); }) } - - pub fn start(mut self) { - spawn_future(async move { - debug!("Started MixTrafficController without graceful shutdown support"); - - while let Some(mix_packets) = self.mix_rx.recv().await { - self.on_messages(mix_packets).await; - } - }) - } } diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index 8c09544ce6..8110d52909 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -8,6 +8,5 @@ pub mod key_manager; pub mod mix_traffic; pub mod real_messages_control; pub mod received_buffer; -#[cfg(feature = "reply-surb")] -pub mod reply_key_storage; +pub mod replies; pub mod topology_control; diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index bd7b34e93d..95c431fd59 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; +use super::action_controller::{AckActionSender, Action}; use futures::StreamExt; use gateway_client::AcknowledgementReceiver; use log::*; @@ -16,14 +16,14 @@ use std::sync::Arc; pub(super) struct AcknowledgementListener { ack_key: Arc, ack_receiver: AcknowledgementReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, } impl AcknowledgementListener { pub(super) fn new( ack_key: Arc, ack_receiver: AcknowledgementReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, ) -> Self { AcknowledgementListener { ack_key, @@ -49,11 +49,6 @@ impl AcknowledgementListener { if frag_id == COVER_FRAG_ID { trace!("Received an ack for a cover message - no need to do anything"); return; - } else if frag_id.is_reply() { - info!("Received an ack for a reply message - no need to do anything! (don't know what to do!)"); - // TODO: probably there will need to be some extra procedure here, something to notify - // user that his reply reached the recipient (since we got an ack) - return; } trace!("Received {} from the mix network", frag_id); @@ -90,14 +85,4 @@ impl AcknowledgementListener { shutdown.recv_timeout().await; log::debug!("AcknowledgementListener: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started AcknowledgementListener without graceful shutdown support"); - - while let Some(acks) = self.ack_receiver.next().await { - self.handle_ack_receiver_item(acks).await - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 09cf48185a..cb2e0cc7ca 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -3,7 +3,7 @@ use super::PendingAcknowledgement; use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender; -use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc; use futures::StreamExt; use log::*; use nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; @@ -13,7 +13,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -pub(crate) type ActionSender = UnboundedSender; +pub(crate) type AckActionSender = mpsc::UnboundedSender; +pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver; // The actual data being sent off as well as potential key to the delay queue type PendingAckEntry = (Arc, Option); @@ -95,7 +96,7 @@ pub(super) struct ActionController { pending_acks_timers: NonExhaustiveDelayQueue, /// Channel for receiving `Action`s from other modules. - incoming_actions: UnboundedReceiver, + incoming_actions: AckActionReceiver, /// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements. retransmission_sender: RetransmissionRequestSender, @@ -105,18 +106,15 @@ impl ActionController { pub(super) fn new( config: Config, retransmission_sender: RetransmissionRequestSender, - ) -> (Self, ActionSender) { - let (sender, receiver) = mpsc::unbounded(); - ( - ActionController { - config, - pending_acks_data: HashMap::new(), - pending_acks_timers: NonExhaustiveDelayQueue::new(), - incoming_actions: receiver, - retransmission_sender, - }, - sender, - ) + incoming_actions: AckActionReceiver, + ) -> Self { + ActionController { + config, + pending_acks_data: HashMap::new(), + pending_acks_timers: NonExhaustiveDelayQueue::new(), + incoming_actions, + retransmission_sender, + } } fn handle_insert(&mut self, pending_acks: Vec) { @@ -138,13 +136,18 @@ impl ActionController { trace!("{} is starting its timer", frag_id); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { - if queue_key.is_some() { - // this branch should be IMPOSSIBLE under ANY condition. It would imply starting - // timer TWICE for the SAME PendingAcknowledgement - panic!("Tried to start an already started ack timer!") - } - let timeout = (pending_ack_data.delay.clone() * self.config.ack_wait_multiplier) - .to_duration() + // the fact that this branch is now POSSIBLE is a sign of a need to refactor this whole + // retransmission procedure + // + // (it can happen as timer is started when ack expires to make sure it's not stuck in memory + // and the second instance can be fired when we finally get reply surbs for data we failed to retransmit) + + // if queue_key.is_some() { + // // this branch should be IMPOSSIBLE under ANY condition. It would imply starting + // // timer TWICE for the SAME PendingAcknowledgement + // panic!("Tried to start an already started ack timer!") + // } + let timeout = (pending_ack_data.delay * self.config.ack_wait_multiplier).to_duration() + self.config.ack_wait_addition; let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); @@ -192,7 +195,8 @@ impl ActionController { trace!("{} is updating its delay", frag_id); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { - // this Action is triggered by `RetransmissionRequestListener` which held the other potential + // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) + // or `ReplyController` (for 'reply' packets) which held the other potential // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference // was dropped hence this unwrap is safe. let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); @@ -277,17 +281,4 @@ impl ActionController { .expect("Task stopped without shutdown called"); log::debug!("ActionController: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started ActionController without graceful shutdown support"); - - loop { - tokio::select! { - action = self.incoming_actions.next() => self.process_action(action.unwrap()), - expired_ack = self.pending_acks_timers.next() => self.handle_expired_ack_timer(expired_ack.unwrap()) - } - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index dec2d153d6..cff6b6d719 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -1,23 +1,14 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; -use super::PendingAcknowledgement; -use crate::client::{ - inbound_messages::{InputMessage, InputMessageReceiver}, - real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage}, - topology_control::TopologyAccessor, -}; +use crate::client::inbound_messages::{InputMessage, InputMessageReceiver}; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::ReplyControllerSender; use client_connections::TransmissionLane; use log::*; -use nymsphinx::anonymous_replies::ReplySurb; -use nymsphinx::preparer::MessagePreparer; -use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use rand::{CryptoRng, Rng}; -use std::sync::Arc; - -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// putting everything into sphinx packets, etc. @@ -26,15 +17,9 @@ pub(super) struct InputMessageListener where R: CryptoRng + Rng, { - ack_key: Arc, - ack_recipient: Recipient, input_receiver: InputMessageReceiver, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, - topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] - reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, } impl InputMessageListener @@ -45,153 +30,83 @@ where // some considerable refactoring #[allow(clippy::too_many_arguments)] pub(super) fn new( - ack_key: Arc, - ack_recipient: Recipient, input_receiver: InputMessageReceiver, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, - topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, ) -> Self { InputMessageListener { - ack_key, - ack_recipient, input_receiver, - message_preparer, - action_sender, - real_message_sender, - topology_access, - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler, + reply_controller_sender, } } - // we require topology for replies to generate surb_acks - async fn handle_reply(&mut self, reply_surb: ReplySurb, data: Vec) -> Option { - let topology_permit = self.topology_access.get_read_permit().await; - let topology = match topology_permit.try_get_valid_topology_ref(&self.ack_recipient, None) { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not process the message - the network topology is invalid"); - return None; - } - }; - - match self - .message_preparer - .prepare_reply_for_use(data, reply_surb, topology, &self.ack_key) - .await - { - Ok((mix_packet, reply_id)) => { - // TODO: later probably write pending ack here - // and deal with them.... - // ... somehow - Some(RealMessage::new(mix_packet, reply_id)) - } - Err(err) => { - // TODO: should we have some mechanism to indicate to the user that the `reply_surb` - // could be reused since technically it wasn't used up here? - warn!("failed to deal with received reply surb - {:?}", err); - None - } - } + async fn handle_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) { + // offload reply handling to the dedicated task + self.reply_controller_sender + .send_reply(recipient_tag, data, lane) } - async fn handle_fresh_message( + async fn handle_plain_message( &mut self, recipient: Recipient, content: Vec, - with_reply_surb: bool, - ) -> Option> { - log::trace!("handling msg size: {}", content.len()); - let topology_permit = self.topology_access.get_read_permit().await; - let topology = match topology_permit - .try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient)) + lane: TransmissionLane, + ) { + if let Err(err) = self + .message_handler + .try_send_plain_message(recipient, content, lane) + .await { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not process the message - the network topology is invalid"); - return None; - } - }; - - // split the message, attach optional reply surb - let (split_message, reply_key) = self - .message_preparer - .prepare_and_split_message(content, with_reply_surb, topology) - .expect("somehow the topology was invalid after all!"); - - #[cfg(feature = "reply-surb")] - if let Some(reply_key) = reply_key { - self.reply_key_storage - .insert_encryption_key(reply_key) - .expect("Failed to insert surb reply key to the store!") + warn!("failed to send a plain message - {err}") } + } - #[cfg(not(feature = "reply-surb"))] - let _reply_key = reply_key; - - // encrypt chunks, put them inside sphinx packets and generate acks - let mut pending_acks = Vec::with_capacity(split_message.len()); - let mut real_messages = Vec::with_capacity(split_message.len()); - for message_chunk in split_message { - // we need to clone it because we need to keep it in memory in case we had to retransmit - // it. And then we'd need to recreate entire ACK again. - let chunk_clone = message_chunk.clone(); - let prepared_fragment = self - .message_preparer - .prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient) - .unwrap(); - - real_messages.push(RealMessage::new( - prepared_fragment.mix_packet, - message_chunk.fragment_identifier(), - )); - - pending_acks.push(PendingAcknowledgement::new( - message_chunk, - prepared_fragment.total_delay, - recipient, - )); + async fn handle_repliable_message( + &mut self, + recipient: Recipient, + content: Vec, + reply_surbs: u32, + lane: TransmissionLane, + ) { + if let Err(err) = self + .message_handler + .try_send_message_with_reply_surbs(recipient, content, reply_surbs, lane) + .await + { + warn!("failed to send a repliable message - {err}") } - - // tells the controller to put this into the hashmap - self.action_sender - .unbounded_send(Action::new_insert(pending_acks)) - .unwrap(); - - Some(real_messages) } async fn on_input_message(&mut self, msg: InputMessage) { - let (real_messages, lane) = match msg { - InputMessage::Fresh { + match msg { + InputMessage::Regular { recipient, data, - with_reply_surb, lane, - } => ( - self.handle_fresh_message(recipient, data, with_reply_surb) - .await, + } => self.handle_plain_message(recipient, data, lane).await, + InputMessage::Anonymous { + recipient, + data, + reply_surbs, lane, - ), - InputMessage::Reply { reply_surb, data } => ( - self.handle_reply(reply_surb, data) + } => { + self.handle_repliable_message(recipient, data, reply_surbs, lane) .await - .map(|message| vec![message]), - TransmissionLane::Reply, - ), + } + InputMessage::Reply { + recipient_tag, + data, + lane, + } => { + self.handle_reply(recipient_tag, data, lane).await; + } }; - - // there's no point in trying to send nothing - if let Some(real_messages) = real_messages { - // tells real message sender (with the poisson timer) to send this to the mix network - self.real_message_sender - .send((real_messages, lane)) - .await - .expect("BatchRealMessageReceiver has stopped receiving!"); - } } pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { @@ -216,13 +131,4 @@ where shutdown.recv_timeout().await; log::debug!("InputMessageListener: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started InputMessageListener without graceful shutdown support"); - while let Some(input_msg) = self.input_receiver.recv().await { - self.on_input_message(input_msg).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index 5bf2331555..5587958f33 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -7,18 +7,20 @@ use self::{ retransmission_request_listener::RetransmissionRequestListener, sent_notification_listener::SentNotificationListener, }; -use super::real_traffic_stream::BatchRealMessageSender; -use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +use crate::client::inbound_messages::InputMessageReceiver; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::ReplyControllerSender; use crate::spawn_future; +use action_controller::AckActionReceiver; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::params::PacketSize; use nymsphinx::{ acknowledgements::AckKey, addressing::clients::Recipient, chunking::fragment::{Fragment, FragmentIdentifier}, - preparer::MessagePreparer, Delay as SphinxDelay, }; use rand::{CryptoRng, Rng}; @@ -27,8 +29,7 @@ use std::{ time::Duration, }; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; +pub(crate) use action_controller::{AckActionSender, Action}; mod acknowledgement_listener; mod action_controller; @@ -50,24 +51,64 @@ pub(super) type SentPacketNotificationSender = mpsc::UnboundedSender; +#[derive(Debug)] +pub(crate) enum PacketDestination { + Anonymous { + recipient_tag: AnonymousSenderTag, + // special flag to indicate whether this was an ack for requesting additional surbs, + // in that case we have to do everything we can to get it through, even if it means going + // below our stored reply surb threshold + extra_surb_request: bool, + }, + KnownRecipient(Box), +} + /// Structure representing a data `Fragment` that is on-route to the specified `Recipient` #[derive(Debug)] pub(crate) struct PendingAcknowledgement { message_chunk: Fragment, delay: SphinxDelay, - recipient: Recipient, + destination: PacketDestination, } impl PendingAcknowledgement { /// Creates new instance of `PendingAcknowledgement` using the provided data. - fn new(message_chunk: Fragment, delay: SphinxDelay, recipient: Recipient) -> Self { + pub(crate) fn new_known( + message_chunk: Fragment, + delay: SphinxDelay, + recipient: Recipient, + ) -> Self { PendingAcknowledgement { message_chunk, delay, - recipient, + destination: PacketDestination::KnownRecipient(recipient.into()), } } + pub(crate) fn new_anonymous( + message_chunk: Fragment, + delay: SphinxDelay, + recipient_tag: AnonymousSenderTag, + extra_surb_request: bool, + ) -> Self { + PendingAcknowledgement { + message_chunk, + delay, + destination: PacketDestination::Anonymous { + recipient_tag, + extra_surb_request, + }, + } + } + + pub(crate) fn inner_fragment_identifier(&self) -> FragmentIdentifier { + self.message_chunk.fragment_identifier() + } + + pub(crate) fn fragment_data(&self) -> Fragment { + self.message_chunk.clone() + } + fn update_delay(&mut self, new_delay: SphinxDelay) { self.delay = new_delay; } @@ -76,10 +117,6 @@ impl PendingAcknowledgement { /// AcknowledgementControllerConnectors represents set of channels for communication with /// other parts of the system in order to support acknowledgements and retransmission. pub(super) struct AcknowledgementControllerConnectors { - /// Channel used for forwarding prepared sphinx messages into the poisson sender - /// to be sent to the mix network. - real_message_sender: BatchRealMessageSender, - /// Channel used for receiving raw messages from a client. The messages need to be put /// into sphinx packets first. input_receiver: InputMessageReceiver, @@ -91,20 +128,28 @@ pub(super) struct AcknowledgementControllerConnectors { /// Channel used for receiving acknowledgements from the mix network. ack_receiver: AcknowledgementReceiver, + + /// Channel used for sending request to `ActionController` to deal with anything ack-related, + ack_action_sender: AckActionSender, + + /// Channel used for receiving request by `ActionController` to deal with anything ack-related, + ack_action_receiver: AckActionReceiver, } impl AcknowledgementControllerConnectors { pub(super) fn new( - real_message_sender: BatchRealMessageSender, input_receiver: InputMessageReceiver, sent_notifier: SentPacketNotificationReceiver, ack_receiver: AcknowledgementReceiver, + ack_action_sender: AckActionSender, + ack_action_receiver: AckActionReceiver, ) -> Self { AcknowledgementControllerConnectors { - real_message_sender, input_receiver, sent_notifier, ack_receiver, + ack_action_sender, + ack_action_receiver, } } } @@ -117,28 +162,15 @@ pub(super) struct Config { /// Given ack timeout in the form a * BASE_DELAY + b, it specifies the multiplier `a` ack_wait_multiplier: f64, - /// Average delay an acknowledgement packet is going to get delayed at a single mixnode. - average_ack_delay: Duration, - - /// Average delay a data packet is going to get delayed at a single mixnode. - average_packet_delay: Duration, - /// Predefined packet size used for the encapsulated messages. packet_size: PacketSize, } impl Config { - pub(super) fn new( - ack_wait_addition: Duration, - ack_wait_multiplier: f64, - average_ack_delay: Duration, - average_packet_delay: Duration, - ) -> Self { + pub(super) fn new(ack_wait_addition: Duration, ack_wait_multiplier: f64) -> Self { Config { ack_wait_addition, ack_wait_multiplier, - average_ack_delay, - average_packet_delay, packet_size: Default::default(), } } @@ -162,68 +194,51 @@ where impl AcknowledgementController where - R: 'static + CryptoRng + Rng + Clone + Send, + R: 'static + CryptoRng + Rng + Clone + Send + Sync, { - #[allow(clippy::too_many_arguments)] pub(super) fn new( config: Config, - rng: R, - topology_access: TopologyAccessor, ack_key: Arc, - ack_recipient: Recipient, connectors: AcknowledgementControllerConnectors, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, ) -> Self { let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); let action_config = action_controller::Config::new(config.ack_wait_addition, config.ack_wait_multiplier); - let (action_controller, action_sender) = - ActionController::new(action_config, retransmission_tx); - - let message_preparer = MessagePreparer::new( - rng, - ack_recipient, - config.average_packet_delay, - config.average_ack_delay, - ) - .with_custom_real_message_packet_size(config.packet_size); + let action_controller = ActionController::new( + action_config, + retransmission_tx, + connectors.ack_action_receiver, + ); // will listen for any acks coming from the network let acknowledgement_listener = AcknowledgementListener::new( Arc::clone(&ack_key), connectors.ack_receiver, - action_sender.clone(), + connectors.ack_action_sender.clone(), ); // will listen for any new messages from the client let input_message_listener = InputMessageListener::new( - Arc::clone(&ack_key), - ack_recipient, connectors.input_receiver, - message_preparer.clone(), - action_sender.clone(), - connectors.real_message_sender.clone(), - topology_access.clone(), - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler.clone(), + reply_controller_sender.clone(), ); // will listen for any ack timeouts and trigger retransmission let retransmission_request_listener = RetransmissionRequestListener::new( - Arc::clone(&ack_key), - ack_recipient, - message_preparer, - action_sender.clone(), - connectors.real_message_sender, + connectors.ack_action_sender.clone(), + message_handler, retransmission_rx, - topology_access, + reply_controller_sender, ); // will listen for events indicating the packet was sent through the network so that // the retransmission timer should be started. let sent_notification_listener = - SentNotificationListener::new(connectors.sent_notifier, action_sender); + SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender); AcknowledgementController { acknowledgement_listener, @@ -278,35 +293,4 @@ where debug!("The controller has finished execution!"); }); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) fn start(self) { - let mut acknowledgement_listener = self.acknowledgement_listener; - let mut input_message_listener = self.input_message_listener; - let mut retransmission_request_listener = self.retransmission_request_listener; - let mut sent_notification_listener = self.sent_notification_listener; - let mut action_controller = self.action_controller; - - spawn_future(async move { - acknowledgement_listener.run().await; - error!("The acknowledgement listener has finished execution!"); - }); - spawn_future(async move { - input_message_listener.run().await; - error!("The input listener has finished execution!"); - }); - spawn_future(async move { - retransmission_request_listener.run().await; - error!("The retransmission request listener has finished execution!"); - }); - spawn_future(async move { - sent_notification_listener.run().await; - error!("The sent notification listener has finished execution!"); - }); - spawn_future(async move { - action_controller.run().await; - error!("The controller has finished execution!"); - }); - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 190f23e50c..b5c6e6a2a8 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -1,82 +1,101 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::{ - action_controller::{Action, ActionSender}, + action_controller::{AckActionSender, Action}, PendingAcknowledgement, RetransmissionRequestReceiver, }; -use crate::client::{ - real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage}, - topology_control::TopologyAccessor, -}; - +use crate::client::real_messages_control::acknowledgement_control::PacketDestination; +use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError}; +use crate::client::real_messages_control::real_traffic_stream::RealMessage; +use crate::client::replies::reply_controller::ReplyControllerSender; use client_connections::TransmissionLane; use futures::StreamExt; use log::*; -use nymsphinx::{ - acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer, -}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::chunking::fragment::Fragment; +use nymsphinx::preparer::PreparedFragment; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; // responsible for packet retransmission upon fired timer -pub(super) struct RetransmissionRequestListener -where - R: CryptoRng + Rng, -{ - ack_key: Arc, - ack_recipient: Recipient, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, +pub(super) struct RetransmissionRequestListener { + action_sender: AckActionSender, + message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + reply_controller_sender: ReplyControllerSender, } impl RetransmissionRequestListener where R: CryptoRng + Rng, { - #[allow(clippy::too_many_arguments)] pub(super) fn new( - ack_key: Arc, - ack_recipient: Recipient, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, + action_sender: AckActionSender, + message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + reply_controller_sender: ReplyControllerSender, ) -> Self { RetransmissionRequestListener { - ack_key, - ack_recipient, - message_preparer, action_sender, - real_message_sender, + message_handler, request_receiver, - topology_access, + reply_controller_sender, } } - async fn on_retransmission_request(&mut self, timed_out_ack: Weak) { - let timed_out_ack = match timed_out_ack.upgrade() { + async fn prepare_normal_retransmission_chunk( + &mut self, + packet_recipient: Recipient, + chunk_data: Fragment, + ) -> Result { + debug!("retransmitting normal packet..."); + + self.message_handler + .try_prepare_single_chunk_for_sending(packet_recipient, chunk_data) + .await + } + + async fn on_retransmission_request( + &mut self, + weak_timed_out_ack: Weak, + ) { + let timed_out_ack = match weak_timed_out_ack.upgrade() { Some(timed_out_ack) => timed_out_ack, None => { debug!("We received an ack JUST as we were about to retransmit [1]"); return; } }; - let packet_recipient = &timed_out_ack.recipient; - let chunk_clone = timed_out_ack.message_chunk.clone(); - let frag_id = chunk_clone.fragment_identifier(); - let topology_permit = self.topology_access.get_read_permit().await; - let topology_ref = match topology_permit - .try_get_valid_topology_ref(&self.ack_recipient, Some(packet_recipient)) - { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not retransmit the packet - the network topology is invalid"); + let maybe_prepared_fragment = match &timed_out_ack.destination { + PacketDestination::Anonymous { + recipient_tag, + extra_surb_request, + } => { + // if this is retransmission for reply, offload it to the dedicated task + // that deals with all the surbs + return self.reply_controller_sender.send_retransmission_data( + *recipient_tag, + weak_timed_out_ack, + *extra_surb_request, + ); + } + PacketDestination::KnownRecipient(recipient) => { + self.prepare_normal_retransmission_chunk( + **recipient, + timed_out_ack.message_chunk.clone(), + ) + .await + } + }; + + let frag_id = timed_out_ack.message_chunk.fragment_identifier(); + + let prepared_fragment = match maybe_prepared_fragment { + Ok(prepared_fragment) => prepared_fragment, + Err(err) => { + warn!("Could not retransmit the packet - {err}"); // we NEED to start timer here otherwise we will have this guy permanently stuck in memory self.action_sender .unbounded_send(Action::new_start_timer(frag_id)) @@ -85,11 +104,6 @@ where } }; - let prepared_fragment = self - .message_preparer - .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient) - .unwrap(); - // if we have the ONLY strong reference to the ack data, it means it was removed from the // pending acks if Arc::strong_count(&timed_out_ack) == 1 { @@ -101,7 +115,6 @@ where // we no longer need the reference - let's drop it so that if somehow `UpdateTimer` action // reached the controller before this function terminated, the controller would not panic. drop(timed_out_ack); - let new_delay = prepared_fragment.total_delay; // We know this update will be reflected by the `StartTimer` Action performed when this @@ -116,13 +129,12 @@ where .unwrap(); // send to `OutQueueControl` to eventually send to the mix network - self.real_message_sender - .send(( + self.message_handler + .forward_messages( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, - )) + ) .await - .expect("BatchRealMessageReceiver has stopped receiving!"); } pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { @@ -145,14 +157,4 @@ where shutdown.recv_timeout().await; log::debug!("RetransmissionRequestListener: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started RetransmissionRequestListener without graceful shutdown support"); - - while let Some(timed_out_ack) = self.request_receiver.next().await { - self.on_retransmission_request(timed_out_ack).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 958c696aa7..c273182d59 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; +use super::action_controller::{AckActionSender, Action}; use super::SentPacketNotificationReceiver; use futures::StreamExt; use log::*; @@ -13,13 +13,13 @@ use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; /// accidentally fire retransmission way quicker than we should have. pub(super) struct SentNotificationListener { sent_notifier: SentPacketNotificationReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, } impl SentNotificationListener { pub(super) fn new( sent_notifier: SentPacketNotificationReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, ) -> Self { SentNotificationListener { sent_notifier, @@ -31,11 +31,6 @@ impl SentNotificationListener { if frag_id == COVER_FRAG_ID { trace!("sent off a cover message - no need to start retransmission timer!"); return; - } else if frag_id.is_reply() { - debug!("sent off a reply message - no need to start retransmission timer!"); - // TODO: probably there will need to be some extra procedure here, like it would - // be nice to know that our reply actually reached the recipient (i.e. we got the ack) - return; } self.action_sender .unbounded_send(Action::new_start_timer(frag_id)) @@ -64,14 +59,4 @@ impl SentNotificationListener { assert!(shutdown.is_shutdown_poll()); log::debug!("SentNotificationListener: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started SentNotificationListener without graceful shutdown support"); - - while let Some(frag_id) = self.sent_notifier.next().await { - self.on_sent_message(frag_id).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/message_handler.rs b/clients/client-core/src/client/real_messages_control/message_handler.rs new file mode 100644 index 0000000000..1c80ef519f --- /dev/null +++ b/clients/client-core/src/client/real_messages_control/message_handler.rs @@ -0,0 +1,548 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; +use crate::client::real_messages_control::real_traffic_stream::{ + BatchRealMessageSender, RealMessage, +}; +use crate::client::real_messages_control::{AckActionSender, Action}; +use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; +use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit}; +use client_connections::TransmissionLane; +use log::{debug, error, info, trace, warn}; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; +use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey}; +use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier}; +use nymsphinx::message::NymMessage; +use nymsphinx::params::{PacketSize, DEFAULT_NUM_MIX_HOPS}; +use nymsphinx::preparer::{MessagePreparer, PreparedFragment}; +use nymsphinx::Delay; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use std::time::Duration; +use thiserror::Error; +use topology::{NymTopology, NymTopologyError}; + +// TODO: move that error elsewhere since it seems to be contaminating different files +#[derive(Debug, Clone, Error)] +pub enum PreparationError { + #[error(transparent)] + NymTopologyError(#[from] NymTopologyError), + + #[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")] + MessageTooLongForSingleSurb { fragments: usize }, + + #[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")] + NotEnoughSurbs { available: usize, required: usize }, +} + +impl PreparationError { + fn return_surbs(self, returned_surbs: Vec) -> SurbWrappedPreparationError { + SurbWrappedPreparationError { + source: self, + returned_surbs: Some(returned_surbs), + } + } +} + +#[derive(Debug, Error)] +#[error("Failed to prepare packets - {source}. {} reply surbs will be returned", .returned_surbs.as_ref().map(|s| s.len()).unwrap_or_default())] +pub struct SurbWrappedPreparationError { + #[source] + source: PreparationError, + + returned_surbs: Option>, +} + +impl From for SurbWrappedPreparationError +where + T: Into, +{ + fn from(err: T) -> Self { + SurbWrappedPreparationError { + source: err.into(), + returned_surbs: None, + } + } +} + +impl SurbWrappedPreparationError { + pub(crate) fn return_unused_surbs( + self, + surb_storage: &ReceivedReplySurbsMap, + target: &AnonymousSenderTag, + ) -> PreparationError { + if let Some(reply_surbs) = self.returned_surbs { + surb_storage.insert_surbs(target, reply_surbs) + } + self.source + } +} + +#[derive(Clone)] +pub(crate) struct Config { + /// Key used to decrypt contents of received SURBAcks + ack_key: Arc, + + /// Address of this client which also represent an address to which all acknowledgements + /// and surb-based are going to be sent. + sender_address: Recipient, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: Duration, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: Duration, + + /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. + /// Note that it does not include gateway hops. + num_mix_hops: u8, + + /// Predefined packet size used for the encapsulated messages. + packet_size: PacketSize, +} + +impl Config { + pub fn new( + ack_key: Arc, + sender_address: Recipient, + average_packet_delay: Duration, + average_ack_delay: Duration, + ) -> Self { + Config { + ack_key, + sender_address, + average_packet_delay, + average_ack_delay, + num_mix_hops: DEFAULT_NUM_MIX_HOPS, + packet_size: PacketSize::default(), + } + } + + /// Allows setting non-default number of expected mix hops in the network. + #[allow(dead_code)] + pub fn with_mix_hops(mut self, hops: u8) -> Self { + self.num_mix_hops = hops; + self + } + + /// Allows setting non-default size of the sphinx packets sent out. + pub fn with_custom_packet_size(mut self, packet_size: PacketSize) -> Self { + self.packet_size = packet_size; + self + } +} + +#[derive(Clone)] +pub(crate) struct MessageHandler { + config: Config, + rng: R, + message_preparer: MessagePreparer, + action_sender: AckActionSender, + real_message_sender: BatchRealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: SentReplyKeys, + tag_storage: UsedSenderTags, +} + +impl MessageHandler +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + rng: R, + action_sender: AckActionSender, + real_message_sender: BatchRealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: SentReplyKeys, + tag_storage: UsedSenderTags, + ) -> Self + where + R: Copy, + { + let message_preparer = MessagePreparer::new( + rng, + config.sender_address, + config.average_packet_delay, + config.average_ack_delay, + ) + .with_custom_real_message_packet_size(config.packet_size) + .with_mix_hops(config.num_mix_hops); + + MessageHandler { + config, + rng, + message_preparer, + action_sender, + real_message_sender, + topology_access, + reply_key_storage, + tag_storage, + } + } + + fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag { + if let Some(existing) = self.tag_storage.try_get_existing(recipient) { + trace!("we already had sender tag for {recipient}"); + existing + } else { + info!("creating new sender tag for {recipient}"); + let new_tag = AnonymousSenderTag::new_random(&mut self.rng); + self.tag_storage.insert_new(recipient, new_tag); + info!("we'll be using {new_tag} for all anonymous messages sent to {recipient}"); + new_tag + } + } + + fn get_topology<'a>( + &self, + permit: &'a TopologyReadPermit<'a>, + ) -> Result<&'a NymTopology, PreparationError> { + match permit.try_get_valid_topology_ref(&self.config.sender_address, None) { + Ok(topology_ref) => Ok(topology_ref), + Err(err) => { + warn!("Could not process the packet - the network topology is invalid - {err}"); + Err(err.into()) + } + } + } + + async fn generate_reply_surbs_with_keys( + &mut self, + amount: usize, + ) -> Result<(Vec, Vec), PreparationError> { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let reply_surbs = self + .message_preparer + .generate_reply_surbs(amount, topology)?; + + let reply_keys = reply_surbs + .iter() + .map(|s| *s.encryption_key()) + .collect::>(); + + Ok((reply_surbs, reply_keys)) + } + + pub(crate) async fn try_send_single_surb_message( + &mut self, + target: AnonymousSenderTag, + message: ReplyMessage, + reply_surb: ReplySurb, + is_extra_surb_request: bool, + ) -> Result<(), SurbWrappedPreparationError> { + let mut fragment = self + .message_preparer + .pad_and_split_message(NymMessage::new_reply(message)); + if fragment.len() > 1 { + // well, it's not a single surb message + return Err(SurbWrappedPreparationError { + source: PreparationError::MessageTooLongForSingleSurb { + fragments: fragment.len(), + }, + returned_surbs: Some(vec![reply_surb]), + }); + } + + let chunk = fragment.pop().unwrap(); + let chunk_clone = chunk.clone(); + let prepared_fragment = self + .try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone) + .await?; + + let real_messages = + RealMessage::new(prepared_fragment.mix_packet, chunk.fragment_identifier()); + let delay = prepared_fragment.total_delay; + let pending_ack = + PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request); + + let lane = if is_extra_surb_request { + TransmissionLane::ReplySurbRequest + } else { + TransmissionLane::General + }; + + self.forward_messages(vec![real_messages], lane).await; + self.insert_pending_acks(vec![pending_ack]); + Ok(()) + } + + pub(crate) async fn try_request_additional_reply_surbs( + &mut self, + from: AnonymousSenderTag, + reply_surb: ReplySurb, + amount: u32, + ) -> Result<(), SurbWrappedPreparationError> { + debug!("requesting {amount} reply SURBs from {from:?}"); + + let surbs_request = + ReplyMessage::new_surb_request_message(self.config.sender_address, amount); + self.try_send_single_surb_message(from, surbs_request, reply_surb, true) + .await + } + + // // TODO: this will require additional argument to make it use different variant of `ReplyMessage` + pub(crate) fn split_reply_message(&mut self, message: Vec) -> Vec { + self.message_preparer + .pad_and_split_message(NymMessage::new_reply(ReplyMessage::new_data_message( + message, + ))) + } + + // the only difference between this method and `try_send_reply_chunks` is that + // here we are not creating acks as acks are already in memory waiting to get cleared. + // we are only updating their existing delays + pub(crate) async fn try_send_retransmission_reply_chunks( + &mut self, + fragments: Vec, + reply_surbs: Vec, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let prepared_fragments = self + .prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs) + .await?; + + let mut real_messages = Vec::with_capacity(prepared_fragments.len()); + + for prepared in prepared_fragments { + self.update_ack_delay(prepared.fragment_identifier, prepared.total_delay); + real_messages.push(prepared.into()) + } + + self.forward_messages(real_messages, lane).await; + Ok(()) + } + + pub(crate) async fn try_send_reply_chunks( + &mut self, + target: AnonymousSenderTag, + fragments: Vec, + reply_surbs: Vec, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let prepared_fragments = self + .prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs) + .await?; + + let mut pending_acks = Vec::with_capacity(fragments.len()); + let mut real_messages = Vec::with_capacity(fragments.len()); + + for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) { + let real_message = RealMessage::new(prepared.mix_packet, prepared.fragment_identifier); + let delay = prepared.total_delay; + let pending_ack = PendingAcknowledgement::new_anonymous(raw, delay, target, false); + + real_messages.push(real_message); + pending_acks.push(pending_ack); + } + + self.forward_messages(real_messages, lane).await; + self.insert_pending_acks(pending_acks); + Ok(()) + } + + pub(crate) async fn try_send_plain_message( + &mut self, + recipient: Recipient, + message: Vec, + lane: TransmissionLane, + ) -> Result<(), PreparationError> { + let message = NymMessage::new_plain(message); + self.try_split_and_send_non_reply_message(message, recipient, lane) + .await + } + + pub(crate) async fn try_split_and_send_non_reply_message( + &mut self, + message: NymMessage, + recipient: Recipient, + lane: TransmissionLane, + ) -> Result<(), PreparationError> { + // TODO: I really dislike existence of this assertion, it implies code has to be re-organised + debug_assert!(!matches!(message, NymMessage::Reply(_))); + + // TODO2: it's really annoying we have to get topology permit again here due to borrow-checker + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let fragments = self.message_preparer.pad_and_split_message(message); + + let mut pending_acks = Vec::with_capacity(fragments.len()); + let mut real_messages = Vec::with_capacity(fragments.len()); + for fragment in fragments { + // we need to clone it because we need to keep it in memory in case we had to retransmit + // it. And then we'd need to recreate entire ACK again. + let chunk_clone = fragment.clone(); + let prepared_fragment = self.message_preparer.prepare_chunk_for_sending( + chunk_clone, + topology, + &self.config.ack_key, + &recipient, + )?; + + let real_message = + RealMessage::new(prepared_fragment.mix_packet, fragment.fragment_identifier()); + let delay = prepared_fragment.total_delay; + let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient); + + real_messages.push(real_message); + pending_acks.push(pending_ack); + } + + self.insert_pending_acks(pending_acks); + self.forward_messages(real_messages, lane).await; + + Ok(()) + } + + pub(crate) async fn try_send_additional_reply_surbs( + &mut self, + recipient: Recipient, + amount: u32, + ) -> Result<(), PreparationError> { + let sender_tag = self.get_or_create_sender_tag(&recipient); + let (reply_surbs, reply_keys) = + self.generate_reply_surbs_with_keys(amount as usize).await?; + + let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs( + sender_tag, + reply_surbs, + )); + + self.try_split_and_send_non_reply_message( + message, + recipient, + TransmissionLane::AdditionalReplySurbs, + ) + .await?; + + log::trace!("storing {} reply keys", reply_keys.len()); + self.reply_key_storage.insert_multiple(reply_keys); + + Ok(()) + } + + pub(crate) async fn try_send_message_with_reply_surbs( + &mut self, + recipient: Recipient, + message: Vec, + num_reply_surbs: u32, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let sender_tag = self.get_or_create_sender_tag(&recipient); + let (reply_surbs, reply_keys) = self + .generate_reply_surbs_with_keys(num_reply_surbs as usize) + .await?; + + let message = + NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs)); + + self.try_split_and_send_non_reply_message(message, recipient, lane) + .await?; + + log::trace!("storing {} reply keys", reply_keys.len()); + self.reply_key_storage.insert_multiple(reply_keys); + + Ok(()) + } + + pub(crate) async fn try_prepare_single_chunk_for_sending( + &mut self, + recipient: Recipient, + chunk: Fragment, + ) -> Result { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let prepared_fragment = self + .message_preparer + .prepare_chunk_for_sending(chunk, topology, &self.config.ack_key, &recipient) + .unwrap(); + + Ok(prepared_fragment) + } + + async fn prepare_reply_chunks_for_sending( + &mut self, + fragments: Vec, + reply_surbs: Vec, + ) -> Result, SurbWrappedPreparationError> { + debug_assert_ne!( + fragments.len(), + reply_surbs.len(), + "attempted to send {} fragments with {} reply surbs", + fragments.len(), + reply_surbs.len() + ); + + let topology_permit = self.topology_access.get_read_permit().await; + let topology = match self.get_topology(&topology_permit) { + Ok(topology) => topology, + Err(err) => return Err(err.return_surbs(reply_surbs)), + }; + + Ok(fragments + .into_iter() + .zip(reply_surbs.into_iter()) + .map(|(fragment, reply_surb)| { + // unwrap here is fine as we know we have a valid topology + self.message_preparer + .prepare_reply_chunk_for_sending( + fragment, + topology, + &self.config.ack_key, + reply_surb, + ) + .unwrap() + }) + .collect()) + } + + pub(crate) async fn try_prepare_single_reply_chunk_for_sending( + &mut self, + reply_surb: ReplySurb, + chunk: Fragment, + ) -> Result { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = match self.get_topology(&topology_permit) { + Ok(topology) => topology, + Err(err) => return Err(err.return_surbs(vec![reply_surb])), + }; + + let prepared_fragment = self + .message_preparer + .prepare_reply_chunk_for_sending(chunk, topology, &self.config.ack_key, reply_surb) + .unwrap(); + + Ok(prepared_fragment) + } + + pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) { + self.action_sender + .unbounded_send(Action::UpdateDelay(id, new_delay)) + .expect("action control task has died") + } + + pub(crate) fn insert_pending_acks(&self, pending_acks: Vec) { + self.action_sender + .unbounded_send(Action::new_insert(pending_acks)) + .expect("action control task has died") + } + + // tells real message sender (with the poisson timer) to send this to the mix network + pub(crate) async fn forward_messages( + &self, + messages: Vec, + transmission_lane: TransmissionLane, + ) { + self.real_message_sender + .send((messages, transmission_lane)) + .await + .expect("real message receiver task (OutQueueControl) has died"); + } +} diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index e46c02e31e..8789cbdf6a 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -8,6 +8,11 @@ use self::{ acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, }; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::{ + ReplyController, ReplyControllerReceiver, ReplyControllerSender, +}; +use crate::client::replies::reply_storage::CombinedReplyStorage; use crate::{ client::{ inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, @@ -27,11 +32,13 @@ use rand::{rngs::OsRng, CryptoRng, Rng}; use std::sync::Arc; use std::time::Duration; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::replies::reply_controller; +use crate::config; +pub(crate) use acknowledgement_control::{AckActionSender, Action}; -mod acknowledgement_control; -mod real_traffic_stream; +pub(crate) mod acknowledgement_control; +pub(crate) mod message_handler; +pub(crate) mod real_traffic_stream; // TODO: ack_key and self_recipient shouldn't really be part of this config pub struct Config { @@ -62,31 +69,102 @@ pub struct Config { /// Predefined packet size used for the encapsulated messages. packet_size: PacketSize, + + /// Defines the minimum number of reply surbs the client would request. + minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + maximum_reply_surb_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + maximum_reply_key_age: Duration, +} + +impl<'a> From<&'a Config> for acknowledgement_control::Config { + fn from(cfg: &'a Config) -> Self { + acknowledgement_control::Config::new(cfg.ack_wait_addition, cfg.ack_wait_multiplier) + .with_custom_packet_size(cfg.packet_size) + } +} + +impl<'a> From<&'a Config> for real_traffic_stream::Config { + fn from(cfg: &'a Config) -> Self { + real_traffic_stream::Config::new( + Arc::clone(&cfg.ack_key), + cfg.self_recipient, + cfg.average_ack_delay_duration, + cfg.average_packet_delay_duration, + cfg.average_message_sending_delay, + cfg.disable_main_poisson_packet_distribution, + ) + .with_custom_cover_packet_size(cfg.packet_size) + } +} + +impl<'a> From<&'a Config> for reply_controller::Config { + fn from(cfg: &'a Config) -> Self { + reply_controller::Config::new( + cfg.minimum_reply_surb_request_size, + cfg.maximum_reply_surb_request_size, + cfg.maximum_allowed_reply_surb_request_size, + cfg.maximum_reply_surb_waiting_period, + cfg.maximum_reply_surb_age, + cfg.maximum_reply_key_age, + ) + } +} + +impl<'a> From<&'a Config> for message_handler::Config { + fn from(cfg: &'a Config) -> Self { + message_handler::Config::new( + Arc::clone(&cfg.ack_key), + cfg.self_recipient, + cfg.average_packet_delay_duration, + cfg.average_ack_delay_duration, + ) + .with_custom_packet_size(cfg.packet_size) + } } impl Config { - // TODO: change the config into a builder - #[allow(clippy::too_many_arguments)] pub fn new( + base_client_debug_config: &config::DebugConfig, ack_key: Arc, - ack_wait_multiplier: f64, - ack_wait_addition: Duration, - average_ack_delay_duration: Duration, - average_message_sending_delay: Duration, - average_packet_delay_duration: Duration, - disable_main_poisson_packet_distribution: bool, self_recipient: Recipient, ) -> Self { Config { ack_key, - ack_wait_addition, - ack_wait_multiplier, self_recipient, - average_message_sending_delay, - average_packet_delay_duration, - average_ack_delay_duration, - disable_main_poisson_packet_distribution, packet_size: Default::default(), + ack_wait_addition: base_client_debug_config.ack_wait_addition, + ack_wait_multiplier: base_client_debug_config.ack_wait_multiplier, + average_message_sending_delay: base_client_debug_config.message_sending_average_delay, + average_packet_delay_duration: base_client_debug_config.average_packet_delay, + average_ack_delay_duration: base_client_debug_config.average_ack_delay, + disable_main_poisson_packet_distribution: base_client_debug_config + .disable_main_poisson_packet_distribution, + minimum_reply_surb_request_size: base_client_debug_config + .minimum_reply_surb_request_size, + maximum_reply_surb_request_size: base_client_debug_config + .maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: base_client_debug_config + .maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period: base_client_debug_config + .maximum_reply_surb_waiting_period, + maximum_reply_surb_age: base_client_debug_config.maximum_reply_surb_age, + maximum_reply_key_age: base_client_debug_config.maximum_reply_key_age, } } @@ -95,75 +173,84 @@ impl Config { } } -pub struct RealMessagesController +pub(crate) struct RealMessagesController where R: CryptoRng + Rng, { out_queue_control: OutQueueControl, ack_control: AcknowledgementController, + reply_control: ReplyController, } // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` impl RealMessagesController { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( config: Config, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, + reply_storage: CombinedReplyStorage, + // so much refactoring needed, but this is temporary just to test things out + reply_controller_sender: ReplyControllerSender, + reply_controller_receiver: ReplyControllerReceiver, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let rng = OsRng; + // create channels for inter-task communication let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1); let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); - + let (ack_action_tx, ack_action_rx) = mpsc::unbounded(); let ack_controller_connectors = AcknowledgementControllerConnectors::new( - real_message_sender, input_receiver, sent_notifier_rx, ack_receiver, + ack_action_tx.clone(), + ack_action_rx, ); - let ack_control_config = acknowledgement_control::Config::new( - config.ack_wait_addition, - config.ack_wait_multiplier, - config.average_ack_delay_duration, - config.average_packet_delay_duration, - ) - .with_custom_packet_size(config.packet_size); + // create all configs for the components + let ack_control_config = (&config).into(); + let out_queue_config = (&config).into(); + let reply_controller_config = (&config).into(); + let message_handler_config = (&config).into(); + + // create the actual components + let message_handler = MessageHandler::new( + message_handler_config, + rng, + ack_action_tx, + real_message_sender, + topology_access.clone(), + reply_storage.key_storage(), + reply_storage.tags_storage(), + ); let ack_control = AcknowledgementController::new( ack_control_config, - rng, - topology_access.clone(), Arc::clone(&config.ack_key), - config.self_recipient, ack_controller_connectors, - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler.clone(), + reply_controller_sender, ); - let out_queue_config = real_traffic_stream::Config::new( - config.average_ack_delay_duration, - config.average_packet_delay_duration, - config.average_message_sending_delay, - config.disable_main_poisson_packet_distribution, - ) - .with_custom_cover_packet_size(config.packet_size); + let reply_control = ReplyController::new( + reply_controller_config, + message_handler, + reply_storage, + reply_controller_receiver, + ); let out_queue_control = OutQueueControl::new( out_queue_config, - Arc::clone(&config.ack_key), + rng, sent_notifier_tx, mix_sender, real_message_receiver, - rng, - config.self_recipient, topology_access, lane_queue_lengths, client_connection_rx, @@ -172,30 +259,26 @@ impl RealMessagesController { RealMessagesController { out_queue_control, ack_control, + reply_control, } } pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) { let mut out_queue_control = self.out_queue_control; let ack_control = self.ack_control; + let mut reply_control = self.reply_control; let shutdown_handle = shutdown.clone(); spawn_future(async move { out_queue_control.run_with_shutdown(shutdown_handle).await; debug!("The out queue controller has finished execution!"); }); + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + reply_control.run_with_shutdown(shutdown_handle).await; + debug!("The reply controller has finished execution!"); + }); + ack_control.start_with_shutdown(shutdown); } - - #[cfg(target_arch = "wasm32")] - pub fn start(self) { - let mut out_queue_control = self.out_queue_control; - let ack_control = self.ack_control; - - spawn_future(async move { - out_queue_control.run().await; - debug!("The out queue controller has finished execution!"); - }); - ack_control.start(); - } } diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 734b75bb70..9b75669960 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -16,6 +16,7 @@ use nymsphinx::chunking::fragment::FragmentIdentifier; use nymsphinx::cover::generate_loop_cover_packet; use nymsphinx::forwarding::packet::MixPacket; use nymsphinx::params::PacketSize; +use nymsphinx::preparer::PreparedFragment; use nymsphinx::utils::sample_poisson_duration; use rand::{CryptoRng, Rng}; use std::pin::Pin; @@ -47,6 +48,12 @@ fn get_time_now() -> wasm_timer::Instant { /// Configurable parameters of the `OutQueueControl` pub(crate) struct Config { + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Represents full address of this client. + our_full_destination: Recipient, + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, @@ -66,12 +73,16 @@ pub(crate) struct Config { impl Config { pub(crate) fn new( + ack_key: Arc, + our_full_destination: Recipient, average_ack_delay: Duration, average_packet_delay: Duration, average_message_sending_delay: Duration, disable_poisson_packet_distribution: bool, ) -> Self { Config { + ack_key, + our_full_destination, average_ack_delay, average_packet_delay, average_message_sending_delay, @@ -93,9 +104,6 @@ where /// Configurable parameters of the `ActionController` config: Config, - /// Key used to encrypt and decrypt content of an ACK packet. - ack_key: Arc, - /// Channel used for notifying of a real packet being sent out. Used to start up retransmission timer. sent_notifier: SentPacketNotificationSender, @@ -119,9 +127,6 @@ where /// before being sent out into the network. real_receiver: BatchRealMessageReceiver, - /// Represents full address of this client. - our_full_destination: Recipient, - /// Instance of a cryptographically secure random number generator. rng: R, @@ -144,6 +149,16 @@ where pub(crate) struct RealMessage { mix_packet: MixPacket, fragment_id: FragmentIdentifier, + // TODO: add info about it being constructed with reply-surb +} + +impl From for RealMessage { + fn from(fragment: PreparedFragment) -> Self { + RealMessage { + mix_packet: fragment.mix_packet, + fragment_id: fragment.fragment_identifier, + } + } } impl RealMessage { @@ -175,25 +190,21 @@ where #[allow(clippy::too_many_arguments)] pub(crate) fn new( config: Config, - ack_key: Arc, + rng: R, sent_notifier: SentPacketNotificationSender, mix_tx: BatchMixMessageSender, real_receiver: BatchRealMessageReceiver, - rng: R, - our_full_destination: Recipient, topology_access: TopologyAccessor, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, ) -> Self { OutQueueControl { config, - ack_key, sent_notifier, next_delay: None, sending_delay_controller: Default::default(), mix_tx, real_receiver, - our_full_destination, rng, topology_access, transmission_buffer: Default::default(), @@ -220,24 +231,23 @@ where // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) - let topology_ref_option = topology_permit.try_get_valid_topology_ref( - &self.our_full_destination, - Some(&self.our_full_destination), - ); - if topology_ref_option.is_none() { - warn!( - "No valid topology detected - won't send any loop cover message this time" - ); - return; - } - let topology_ref = topology_ref_option.unwrap(); + let topology_ref = match topology_permit.try_get_valid_topology_ref( + &self.config.our_full_destination, + Some(&self.config.our_full_destination), + ) { + Ok(topology) => topology, + Err(err) => { + warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + return; + } + }; ( generate_loop_cover_packet( &mut self.rng, topology_ref, - &self.ack_key, - &self.our_full_destination, + &self.config.ack_key, + &self.config.our_full_destination, self.config.average_ack_delay, self.config.average_packet_delay, self.config.cover_packet_size, @@ -556,16 +566,6 @@ where } log::debug!("OutQueueControl: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - pub(super) async fn run(&mut self) { - debug!("Started OutQueueControl without graceful shutdown support"); - - while let Some(next_message) = self.next().await { - self.on_message(next_message).await; - } - } } impl Stream for OutQueueControl diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index ed693993a2..2e7876ae62 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -1,26 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::replies::reply_controller::ReplyControllerSender; +use crate::client::replies::reply_storage::SentReplyKeys; use crate::spawn_future; use crypto::asymmetric::encryption; +use crypto::Digest; use futures::channel::mpsc; use futures::lock::Mutex; use futures::StreamExt; use gateway_client::MixnetMessageReceiver; use log::*; +use nymsphinx::anonymous_replies::requests::{ + RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent, +}; +use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; +use nymsphinx::message::{NymMessage, PlainMessage}; +use nymsphinx::params::ReplySurbKeyDigestAlgorithm; use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; use std::collections::HashSet; use std::sync::Arc; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; -#[cfg(feature = "reply-surb")] -use crypto::{symmetric::stream_cipher, Digest}; -#[cfg(feature = "reply-surb")] -use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; -#[cfg(feature = "reply-surb")] -use nymsphinx::params::{ReplySurbEncryptionAlgorithm, ReplySurbKeyDigestAlgorithm}; - // Buffer Requests to say "hey, send any reconstructed messages to this channel" // or to say "hey, I'm going offline, don't send anything more to me. Just buffer them instead" pub type ReceivedBufferRequestSender = mpsc::UnboundedSender; @@ -46,26 +46,15 @@ struct ReceivedMessagesBufferInner { } impl ReceivedMessagesBufferInner { - fn process_received_fragment(&mut self, raw_fragment: Vec) -> Option { - let fragment_data = match self - .message_receiver - .recover_plaintext(self.local_encryption_keypair.private_key(), raw_fragment) - { - Err(e) => { - warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - Ok(frag_data) => frag_data, - }; - - if nymsphinx::cover::is_cover(&fragment_data) { + fn recover_from_fragment(&mut self, fragment_data: &[u8]) -> Option { + if nymsphinx::cover::is_cover(fragment_data) { trace!("The message was a loop cover message! Skipping it"); return None; } - let fragment = match self.message_receiver.recover_fragment(&fragment_data) { - Err(e) => { - warn!("failed to recover fragment from raw data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + let fragment = match self.message_receiver.recover_fragment(fragment_data) { + Err(err) => { + warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!"); return None; } Ok(frag) => frag, @@ -79,9 +68,10 @@ impl ReceivedMessagesBufferInner { // if we returned an error the underlying message is malformed in some way match self.message_receiver.insert_new_fragment(fragment) { Err(err) => match err { - MessageRecoveryError::MalformedReconstructedMessage(message_sets) => { + MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => { + error!("message reconstruction failed - {source}. Attempting to re-use the message sets..."); // TODO: should we really insert reconstructed sets? could this be abused for some attack? - for set_id in message_sets { + for set_id in used_sets { if !self.recently_reconstructed.insert(set_id) { // or perhaps we should even panic at this point? error!("Reconstructed another message containing already used set id!") @@ -107,6 +97,34 @@ impl ReceivedMessagesBufferInner { }, } } + + fn process_received_reply( + &mut self, + reply_ciphertext: &mut [u8], + reply_key: SurbEncryptionKey, + ) -> Option { + // note: this performs decryption IN PLACE without extra allocation + self.message_receiver + .recover_plaintext_from_reply(reply_ciphertext, reply_key); + let fragment_data = reply_ciphertext; + + self.recover_from_fragment(fragment_data) + } + + fn process_received_regular_packet(&mut self, mut raw_fragment: Vec) -> Option { + let fragment_data = match self.message_receiver.recover_plaintext_from_regular_packet( + self.local_encryption_keypair.private_key(), + &mut raw_fragment, + ) { + Err(err) => { + warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!"); + return None; + } + Ok(frag_data) => frag_data, + }; + + self.recover_from_fragment(fragment_data) + } } #[derive(Debug, Clone)] @@ -114,17 +132,15 @@ impl ReceivedMessagesBufferInner { // You should always use .clone() to create additional instances struct ReceivedMessagesBuffer { inner: Arc>, - - /// Storage containing keys to all [`ReplySURB`]s ever sent out that we did not receive back. - // There's no need to put it behind a Mutex since it's already properly concurrent - #[cfg(feature = "reply-surb")] - reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, } impl ReceivedMessagesBuffer { fn new( local_encryption_keypair: Arc, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, ) -> Self { ReceivedMessagesBuffer { inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { @@ -134,8 +150,8 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), })), - #[cfg(feature = "reply-surb")] reply_key_storage, + reply_controller_sender, } } @@ -177,34 +193,139 @@ impl ReceivedMessagesBuffer { guard.message_sender = Some(sender); } - async fn add_reconstructed_messages(&mut self, msgs: Vec) { - debug!("Adding {:?} new messages to the buffer!", msgs.len()); - trace!("Adding new messages to the buffer! {:?}", msgs); - self.inner.lock().await.messages.extend(msgs) + fn handle_reconstructed_plain_messages( + &mut self, + msgs: Vec, + ) -> Vec { + msgs.into_iter().map(Into::into).collect() } - #[cfg(feature = "reply-surb")] - fn process_received_reply( - reply_ciphertext: &[u8], - reply_key: SurbEncryptionKey, - ) -> Option { - let zero_iv = stream_cipher::zero_iv::(); + fn handle_reconstructed_repliable_messages( + &mut self, + msgs: Vec, + ) -> Vec { + let mut reconstructed = Vec::new(); + for msg in msgs { + let (reply_surbs, from_surb_request) = match msg.content { + RepliableMessageContent::Data { + message, + reply_surbs, + } => { + trace!( + "received message that also contained additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); - let mut reply_msg = stream_cipher::decrypt::( - reply_key.inner(), - &zero_iv, - reply_ciphertext, - ); - if let Err(err) = MessageReceiver::remove_padding(&mut reply_msg) { - warn!("Received reply had malformed padding! - {:?}", err); - None - } else { - // TODO: perhaps having to say it doesn't have a surb an indication the type should be changed? - Some(ReconstructedMessage { - message: reply_msg, - reply_surb: None, - }) + reconstructed.push(ReconstructedMessage::new(message, msg.sender_tag)); + + (reply_surbs, false) + } + RepliableMessageContent::AdditionalSurbs { reply_surbs } => { + trace!( + "received additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); + (reply_surbs, true) + } + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); + (additional_reply_surbs, false) + } + }; + + self.reply_controller_sender.send_additional_surbs( + msg.sender_tag, + reply_surbs, + from_surb_request, + ) } + reconstructed + } + + fn handle_reconstructed_reply_messages( + &mut self, + msgs: Vec, + ) -> Vec { + let mut reconstructed = Vec::new(); + for msg in msgs { + match msg.content { + ReplyMessageContent::Data { message } => reconstructed.push(message.into()), + ReplyMessageContent::SurbRequest { recipient, amount } => { + debug!("received request for {amount} additional reply SURBs from {recipient}"); + self.reply_controller_sender + .send_additional_surbs_request(*recipient, amount); + } + } + } + reconstructed + } + + async fn handle_reconstructed_messages(&mut self, msgs: Vec) { + if msgs.is_empty() { + return; + } + + let mut plain_messages = Vec::new(); + let mut repliable_messages = Vec::new(); + let mut reply_messages = Vec::new(); + + for msg in msgs { + match msg { + NymMessage::Plain(plain) => plain_messages.push(plain), + NymMessage::Repliable(repliable) => repliable_messages.push(repliable), + NymMessage::Reply(reply) => reply_messages.push(reply), + } + } + + let mut reconstructed_messages = self.handle_reconstructed_plain_messages(plain_messages); + reconstructed_messages + .append(&mut self.handle_reconstructed_repliable_messages(repliable_messages)); + reconstructed_messages + .append(&mut self.handle_reconstructed_reply_messages(reply_messages)); + + let mut inner_guard = self.inner.lock().await; + debug!( + "Adding {:?} new messages to the buffer!", + reconstructed_messages.len() + ); + + if let Some(sender) = &inner_guard.message_sender { + trace!("Sending reconstructed messages to announced sender"); + if let Err(err) = sender.unbounded_send(reconstructed_messages) { + warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})"); + inner_guard.message_sender = None; + inner_guard.messages.extend(err.into_inner()); + } + } else { + trace!("No sender available - buffering reconstructed messages"); + inner_guard.messages.extend(reconstructed_messages) + } + } + + // this function doesn't really belong here... + fn get_reply_key<'a>( + &self, + raw_message: &'a mut [u8], + ) -> Option<(SurbEncryptionKey, &'a mut [u8])> { + let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); + if raw_message.len() < reply_surb_digest_size { + return None; + } + + let possible_key_digest = + EncryptionKeyDigest::clone_from_slice(&raw_message[..reply_surb_digest_size]); + self.reply_key_storage + .try_pop(possible_key_digest) + .map(|reply_encryption_key| { + ( + *reply_encryption_key, + &mut raw_message[reply_surb_digest_size..], + ) + }) } async fn handle_new_received(&mut self, msgs: Vec>) { @@ -217,69 +338,27 @@ impl ReceivedMessagesBuffer { let mut inner_guard = self.inner.lock().await; // first check if this is a reply or a chunked message - // TODO: verify with @AP if this way of doing it is safe or whether it could - // cause some attacks due to, I don't know, stupid edge case collisions? - // Update: this DOES introduce a possible leakage: https://github.com/nymtech/nym/issues/296 - for msg in msgs { - // TODO: - // 1. make it nicer - // 2. make it not feature-locked - - #[cfg(feature = "reply-surb")] - { - let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); - - let possible_key_digest = - EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]); - - // check first `HasherOutputSize` bytes if they correspond to known encryption key - // if yes - this is a reply message - - // TODO: this might be a bottleneck - since the keys are stored on disk we, presumably, - // are doing a disk operation every single received fragment - if let Some(reply_encryption_key) = self - .reply_key_storage - .get_and_remove_encryption_key(possible_key_digest) - .expect("storage operation failed!") - { - if let Some(completed_message) = Self::process_received_reply( - &msg[reply_surb_digest_size..], - reply_encryption_key, - ) { - completed_messages.push(completed_message) - } + // note: there's a possible information leakage associated with this check https://github.com/nymtech/nym/issues/296 + for mut msg in msgs { + // check first `HasherOutputSize` bytes if they correspond to known encryption key + // if yes - this is a reply message + let completed_message = + if let Some((reply_key, reply_message)) = self.get_reply_key(&mut msg) { + inner_guard.process_received_reply(reply_message, reply_key) } else { - // otherwise - it's a 'normal' message - if let Some(completed_message) = inner_guard.process_received_fragment(msg) { - completed_messages.push(completed_message) - } - } - } + inner_guard.process_received_regular_packet(msg) + }; - #[cfg(not(feature = "reply-surb"))] - if let Some(completed_message) = inner_guard.process_received_fragment(msg) { - completed_messages.push(completed_message) + if let Some(completed) = completed_message { + info!("received {completed}"); + completed_messages.push(completed) } } + drop(inner_guard); + if !completed_messages.is_empty() { - if let Some(sender) = &inner_guard.message_sender { - trace!("Sending reconstructed messages to announced sender"); - if let Err(err) = sender.unbounded_send(completed_messages) { - warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {:?})", err); - // make sure to drop the lock to not deadlock - // (it is required by `add_reconstructed_messages`) - inner_guard.message_sender = None; - drop(inner_guard); - self.add_reconstructed_messages(err.into_inner()).await; - } - } else { - // make sure to drop the lock to not deadlock - // (it is required by `add_reconstructed_messages`) - drop(inner_guard); - trace!("No sender available - buffering reconstructed messages"); - self.add_reconstructed_messages(completed_messages).await; - } + self.handle_reconstructed_messages(completed_messages).await } } } @@ -342,16 +421,6 @@ impl RequestReceiver { shutdown.recv_timeout().await; log::debug!("RequestReceiver: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - async fn run(&mut self) { - debug!("Started RequestReceiver without graceful shutdown support"); - - while let Some(message) = self.query_receiver.next().await { - self.handle_message(message).await - } - } } struct FragmentedMessageReceiver { @@ -391,34 +460,25 @@ impl FragmentedMessageReceiver { shutdown.recv_timeout().await; log::debug!("FragmentedMessageReceiver: Exiting"); } - - // todo: think whether this is still required - #[allow(dead_code)] - async fn run(&mut self) { - debug!("Started FragmentedMessageReceiver without graceful shutdown support"); - - while let Some(new_messages) = self.mixnet_packet_receiver.next().await { - self.received_buffer.handle_new_received(new_messages).await; - } - } } -pub struct ReceivedMessagesBufferController { +pub(crate) struct ReceivedMessagesBufferController { fragmented_message_receiver: FragmentedMessageReceiver, request_receiver: RequestReceiver, } impl ReceivedMessagesBufferController { - pub fn new( + pub(crate) fn new( local_encryption_keypair: Arc, query_receiver: ReceivedBufferRequestReceiver, mixnet_packet_receiver: MixnetMessageReceiver, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new( local_encryption_keypair, - #[cfg(feature = "reply-surb")] reply_key_storage, + reply_controller_sender, ); ReceivedMessagesBufferController { @@ -444,16 +504,4 @@ impl ReceivedMessagesBufferController { request_receiver.run_with_shutdown(shutdown).await; }); } - - #[cfg(target_arch = "wasm32")] - pub fn start(self) { - let mut fragmented_message_receiver = self.fragmented_message_receiver; - let mut request_receiver = self.request_receiver; - spawn_future(async move { - fragmented_message_receiver.run().await; - }); - spawn_future(async move { - request_receiver.run().await; - }); - } } diff --git a/clients/client-core/src/client/replies/mod.rs b/clients/client-core/src/client/replies/mod.rs new file mode 100644 index 0000000000..61b5f19abf --- /dev/null +++ b/clients/client-core/src/client/replies/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod reply_controller; +pub mod reply_storage; diff --git a/clients/client-core/src/client/replies/reply_controller.rs b/clients/client-core/src/client/replies/reply_controller.rs new file mode 100644 index 0000000000..99687527f0 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_controller.rs @@ -0,0 +1,928 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; +use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError}; +use crate::client::replies::reply_storage::CombinedReplyStorage; +use client_connections::TransmissionLane; +use futures::channel::mpsc; +use futures::StreamExt; +use log::{debug, error, info, trace, warn}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier}; +use rand::{CryptoRng, Rng}; +use std::cmp::{max, min}; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::sync::{Arc, Weak}; +use std::time::Duration; +use time::OffsetDateTime; + +#[cfg(not(target_arch = "wasm32"))] +type IntervalStream = tokio_stream::wrappers::IntervalStream; + +#[cfg(target_arch = "wasm32")] +type IntervalStream = gloo_timers::future::IntervalStream; + +pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) { + let (tx, rx) = mpsc::unbounded(); + (tx.into(), rx) +} + +#[derive(Debug, Clone)] +pub(crate) struct ReplyControllerSender(mpsc::UnboundedSender); + +impl From> for ReplyControllerSender { + fn from(inner: mpsc::UnboundedSender) -> Self { + ReplyControllerSender(inner) + } +} + +impl ReplyControllerSender { + pub(crate) fn send_retransmission_data( + &self, + recipient: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surb_request: bool, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::RetransmitReply { + recipient, + timed_out_ack, + extra_surb_request, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_reply( + &self, + recipient: AnonymousSenderTag, + message: Vec, + lane: TransmissionLane, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::SendReply { + recipient, + message, + lane, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_additional_surbs( + &self, + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::AdditionalSurbs { + sender_tag, + reply_surbs, + from_surb_request, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_additional_surbs_request(&self, recipient: Recipient, amount: u32) { + self.0 + .unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest { + recipient: Box::new(recipient), + amount, + }) + .expect("ReplyControllerReceiver has died!") + } +} + +pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver; + +#[derive(Debug)] +pub(crate) enum ReplyControllerMessage { + RetransmitReply { + recipient: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surb_request: bool, + }, + + SendReply { + recipient: AnonymousSenderTag, + message: Vec, + lane: TransmissionLane, + }, + + AdditionalSurbs { + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + }, + + // Should this also be handled in here? it's technically a completely different side of the pipe + // let's see how it works when combined, might split it before creating PR + AdditionalSurbsRequest { + recipient: Box, + amount: u32, + }, +} + +pub struct Config { + min_surb_request_size: u32, + max_surb_request_size: u32, + maximum_allowed_reply_surb_request_size: u32, + max_surb_waiting_period: Duration, + max_reply_surb_age: Duration, + max_reply_key_age: Duration, +} + +impl Config { + pub(crate) fn new( + min_surb_request_size: u32, + max_surb_request_size: u32, + maximum_allowed_reply_surb_request_size: u32, + max_surb_waiting_period: Duration, + max_reply_surb_age: Duration, + max_reply_key_age: Duration, + ) -> Self { + Self { + min_surb_request_size, + max_surb_request_size, + maximum_allowed_reply_surb_request_size, + max_surb_waiting_period, + max_reply_surb_age, + max_reply_key_age, + } + } +} + +// the purpose of this task: +// - buffers split messages from input message listener if there were insufficient surbs to send them +// - upon getting extra surbs, resends them +// - so I guess it will handle all 'RepliableMessage' and requests from 'ReplyMessage' +// - replies to "give additional surbs" requests +// - will reply to future heartbeats + +// TODO: this should be split into ingress and egress controllers +// because currently its trying to perform two distinct jobs +pub struct ReplyController { + config: Config, + + // TODO: incorporate that field at some point + // and use binomial distribution to determine the expected required number + // of surbs required to send the message through + // expected_reliability: f32, + request_receiver: ReplyControllerReceiver, + pending_replies: HashMap>, + + /// Retransmission packets that have already timed out and are waiting for additional reply SURBs + /// so that they could be sent back to the network. Once we receive more SURBs, we should send them ASAP. + // TODO: when purging stale entries, we must take extra care to also purge all pending ACK data!! + pending_retransmissions: + HashMap>>, + + message_handler: MessageHandler, + full_reply_storage: CombinedReplyStorage, +} + +impl ReplyController +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + message_handler: MessageHandler, + full_reply_storage: CombinedReplyStorage, + request_receiver: ReplyControllerReceiver, + ) -> Self { + ReplyController { + config, + request_receiver, + pending_replies: HashMap::new(), + pending_retransmissions: HashMap::new(), + message_handler, + full_reply_storage, + } + } + + /// Inserts the pending replies into the BACK of the queue fn insert_pending_replies>>( + fn insert_pending_replies>>( + &mut self, + recipient: &AnonymousSenderTag, + fragments: V, + ) { + if let Some(existing) = self.pending_replies.get_mut(recipient) { + existing.append(&mut fragments.into()) + } else { + self.pending_replies.insert(*recipient, fragments.into()); + } + } + + fn re_insert_pending_retransmission( + &mut self, + recipient: &AnonymousSenderTag, + data: Vec>, + ) { + // the underlying entry MUST exist as we've just got data from there + let map_entry = self + .pending_retransmissions + .get_mut(recipient) + .expect("our pending retransmission entry is somehow gone!"); + + for pending in data { + // if it's 0, we don't need to do anything - we just got that ack! + if Arc::strong_count(&pending) > 1 { + let id = pending.inner_fragment_identifier(); + let downgraded = Arc::downgrade(&pending); + map_entry.insert(id, downgraded); + } + } + } + + fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool { + trace!("checking if we should request more surbs from {:?}", target); + + let pending_queue_size = self + .pending_replies + .get(target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let retransmission_queue = self + .pending_retransmissions + .get(target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let total_queue = pending_queue_size + retransmission_queue; + + // simple as that - there's absolutely nothing to retransmit + if total_queue == 0 { + return false; + } + + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(target); + let pending_surbs = self + .full_reply_storage + .surbs_storage_ref() + .pending_reception(target) as usize; + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + let max_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .max_surb_threshold(); + + debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}"); + + (pending_surbs + available_surbs) < max_surbs_threshold + && (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold) + } + + async fn handle_send_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) { + if !self + .full_reply_storage + .surbs_storage_ref() + .contains_surbs_for(&recipient_tag) + { + warn!("received reply request for {:?} but we don't have any surbs stored for that recipient!", recipient_tag); + return; + } + + trace!("handling reply to {:?}", recipient_tag); + let fragments = self.message_handler.split_reply_message(data); + + let required_surbs = fragments.len(); + trace!("This reply requires {:?} SURBs", required_surbs); + + // TODO: edge case: + // we're making a lot of requests and have to request a lot of surbs + // (but at some point we run out of surbs for surb requests) + + let (surbs, _surbs_left) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&recipient_tag, required_surbs); + + if let Some(reply_surbs) = surbs { + if let Err(err) = self + .message_handler + .try_send_reply_chunks(recipient_tag, fragments, reply_surbs, lane) + .await + { + let err = err.return_unused_surbs( + self.full_reply_storage.surbs_storage_ref(), + &recipient_tag, + ); + warn!("failed to send reply to {:?} - {err}", recipient_tag); + + // TODO: should we buffer that data to try again? + } + } else { + // we don't have enough surbs for this reply + self.insert_pending_replies(&recipient_tag, fragments); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + } + + async fn request_additional_reply_surbs( + &mut self, + target: AnonymousSenderTag, + amount: u32, + ) -> Result<(), PreparationError> { + let reply_surb = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surb_ignoring_threshold(&target) + .and_then(|(reply_surb, _)| reply_surb) + .ok_or(PreparationError::NotEnoughSurbs { + available: 0, + required: 1, + })?; + + if let Err(err) = self + .message_handler + .try_request_additional_reply_surbs(target, reply_surb, amount) + .await + { + let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + warn!( + "failed to request additional surbs from {:?} - {err}", + target + ); + return Err(err); + } else { + self.full_reply_storage + .surbs_storage_ref() + .increment_pending_reception(&target, amount); + } + + Ok(()) + } + + async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending retransmission queue"); + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(&target); + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for retransmission queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + let Some(pending) = self.pending_retransmissions.get_mut(&target) else { + trace!("there are no pending retransmissions for {target}!"); + return; + }; + + let mut to_take = Vec::new(); + let mut to_remove = Vec::new(); + + // TODO: once rust 1.66.0 is stabilised on 15.12.22, just change it to + // `.pop_front()` to directly take ownership + for (k, data) in pending.iter() { + let upgraded = match data.upgrade() { + Some(upgraded) => upgraded, + None => { + // we got the ack while the data was waiting in the queue + to_remove.push(*k); + continue; + } + }; + + to_take.push(upgraded); + + // we have taken as many entries as we could have + if to_take.len() >= max_to_clear { + break; + } + // TODO: use if upgraded.is_extra_surb_request() to bypass the limit + } + + for ack in &to_take { + pending.remove(&ack.inner_fragment_identifier()); + } + + for id in to_remove { + pending.remove(&id); + } + + if to_take.is_empty() { + // no need to do anything + return; + } + + let (surbs_for_reply, _) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&target, to_take.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.re_insert_pending_retransmission(&target, to_take); + return; + }; + + let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect(); + + if let Err(err) = self + .message_handler + .try_send_retransmission_reply_chunks( + to_send_vec, + surbs_for_reply, + TransmissionLane::Retransmission, + ) + .await + { + let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + self.re_insert_pending_retransmission(&target, to_take); + + warn!( + "failed to clear pending retransmission queue for {:?} - {err}", + target + ); + } + } + + fn pop_at_most_pending_replies( + &mut self, + from: &AnonymousSenderTag, + amount: usize, + ) -> Option> { + // if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb + let total = self.pending_replies.get(from)?.len(); + trace!("pending queue has {total} elements"); + if total == 0 { + return None; + } + if total < amount { + self.pending_replies.remove(from) + } else { + Some( + self.pending_replies + .get_mut(from)? + .drain(..amount) + .collect(), + ) + } + } + + async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending queue"); + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(&target); + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + // we're guaranteed to not get more entries than we have reply surbs for + if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) { + let to_send_vec = to_send.iter().cloned().collect::>(); + + if to_send_vec.is_empty() { + panic!( + "please let the devs know if you ever see this message (reply_controller.rs)" + ); + } + + let (surbs_for_reply, _) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&target, to_send_vec.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.insert_pending_replies(&target, to_send); + return; + }; + + if let Err(err) = self + .message_handler + .try_send_reply_chunks( + target, + to_send_vec, + surbs_for_reply, + TransmissionLane::General, + ) + .await + { + let err = + err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + self.insert_pending_replies(&target, to_send); + warn!("failed to clear pending queue for {:?} - {err}", target); + } + } else { + trace!("the pending queue is empty"); + } + } + + async fn handle_received_surbs( + &mut self, + from: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + ) { + trace!("handling received surbs"); + + // clear the requesting flag since we should have been asking for surbs + self.full_reply_storage + .surbs_storage_ref() + .reset_surbs_last_received_at(&from); + if from_surb_request { + self.full_reply_storage + .surbs_storage_ref() + .decrement_pending_reception(&from, reply_surbs.len() as u32); + } + + // store received surbs + self.full_reply_storage + .surbs_storage_ref() + .insert_surbs(&from, reply_surbs); + + // use as many as we can for clearing pending retransmission queue + self.try_clear_pending_retransmission(from).await; + + // use as many as we can for clearing pending 'normal' queue + self.try_clear_pending_queue(from).await; + + // if we have to, request more + if self.should_request_more_surbs(&from) { + self.request_reply_surbs_for_queue_clearing(from).await; + } + } + + async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) { + // 1. check whether we sent any surbs in the past to this recipient, otherwise + // they have no business in asking for more + if !self + .full_reply_storage + .tags_storage_ref() + .exists(&recipient) + { + warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"); + return; + } + + // 2. check whether the requested amount is within sane range + if amount > self.config.maximum_allowed_reply_surb_request_size { + warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.maximum_allowed_reply_surb_request_size); + amount = self.config.maximum_allowed_reply_surb_request_size; + } + + // 3. construct and send the surbs away + // (send them in smaller batches to make the experience a bit smoother + let mut remaining = amount; + while remaining > 0 { + let to_send = min(remaining, 100); + if let Err(err) = self + .message_handler + .try_send_additional_reply_surbs(recipient, to_send) + .await + { + warn!("failed to send additional surbs to {recipient} - {err}"); + } else { + trace!("sent {to_send} reply SURBs to {recipient}"); + } + + remaining -= to_send; + } + } + + fn buffer_pending_ack( + &mut self, + recipient: AnonymousSenderTag, + ack_ref: Arc, + weak_ack_ref: Weak, + ) { + let frag_id = ack_ref.inner_fragment_identifier(); + if let Some(existing) = self.pending_retransmissions.get_mut(&recipient) { + if let Entry::Vacant(e) = existing.entry(frag_id) { + e.insert(weak_ack_ref); + } else { + warn!("we're already trying to retransmit {frag_id}. We must be really behind in surbs!"); + } + } else { + let mut inner = BTreeMap::new(); + inner.insert(frag_id, weak_ack_ref); + self.pending_retransmissions.insert(recipient, inner); + } + } + + async fn handle_reply_retransmission( + &mut self, + recipient_tag: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surbs_request: bool, + ) { + // seems we got the ack in the end + let ack_ref = match timed_out_ack.upgrade() { + Some(ack) => ack, + None => { + debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue"); + return; + } + }; + + // if this is retransmission for obtaining additional reply surbs, + // we can dip below the storage threshold + let (maybe_reply_surb, _) = if extra_surbs_request { + self.full_reply_storage + .surbs_storage_ref() + .get_reply_surb_ignoring_threshold(&recipient_tag) + } else { + self.full_reply_storage + .surbs_storage_ref() + .get_reply_surb(&recipient_tag) + } + .expect("attempted to retransmit a packet to an unknown recipient - we shouldn't have sent the original packet in the first place!"); + + if let Some(reply_surb) = maybe_reply_surb { + match self + .message_handler + .try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data()) + .await + { + Ok(prepared) => { + // drop the ack ref so that controller would not panic on `UpdateTimer` if that task + // got to handle the action before this function terminated (which is very much + // possible if `forward_messages` takes a while) + drop(ack_ref); + + self.message_handler + .update_ack_delay(prepared.fragment_identifier, prepared.total_delay); + self.message_handler + .forward_messages(vec![prepared.into()], TransmissionLane::Retransmission) + .await; + } + Err(err) => { + let err = err.return_unused_surbs( + self.full_reply_storage.surbs_storage_ref(), + &recipient_tag, + ); + warn!("failed to prepare message for retransmission - {err}"); + // we buffer that packet and to try another day + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + }; + } else { + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + } + + async fn handle_request(&mut self, request: ReplyControllerMessage) { + match request { + ReplyControllerMessage::RetransmitReply { + recipient, + timed_out_ack, + extra_surb_request, + } => { + self.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request) + .await + } + ReplyControllerMessage::SendReply { + recipient, + message, + lane, + } => self.handle_send_reply(recipient, message, lane).await, + ReplyControllerMessage::AdditionalSurbs { + sender_tag, + reply_surbs, + from_surb_request, + } => { + self.handle_received_surbs(sender_tag, reply_surbs, from_surb_request) + .await + } + ReplyControllerMessage::AdditionalSurbsRequest { recipient, amount } => { + self.handle_surb_request(*recipient, amount).await + } + } + } + + async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) { + trace!("requesting surbs for queues clearing"); + + let pending_queue_size = self + .pending_replies + .get(&target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let retransmission_queue = self + .pending_retransmissions + .get(&target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let total_queue = (pending_queue_size + retransmission_queue) as u32; + + if total_queue == 0 { + trace!("the pending queues for {:?} are already empty", target); + return; + } + + let request_size = min( + self.config.max_surb_request_size, + max(total_queue, self.config.min_surb_request_size), + ); + + if let Err(err) = self + .request_additional_reply_surbs(target, request_size) + .await + { + warn!("failed to request additional surbs... - {err}") + } + } + + async fn inspect_stale_entries(&mut self) { + let mut to_request = Vec::new(); + let mut to_remove = Vec::new(); + + let now = OffsetDateTime::now_utc(); + for (pending_reply_target, vals) in &self.pending_replies { + if vals.is_empty() { + continue; + } + + let Some(last_received) = self.full_reply_storage.surbs_storage_ref().surbs_last_received_at(pending_reply_target) else { + error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", vals.len()); + to_remove.push(*pending_reply_target); + continue; + }; + + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { + error!("somehow our stored timestamp ({last_received}) for surbs from {pending_reply_target} is corrupted!. Going to remove all the associated entries"); + to_remove.push(*pending_reply_target); + continue; + }; + + let diff = now - last_received_time; + + if diff > self.config.max_surb_waiting_period { + warn!("We haven't received any surbs in {:?} from {pending_reply_target}. Going to explicitly ask for more", diff); + to_request.push(*pending_reply_target); + } + } + + for pending_reply_target in to_request { + self.request_reply_surbs_for_queue_clearing(pending_reply_target) + .await; + self.full_reply_storage + .surbs_storage_ref() + .reset_pending_reception(&pending_reply_target) + } + for to_remove in to_remove { + self.pending_replies.remove(&to_remove); + } + } + + async fn invalidate_old_data(&self) { + let now = OffsetDateTime::now_utc(); + + let mut to_remove_surbs = Vec::new(); + let mut to_remove_keys = Vec::new(); + for map_ref in self.full_reply_storage.surbs_storage_ref().as_raw_iter() { + let (sender, received) = map_ref.pair(); + // TODO: handle the following edge case: + // there's a malicious client sending us exactly one reply surb just before we should have invalidated + // the data thus making us keep everything in memory + // possible solution: keep timestamp PER reply surb (but that seems like an overkill) + // but I doubt this is ever going to be a problem... + // ... + // However, if you're reading this message, it probably became a legit problem, + // so I guess add timestamp per surb then? chop-chop. + + let last_received = received.surbs_last_received_at(); + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { + error!("somehow our stored timestamp ({last_received}) for surbs from {sender} is corrupted!. Going to remove all the associated entries"); + to_remove_surbs.push(*sender); + continue; + }; + let diff = now - last_received_time; + + if diff > self.config.max_reply_surb_age { + info!("it's been {diff:?} since we last received any reply surb from {sender}. Going to remove all stored entries..."); + + to_remove_surbs.push(*sender); + } + } + + for map_ref in self.full_reply_storage.key_storage_ref().as_raw_iter() { + let (digest, reply_key) = map_ref.pair(); + + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(sent_at) = OffsetDateTime::from_unix_timestamp(reply_key.sent_at_timestamp) else { + error!("somehow our stored timestamp ({}) for one of our reply key is corrupted!. Going to remove all the entry", reply_key.sent_at_timestamp); + to_remove_keys.push(*digest); + continue; + }; + + let diff = now - sent_at; + + if diff > self.config.max_reply_key_age { + debug!("it's been {diff:?} since we created this reply key. it's probably never going to get used, so we're going to purge it..."); + to_remove_keys.push(*digest); + } + } + + for to_remove in to_remove_surbs { + self.full_reply_storage + .surbs_storage_ref() + .remove(&to_remove); + } + + for to_remove in to_remove_keys { + self.full_reply_storage.key_storage().remove(to_remove) + } + } + + fn create_interval_stream(polling_rate: Duration) -> IntervalStream { + #[cfg(not(target_arch = "wasm32"))] + return tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(polling_rate)); + + #[cfg(target_arch = "wasm32")] + return gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32); + } + + pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started ReplyController with graceful shutdown support"); + + let polling_rate = Duration::from_secs(5); + let mut stale_inspection = Self::create_interval_stream(polling_rate); + + // this is in the order of hours/days so we don't have to poll it that often + let polling_rate = Duration::from_secs(self.config.max_reply_surb_age.as_secs() / 10); + let mut invalidation_inspection = Self::create_interval_stream(polling_rate); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("ReplyController: Received shutdown"); + }, + req = self.request_receiver.next() => match req { + Some(req) => self.handle_request(req).await, + None => { + log::trace!("ReplyController: Stopping since channel closed"); + break; + } + }, + _ = stale_inspection.next() => { + self.inspect_stale_entries().await + }, + _ = invalidation_inspection.next() => { + self.invalidate_old_data().await + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("ReplyController: Exiting"); + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs new file mode 100644 index 0000000000..9711ea759f --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs @@ -0,0 +1,43 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::Empty; +use crate::client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend}; +use async_trait::async_trait; + +// well, right now we don't have the browser storage : ( +// so we keep everything in memory +pub struct Backend { + empty: Empty, +} + +impl Backend { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> Self { + Backend { + empty: Empty { + min_surb_threshold, + max_surb_threshold, + }, + } + } +} + +#[async_trait] +impl ReplyStorageBackend for Backend { + type StorageError = ::StorageError; + + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + self.empty.flush_surb_storage(storage).await + } + + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> { + self.empty.init_fresh(fresh).await + } + + async fn load_surb_storage(&self) -> Result { + self.empty.load_surb_storage().await + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs new file mode 100644 index 0000000000..8164762123 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs @@ -0,0 +1,53 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StorageError { + #[error("the provided database path doesn't have a filename defined")] + DatabasePathWithoutFilename { provided_path: PathBuf }, + + #[error("failed to rename our databse file - {source}")] + DatabaseRenameError { + #[source] + source: io::Error, + }, + + #[error("failed to rename our old databse file - {source}")] + DatabaseOldFileRemoveError { + #[source] + source: io::Error, + }, + + #[error("failed to perform sqlx migration: {source}")] + MigrationError { + #[source] + #[from] + source: sqlx::migrate::MigrateError, + }, + + #[error("failed to connect to the underlying connection pool: {source}")] + DatabaseConnectionError { + #[source] + source: sqlx::error::Error, + }, + + #[error("failed to run the SQL query: {source}")] + QueryError { + #[source] + #[from] + source: sqlx::error::Error, + }, + + #[error("The loaded data is inconsistent - it seems that on the last shutdown the client hasn't finished the data flush. You may have to remove the entire storage manually")] + IncompleteDataFlush, + + #[error("data retrieved from the underlying storage is corrupted: {details}")] + CorruptedData { + details: String, + // err: Option> + }, +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs new file mode 100644 index 0000000000..578c0021a3 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs @@ -0,0 +1,257 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; +use crate::client::replies::reply_storage::backend::fs_backend::models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, +}; +use log::{error, info}; +use sqlx::ConnectOptions; +use std::path::Path; + +#[derive(Debug, Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub(crate) async fn init>( + database_path: P, + fresh: bool, + ) -> Result { + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(fresh); + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(pool) => pool, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(StorageError::DatabaseConnectionError { source: err }); + } + }; + + if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") + .run(&connection_pool) + .await + { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + Ok(StorageManager { connection_pool }) + } + + #[allow(dead_code)] + pub(crate) async fn status_table_exists(&self) -> Result { + sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") + .fetch_optional(&self.connection_pool) + .await + .map(|r| r.is_some()) + } + + pub(crate) async fn create_status_table(&self) -> Result<(), sqlx::Error> { + sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_flush_status(&self) -> Result { + sqlx::query!("SELECT flush_in_progress FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.flush_in_progress > 0) + } + + pub(crate) async fn set_previous_flush_timestamp( + &self, + timestamp: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_previous_flush_timestamp(&self) -> Result { + sqlx::query!("SELECT previous_flush_timestamp FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.previous_flush_timestamp) + } + + pub(crate) async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { + let in_progress_int = i64::from(in_progress); + sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_client_in_use_status(&self) -> Result { + sqlx::query!("SELECT client_in_use FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.client_in_use > 0) + } + + pub(crate) async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { + let in_use_int = i64::from(in_use); + sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM sender_tag;") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_tags(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO sender_tag(recipient, tag) VALUES (?, ?); + "#, + stored_tag.recipient, + stored_tag.tag + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM reply_key;") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_reply_keys(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_reply_key( + &self, + stored_reply_key: StoredReplyKey, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?); + "#, + stored_reply_key.key_digest, + stored_reply_key.reply_key, + stored_reply_key.sent_at_timestamp + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_surb_senders(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_surb_sender( + &self, + stored_surb_sender: StoredSurbSender, + ) -> Result { + let id = sqlx::query!( + r#" + INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?); + "#, + stored_surb_sender.tag, + stored_surb_sender.last_sent_timestamp + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn get_reply_surbs( + &self, + sender_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + StoredReplySurb, + "SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?", + sender_id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM reply_surb;") + .execute(&self.connection_pool) + .await?; + + sqlx::query!("DELETE FROM reply_surb_sender;") + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_reply_surb( + &self, + stored_reply_surb: StoredReplySurb, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?); + "#, + stored_reply_surb.reply_surb_sender_id, + stored_reply_surb.reply_surb + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_reply_surb_storage_metadata( + &self, + ) -> Result { + sqlx::query_as!( + ReplySurbStorageMetadata, + r#" + SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; + "#, + ) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn insert_reply_surb_storage_metadata( + &self, + metadata: ReplySurbStorageMetadata, + ) -> Result<(), sqlx::Error> { + sqlx::query!(r#" + INSERT INTO reply_surb_storage_metadata(min_reply_surb_threshold, max_reply_surb_threshold) + VALUES (?, ?); + "#, + metadata.min_reply_surb_threshold, + metadata.max_reply_surb_threshold, + ).execute(&self.connection_pool).await?; + Ok(()) + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs new file mode 100644 index 0000000000..50916e00c3 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs @@ -0,0 +1,336 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use self::error::StorageError; +use crate::client::replies::reply_storage::backend::fs_backend::manager::StorageManager; +use crate::client::replies::reply_storage::backend::fs_backend::models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, +}; +use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbs; +use crate::client::replies::reply_storage::{ + CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, +}; +use async_trait::async_trait; +use log::{error, info, warn}; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use std::fs; +use std::path::{Path, PathBuf}; +use time::OffsetDateTime; + +mod error; +mod manager; +mod models; + +#[derive(Debug)] +pub struct Backend { + temporary_old_path: Option, + database_path: PathBuf, + manager: StorageManager, +} + +impl Backend { + const OLD_EXTENSION: &'static str = "old"; + + pub async fn init>(database_path: P) -> Result { + let owned_path: PathBuf = database_path.as_ref().into(); + if owned_path.file_name().is_none() { + return Err(StorageError::DatabasePathWithoutFilename { + provided_path: owned_path, + }); + } + + let backend = Backend { + temporary_old_path: None, + database_path: owned_path, + manager: StorageManager::init(database_path, true).await?, + }; + + backend.manager.create_status_table().await?; + + Ok(backend) + } + + pub async fn try_load>(database_path: P) -> Result { + let owned_path: PathBuf = database_path.as_ref().into(); + if owned_path.file_name().is_none() { + return Err(StorageError::DatabasePathWithoutFilename { + provided_path: owned_path, + }); + } + + let manager = StorageManager::init(database_path, false).await?; + + // the database flush wasn't fully finished and thus the data is in inconsistent state + // (we don't really know what's properly saved or what's not) + if manager.get_flush_status().await? { + return Err(StorageError::IncompleteDataFlush); + } + + // the process has gone down without full graceful shutdown, + // meaning the database doesn't contain valid data anymore + // so we have to purge it + if manager.get_client_in_use_status().await? { + error!("the client hasn't undergone through graceful shutdown the last time it's gone down - we can't trust its reply surbs or stored encryption keys. They shall get purged"); + manager.delete_all_reply_surb_data().await?; + manager.delete_all_reply_keys().await?; + } + + let last_flush_timestamp = manager.get_previous_flush_timestamp().await?; + let last_flush = match OffsetDateTime::from_unix_timestamp(last_flush_timestamp) { + Ok(last_flush) => last_flush, + Err(err) => { + return Err(StorageError::CorruptedData { + details: format!("failed to parse stored timestamp - {err}"), + }); + } + }; + + // in theory clients can use our reply surbs whenever they want, even a year in the future + // (assuming no key rotation has happened) + // but the way it's currently coded, everyone will purge old data + let since_last_flush = OffsetDateTime::now_utc() - last_flush; + if since_last_flush.whole_days() > 0 { + info!("it's been over {} days and {} hours since we last used our data store. our reply surbs are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_reply_surb_data().await?; + } + + if since_last_flush.whole_days() > 1 { + info!("it's been over {} days and {} hours since we last used our data store. our reply keys are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_reply_keys().await?; + } + + if since_last_flush.whole_days() > 2 { + info!("it's been over {} days and {} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_tags().await?; + } + + Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }) + } + + async fn close_pool(&mut self) { + self.manager.connection_pool.close().await; + } + + async fn rotate(&mut self) -> Result<(), StorageError> { + self.close_pool().await; + + let new_extension = if let Some(existing_extension) = + self.database_path.extension().and_then(|ext| ext.to_str()) + { + format!("{existing_extension}.{}", Self::OLD_EXTENSION) + } else { + Self::OLD_EXTENSION.to_string() + }; + + let mut temp_old = self.database_path.clone(); + temp_old.set_extension(new_extension); + + fs::rename(&self.database_path, &temp_old) + .map_err(|err| StorageError::DatabaseRenameError { source: err })?; + self.manager = StorageManager::init(&self.database_path, true).await?; + self.manager.create_status_table().await?; + + self.temporary_old_path = Some(temp_old); + Ok(()) + } + + fn remove_old(&mut self) -> Result<(), StorageError> { + if let Some(old_path) = self.temporary_old_path.take() { + fs::remove_file(old_path) + .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) + } else { + warn!("the old database file doesn't seem to exist!"); + Ok(()) + } + } + + async fn start_storage_flush(&self) -> Result<(), StorageError> { + Ok(self.manager.set_flush_status(true).await?) + } + + async fn end_storage_flush(&self) -> Result<(), StorageError> { + self.manager + .set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp()) + .await?; + Ok(self.manager.set_flush_status(false).await?) + } + + async fn start_client_use(&self) -> Result<(), StorageError> { + Ok(self.manager.set_client_in_use_status(true).await?) + } + + async fn stop_client_use(&self) -> Result<(), StorageError> { + Ok(self.manager.set_client_in_use_status(false).await?) + } + + async fn get_stored_tags(&self) -> Result { + let stored = self.manager.get_tags().await?; + + // stop at the first instance of corruption. if even a single entry is malformed, + // something weird has happened and we can't trust the rest of the data + let raw = stored + .into_iter() + .map(TryInto::try_into) + .collect::>()?; + + Ok(UsedSenderTags::from_raw(raw)) + } + + async fn dump_sender_tags(&self, tags: &UsedSenderTags) -> Result<(), StorageError> { + for map_ref in tags.as_raw_iter() { + let (recipient, tag) = map_ref.pair(); + self.manager + .insert_tag(StoredSenderTag::new(*recipient, *tag)) + .await?; + } + Ok(()) + } + + async fn get_stored_reply_keys(&self) -> Result { + let stored = self.manager.get_reply_keys().await?; + + // stop at the first instance of corruption. if even a single entry is malformed, + // something weird has happened and we can't trust the rest of the data + let raw = stored + .into_iter() + .map(TryInto::try_into) + .collect::>()?; + + Ok(SentReplyKeys::from_raw(raw)) + } + + async fn dump_sender_reply_keys(&self, reply_keys: &SentReplyKeys) -> Result<(), StorageError> { + for map_ref in reply_keys.as_raw_iter() { + let (digest, key) = map_ref.pair(); + self.manager + .insert_reply_key(StoredReplyKey::new(*digest, *key)) + .await?; + } + Ok(()) + } + + async fn get_stored_reply_surbs(&self) -> Result { + let surb_senders = self.manager.get_surb_senders().await?; + + let metadata = self.get_reply_surb_storage_metadata().await?; + let mut received_surbs = Vec::with_capacity(surb_senders.len()); + for sender in surb_senders { + let sender_id = sender.id; + let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) = + sender.try_into()?; + let stored_surbs = self + .manager + .get_reply_surbs(sender_id) + .await? + .into_iter() + .map(|raw| raw.try_into()) + .collect::>()?; + + received_surbs.push(( + sender_tag, + ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp), + )) + } + + Ok(ReceivedReplySurbsMap::from_raw( + metadata.min_reply_surb_threshold as usize, + metadata.max_reply_surb_threshold as usize, + received_surbs, + )) + } + + async fn dump_reply_surbs( + &self, + reply_surbs: &ReceivedReplySurbsMap, + ) -> Result<(), StorageError> { + for map_ref in reply_surbs.as_raw_iter() { + let (tag, received_surbs) = map_ref.pair(); + let sender_id = self + .manager + .insert_surb_sender(StoredSurbSender::new( + *tag, + received_surbs.surbs_last_received_at(), + )) + .await?; + + for reply_surb in received_surbs.surbs_ref() { + self.manager + .insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb)) + .await? + } + } + Ok(()) + } + + async fn get_reply_surb_storage_metadata( + &self, + ) -> Result { + self.manager + .get_reply_surb_storage_metadata() + .await + .map_err(Into::into) + } + + async fn dump_reply_surb_storage_metadata( + &self, + reply_surbs: &ReceivedReplySurbsMap, + ) -> Result<(), StorageError> { + self.manager + .insert_reply_surb_storage_metadata(ReplySurbStorageMetadata::new( + reply_surbs.min_surb_threshold(), + reply_surbs.max_surb_threshold(), + )) + .await + .map_err(Into::into) + } +} + +#[async_trait] +impl ReplyStorageBackend for Backend { + type StorageError = error::StorageError; + + async fn start_storage_session(&self) -> Result<(), Self::StorageError> { + self.start_client_use().await + } + + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + // close all connections (there should be none! and rename the file to contain .old extension) + self.rotate().await?; + self.start_storage_flush().await?; + + self.dump_sender_tags(storage.tags_storage_ref()).await?; + self.dump_sender_reply_keys(storage.key_storage_ref()) + .await?; + let surbs_ref = storage.surbs_storage_ref(); + self.dump_reply_surb_storage_metadata(surbs_ref).await?; + self.dump_reply_surbs(surbs_ref).await?; + + self.remove_old()?; + self.end_storage_flush().await + } + + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> { + // for now nothing more to do apart from dumping the metadata + self.dump_reply_surb_storage_metadata(fresh.surbs_storage_ref()) + .await + } + + async fn load_surb_storage(&self) -> Result { + let reply_keys = self.get_stored_reply_keys().await?; + let tags = self.get_stored_tags().await?; + let reply_surbs = self.get_stored_reply_surbs().await?; + + Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags)) + } + + async fn stop_storage_session(self) -> Result<(), Self::StorageError> { + self.stop_client_use().await + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs new file mode 100644 index 0000000000..65088ad05e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs @@ -0,0 +1,185 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; +use crate::client::replies::reply_storage::key_storage::UsedReplyKey; +use crypto::generic_array::typenum::Unsigned; +use crypto::Digest; +use nymsphinx::addressing::clients::{Recipient, RecipientBytes}; +use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; +use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize}; +use nymsphinx::params::ReplySurbKeyDigestAlgorithm; + +#[derive(Debug, Clone)] +pub(crate) struct StoredSenderTag { + pub(crate) recipient: Vec, + pub(crate) tag: Vec, +} + +impl StoredSenderTag { + pub(crate) fn new(recipient: RecipientBytes, tag: AnonymousSenderTag) -> StoredSenderTag { + StoredSenderTag { + recipient: recipient.to_vec(), + tag: tag.to_bytes().to_vec(), + } + } +} + +impl TryFrom for (RecipientBytes, AnonymousSenderTag) { + type Error = StorageError; + + fn try_from(value: StoredSenderTag) -> Result { + let recipient_len = value.recipient.len(); + let Ok(recipient_bytes) = value.recipient.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved recipient has length of {recipient_len} while {} was expected", + Recipient::LEN + ), + }); + }; + + let tag_len = value.tag.len(); + let Ok(sender_tag_bytes) = value.tag.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved sender tag has length of {tag_len} while {} was expected", + SENDER_TAG_SIZE + ), + }); + }; + + Ok(( + recipient_bytes, + AnonymousSenderTag::from_bytes(sender_tag_bytes), + )) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct StoredReplyKey { + pub(crate) key_digest: Vec, + pub(crate) reply_key: Vec, + pub(crate) sent_at_timestamp: i64, +} + +impl StoredReplyKey { + pub(crate) fn new(key_digest: EncryptionKeyDigest, reply_key: UsedReplyKey) -> StoredReplyKey { + StoredReplyKey { + key_digest: key_digest.to_vec(), + reply_key: (*reply_key).to_bytes(), + sent_at_timestamp: reply_key.sent_at_timestamp, + } + } +} + +impl TryFrom for (EncryptionKeyDigest, UsedReplyKey) { + type Error = StorageError; + + fn try_from(value: StoredReplyKey) -> Result { + let expected_reply_key_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); + let reply_key_digest_size = value.key_digest.len(); + + let Some(digest) = EncryptionKeyDigest::from_exact_iter(value.key_digest) else { + return Err(StorageError::CorruptedData { + details: format!( + "the reply surb digest has length of {reply_key_digest_size} while {expected_reply_key_digest_size} was expected", + ), + }); + }; + + let reply_key_len = value.reply_key.len(); + let Ok(reply_key) = SurbEncryptionKey::try_from_bytes(&value.reply_key) else { + return Err(StorageError::CorruptedData { + details: format!( + "the reply key has length of {reply_key_len} while {} was expected", + SurbEncryptionKeySize::USIZE + ), + }); + }; + + Ok(( + digest, + UsedReplyKey::new(reply_key, value.sent_at_timestamp), + )) + } +} + +pub(crate) struct StoredSurbSender { + pub(crate) id: i64, + pub(crate) tag: Vec, + pub(crate) last_sent_timestamp: i64, +} + +impl StoredSurbSender { + pub(crate) fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self { + StoredSurbSender { + // for the purposes of STORING data, + // we ignore that field anyway + id: 0, + tag: tag.to_bytes().to_vec(), + last_sent_timestamp, + } + } +} + +impl TryFrom for (AnonymousSenderTag, i64) { + type Error = StorageError; + + fn try_from(value: StoredSurbSender) -> Result { + let tag_len = value.tag.len(); + let Ok(sender_tag_bytes) = value.tag.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved sender tag has length of {tag_len} while {} was expected", + SENDER_TAG_SIZE + ), + }); + }; + + Ok(( + AnonymousSenderTag::from_bytes(sender_tag_bytes), + value.last_sent_timestamp, + )) + } +} + +pub(crate) struct StoredReplySurb { + pub(crate) reply_surb_sender_id: i64, + pub(crate) reply_surb: Vec, +} + +impl StoredReplySurb { + pub(crate) fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self { + StoredReplySurb { + reply_surb_sender_id, + reply_surb: reply_surb.to_bytes(), + } + } +} + +impl TryFrom for ReplySurb { + type Error = StorageError; + + fn try_from(value: StoredReplySurb) -> Result { + ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData { + details: format!("failed to recover the reply surb: {err}"), + }) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct ReplySurbStorageMetadata { + pub(crate) min_reply_surb_threshold: u32, + pub(crate) max_reply_surb_threshold: u32, +} + +impl ReplySurbStorageMetadata { + pub(crate) fn new(min_reply_surb_threshold: usize, max_reply_surb_threshold: usize) -> Self { + Self { + min_reply_surb_threshold: min_reply_surb_threshold as u32, + max_reply_surb_threshold: max_reply_surb_threshold as u32, + } + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs new file mode 100644 index 0000000000..d80231ae1e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::CombinedReplyStorage; +use async_trait::async_trait; +use std::error::Error; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +pub mod browser_backend; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +pub mod fs_backend; + +// #[cfg(all(test, feature = "std"))] +// third case: node with actual filesystem + +#[derive(Debug, Error)] +#[error("no information provided")] +pub struct UndefinedError; + +pub struct Empty { + // we need to keep 'basic' metadata here to "load" the CombinedReplyStorage + min_surb_threshold: usize, + max_surb_threshold: usize, +} + +#[async_trait] +impl ReplyStorageBackend for Empty { + type StorageError = UndefinedError; + + async fn flush_surb_storage( + &mut self, + _storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + Ok(()) + } + + async fn init_fresh( + &mut self, + _fresh: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + Ok(()) + } + + async fn load_surb_storage(&self) -> Result { + Ok(CombinedReplyStorage::new( + self.min_surb_threshold, + self.max_surb_threshold, + )) + } +} + +#[async_trait] +pub trait ReplyStorageBackend: Sized { + type StorageError: Error + 'static; + + async fn start_storage_session(&self) -> Result<(), Self::StorageError> { + Ok(()) + } + + // reply keys and surbs would need additional field set when data is loaded + // so if there's some failure, we'd trash it all + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError>; + + /// The purpose of this call is to save any metadata that might be present. + /// (such as surb thresholds) + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>; + + async fn load_surb_storage(&self) -> Result; + + async fn stop_storage_session(self) -> Result<(), Self::StorageError> { + Ok(()) + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/combined.rs b/clients/client-core/src/client/replies/reply_storage/combined.rs new file mode 100644 index 0000000000..c9fb9e4a5c --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/combined.rs @@ -0,0 +1,60 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; + +#[derive(Debug, Clone)] +pub struct CombinedReplyStorage { + sent_reply_keys: SentReplyKeys, + received_reply_surbs: ReceivedReplySurbsMap, + used_tags: UsedSenderTags, +} + +impl CombinedReplyStorage { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> CombinedReplyStorage { + CombinedReplyStorage { + sent_reply_keys: SentReplyKeys::new(), + received_reply_surbs: ReceivedReplySurbsMap::new( + min_surb_threshold, + max_surb_threshold, + ), + used_tags: UsedSenderTags::new(), + } + } + + pub fn load( + sent_reply_keys: SentReplyKeys, + received_reply_surbs: ReceivedReplySurbsMap, + used_tags: UsedSenderTags, + ) -> Self { + CombinedReplyStorage { + sent_reply_keys, + received_reply_surbs, + used_tags, + } + } + + pub fn key_storage(&self) -> SentReplyKeys { + self.sent_reply_keys.clone() + } + + pub fn surbs_storage(&self) -> ReceivedReplySurbsMap { + self.received_reply_surbs.clone() + } + + pub fn tags_storage(&self) -> UsedSenderTags { + self.used_tags.clone() + } + + pub fn key_storage_ref(&self) -> &SentReplyKeys { + &self.sent_reply_keys + } + + pub fn surbs_storage_ref(&self) -> &ReceivedReplySurbsMap { + &self.received_reply_surbs + } + + pub fn tags_storage_ref(&self) -> &UsedSenderTags { + &self.used_tags + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/key_storage.rs b/clients/client-core/src/client/replies/reply_storage/key_storage.rs new file mode 100644 index 0000000000..9c68f40e06 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/key_storage.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::iter::Iter; +use dashmap::DashMap; +use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; +use nymsphinx::anonymous_replies::SurbEncryptionKey; +use std::ops::Deref; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct SentReplyKeys { + inner: Arc, +} + +#[derive(Debug)] +struct SentReplyKeysInner { + data: DashMap, +} + +impl SentReplyKeys { + pub(crate) fn new() -> SentReplyKeys { + SentReplyKeys { + inner: Arc::new(SentReplyKeysInner { + data: DashMap::new(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw(raw: Vec<(EncryptionKeyDigest, UsedReplyKey)>) -> SentReplyKeys { + SentReplyKeys { + inner: Arc::new(SentReplyKeysInner { + data: raw.into_iter().collect(), + }), + } + } + + pub(crate) fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, UsedReplyKey> { + self.inner.data.iter() + } + + pub(crate) fn insert_multiple(&self, keys: Vec) { + let now = OffsetDateTime::now_utc().unix_timestamp(); + for key in keys { + self.insert(UsedReplyKey::new(key, now)) + } + } + + pub(crate) fn insert(&self, key: UsedReplyKey) { + self.inner.data.insert(key.compute_digest(), key); + } + + pub(crate) fn try_pop(&self, digest: EncryptionKeyDigest) -> Option { + self.inner.data.remove(&digest).map(|(_k, v)| v) + } + + pub(crate) fn remove(&self, digest: EncryptionKeyDigest) { + self.inner.data.remove(&digest); + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct UsedReplyKey { + key: SurbEncryptionKey, + // the purpose of this field is to perform invalidation at relatively very long intervals + pub(crate) sent_at_timestamp: i64, +} + +impl UsedReplyKey { + pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self { + UsedReplyKey { + key, + sent_at_timestamp, + } + } +} + +impl Deref for UsedReplyKey { + type Target = SurbEncryptionKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/mod.rs b/clients/client-core/src/client/replies/reply_storage/mod.rs new file mode 100644 index 0000000000..83c0e0e34f --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/mod.rs @@ -0,0 +1,64 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use crate::client::replies::reply_storage::combined::CombinedReplyStorage; +pub use crate::client::replies::reply_storage::key_storage::SentReplyKeys; +pub use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbsMap; +pub use crate::client::replies::reply_storage::tag_storage::UsedSenderTags; +pub use backend::*; + +mod backend; +mod combined; +mod key_storage; +mod surb_storage; +mod tag_storage; + +// only really exists to get information about shutdown and save data to the backing storage +pub struct PersistentReplyStorage +where + T: ReplyStorageBackend, +{ + backend: T, +} + +impl PersistentReplyStorage +where + T: ReplyStorageBackend + Send + Sync, +{ + pub fn new(backend: T) -> Self { + PersistentReplyStorage { backend } + } + + pub async fn load_state_from_backend(&self) -> Result { + self.backend.load_surb_storage().await + } + + // this will have to get enabled after merging develop + pub async fn flush_on_shutdown( + mut self, + mem_state: CombinedReplyStorage, + mut shutdown: task::ShutdownListener, + ) { + use log::{debug, error, info, warn}; + + debug!("Started PersistentReplyStorage"); + if let Err(err) = self.backend.start_storage_session().await { + error!("failed to start the storage session - {err}"); + return; + } + + shutdown.recv().await; + + info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); + warn!("you MUST NOT forcefully shutdown now or you risk data corruption!"); + if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { + error!("failed to flush our reply-related data to the persistent storage: {err}") + } else { + info!("Data flush is complete") + } + + if let Err(err) = self.backend.stop_storage_session().await { + error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it") + } + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/surb_storage.rs b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs new file mode 100644 index 0000000000..ed2fd8d846 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs @@ -0,0 +1,278 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::iter::Iter; +use dashmap::DashMap; +use log::trace; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx::anonymous_replies::ReplySurb; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct ReceivedReplySurbsMap { + inner: Arc, +} + +#[derive(Debug)] +struct ReceivedReplySurbsMapInner { + data: DashMap, + + // the minimum amount of surbs that have to be kept in storage for requests for more surbs + min_surb_threshold: AtomicUsize, + + // the maximum amount of surbs that we want to keep in storage so that we don't over-request them + max_surb_threshold: AtomicUsize, +} + +impl ReceivedReplySurbsMap { + pub(crate) fn new( + min_surb_threshold: usize, + max_surb_threshold: usize, + ) -> ReceivedReplySurbsMap { + ReceivedReplySurbsMap { + inner: Arc::new(ReceivedReplySurbsMapInner { + data: DashMap::new(), + min_surb_threshold: AtomicUsize::new(min_surb_threshold), + max_surb_threshold: AtomicUsize::new(max_surb_threshold), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw( + min_surb_threshold: usize, + max_surb_threshold: usize, + raw: Vec<(AnonymousSenderTag, ReceivedReplySurbs)>, + ) -> ReceivedReplySurbsMap { + ReceivedReplySurbsMap { + inner: Arc::new(ReceivedReplySurbsMapInner { + data: raw.into_iter().collect(), + min_surb_threshold: AtomicUsize::new(min_surb_threshold), + max_surb_threshold: AtomicUsize::new(max_surb_threshold), + }), + } + } + + pub(crate) fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> { + self.inner.data.iter() + } + + pub(crate) fn remove(&self, target: &AnonymousSenderTag) { + self.inner.data.remove(target); + } + + pub(crate) fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) { + if let Some(mut entry) = self.inner.data.get_mut(target) { + entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + } + } + + pub(crate) fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { + self.inner + .data + .get(target) + .map(|e| e.surbs_last_received_at()) + } + + pub(crate) fn pending_reception(&self, target: &AnonymousSenderTag) -> u32 { + self.inner + .data + .get(target) + .map(|e| e.pending_reception()) + .unwrap_or_default() + } + + pub(crate) fn increment_pending_reception( + &self, + target: &AnonymousSenderTag, + amount: u32, + ) -> Option { + self.inner + .data + .get_mut(target) + .map(|mut e| e.increment_pending_reception(amount)) + } + + pub(crate) fn decrement_pending_reception( + &self, + target: &AnonymousSenderTag, + amount: u32, + ) -> Option { + self.inner + .data + .get_mut(target) + .map(|mut e| e.decrement_pending_reception(amount)) + } + + pub(crate) fn reset_pending_reception(&self, target: &AnonymousSenderTag) { + if let Some(mut e) = self.inner.data.get_mut(target) { + e.reset_pending_reception() + } + } + + pub(crate) fn min_surb_threshold(&self) -> usize { + self.inner.min_surb_threshold.load(Ordering::Relaxed) + } + + pub(crate) fn max_surb_threshold(&self) -> usize { + self.inner.max_surb_threshold.load(Ordering::Relaxed) + } + + pub(crate) fn available_surbs(&self, target: &AnonymousSenderTag) -> usize { + self.inner + .data + .get(target) + .map(|entry| entry.items_left()) + .unwrap_or_default() + } + + pub(crate) fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { + self.inner.data.contains_key(target) + } + + pub(crate) fn get_reply_surbs( + &self, + target: &AnonymousSenderTag, + amount: usize, + ) -> (Option>, usize) { + if let Some(mut entry) = self.inner.data.get_mut(target) { + let surbs_left = entry.items_left(); + if surbs_left < self.min_surb_threshold() + amount { + (None, surbs_left) + } else { + entry.get_reply_surbs(amount) + } + } else { + (None, 0) + } + } + + pub(crate) fn get_reply_surb_ignoring_threshold( + &self, + target: &AnonymousSenderTag, + ) -> Option<(Option, usize)> { + self.inner + .data + .get_mut(target) + .map(|mut s| s.get_reply_surb()) + } + + pub(crate) fn get_reply_surb( + &self, + target: &AnonymousSenderTag, + ) -> Option<(Option, usize)> { + self.inner.data.get_mut(target).map(|mut entry| { + let surbs_left = entry.items_left(); + if surbs_left < self.min_surb_threshold() { + (None, surbs_left) + } else { + entry.get_reply_surb() + } + }) + } + + pub(crate) fn insert_surbs>( + &self, + target: &AnonymousSenderTag, + surbs: I, + ) { + if let Some(mut existing_data) = self.inner.data.get_mut(target) { + existing_data.insert_reply_surbs(surbs) + } else { + let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect()); + self.inner.data.insert(*target, new_entry); + } + } +} + +#[derive(Debug)] +pub(crate) struct ReceivedReplySurbs { + // in the future we'd probably want to put extra data here to indicate when the SURBs got received + // so we could invalidate entries from the previous key rotations + data: VecDeque, + + pending_reception: u32, + surbs_last_received_at_timestamp: i64, +} + +impl ReceivedReplySurbs { + fn new(initial_surbs: VecDeque) -> Self { + ReceivedReplySurbs { + data: initial_surbs, + pending_reception: 0, + surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn new_retrieved( + surbs: Vec, + surbs_last_received_at_timestamp: i64, + ) -> ReceivedReplySurbs { + ReceivedReplySurbs { + data: surbs.into(), + pending_reception: 0, + surbs_last_received_at_timestamp, + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn surbs_ref(&self) -> &VecDeque { + &self.data + } + + pub(crate) fn surbs_last_received_at(&self) -> i64 { + self.surbs_last_received_at_timestamp + } + + pub(crate) fn pending_reception(&self) -> u32 { + self.pending_reception + } + + pub(crate) fn increment_pending_reception(&mut self, amount: u32) -> u32 { + self.pending_reception += amount; + self.pending_reception + } + + pub(crate) fn decrement_pending_reception(&mut self, amount: u32) -> u32 { + self.pending_reception = self.pending_reception.saturating_sub(amount); + self.pending_reception + } + + pub(crate) fn reset_pending_reception(&mut self) { + self.pending_reception = 0; + } + + pub(crate) fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { + if self.items_left() < amount { + (None, self.items_left()) + } else { + let surbs = self.data.drain(..amount).collect(); + (Some(surbs), self.items_left()) + } + } + + pub(crate) fn get_reply_surb(&mut self) -> (Option, usize) { + (self.pop_surb(), self.items_left()) + } + + fn pop_surb(&mut self) -> Option { + self.data.pop_front() + } + + fn items_left(&self) -> usize { + self.data.len() + } + + // realistically we're always going to be getting multiple surbs at once + pub(crate) fn insert_reply_surbs>(&mut self, surbs: I) { + let mut v = surbs.into_iter().collect::>(); + trace!("storing {} surbs in the storage", v.len()); + self.data.append(&mut v); + self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + trace!("we now have {} surbs!", self.data.len()); + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/tag_storage.rs b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs new file mode 100644 index 0000000000..c91d2b003e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs @@ -0,0 +1,59 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::DashMap; +use nymsphinx::addressing::clients::{Recipient, RecipientBytes}; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use std::sync::Arc; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +use dashmap::iter::Iter; + +#[derive(Debug, Clone)] +pub struct UsedSenderTags { + inner: Arc, +} + +#[derive(Debug)] +struct UsedSenderTagsInner { + data: DashMap, +} + +impl UsedSenderTags { + pub(crate) fn new() -> UsedSenderTags { + UsedSenderTags { + inner: Arc::new(UsedSenderTagsInner { + data: DashMap::new(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags { + UsedSenderTags { + inner: Arc::new(UsedSenderTagsInner { + data: raw.into_iter().collect(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> { + self.inner.data.iter() + } + + pub(crate) fn insert_new(&self, recipient: &Recipient, tag: AnonymousSenderTag) { + self.inner.data.insert(recipient.to_bytes(), tag); + } + + pub(crate) fn try_get_existing(&self, recipient: &Recipient) -> Option { + self.inner + .data + .get(&recipient.to_bytes()) + .map(|r| *r.value()) + } + + pub(crate) fn exists(&self, recipient: &Recipient) -> bool { + self.inner.data.contains_key(&recipient.to_bytes()) + } +} diff --git a/clients/client-core/src/client/reply_key_storage.rs b/clients/client-core/src/client/reply_key_storage.rs deleted file mode 100644 index bfe1c43b9b..0000000000 --- a/clients/client-core/src/client/reply_key_storage.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crypto::generic_array::typenum::Unsigned; -use log::*; -use nymsphinx::anonymous_replies::{ - encryption_key::EncryptionKeyDigest, SurbEncryptionKey, SurbEncryptionKeySize, -}; -use std::path::Path; - -#[derive(Debug, thiserror::Error)] -pub enum ReplyKeyStorageError { - #[error("DB Read Error: {0}")] - DbReadError(sled::Error), - #[error("DB Write Error: {0}")] - DbWriteError(sled::Error), - #[error("DB Open Error: {0}")] - DbOpenError(sled::Error), -} - -/// Permanent storage for keys in all sent [`ReplySURB`] -/// -/// Each sent out [`ReplySURB`] has a new key associated with it that is going to be used for -/// payload encryption. In order to -decrypt whatever reply we receive, we need to know which -/// key to use for that purpose. We do it based on received `H(t)` which has to be included -/// with each reply. -/// Moreover, there is no restriction when the [`ReplySURB`] might get used so we need to -/// have a permanent storage for all the keys that we might ever see in the future. -#[derive(Debug, Clone)] -pub struct ReplyKeyStorage { - db: sled::Db, -} - -impl ReplyKeyStorage { - pub fn load>(path: P) -> Result { - let db = match sled::open(path) { - Err(e) => return Err(ReplyKeyStorageError::DbOpenError(e)), - Ok(db) => db, - }; - - Ok(ReplyKeyStorage { db }) - } - - fn read_encryption_key(&self, raw_key: sled::IVec) -> SurbEncryptionKey { - let key_bytes_ref = raw_key.as_ref(); - // if this fails it means we have some database corruption and we - // absolutely can't continue - - if key_bytes_ref.len() != SurbEncryptionKeySize::USIZE { - error!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); - panic!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); - } - - // this can only fail if the bytes have invalid length but we already asserted it - SurbEncryptionKey::try_from_bytes(key_bytes_ref).unwrap() - } - - // TOOD: perhaps we could also store some part of original message here too? - pub fn insert_encryption_key( - &mut self, - encryption_key: SurbEncryptionKey, - ) -> Result<(), ReplyKeyStorageError> { - let digest = encryption_key.compute_digest(); - - let insertion_result = match self.db.insert(digest, encryption_key.to_bytes()) { - Err(e) => Err(ReplyKeyStorageError::DbWriteError(e)), - Ok(existing_key) => { - if existing_key.is_some() { - panic!("HASH COLLISION DETECTED") - }; - Ok(()) - } - }; - - // TODO: perhaps we could implement some batching mechanism to avoid frequent flushes? - self.db.flush().unwrap(); - insertion_result - } - - // Once we use key once, we do not expect to use it again - pub fn get_and_remove_encryption_key( - &self, - key_digest: EncryptionKeyDigest, - ) -> Result, ReplyKeyStorageError> { - let removal_result = match self.db.remove(key_digest) { - Err(e) => Err(ReplyKeyStorageError::DbReadError(e)), - Ok(existing_key) => { - Ok(existing_key.map(|existing_key| self.read_encryption_key(existing_key))) - } - }; - - // TODO: not sure how to feel about flushing it every single time here... - // same with insertion - self.db.flush().unwrap(); - removal_result - } -} diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index 82f6249f40..ad9e4a4ce6 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::spawn_future; @@ -10,10 +10,9 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::ops::Deref; use std::sync::Arc; -use std::time; use std::time::Duration; use tokio::sync::{RwLock, RwLockReadGuard}; -use topology::{nym_topology_from_detailed, NymTopology}; +use topology::{nym_topology_from_detailed, NymTopology, NymTopologyError}; use url::Url; // I'm extremely curious why compiler NEVER complained about lack of Debug here before @@ -55,18 +54,36 @@ impl<'a> TopologyReadPermit<'a> { &'a self, ack_recipient: &Recipient, packet_recipient: Option<&Recipient>, - ) -> Option<&'a NymTopology> { - // Note: implicit deref with Deref for TopologyReadPermit is happening here - let topology_ref_option = self.permit.as_ref(); - topology_ref_option.as_ref().filter(|topology_ref| { - !(!topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS) - || !topology_ref.gateway_exists(ack_recipient.gateway()) - || if let Some(packet_recipient) = packet_recipient { - !topology_ref.gateway_exists(packet_recipient.gateway()) - } else { - false - }) - }) + ) -> Result<&'a NymTopology, NymTopologyError> { + // 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through? + let topology = self + .permit + .as_ref() + .as_ref() + .ok_or(NymTopologyError::EmptyNetworkTopology)?; + + // 2. does it have any mixnode at all? + // 3. does it have any gateways at all? + // 4. does it have a mixnode on each layer? + topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?; + + // 5. does it contain OUR gateway (so that we could create an ack packet)? + if !topology.gateway_exists(ack_recipient.gateway()) { + return Err(NymTopologyError::NonExistentGatewayError { + identity_key: ack_recipient.gateway().to_base58_string(), + }); + } + + // 6. for our target recipient, does it contain THEIR gateway (so that we could create + if let Some(recipient) = packet_recipient { + if !topology.gateway_exists(recipient.gateway()) { + return Err(NymTopologyError::NonExistentGatewayError { + identity_key: recipient.gateway().to_base58_string(), + }); + } + } + + Ok(topology) } } @@ -104,10 +121,10 @@ impl TopologyAccessor { // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because health checker is disabled due to required changes) - pub async fn is_routable(&self) -> bool { + pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> { match &self.inner.read().await.0 { - None => false, - Some(ref topology) => topology.can_construct_path_through(DEFAULT_NUM_MIX_HOPS), + None => Err(NymTopologyError::EmptyNetworkTopology), + Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS), } } } @@ -120,14 +137,14 @@ impl Default for TopologyAccessor { pub struct TopologyRefresherConfig { validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, } impl TopologyRefresherConfig { pub fn new( validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, ) -> Self { TopologyRefresherConfig { @@ -241,7 +258,7 @@ impl TopologyRefresher { let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { Err(err) => { - error!("failed to get network mixnodes - {}", err); + error!("failed to get network mixnodes - {err}"); return None; } Ok(mixes) => mixes, @@ -249,7 +266,7 @@ impl TopologyRefresher { let gateways = match self.validator_client.get_cached_gateways().await { Err(err) => { - error!("failed to get network gateways - {}", err); + error!("failed to get network gateways - {err}"); return None; } Ok(gateways) => gateways, @@ -289,8 +306,8 @@ impl TopologyRefresher { .await; } - pub async fn is_topology_routable(&self) -> bool { - self.topology_accessor.is_routable().await + pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> { + self.topology_accessor.ensure_is_routable().await } pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { @@ -320,21 +337,4 @@ impl TopologyRefresher { log::debug!("TopologyRefresher: Exiting"); }) } - - pub fn start(mut self) { - spawn_future(async move { - #[cfg(not(target_arch = "wasm32"))] - let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval( - self.refresh_rate, - )); - - #[cfg(target_arch = "wasm32")] - let mut interval = - gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); - - while (interval.next().await).is_some() { - self.refresh().await; - } - }) - } } diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 8236fff213..0ce94ba646 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -30,6 +30,28 @@ const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_00 // bandwidth bridging protocol, we can come back to a smaller timeout value const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); +// reply-surbs related: + +// define when to request +// clients/client-core/src/client/replies/reply_storage/surb_storage.rs +const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200; + +// define how much to request at once +// clients/client-core/src/client/replies/reply_controller.rs +const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; + +const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; + +const DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD: Duration = Duration::from_secs(10); + +// 12 hours +const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60); + +// 24 hours +const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); + pub fn missing_string_value() -> String { MISSING_VALUE.to_string() } @@ -48,7 +70,6 @@ pub struct Config { #[serde(default)] debug: DebugConfig, } - impl ClientCoreConfigTrait for Config { fn get_gateway_endpoint(&self) -> &GatewayEndpointConfig { &self.client.gateway_endpoint @@ -60,12 +81,10 @@ impl Config { where T: NymConfig, { - let mut cfg = Config::default(); - cfg.with_id(id); - cfg + Config::default().with_id(id) } - pub fn with_id>(&mut self, id: S) + pub fn with_id>(mut self, id: S) -> Self where T: NymConfig, { @@ -112,14 +131,9 @@ impl Config { self.client.ack_key_file = self::Client::::default_ack_key_file(&id); } - if self - .client - .reply_encryption_key_store_path - .as_os_str() - .is_empty() - { - self.client.reply_encryption_key_store_path = - self::Client::::default_reply_encryption_key_store_path(&id); + if self.client.reply_surb_database_path.as_os_str().is_empty() { + self.client.reply_surb_database_path = + self::Client::::default_reply_surb_database_path(&id); } if self.client.database_path.as_os_str().is_empty() { @@ -127,6 +141,7 @@ impl Config { } self.client.id = id; + self } pub fn with_disabled_credentials(&mut self, disabled_credentials_mode: bool) { @@ -198,10 +213,6 @@ impl Config { self.client.gateway_shared_key_file.clone() } - pub fn get_reply_encryption_key_store_path(&self) -> PathBuf { - self.client.reply_encryption_key_store_path.clone() - } - pub fn get_ack_key_file(&self) -> PathBuf { self.client.ack_key_file.clone() } @@ -234,6 +245,14 @@ impl Config { self.client.database_path.clone() } + pub fn get_reply_surb_database_path(&self) -> PathBuf { + self.client.reply_surb_database_path.clone() + } + + pub fn get_version(&self) -> &str { + &self.client.version + } + // Debug getters pub fn get_debug_config(&self) -> &DebugConfig { &self.debug @@ -287,8 +306,36 @@ impl Config { self.debug.use_extended_packet_size } - pub fn get_version(&self) -> &str { - &self.client.version + pub fn get_minimum_reply_surb_storage_threshold(&self) -> usize { + self.debug.minimum_reply_surb_storage_threshold + } + + pub fn get_maximum_reply_surb_storage_threshold(&self) -> usize { + self.debug.maximum_reply_surb_storage_threshold + } + + pub fn get_minimum_reply_surb_request_size(&self) -> u32 { + self.debug.minimum_reply_surb_request_size + } + + pub fn get_maximum_reply_surb_request_size(&self) -> u32 { + self.debug.maximum_reply_surb_request_size + } + + pub fn get_maximum_allowed_reply_surb_request_size(&self) -> u32 { + self.debug.maximum_allowed_reply_surb_request_size + } + + pub fn get_maximum_reply_surb_waiting_period(&self) -> Duration { + self.debug.maximum_reply_surb_waiting_period + } + + pub fn get_maximum_reply_surb_age(&self) -> Duration { + self.debug.maximum_reply_surb_age + } + + pub fn get_maximum_reply_key_age(&self) -> Duration { + self.debug.maximum_reply_key_age } } @@ -316,6 +363,22 @@ pub struct GatewayEndpointConfig { pub gateway_listener: String, } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl GatewayEndpointConfig { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] + pub fn new( + gateway_id: String, + gateway_owner: String, + gateway_listener: String, + ) -> GatewayEndpointConfig { + GatewayEndpointConfig { + gateway_id, + gateway_owner, + gateway_listener, + } + } +} + impl From for GatewayEndpointConfig { fn from(node: topology::gateway::Node) -> GatewayEndpointConfig { let gateway_listener = node.clients_address(); @@ -368,16 +431,15 @@ pub struct Client { /// acknowledgement so that nobody besides the client knows which packet it refers to. ack_key_file: PathBuf, - /// Full path to file containing reply encryption keys of all reply-SURBs we have ever - /// sent but not received back. - reply_encryption_key_store_path: PathBuf, - /// Information regarding how the client should send data to gateway. gateway_endpoint: GatewayEndpointConfig, /// Path to the database containing bandwidth credentials of this client. database_path: PathBuf, + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + reply_surb_database_path: PathBuf, + /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. nym_root_directory: PathBuf, @@ -401,9 +463,9 @@ impl Default for Client { public_encryption_key_file: Default::default(), gateway_shared_key_file: Default::default(), ack_key_file: Default::default(), - reply_encryption_key_store_path: Default::default(), gateway_endpoint: Default::default(), database_path: Default::default(), + reply_surb_database_path: Default::default(), nym_root_directory: T::default_root_directory(), super_struct: Default::default(), } @@ -435,9 +497,10 @@ impl Client { T::default_data_directory(Some(id)).join("ack_key.pem") } - fn default_reply_encryption_key_store_path(id: &str) -> PathBuf { - T::default_data_directory(Some(id)).join("reply_key_store") + fn default_reply_surb_database_path(id: &str) -> PathBuf { + T::default_data_directory(Some(id)).join("persistent_reply_store.sqlite") } + fn default_database_path(id: &str) -> PathBuf { T::default_data_directory(Some(id)).join(DB_FILE_NAME) } @@ -513,6 +576,37 @@ pub struct DebugConfig { /// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size. pub use_extended_packet_size: Option, + + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_key_age: Duration, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -538,6 +632,14 @@ impl Default for DebugConfig { disable_loop_cover_traffic_stream: false, disable_main_poisson_packet_distribution: false, use_extended_packet_size: None, + minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD, + maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD, + minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE, + maximum_reply_surb_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD, + maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, + maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, } } } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 37538d9e04..3b179567d1 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -1,47 +1,56 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorageError; +use crate::client::replies::reply_storage::ReplyStorageBackend; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; +use topology::NymTopologyError; use validator_client::ValidatorClientError; #[derive(thiserror::Error, Debug)] -pub enum ClientCoreError { +pub enum ClientCoreError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), + #[error("Gateway client error: {0}")] GatewayClientError(#[from] GatewayClientError), + #[error("Ed25519 error: {0}")] Ed25519RecoveryError(#[from] Ed25519RecoveryError), + #[error("Validator client error: {0}")] ValidatorClientError(#[from] ValidatorClientError), - #[cfg(feature = "reply-surb")] - #[error("Reply key storage error: {0}")] - ReplyKeyStorageError(#[from] ReplyKeyStorageError), - #[error("No gateway with id: {0}")] NoGatewayWithId(String), + #[error("No gateways on network")] NoGatewaysOnNetwork, + #[error("Failed to setup gateway")] FailedToSetupGateway, + #[error("List of validator apis is empty")] ListOfValidatorApisIsEmpty, + #[error("Could not load existing gateway configuration: {0}")] CouldNotLoadExistingGatewayConfiguration(std::io::Error), + #[error("The current network topology seem to be insufficient to route any packets through")] - InsufficientNetworkTopology, + InsufficientNetworkTopology(#[from] NymTopologyError), + + #[error("experienced a failure with our reply surb persistent storage: {source}")] + SurbStorageError { source: B::StorageError }, #[error("The gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), #[error("The identity of the gateway is unknwown - did you run init?")] GatewayIdUnknown, + #[error("The owner of the gateway is unknown - did you run init?")] GatewayOwnerUnknown, + #[error("The address of the gateway is unknown - did you run init?")] GatwayAddressUnknown, diff --git a/clients/client-core/src/init/helpers.rs b/clients/client-core/src/init/helpers.rs index c838f8c6db..7947d614a1 100644 --- a/clients/client-core/src/init/helpers.rs +++ b/clients/client-core/src/init/helpers.rs @@ -1,25 +1,29 @@ -use std::{sync::Arc, time::Duration}; - -use rand::{rngs::OsRng, seq::SliceRandom, thread_rng}; -use tap::TapFallible; -use url::Url; - -use config::NymConfig; -use crypto::asymmetric::identity; -use gateway_client::GatewayClient; -use gateway_requests::registration::handshake::SharedKeys; -use topology::{filter::VersionFilterable, gateway}; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +use crate::client::replies::reply_storage::ReplyStorageBackend; use crate::{ client::key_manager::KeyManager, config::{persistence::key_pathfinder::ClientKeyPathfinder, Config}, error::ClientCoreError, }; +use config::NymConfig; +use crypto::asymmetric::identity; +use gateway_client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use rand::{rngs::OsRng, seq::SliceRandom, thread_rng}; +use std::{sync::Arc, time::Duration}; +use tap::TapFallible; +use topology::{filter::VersionFilterable, gateway}; +use url::Url; -pub(super) async fn query_gateway_details( +pub(super) async fn query_gateway_details( validator_servers: Vec, chosen_gateway_id: Option, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, +{ let validator_api = validator_servers .choose(&mut thread_rng()) .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; @@ -51,10 +55,13 @@ pub(super) async fn query_gateway_details( } } -async fn register_with_gateway( +async fn register_with_gateway( gateway: &gateway::Node, our_identity: Arc, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> +where + B: ReplyStorageBackend, +{ let timeout = Duration::from_millis(1500); let mut gateway_client = GatewayClient::new_init( gateway.clients_address(), @@ -74,12 +81,13 @@ async fn register_with_gateway( Ok(shared_keys) } -pub(super) async fn register_with_gateway_and_store_keys( +pub(super) async fn register_with_gateway_and_store_keys( gateway_details: gateway::Node, config: &Config, -) -> Result<(), ClientCoreError> +) -> Result<(), ClientCoreError> where T: NymConfig, + B: ReplyStorageBackend, { let mut rng = OsRng; let mut key_manager = KeyManager::new(&mut rng); diff --git a/clients/client-core/src/init/mod.rs b/clients/client-core/src/init/mod.rs index 705cbf4eca..cdf210a27e 100644 --- a/clients/client-core/src/init/mod.rs +++ b/clients/client-core/src/init/mod.rs @@ -12,6 +12,7 @@ use tap::TapFallible; use config::NymConfig; use crypto::asymmetric::{encryption, identity}; +use crate::client::replies::reply_storage::ReplyStorageBackend; use crate::{ config::{ persistence::key_pathfinder::ClientKeyPathfinder, ClientCoreConfigTrait, Config, @@ -62,28 +63,37 @@ impl Display for InitResults { /// Convenience function for setting up the gateway for a client. Depending on the arguments given /// it will do the sensible thing. -pub async fn setup_gateway( +pub async fn setup_gateway( register_gateway: bool, user_chosen_gateway_id: Option, config: &Config, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, + C: NymConfig + ClientCoreConfigTrait, + T: NymConfig, +{ let id = config.get_id(); if register_gateway { register_with_gateway(user_chosen_gateway_id, config).await } else if let Some(user_chosen_gateway_id) = user_chosen_gateway_id { config_gateway_with_existing_keys(user_chosen_gateway_id, config).await } else { - reuse_existing_gateway_config::(&id) + reuse_existing_gateway_config::(&id) } } /// Get the gateway details by querying the validator-api. Either pick one at random or use /// the chosen one if it's among the available ones. /// Saves keys to disk, specified by the paths in `config`. -pub async fn register_with_gateway( +pub async fn register_with_gateway( user_chosen_gateway_id: Option, config: &Config, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig, +{ println!("Configuring gateway"); let gateway = query_gateway_details(config.get_validator_api_endpoints(), user_chosen_gateway_id).await?; @@ -101,10 +111,14 @@ pub async fn register_with_gateway( /// create any keys. /// This assumes that the user knows what they are doing, and that the existing keys are valid for /// the gateway being used -pub async fn config_gateway_with_existing_keys( +pub async fn config_gateway_with_existing_keys( user_chosen_gateway_id: String, config: &Config, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig, +{ println!("Using gateway provided by user, keeping existing keys"); let gateway = query_gateway_details( config.get_validator_api_endpoints(), @@ -116,9 +130,13 @@ pub async fn config_gateway_with_existing_keys( } /// Read and reuse the existing gateway configuration from a file that was generate earlier. -pub fn reuse_existing_gateway_config( +pub fn reuse_existing_gateway_config( id: &str, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig + ClientCoreConfigTrait, +{ println!("Not registering gateway, will reuse existing config and keys"); T::load_from_file(Some(id)) .map(|existing_config| existing_config.get_gateway_endpoint().clone()) @@ -135,15 +153,19 @@ pub fn reuse_existing_gateway_config( } /// Get the client address by loading the keys from stored files. -pub fn get_client_address_from_stored_keys( +pub fn get_client_address_from_stored_keys( config: &Config, -) -> Result +) -> Result> where T: config::NymConfig, + B: ReplyStorageBackend, { - fn load_identity_keys( + fn load_identity_keys( pathfinder: &ClientKeyPathfinder, - ) -> Result { + ) -> Result> + where + B: ReplyStorageBackend, + { let identity_keypair: identity::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_identity_key().to_owned(), @@ -153,9 +175,12 @@ where Ok(identity_keypair) } - fn load_sphinx_keys( + fn load_sphinx_keys( pathfinder: &ClientKeyPathfinder, - ) -> Result { + ) -> Result> + where + B: ReplyStorageBackend, + { let sphinx_keypair: encryption::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_encryption_key().to_owned(), diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 1b19bf32ad..97e2a8d504 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -27,14 +27,13 @@ pretty_env_logger = "0.4" # for formatting log messages rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization serde_json = "1.0" -sled = "0.34" # for storage of replySURB decryption keys -tap = "1.0.1" thiserror = "1.0.34" +tap = "1.0.1" tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime tokio-tungstenite = "0.14" # websocket ## internal -client-core = { path = "../client-core" } +client-core = { path = "../client-core", features = ["fs-surb-storage"] } client-connections = { path = "../../common/client-connections" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index f103815af2..2c22f0d03b 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -25,58 +25,59 @@ async fn get_self_address(ws_stream: &mut WebSocketStream recipient, + ServerResponse::SelfAddress(recipient) => *recipient, _ => panic!("received an unexpected response!"), } } async fn send_file_with_reply() { - let uri = "ws://localhost:1977"; - let (mut ws_stream, _) = connect_async(uri).await.unwrap(); - - let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient); - - let read_data = std::fs::read("examples/dummy_file").unwrap(); - - let send_request = ClientRequest::Send { - recipient, - message: read_data, - with_reply_surb: true, - connection_id: Some(0), - }; - - println!("sending content of 'dummy_file' over the mix network..."); - let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; - - let received = match response { - ServerResponse::Received(received) => received, - _ => panic!("received an unexpected response!"), - }; - - println!("writing the file back to the disk!"); - std::fs::write("examples/received_file_withreply", received.message).unwrap(); - - let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec(); - let reply_request = ClientRequest::Reply { - message: reply_message.clone(), - reply_surb: received.reply_surb.unwrap(), - }; - - println!( - "sending {:?} (using reply SURB!) over the mix network...", - String::from_utf8(reply_message).unwrap() - ); - let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await; - let received = match response { - ServerResponse::Received(received) => received, - _ => panic!("received an unexpected response!"), - }; - - println!( - "received {:#?} from the mix network!", - String::from_utf8(received.message).unwrap() - ); + todo!("reimplement surb usage here : )") + // let uri = "ws://localhost:1977"; + // let (mut ws_stream, _) = connect_async(uri).await.unwrap(); + // + // let recipient = get_self_address(&mut ws_stream).await; + // println!("our full address is: {}", recipient); + // + // let read_data = std::fs::read("examples/dummy_file").unwrap(); + // + // let send_request = ClientRequest::Send { + // recipient, + // message: read_data, + // with_reply_surb: true, + // connection_id: Some(0), + // }; + // + // println!("sending content of 'dummy_file' over the mix network..."); + // let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; + // + // let received = match response { + // ServerResponse::Received(received) => received, + // _ => panic!("received an unexpected response!"), + // }; + // + // println!("writing the file back to the disk!"); + // std::fs::write("examples/received_file_withreply", received.message).unwrap(); + // + // let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec(); + // let reply_request = ClientRequest::Reply { + // message: reply_message.clone(), + // reply_surb: received.reply_surb.unwrap(), + // }; + // + // println!( + // "sending {:?} (using reply SURB!) over the mix network...", + // String::from_utf8(reply_message).unwrap() + // ); + // let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await; + // let received = match response { + // ServerResponse::Received(received) => received, + // _ => panic!("received an unexpected response!"), + // }; + // + // println!( + // "received {:#?} from the mix network!", + // String::from_utf8(received.message).unwrap() + // ); } async fn send_file_without_reply() { @@ -91,7 +92,6 @@ async fn send_file_without_reply() { let send_request = ClientRequest::Send { recipient, message: read_data, - with_reply_surb: false, connection_id: Some(0), }; diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index bb2b7d7a15..67902138ce 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -3,7 +3,7 @@ use crate::client::config::template::config_template; pub use client_core::config::MISSING_VALUE; -use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig}; +use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use config::NymConfig; use serde::{Deserialize, Serialize}; @@ -28,7 +28,7 @@ impl SocketType { } } - pub fn is_websocket(self) -> bool { + pub fn is_websocket(&self) -> bool { matches!(self, SocketType::WebSocket) } } @@ -110,6 +110,10 @@ impl Config { &mut self.base } + pub fn get_debug_settings(&self) -> &DebugConfig { + self.get_base().get_debug_config() + } + pub fn get_socket_type(&self) -> SocketType { self.socket.socket_type } diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index c327d286ff..31ca091282 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -49,13 +49,12 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}' # Path to file containing public encryption key. public_encryption_key_file = '{{ client.public_encryption_key_file }}' -# Full path to file containing reply encryption keys of all reply-SURBs we have ever -# sent but not received back. -reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' - # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' +# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. +reply_surb_database_path = '{{ client.reply_surb_database_path }}' + ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index b997c47fb7..cabe049c45 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,11 +1,13 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::client::config::Config; use crate::error::ClientError; use crate::websocket; use client_connections::TransmissionLane; -use client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, +}; use client_core::client::inbound_messages::InputMessage; use client_core::client::key_manager::KeyManager; use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; @@ -14,7 +16,7 @@ use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::receiver::ReconstructedMessage; use task::{wait_for_signal, ShutdownNotifier}; @@ -107,7 +109,7 @@ impl SocketClient { /// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent) pub async fn run_socket_forever(self) -> Result<(), ClientError> { - let shutdown = self.start_socket().await?; + let mut shutdown = self.start_socket().await?; wait_for_signal().await; println!( @@ -120,8 +122,8 @@ impl SocketClient { // Some of these components have shutdown signalling implemented as part of socks5 work, // but since it's not fully implemented (yet) for all the components of the native client, // we don't try to wait and instead just stop immediately. - //log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); - //shutdown.wait_for_shutdown().await; + log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + shutdown.wait_for_shutdown().await; log::info!("Stopping nym-client"); Ok(()) @@ -136,6 +138,11 @@ impl SocketClient { self.config.get_base(), self.key_manager, Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, ); let self_address = base_builder.as_mix_recipient(); @@ -160,6 +167,11 @@ impl SocketClient { self.config.get_base(), self.key_manager, Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, ); let mut started_client = base_client.start_base().await?; @@ -197,14 +209,9 @@ impl DirectClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub async fn send_message( - &mut self, - recipient: Recipient, - message: Vec, - with_reply_surb: bool, - ) { + pub async fn send_regular_message(&mut self, recipient: Recipient, message: Vec) { let lane = TransmissionLane::General; - let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); + let input_msg = InputMessage::new_regular(recipient, message, lane); self.client_input .input_sender @@ -216,8 +223,28 @@ impl DirectClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { - let input_msg = InputMessage::new_reply(reply_surb, message); + pub async fn send_anonymous_message( + &mut self, + recipient: Recipient, + message: Vec, + reply_surbs: u32, + ) { + let lane = TransmissionLane::General; + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); + + self.client_input + .input_sender + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's untested and there are absolutely no guarantees about it (but seems to have worked + /// well enough in local tests) + pub async fn send_reply(&mut self, recipient_tag: AnonymousSenderTag, message: Vec) { + let lane = TransmissionLane::General; + let input_msg = InputMessage::new_reply(recipient_tag, message, lane); self.client_input .input_sender diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 050e680f8e..3caf3a5d0d 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -1,19 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::Display; - -use clap::Args; -use config::NymConfig; -use nymsphinx::addressing::clients::Recipient; -use serde::Serialize; -use tap::TapFallible; - use crate::{ client::config::Config, commands::{override_config, OverrideConfig}, error::ClientError, }; +use clap::Args; +use config::NymConfig; +use nymsphinx::addressing::clients::Recipient; +use serde::Serialize; +use std::fmt::Display; +use tap::TapFallible; #[derive(Args, Clone)] pub(crate) struct Init { @@ -136,7 +134,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> { // Setup gateway by either registering a new one, or creating a new config from the selected // one but with keys kept, or reusing the gateway configuration. - let gateway = client_core::init::setup_gateway::( + let gateway = client_core::init::setup_gateway::<_, Config, _>( register_gateway, user_chosen_gateway_id, config.get_base(), diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 5ca3319cb7..050e79ebc5 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,14 +1,17 @@ +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; #[derive(thiserror::Error, Debug)] pub enum ClientError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), + #[error("client-core error: {0}")] - ClientCoreError(#[from] ClientCoreError), + ClientCoreError(#[from] ClientCoreError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 6dc0d398ad..7d8d16e97f 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -14,7 +14,7 @@ use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::receiver::ReconstructedMessage; use tokio::net::TcpStream; use tokio_tungstenite::{ @@ -62,9 +62,13 @@ impl Clone for Handler { impl Drop for Handler { fn drop(&mut self) { - self.buffer_requester + if self + .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - .expect("the buffer request failed!") + .is_err() + { + error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!") + } } } @@ -89,18 +93,22 @@ impl Handler { async fn handle_send( &mut self, - recipient: &Recipient, + recipient: Recipient, message: Vec, - with_reply_surb: bool, connection_id: Option, ) -> Option { + info!( + "Attempting to send {:.2} kiB message to {recipient} on connection_id {connection_id:?}", + message.len() as f64 / 1024.0 + ); + // We map the absence of a connection id as going into the general lane. let lane = connection_id.map_or(TransmissionLane::General, |id| { TransmissionLane::ConnectionId(id) }); // the ack control is now responsible for chunking, etc. - let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); + let input_msg = InputMessage::new_regular(recipient, message, lane); self.msg_input .send(input_msg) .await @@ -109,53 +117,119 @@ impl Handler { // Only reply back with a `LaneQueueLength` if the sender providided a connection id let connection_id = match lane { TransmissionLane::General - | TransmissionLane::Reply + | TransmissionLane::ReplySurbRequest | TransmissionLane::Retransmission - | TransmissionLane::Control => return None, + | TransmissionLane::AdditionalReplySurbs => return None, TransmissionLane::ConnectionId(id) => id, }; // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not // yet reach `OutQueueControl`, so it might be a tad low. - let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { - log::warn!( - "Failed to get the lane queue length lock, \ - not responding back with the current queue length" - ); - return None; - }; - - let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) - } - - async fn handle_reply( - &mut self, - reply_surb: ReplySurb, - message: Vec, - ) -> Option { - if message.len() > ReplySurb::max_msg_len(Default::default()) { - return Some( - ServerResponse::new_error( - format!( - "too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", - message.len(), ReplySurb::max_msg_len(Default::default())) - ) - ); + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); } - let input_msg = InputMessage::new_reply(reply_surb, message); + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); + None + } + + async fn handle_send_anonymous( + &mut self, + recipient: Recipient, + message: Vec, + reply_surbs: u32, + connection_id: Option, + ) -> Option { + info!( + "Attempting to anonymously send {:.2} kiB message to {recipient} on connection_id {connection_id:?} while attaching {reply_surbs} replySURBs.", + message.len() as f64 / 1024.0 + ); + + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); self.msg_input .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::ReplySurbRequest + | TransmissionLane::Retransmission + | TransmissionLane::AdditionalReplySurbs => return None, + TransmissionLane::ConnectionId(id) => id, + }; + + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); + None + } + + async fn handle_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + message: Vec, + connection_id: Option, + ) -> Option { + info!("Attempting to send {:.2} kiB reply message to {recipient_tag} on connection_id {connection_id:?}", message.len() as f64 / 1024.0); + + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + + let input_msg = InputMessage::new_reply(recipient_tag, message, lane); + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::ReplySurbRequest + | TransmissionLane::Retransmission + | TransmissionLane::AdditionalReplySurbs => return None, + TransmissionLane::ConnectionId(id) => id, + }; + + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); None } fn handle_self_address(&self) -> ServerResponse { - ServerResponse::SelfAddress(self.self_full_address) + ServerResponse::SelfAddress(Box::new(self.self_full_address)) } fn handle_closed_connection(&self, connection_id: u64) -> Option { @@ -175,7 +249,10 @@ impl Handler { let lane = TransmissionLane::ConnectionId(connection_id); let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) + Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }) } async fn handle_request(&mut self, request: ClientRequest) -> Option { @@ -183,16 +260,25 @@ impl Handler { ClientRequest::Send { recipient, message, - with_reply_surb, + connection_id, + } => self.handle_send(recipient, message, connection_id).await, + + ClientRequest::SendAnonymous { + recipient, + message, + reply_surbs, connection_id, } => { - self.handle_send(&recipient, message, with_reply_surb, connection_id) + self.handle_send_anonymous(recipient, message, reply_surbs, connection_id) .await } + ClientRequest::Reply { message, - reply_surb, - } => self.handle_reply(reply_surb, message).await, + sender_tag, + connection_id, + } => self.handle_reply(sender_tag, message, connection_id).await, + ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id), @@ -299,8 +385,7 @@ impl Handler { if let Some(response) = self.handle_ws_request(socket_msg).await { if let Err(err) = self.send_websocket_response(response).await { warn!( - "Failed to send message over websocket: {}. Assuming the connection is dead.", - err + "Failed to send message over websocket: {err}. Assuming the connection is dead.", ); break; } @@ -308,9 +393,10 @@ impl Handler { } // or a reconstructed mix message that we need to push back to the client mix_messages = msg_receiver.next() => { - let mix_messages = mix_messages.expect( - "mix messages sender was unexpectedly closed! this shouldn't have ever happened!", - ); + let Some(mix_messages) = mix_messages else { + error!("mix messages sender was unexpectedly closed! this shouldn't have ever happened! (unless we're shutting down - TODO: implement proper graceful shutdown handler)"); + return + }; if let Err(e) = self.push_websocket_received_plaintexts(mix_messages).await { warn!("failed to send sphinx packets back to the client - {:?}, assuming the connection is dead", e); break; diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index bdaa27d4fc..c7825fb345 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -54,9 +54,8 @@ impl Listener { Ok((mut socket, remote_addr)) => { debug!("Received connection from {:?}", remote_addr); if self.state.is_connected() { - warn!("tried to duplicate!"); + warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr); // if we've already got a connection, don't allow another one - debug!("but there was already a connection present!"); // while we only ever want to accept a single connection, we don't want // to leave clients hanging (and also allow for reconnection if it somehow // was dropped) diff --git a/clients/native/websocket-requests/src/error.rs b/clients/native/websocket-requests/src/error.rs index f9fdd029cf..2d9035c19d 100644 --- a/clients/native/websocket-requests/src/error.rs +++ b/clients/native/websocket-requests/src/error.rs @@ -24,8 +24,11 @@ impl fmt::Debug for Error { } impl Error { - pub fn new(kind: ErrorKind, message: String) -> Self { - Error { kind, message } + pub fn new>(kind: ErrorKind, message: S) -> Self { + Error { + kind, + message: message.into(), + } } } @@ -62,6 +65,31 @@ pub enum ErrorKind { Other = 0xFF, } +impl TryFrom for ErrorKind { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (ErrorKind::EmptyRequest as u8) => Ok(ErrorKind::EmptyRequest), + _ if value == (ErrorKind::TooShortRequest as u8) => Ok(ErrorKind::TooShortRequest), + _ if value == (ErrorKind::UnknownRequest as u8) => Ok(ErrorKind::UnknownRequest), + _ if value == (ErrorKind::MalformedRequest as u8) => Ok(ErrorKind::MalformedRequest), + + _ if value == (ErrorKind::EmptyResponse as u8) => Ok(ErrorKind::EmptyResponse), + _ if value == (ErrorKind::TooShortResponse as u8) => Ok(ErrorKind::TooShortResponse), + _ if value == (ErrorKind::UnknownResponse as u8) => Ok(ErrorKind::UnknownResponse), + _ if value == (ErrorKind::MalformedResponse as u8) => Ok(ErrorKind::MalformedResponse), + + _ if value == (ErrorKind::Other as u8) => Ok(ErrorKind::Other), + + n => Err(Error::new( + ErrorKind::MalformedResponse, + format!("invalid error code {}", n), + )), + } + } +} + impl ErrorKind { pub(crate) fn as_str(&self) -> &'static str { match *self { diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 315399ae3d..f50364a3cf 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // all variable size data is always prefixed with u64 length @@ -7,69 +7,115 @@ use crate::error::{self, ErrorKind}; use crate::text::ClientRequestText; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; use std::convert::{TryFrom, TryInto}; use std::mem::size_of; -/// Value tag representing [`Send`] variant of the [`ClientRequest`] -pub const SEND_REQUEST_TAG: u8 = 0x00; +#[repr(u8)] +enum ClientRequestTag { + /// Value tag representing [`Send`] variant of the [`ClientRequest`] + Send = 0x00, -/// Value tag representing [`Reply`] variant of the [`ClientRequest`] -pub const REPLY_REQUEST_TAG: u8 = 0x01; + /// Value tag representing [`SendAnonymous`] variant of the [`ClientRequest`] + SendAnonymous = 0x01, -/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`] -pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; + /// Value tag representing [`Reply`] variant of the [`ClientRequest`] + Reply = 0x02, -/// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] -pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03; + /// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`] + SelfAddress = 0x03, -/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] -pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04; + /// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] + ClosedConnection = 0x04, + + /// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] + GetLaneQueueLength = 0x05, +} + +impl TryFrom for ClientRequestTag { + type Error = error::Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (Self::Send as u8) => Ok(Self::Send), + _ if value == (Self::SendAnonymous as u8) => Ok(Self::SendAnonymous), + _ if value == (Self::Reply as u8) => Ok(Self::Reply), + _ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress), + _ if value == (Self::ClosedConnection as u8) => Ok(Self::ClosedConnection), + _ if value == (Self::GetLaneQueueLength as u8) => Ok(Self::GetLaneQueueLength), + n => Err(error::Error::new( + ErrorKind::UnknownRequest, + format!("{n} does not correspond to any valid request tag"), + )), + } + } +} #[allow(non_snake_case)] #[derive(Debug)] pub enum ClientRequest { + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant Send { recipient: Recipient, message: Vec, - // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? - with_reply_surb: bool, connection_id: Option, }, - Reply { + + /// Create a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + SendAnonymous { + recipient: Recipient, message: Vec, - reply_surb: ReplySurb, + reply_surbs: u32, + connection_id: Option, }, + + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + Reply { + sender_tag: AnonymousSenderTag, + message: Vec, + connection_id: Option, + }, + SelfAddress, + ClosedConnection(u64), + GetLaneQueueLength(u64), } // we could have been parsing it directly TryFrom, but we want to retain // information about whether it came from binary or text to send appropriate response back impl ClientRequest { - // SEND_REQUEST_TAG || with_surb || recipient || conn_id || data_len || data - fn serialize_send( - recipient: Recipient, - data: Vec, - with_reply_surb: bool, - connection_id: Option, - ) -> Vec { + // SEND_REQUEST_TAG || recipient || conn_id || data_len || data + fn serialize_send(recipient: Recipient, data: Vec, connection_id: Option) -> Vec { let data_len_bytes = (data.len() as u64).to_be_bytes(); let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); - std::iter::once(SEND_REQUEST_TAG) - .chain(std::iter::once(with_reply_surb as u8)) - .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant - .chain(conn_id_bytes.iter().cloned()) - .chain(data_len_bytes.iter().cloned()) + + std::iter::once(ClientRequestTag::Send as u8) + .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes.into_iter()) + .chain(data_len_bytes.into_iter()) .chain(data.into_iter()) .collect() } - // SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data + // SEND_REQUEST_TAG || recipient || conn_id || data_len || data fn deserialize_send(b: &[u8]) -> Result { - // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof bytes - if b.len() < 2 + Recipient::LEN + 2 * size_of::() { + // we need to have at least 1 (tag) + Recipient::LEN + 2*sizeof bytes + if b.len() < 1 + Recipient::LEN + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'send'".to_string(), @@ -77,21 +123,10 @@ impl ClientRequest { } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SEND_REQUEST_TAG); - - let with_reply_surb = match b[1] { - 0 => false, - 1 => true, - n => { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!("invalid reply surb flag {}", n), - )) - } - }; + debug_assert_eq!(b[0], ClientRequestTag::Send as u8); let mut recipient_bytes = [0u8; Recipient::LEN]; - recipient_bytes.copy_from_slice(&b[2..2 + Recipient::LEN]); + recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]); let recipient = match Recipient::try_from_bytes(recipient_bytes) { Ok(recipient) => recipient, Err(err) => { @@ -104,7 +139,7 @@ impl ClientRequest { let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes - .copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]); + .copy_from_slice(&b[1 + Recipient::LEN..1 + Recipient::LEN + size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); let connection_id = if connection_id == 0 { None @@ -113,9 +148,9 @@ impl ClientRequest { }; let data_len_bytes = - &b[2 + Recipient::LEN + size_of::()..2 + Recipient::LEN + 2 * size_of::()]; + &b[1 + Recipient::LEN + size_of::()..1 + Recipient::LEN + 2 * size_of::()]; let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap()); - let data = &b[2 + Recipient::LEN + 2 * size_of::()..]; + let data = &b[1 + Recipient::LEN + 2 * size_of::()..]; if data.len() as u64 != data_len { return Err(error::Error::new( ErrorKind::MalformedRequest, @@ -128,33 +163,111 @@ impl ClientRequest { } Ok(ClientRequest::Send { - with_reply_surb, recipient, message: data.to_vec(), connection_id, }) } - // REPLY_REQUEST_TAG || surb_len || surb || message_len || message - fn serialize_reply(message: Vec, reply_surb: &ReplySurb) -> Vec { - let reply_surb_bytes = reply_surb.to_bytes(); - let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); - let message_len_bytes = (message.len() as u64).to_be_bytes(); + // SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || conn_id || data_len || data + fn serialize_send_anonymous( + recipient: Recipient, + data: Vec, + reply_surbs: u32, + connection_id: Option, + ) -> Vec { + let data_len_bytes = (data.len() as u64).to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); - std::iter::once(REPLY_REQUEST_TAG) - .chain(surb_len_bytes.iter().cloned()) - .chain(reply_surb_bytes.into_iter()) - .chain(message_len_bytes.iter().cloned()) + std::iter::once(ClientRequestTag::SendAnonymous as u8) + .chain(reply_surbs.to_be_bytes().into_iter()) + .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes.into_iter()) + .chain(data_len_bytes.into_iter()) + .chain(data.into_iter()) + .collect() + } + + // SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || data_len || data + fn deserialize_send_anonymous(b: &[u8]) -> Result { + // we need to have at least 1 (tag) + sizeof (num surbs) + Recipient::LEN + 2 *sizeof bytes + if b.len() < 1 + size_of::() + Recipient::LEN + 2 * size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortRequest, + "not enough data provided to recover 'send_anonymous'".to_string(), + )); + } + + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], ClientRequestTag::SendAnonymous as u8); + + let reply_surbs = u32::from_be_bytes([b[1], b[2], b[3], b[4]]); + + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&b[5..5 + Recipient::LEN]); + let recipient = match Recipient::try_from_bytes(recipient_bytes) { + Ok(recipient) => recipient, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!("malformed recipient: {:?}", err), + )) + } + }; + + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes + .copy_from_slice(&b[5 + Recipient::LEN..5 + Recipient::LEN + size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) + }; + + let data_len_bytes = + &b[5 + Recipient::LEN + size_of::()..5 + Recipient::LEN + 2 * size_of::()]; + let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap()); + let data = &b[5 + Recipient::LEN + 2 * size_of::()..]; + if data.len() as u64 != data_len { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!( + "data len has inconsistent length. specified: {} got: {}", + data_len, + data.len() + ), + )); + } + + Ok(ClientRequest::SendAnonymous { + reply_surbs, + recipient, + message: data.to_vec(), + connection_id, + }) + } + + // REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message + fn serialize_reply( + message: Vec, + sender_tag: AnonymousSenderTag, + connection_id: Option, + ) -> Vec { + let message_len_bytes = (message.len() as u64).to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); + + std::iter::once(ClientRequestTag::Reply as u8) + .chain(sender_tag.to_bytes().into_iter()) + .chain(conn_id_bytes.into_iter()) + .chain(message_len_bytes.into_iter()) .chain(message.into_iter()) .collect() } - // REPLY_REQUEST_TAG || surb_len || surb || message_len || message + // REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message fn deserialize_reply(b: &[u8]) -> Result { - // we need to have at the very least 2 * sizeof bytes (in case, for some peculiar reason - // message and reply surb were 0 len - the request would still be malformed, but would in theory - // be parse'able) - if b.len() < 1 + 2 * size_of::() { + if b.len() < 1 + SENDER_TAG_SIZE + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'reply'".to_string(), @@ -162,42 +275,28 @@ impl ClientRequest { } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], REPLY_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::Reply as u8); - let reply_surb_len = - u64::from_be_bytes(b[1..1 + size_of::()].as_ref().try_into().unwrap()); + // the unwrap here is fine as we're definitely using exactly SENDER_TAG_SIZE bytes + let sender_tag = + AnonymousSenderTag::from_bytes(b[1..1 + SENDER_TAG_SIZE].try_into().unwrap()); - // make sure we won't go out of bounds here - if reply_surb_len > (b.len() - 1 + 2 * size_of::()) as u64 { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!( - "not enough data to recover reply surb with specified length {}", - reply_surb_len - ), - )); - } - - let surb_bound = 1 + size_of::() + reply_surb_len as usize; - - let reply_surb_bytes = &b[1 + size_of::()..surb_bound]; - let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) { - Ok(reply_surb) => reply_surb, - Err(err) => { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!("malformed reply surb: {:?}", err), - )) - } + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes + .copy_from_slice(&b[1 + SENDER_TAG_SIZE..1 + SENDER_TAG_SIZE + size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) }; let message_len = u64::from_be_bytes( - b[surb_bound..surb_bound + size_of::()] - .as_ref() + b[1 + SENDER_TAG_SIZE + size_of::()..1 + SENDER_TAG_SIZE + 2 * size_of::()] .try_into() .unwrap(), ); - let message = &b[surb_bound + size_of::()..]; + let message = &b[1 + SENDER_TAG_SIZE + 2 * size_of::()..]; if message.len() as u64 != message_len { return Err(error::Error::new( ErrorKind::MalformedRequest, @@ -208,33 +307,32 @@ impl ClientRequest { ), )); } - // TODO: should this blow HERE, i.e. during deserialization that the data you're trying - // to send via reply is too long? Ok(ClientRequest::Reply { - reply_surb, message: message.to_vec(), + sender_tag, + connection_id, }) } // SELF_ADDRESS_REQUEST_TAG fn serialize_self_address() -> Vec { - std::iter::once(SELF_ADDRESS_REQUEST_TAG).collect() + vec![ClientRequestTag::SelfAddress as u8] } // SELF_ADDRESS_REQUEST_TAG - fn deserialize_self_address(b: &[u8]) -> Self { + fn deserialize_self_address(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SELF_ADDRESS_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::SelfAddress as u8); - ClientRequest::SelfAddress + Ok(ClientRequest::SelfAddress) } // CLOSED_CONNECTION_REQUEST_TAG fn serialize_closed_connection(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); - std::iter::once(CLOSED_CONNECTION_REQUEST_TAG) - .chain(conn_id_bytes.iter().copied()) + std::iter::once(ClientRequestTag::ClosedConnection as u8) + .chain(conn_id_bytes.into_iter()) .collect() } @@ -243,12 +341,12 @@ impl ClientRequest { if b.len() != 1 + size_of::() { return Err(error::Error::new( ErrorKind::MalformedRequest, - "the received closed connection has invalid length".to_string(), + "The received closed connection has invalid length", )); } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::ClosedConnection as u8); let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -260,8 +358,8 @@ impl ClientRequest { // GET_LANE_QUEUE_LENGHT_TAG fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); - std::iter::once(GET_LANE_QUEUE_LENGHT_TAG) - .chain(conn_id_bytes.iter().copied()) + std::iter::once(ClientRequestTag::GetLaneQueueLength as u8) + .chain(conn_id_bytes.into_iter()) .collect() } @@ -270,12 +368,12 @@ impl ClientRequest { if b.len() != 1 + size_of::() { return Err(error::Error::new( ErrorKind::MalformedRequest, - "the received get lane queue length has invalid length".to_string(), + "The received get lane queue lengths has invalid length", )); } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); + debug_assert_eq!(b[0], ClientRequestTag::GetLaneQueueLength as u8); let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -289,14 +387,21 @@ impl ClientRequest { ClientRequest::Send { recipient, message, - with_reply_surb, connection_id, - } => Self::serialize_send(recipient, message, with_reply_surb, connection_id), + } => Self::serialize_send(recipient, message, connection_id), + + ClientRequest::SendAnonymous { + recipient, + message, + reply_surbs, + connection_id, + } => Self::serialize_send_anonymous(recipient, message, reply_surbs, connection_id), ClientRequest::Reply { message, - reply_surb, - } => Self::serialize_reply(message, &reply_surb), + sender_tag, + connection_id, + } => Self::serialize_reply(message, sender_tag, connection_id), ClientRequest::SelfAddress => Self::serialize_self_address(), @@ -316,28 +421,16 @@ impl ClientRequest { )); } - if b.len() < size_of::() { - return Err(error::Error::new( - ErrorKind::TooShortRequest, - format!( - "not enough data provided to recover request tag. Provided only {} bytes", - b.len() - ), - )); - } - let request_tag = b[0]; + let request_tag = ClientRequestTag::try_from(b[0])?; // determine what kind of request that is and try to deserialize it match request_tag { - SEND_REQUEST_TAG => Self::deserialize_send(b), - REPLY_REQUEST_TAG => Self::deserialize_reply(b), - SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), - CLOSED_CONNECTION_REQUEST_TAG => Self::deserialize_closed_connection(b), - GET_LANE_QUEUE_LENGHT_TAG => Self::deserialize_get_lane_queue_length(b), - n => Err(error::Error::new( - ErrorKind::UnknownRequest, - format!("type {n}"), - )), + ClientRequestTag::Send => Self::deserialize_send(b), + ClientRequestTag::SendAnonymous => Self::deserialize_send_anonymous(b), + ClientRequestTag::Reply => Self::deserialize_reply(b), + ClientRequestTag::SelfAddress => Self::deserialize_self_address(b), + ClientRequestTag::ClosedConnection => Self::deserialize_closed_connection(b), + ClientRequestTag::GetLaneQueueLength => Self::deserialize_get_lane_queue_length(b), } } @@ -365,50 +458,52 @@ mod tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_string = recipient.to_string(); - let send_request_no_surb = ClientRequest::Send { + let send_request = ClientRequest::Send { recipient, message: b"foomp".to_vec(), - with_reply_surb: false, connection_id: Some(42), }; - let bytes = send_request_no_surb.serialize(); + let bytes = send_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { ClientRequest::Send { recipient, message, - with_reply_surb, connection_id, } => { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); - assert!(!with_reply_surb); assert_eq!(connection_id, Some(42)) } _ => unreachable!(), } + } - let send_request_surb = ClientRequest::Send { - recipient, + #[test] + fn send_anonymous_request_serialization_works() { + let original_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); + + let send_anonymous_request = ClientRequest::SendAnonymous { + recipient: original_recipient, message: b"foomp".to_vec(), - with_reply_surb: true, - connection_id: None, + reply_surbs: 666, + connection_id: Some(42), }; - let bytes = send_request_surb.serialize(); + let bytes = send_anonymous_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { - ClientRequest::Send { + ClientRequest::SendAnonymous { recipient, message, - with_reply_surb, + reply_surbs, connection_id, } => { - assert_eq!(recipient.to_string(), recipient_string); + assert_eq!(recipient, original_recipient); assert_eq!(message, b"foomp".to_vec()); - assert!(with_reply_surb); - assert_eq!(connection_id, None) + assert_eq!(connection_id, Some(42)); + assert_eq!(reply_surbs, 666) } _ => unreachable!(), } @@ -416,22 +511,23 @@ mod tests { #[test] fn reply_request_serialization_works() { - let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; - let reply_surb = ReplySurb::from_base58_string(reply_surb_string).unwrap(); let reply_request = ClientRequest::Reply { + sender_tag: [8u8; SENDER_TAG_SIZE].into(), message: b"foomp".to_vec(), - reply_surb, + connection_id: Some(42), }; let bytes = reply_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { ClientRequest::Reply { - reply_surb, + sender_tag, message, + connection_id, } => { - assert_eq!(reply_surb.to_base58_string(), reply_surb_string); + assert_eq!(sender_tag, [8u8; SENDER_TAG_SIZE].into()); assert_eq!(message, b"foomp".to_vec()); + assert_eq!(connection_id, Some(42)); } _ => unreachable!(), } @@ -458,4 +554,15 @@ mod tests { _ => unreachable!(), } } + + #[test] + fn get_lane_queue_length_request_serialization_works() { + let close_connection_request = ClientRequest::GetLaneQueueLength(42); + let bytes = close_connection_request.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::GetLaneQueueLength(id) => assert_eq!(id, 42), + _ => unreachable!(), + } + } } diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs index 7a32645957..cbefe54242 100644 --- a/clients/native/websocket-requests/src/responses.rs +++ b/clients/native/websocket-requests/src/responses.rs @@ -1,36 +1,54 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // all variable size data is always prefixed with u64 length // tags are u8 -#![allow(unknown_lints)] // due to using `clippy::branches_sharing_code` which does not exist on `stable` just yet - use crate::error::{self, ErrorKind}; use crate::text::ServerResponseText; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; use nymsphinx::receiver::ReconstructedMessage; use std::convert::TryInto; use std::mem::size_of; -/// Value tag representing [`Error`] variant of the [`ServerResponse`] -pub const ERROR_RESPONSE_TAG: u8 = 0x00; +#[repr(u8)] +enum ServerResponseTag { + /// Value tag representing [`Error`] variant of the [`ServerResponse`] + Error = 0x00, -/// Value tag representing [`Received`] variant of the [`ServerResponse`] -pub const RECEIVED_RESPONSE_TAG: u8 = 0x01; + /// Value tag representing [`Received`] variant of the [`ServerResponse`] + Received = 0x01, -/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] -pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02; + /// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] + SelfAddress = 0x02, -/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] -pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03; + /// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] + LaneQueueLength = 0x03, +} + +impl TryFrom for ServerResponseTag { + type Error = error::Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (Self::Error as u8) => Ok(Self::Error), + _ if value == (Self::Received as u8) => Ok(Self::Received), + _ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress), + _ if value == (Self::LaneQueueLength as u8) => Ok(Self::LaneQueueLength), + n => Err(error::Error::new( + ErrorKind::UnknownResponse, + format!("{n} does not correspond to any valid response tag"), + )), + } + } +} #[derive(Debug)] pub enum ServerResponse { Received(ReconstructedMessage), - SelfAddress(Recipient), - LaneQueueLength(u64, usize), + SelfAddress(Box), + LaneQueueLength { lane: u64, queue_length: usize }, Error(error::Error), } @@ -42,24 +60,19 @@ impl ServerResponse { }) } - // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + // RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option || msg_len || msg fn serialize_received(reconstructed_message: ReconstructedMessage) -> Vec { let message_len_bytes = (reconstructed_message.message.len() as u64).to_be_bytes(); - if let Some(reply_surb) = reconstructed_message.reply_surb { - let reply_surb_bytes = reply_surb.to_bytes(); - let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); - // with_reply || surb_len || surb || msg_len || msg - std::iter::once(RECEIVED_RESPONSE_TAG) + if let Some(sender_tag) = reconstructed_message.sender_tag { + std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(true as u8)) - .chain(surb_len_bytes.iter().cloned()) - .chain(reply_surb_bytes.iter().cloned()) + .chain(sender_tag.to_bytes().into_iter()) .chain(message_len_bytes.iter().cloned()) .chain(reconstructed_message.message.into_iter()) .collect() } else { - // without_reply || msg_len || msg - std::iter::once(RECEIVED_RESPONSE_TAG) + std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(false as u8)) .chain(message_len_bytes.iter().cloned()) .chain(reconstructed_message.message.into_iter()) @@ -67,10 +80,9 @@ impl ServerResponse { } } - // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + // RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option || msg_len || msg fn deserialize_received(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], RECEIVED_RESPONSE_TAG); // we must be able to read at the very least if it has a reply_surb and length of some field if b.len() < 2 + size_of::() { @@ -79,101 +91,70 @@ impl ServerResponse { "not enough data provided to recover 'received'".to_string(), )); } + debug_assert_eq!(b[0], ServerResponseTag::Received as u8); - let with_reply_surb = match b[1] { + let has_sender_tag = match b[1] { 0 => false, 1 => true, n => { return Err(error::Error::new( ErrorKind::MalformedResponse, - format!("invalid reply flag {}", n), + format!("invalid sender tag flag {n}"), )) } }; - // this is a false positive as even though the code is the same, it refers to different things - #[allow(clippy::branches_sharing_code)] - if with_reply_surb { - let reply_surb_len = - u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); - - // make sure we won't go out of bounds here - if reply_surb_len > (b.len() - 2 + 2 * size_of::()) as u64 { + let mut i = 2; + let sender_tag = if has_sender_tag { + if b[2..].len() < SENDER_TAG_SIZE { return Err(error::Error::new( - ErrorKind::MalformedResponse, - "not enough bytes to read reply_surb bytes!".to_string(), + ErrorKind::TooShortResponse, + "not enough data provided to recover 'received'".to_string(), )); } - - let surb_bound = 2 + size_of::() + reply_surb_len as usize; - - let reply_surb_bytes = &b[2 + size_of::()..surb_bound]; - let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) { - Ok(reply_surb) => reply_surb, - Err(err) => { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!("malformed reply SURB: {:?}", err), - )) - } - }; - - let message_len = u64::from_be_bytes( - b[surb_bound..surb_bound + size_of::()] - .as_ref() - .try_into() - .unwrap(), - ); - let message = &b[surb_bound + size_of::()..]; - if message.len() as u64 != message_len { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!( - "message len has inconsistent length. specified: {} got: {}", - message_len, - message.len() - ), - )); - } - - Ok(ServerResponse::Received(ReconstructedMessage { - message: message.to_vec(), - reply_surb: Some(reply_surb), - })) + i += SENDER_TAG_SIZE; + Some(AnonymousSenderTag::from_bytes( + b[2..2 + SENDER_TAG_SIZE].try_into().unwrap(), + )) } else { - let message_len = - u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); - let message = &b[2 + size_of::()..]; - if message.len() as u64 != message_len { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!( - "message len has inconsistent length. specified: {} got: {}", - message_len, - message.len() - ), - )); - } + None + }; - Ok(ServerResponse::Received(ReconstructedMessage { - message: message.to_vec(), - reply_surb: None, - })) + if b[i..].len() < size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + "not enough data provided to recover 'received'".to_string(), + )); } + + let message_len = u64::from_be_bytes(b[i..i + size_of::()].try_into().unwrap()); + let message = &b[i + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + + Ok(ServerResponse::Received(ReconstructedMessage { + message: message.to_vec(), + sender_tag, + })) } // SELF_ADDRESS_RESPONSE_TAG || self_address fn serialize_self_address(address: Recipient) -> Vec { - std::iter::once(SELF_ADDRESS_RESPONSE_TAG) - .chain(address.to_bytes().iter().cloned()) + std::iter::once(ServerResponseTag::SelfAddress as u8) + .chain(address.to_bytes().into_iter()) .collect() } // SELF_ADDRESS_RESPONSE_TAG || self_address fn deserialize_self_address(b: &[u8]) -> Result { - // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SELF_ADDRESS_RESPONSE_TAG); - if b.len() != 1 + Recipient::LEN { return Err(error::Error::new( ErrorKind::TooShortResponse, @@ -181,6 +162,9 @@ impl ServerResponse { )); } + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], ServerResponseTag::SelfAddress as u8); + let mut recipient_bytes = [0u8; Recipient::LEN]; recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]); @@ -194,12 +178,12 @@ impl ServerResponse { } }; - Ok(ServerResponse::SelfAddress(recipient)) + Ok(ServerResponse::SelfAddress(Box::new(recipient))) } // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec { - std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG) + std::iter::once(ServerResponseTag::LaneQueueLength as u8) .chain(lane.to_be_bytes().iter().cloned()) .chain(queue_length.to_be_bytes().iter().cloned()) .collect() @@ -208,7 +192,7 @@ impl ServerResponse { // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length fn deserialize_lane_queue_length(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG); + debug_assert_eq!(b[0], ServerResponseTag::LaneQueueLength as u8); let mut lane_bytes = [0u8; size_of::()]; lane_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -219,15 +203,15 @@ impl ServerResponse { .copy_from_slice(&b[1 + size_of::()..1 + size_of::() + size_of::()]); let queue_length = usize::from_be_bytes(queue_length_bytes); - Ok(ServerResponse::LaneQueueLength(lane, queue_length)) + Ok(ServerResponse::LaneQueueLength { lane, queue_length }) } // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn serialize_error(error: error::Error) -> Vec { let message_len_bytes = (error.message.len() as u64).to_be_bytes(); - std::iter::once(ERROR_RESPONSE_TAG) + std::iter::once(ServerResponseTag::Error as u8) .chain(std::iter::once(error.kind as u8)) - .chain(message_len_bytes.iter().cloned()) + .chain(message_len_bytes.into_iter()) .chain(error.message.into_bytes().into_iter()) .collect() } @@ -235,7 +219,7 @@ impl ServerResponse { // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn deserialize_error(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], ERROR_RESPONSE_TAG); + debug_assert_eq!(b[0], ServerResponseTag::Error as u8); if b.len() < size_of::() + size_of::() { return Err(error::Error::new( @@ -244,26 +228,7 @@ impl ServerResponse { )); } - let error_kind = match b[1] { - _ if b[1] == (ErrorKind::EmptyRequest as u8) => ErrorKind::EmptyRequest, - _ if b[1] == (ErrorKind::TooShortRequest as u8) => ErrorKind::TooShortRequest, - _ if b[1] == (ErrorKind::UnknownRequest as u8) => ErrorKind::UnknownRequest, - _ if b[1] == (ErrorKind::MalformedRequest as u8) => ErrorKind::MalformedRequest, - - _ if b[1] == (ErrorKind::EmptyResponse as u8) => ErrorKind::EmptyResponse, - _ if b[1] == (ErrorKind::TooShortResponse as u8) => ErrorKind::TooShortResponse, - _ if b[1] == (ErrorKind::UnknownResponse as u8) => ErrorKind::UnknownResponse, - _ if b[1] == (ErrorKind::MalformedResponse as u8) => ErrorKind::MalformedResponse, - - _ if b[1] == (ErrorKind::Other as u8) => ErrorKind::Other, - - n => { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!("invalid error code {}", n), - )) - } - }; + let error_kind = ErrorKind::try_from(b[1])?; let message_len = u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); @@ -300,8 +265,8 @@ impl ServerResponse { ServerResponse::Received(reconstructed_message) => { Self::serialize_received(reconstructed_message) } - ServerResponse::SelfAddress(address) => Self::serialize_self_address(address), - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::SelfAddress(address) => Self::serialize_self_address(*address), + ServerResponse::LaneQueueLength { lane, queue_length } => { Self::serialize_lane_queue_length(lane, queue_length) } ServerResponse::Error(err) => Self::serialize_error(err), @@ -328,18 +293,14 @@ impl ServerResponse { )); } - let response_tag = b[0]; + let response_tag = ServerResponseTag::try_from(b[0])?; // determine what kind of response that is and try to deserialize it match response_tag { - RECEIVED_RESPONSE_TAG => Self::deserialize_received(b), - SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b), - LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b), - ERROR_RESPONSE_TAG => Self::deserialize_error(b), - n => Err(error::Error::new( - ErrorKind::UnknownResponse, - format!("type {}", n), - )), + ServerResponseTag::Received => Self::deserialize_received(b), + ServerResponseTag::SelfAddress => Self::deserialize_self_address(b), + ServerResponseTag::LaneQueueLength => Self::deserialize_lane_queue_length(b), + ServerResponseTag::Error => Self::deserialize_error(b), } } @@ -361,35 +322,33 @@ mod tests { #[test] fn received_response_serialization_works() { - let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; - - let received_with_surb = ServerResponse::Received(ReconstructedMessage { + let received_with_sender_tag = ServerResponse::Received(ReconstructedMessage { message: b"foomp".to_vec(), - reply_surb: Some(ReplySurb::from_base58_string(reply_surb_string).unwrap()), + sender_tag: Some([42u8; SENDER_TAG_SIZE].into()), }); - let bytes = received_with_surb.serialize(); + let bytes = received_with_sender_tag.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { ServerResponse::Received(reconstructed) => { assert_eq!(reconstructed.message, b"foomp".to_vec()); assert_eq!( - reconstructed.reply_surb.unwrap().to_base58_string(), - reply_surb_string + reconstructed.sender_tag, + Some([42u8; SENDER_TAG_SIZE].into()) ) } _ => unreachable!(), } - let received_without_surb = ServerResponse::Received(ReconstructedMessage { + let received_without_sender_tag = ServerResponse::Received(ReconstructedMessage { message: b"foomp".to_vec(), - reply_surb: None, + sender_tag: None, }); - let bytes = received_without_surb.serialize(); + let bytes = received_without_sender_tag.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { ServerResponse::Received(reconstructed) => { assert_eq!(reconstructed.message, b"foomp".to_vec()); - assert!(reconstructed.reply_surb.is_none()) + assert!(reconstructed.sender_tag.is_none()) } _ => unreachable!(), } @@ -400,7 +359,7 @@ mod tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_string = recipient.to_string(); - let self_address_response = ServerResponse::SelfAddress(recipient); + let self_address_response = ServerResponse::SelfAddress(Box::new(recipient)); let bytes = self_address_response.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { @@ -413,11 +372,14 @@ mod tests { #[test] fn lane_queue_length_response_serialization_works() { - let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42); + let lane_queue_length_response = ServerResponse::LaneQueueLength { + lane: 13, + queue_length: 42, + }; let bytes = lane_queue_length_response.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::LaneQueueLength { lane, queue_length } => { assert_eq!(lane, 13); assert_eq!(queue_length, 42) } diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index 1083ecd9a8..68091dbecf 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -1,11 +1,11 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::ErrorKind; use crate::requests::ClientRequest; use crate::responses::ServerResponse; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; @@ -19,15 +19,22 @@ pub(super) enum ClientRequestText { Send { message: String, recipient: String, - with_reply_surb: bool, + connection_id: Option, + }, + #[serde(rename_all = "camelCase")] + SendAnonymous { + recipient: String, + message: String, + reply_surbs: u32, + connection_id: Option, + }, + #[serde(rename_all = "camelCase")] + Reply { + sender_tag: String, + message: String, connection_id: Option, }, SelfAddress, - #[serde(rename_all = "camelCase")] - Reply { - message: String, - reply_surb: String, - }, } impl TryFrom for ClientRequestText { @@ -46,7 +53,6 @@ impl TryInto for ClientRequestText { ClientRequestText::Send { message, recipient, - with_reply_surb, connection_id, } => { let message_bytes = message.into_bytes(); @@ -57,23 +63,42 @@ impl TryInto for ClientRequestText { Ok(ClientRequest::Send { message: message_bytes, recipient, - with_reply_surb, + connection_id, + }) + } + ClientRequestText::SendAnonymous { + recipient, + message, + reply_surbs, + connection_id, + } => { + let message_bytes = message.into_bytes(); + let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; + Ok(ClientRequest::SendAnonymous { + recipient, + message: message_bytes, + reply_surbs, connection_id, }) } ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress), ClientRequestText::Reply { + sender_tag, message, - reply_surb, + connection_id, } => { let message_bytes = message.into_bytes(); - let reply_surb = ReplySurb::from_base58_string(reply_surb).map_err(|err| { - Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) - })?; + let sender_tag = + AnonymousSenderTag::try_from_base58_string(sender_tag).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; Ok(ClientRequest::Reply { + sender_tag, message: message_bytes, - reply_surb, + connection_id, }) } } @@ -89,7 +114,7 @@ pub(super) enum ServerResponseText { #[serde(rename_all = "camelCase")] Received { message: String, - reply_surb: Option, + sender_tag: Option, }, SelfAddress { address: String, @@ -131,15 +156,13 @@ impl From for ServerResponseText { // TODO: ask DH what is more appropriate, lossy utf8 conversion or returning error and then // pure binary later message: String::from_utf8_lossy(&reconstructed.message).into_owned(), - reply_surb: reconstructed - .reply_surb - .map(|reply_surb| reply_surb.to_base58_string()), + sender_tag: reconstructed.sender_tag.map(|tag| tag.to_base58_string()), } } ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress { address: recipient.to_string(), }, - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::LaneQueueLength { lane, queue_length } => { ServerResponseText::LaneQueueLength { lane, queue_length } } ServerResponse::Error(err) => ServerResponseText::Error { diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 42c477efec..4a4e8edaff 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -26,7 +26,7 @@ tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } url = "2.2" # internal -client-core = { path = "../client-core" } +client-core = { path = "../client-core", features = ["fs-surb-storage"] } client-connections = { path = "../../common/client-connections" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } diff --git a/clients/socks5/src/client/config/mod.rs b/clients/socks5/src/client/config/mod.rs index 0bbab949ef..97372e4c86 100644 --- a/clients/socks5/src/client/config/mod.rs +++ b/clients/socks5/src/client/config/mod.rs @@ -3,7 +3,7 @@ use crate::client::config::template::config_template; pub use client_core::config::MISSING_VALUE; -use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig}; +use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_SOCKS5_LISTENING_PORT; use config::NymConfig; use nymsphinx::addressing::clients::Recipient; @@ -12,6 +12,9 @@ use std::path::PathBuf; mod template; +const DEFAULT_CONNECTION_START_SURBS: u32 = 20; +const DEFAULT_PER_REQUEST_SURBS: u32 = 3; + #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { @@ -19,6 +22,9 @@ pub struct Config { base: BaseConfig, socks5: Socks5, + + #[serde(default)] + socks5_debug: Socks5Debug, } impl NymConfig for Config { @@ -63,6 +69,7 @@ impl Config { Config { base: BaseConfig::new(id), socks5: Socks5::new(provider_mix_address), + socks5_debug: Socks5Debug::default(), } } @@ -78,7 +85,24 @@ impl Config { self } + pub fn with_anonymous_replies(mut self, anonymous_replies: bool) -> Self { + self.socks5.send_anonymously = anonymous_replies; + self + } + // getters + pub fn get_base(&self) -> &BaseConfig { + &self.base + } + + pub fn get_base_mut(&mut self) -> &mut BaseConfig { + &mut self.base + } + + pub fn get_debug_settings(&self) -> &DebugConfig { + self.get_base().get_debug_config() + } + pub fn get_config_file_save_location(&self) -> PathBuf { self.config_directory().join(Self::config_file_name()) } @@ -88,17 +112,21 @@ impl Config { .expect("malformed provider address") } - pub fn get_base(&self) -> &BaseConfig { - &self.base - } - - pub fn get_base_mut(&mut self) -> &mut BaseConfig { - &mut self.base + pub fn get_send_anonymously(&self) -> bool { + self.socks5.send_anonymously } pub fn get_listening_port(&self) -> u16 { self.socks5.listening_port } + + pub fn get_connection_start_surbs(&self) -> u32 { + self.socks5_debug.connection_start_surbs + } + + pub fn get_per_request_surbs(&self) -> u32 { + self.socks5_debug.per_request_surbs + } } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -109,6 +137,13 @@ pub struct Socks5 { /// The mix address of the provider to which all requests are going to be sent. provider_mix_address: String, + + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + send_anonymously: bool, } impl Socks5 { @@ -116,6 +151,7 @@ impl Socks5 { Socks5 { listening_port: DEFAULT_SOCKS5_LISTENING_PORT, provider_mix_address: provider_mix_address.into(), + send_anonymously: false, } } } @@ -125,6 +161,26 @@ impl Default for Socks5 { Socks5 { listening_port: DEFAULT_SOCKS5_LISTENING_PORT, provider_mix_address: "".into(), + send_anonymously: false, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Socks5Debug { + /// Number of reply SURBs attached to each `Request::Connect` message. + connection_start_surbs: u32, + + /// Number of reply SURBs attached to each `Request::Send` message. + per_request_surbs: u32, +} + +impl Default for Socks5Debug { + fn default() -> Self { + Socks5Debug { + connection_start_surbs: DEFAULT_CONNECTION_START_SURBS, + per_request_surbs: DEFAULT_PER_REQUEST_SURBS, } } } diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 11d3185f56..527da38274 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -49,13 +49,12 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}' # Path to file containing public encryption key. public_encryption_key_file = '{{ client.public_encryption_key_file }}' -# Full path to file containing reply encryption keys of all reply-SURBs we have ever -# sent but not received back. -reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' - # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' +# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. +reply_surb_database_path = '{{ client.reply_surb_database_path }}' + ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for @@ -92,6 +91,12 @@ provider_mix_address = '{{ socks5.provider_mix_address }}' # The port on which the client will be listening for incoming requests listening_port = {{ socks5.listening_port }} +# Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. +# While this is going to hide its actual address information, it will make the actual communication +# slower and consume nearly double the bandwidth as it will require sending reply SURBs. +# +# Note that some service providers might not support this. +send_anonymously = {{ socks5.send_anonymously }} ##### logging configuration options ##### @@ -104,6 +109,9 @@ listening_port = {{ socks5.listening_port }} # The following options should not be modified unless you know EXACTLY what you are doing # as if set incorrectly, they may impact your anonymity. +# [socks5_debug] + + [debug] average_packet_delay = '{{ debug.average_packet_delay }}' diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 4103dd2788..2b4bff7852 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -3,11 +3,14 @@ use crate::client::config::Config; use crate::error::Socks5ClientError; +use crate::socks; use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, +}; use client_core::client::key_manager::KeyManager; use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; use futures::channel::mpsc; @@ -114,6 +117,11 @@ impl NymClient { config.get_provider_mix_address(), self_address, shared_lane_queue_lengths, + socks::client::Config::new( + config.get_send_anonymously(), + config.get_connection_start_surbs(), + config.get_per_request_surbs(), + ), shutdown.clone(), ); task::spawn_with_report_error( @@ -158,7 +166,6 @@ impl NymClient { // Listen to status messages from task, that we forward back to the caller shutdown.start_status_listener(sender); - // Listen for conditions to stop let res = tokio::select! { biased; message = receiver.next() => { @@ -198,6 +205,11 @@ impl NymClient { self.config.get_base(), self.key_manager, Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, ); let self_address = base_builder.as_mix_recipient(); diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 1e8b5c47f0..70e8935ca3 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -1,19 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::Display; - -use clap::Args; -use config::NymConfig; -use nymsphinx::addressing::clients::Recipient; -use serde::Serialize; -use tap::TapFallible; - use crate::{ client::config::Config, commands::{override_config, OverrideConfig}, error::Socks5ClientError, }; +use clap::Args; +use config::NymConfig; +use nymsphinx::addressing::clients::Recipient; +use serde::Serialize; +use std::fmt::Display; +use tap::TapFallible; #[derive(Args, Clone)] pub(crate) struct Init { @@ -25,6 +23,14 @@ pub(crate) struct Init { #[clap(long)] provider: String, + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + #[clap(long)] + use_anonymous_sender_tag: bool, + /// Id of the gateway we are going to connect to. #[clap(long)] gateway: Option, @@ -72,6 +78,7 @@ impl From for OverrideConfig { nymd_validators: init_config.nymd_validators, api_validators: init_config.api_validators, port: init_config.port, + use_anonymous_sender_tag: init_config.use_anonymous_sender_tag, fastmode: init_config.fastmode, no_cover: init_config.no_cover, #[cfg(feature = "coconut")] @@ -138,7 +145,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> { // Setup gateway by either registering a new one, or creating a new config from the selected // one but with keys kept, or reusing the gateway configuration. - let gateway = client_core::init::setup_gateway::( + let gateway = client_core::init::setup_gateway::<_, Config, _>( register_gateway, user_chosen_gateway_id, config.get_base(), diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index f1da5a6fda..5f5bfda1de 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -82,6 +82,7 @@ pub(crate) struct OverrideConfig { nymd_validators: Option, api_validators: Option, port: Option, + use_anonymous_sender_tag: bool, fastmode: bool, no_cover: bool, @@ -122,6 +123,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi .set_custom_validator_apis(parse_validators(&raw_validators)); } + if args.use_anonymous_sender_tag { + config = config.with_anonymous_replies(true) + } + if let Some(port) = args.port { config = config.with_port(port); } diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index c1886fc003..612ecc02c2 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -22,6 +22,14 @@ pub(crate) struct Run { #[clap(long)] config: Option, + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + #[clap(long)] + use_anonymous_sender_tag: bool, + /// Address of the socks5 provider to send messages to. #[clap(long)] provider: Option, @@ -65,6 +73,7 @@ impl From for OverrideConfig { nymd_validators: run_config.nymd_validators, api_validators: run_config.api_validators, port: run_config.port, + use_anonymous_sender_tag: run_config.use_anonymous_sender_tag, fastmode: run_config.fastmode, no_cover: run_config.no_cover, #[cfg(feature = "coconut")] diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 2ff6ffbfc2..23cd715cf5 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,21 +1,24 @@ -use client_core::error::ClientCoreError; - use crate::socks::types::SocksProxyError; +use client_core::client::replies::reply_storage::fs_backend; +use client_core::error::ClientCoreError; #[derive(thiserror::Error, Debug)] pub enum Socks5ClientError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), + #[error("client-core error: {0}")] - ClientCoreError(#[from] ClientCoreError), + ClientCoreError(#[from] ClientCoreError), #[error("SOCKS proxy error")] SocksProxyError(SocksProxyError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, + #[error("Fail to bind address")] FailToBindAddress, } diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index acb0a989ad..3b35a4caf9 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -126,11 +126,33 @@ impl AsyncWrite for StreamState { } } +#[derive(Debug, Copy, Clone)] +pub(crate) struct Config { + use_surbs_for_responses: bool, + connection_start_surbs: u32, + per_request_surbs: u32, +} + +impl Config { + pub(crate) fn new( + use_surbs_for_responses: bool, + connection_start_surbs: u32, + per_request_surbs: u32, + ) -> Self { + Self { + use_surbs_for_responses, + connection_start_surbs, + per_request_surbs, + } + } +} + /// A client connecting to the Socks proxy server, because /// it wants to make a Nym-protected outbound request. Typically, this is /// something like e.g. a wallet app running on your laptop connecting to /// `SphinxSocksServer`. pub(crate) struct SocksClient { + config: Config, controller_sender: ControllerSender, stream: StreamState, auth_nmethods: u8, @@ -160,6 +182,7 @@ impl Drop for SocksClient { impl SocksClient { #[allow(clippy::too_many_arguments)] pub fn new( + config: Config, stream: TcpStream, authenticator: Authenticator, input_sender: InputMessageSender, @@ -175,6 +198,7 @@ impl SocksClient { let connection_id = Self::generate_random(); SocksClient { + config, controller_sender, connection_id, stream: StreamState::Available(stream), @@ -268,14 +292,14 @@ impl SocksClient { self.handle_request().await } - async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { - let req = Request::new_connect(self.connection_id, remote_address, self.self_address); + async fn send_anonymous_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + let req = Request::new_connect(self.connection_id, remote_address, None); let msg = Message::Request(req); - let input_message = InputMessage::new_fresh( + let input_message = InputMessage::new_anonymous( self.service_provider, msg.into_bytes(), - false, + self.config.connection_start_surbs, TransmissionLane::ConnectionId(self.connection_id), ); self.input_sender @@ -284,6 +308,30 @@ impl SocksClient { .expect("InputMessageReceiver has stopped receiving!"); } + async fn send_connect_to_mixnet_with_return_address(&mut self, remote_address: RemoteAddress) { + let req = Request::new_connect(self.connection_id, remote_address, Some(self.self_address)); + let msg = Message::Request(req); + + let input_message = InputMessage::new_regular( + self.service_provider, + msg.into_bytes(), + TransmissionLane::ConnectionId(self.connection_id), + ); + self.input_sender + .send(input_message) + .await + .expect("InputMessageReceiver has stopped receiving!"); + } + + async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + if self.config.use_surbs_for_responses { + self.send_anonymous_connect_to_mixnet(remote_address).await + } else { + self.send_connect_to_mixnet_with_return_address(remote_address) + .await + } + } + async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { self.send_connect_to_mixnet(remote_proxy_target.clone()) .await; @@ -300,6 +348,8 @@ impl SocksClient { let connection_id = self.connection_id; let input_sender = self.input_sender.clone(); + let anonymous = self.config.use_surbs_for_responses; + let per_request_surbs = self.config.per_request_surbs; let recipient = self.service_provider; let (stream, _) = ProxyRunner::new( @@ -316,7 +366,16 @@ impl SocksClient { let provider_request = Request::new_send(conn_id, read_data, socket_closed); let provider_message = Message::Request(provider_request); let lane = TransmissionLane::ConnectionId(conn_id); - InputMessage::new_fresh(recipient, provider_message.into_bytes(), false, lane) + if anonymous { + InputMessage::new_anonymous( + recipient, + provider_message.into_bytes(), + per_request_surbs, + lane, + ) + } else { + InputMessage::new_regular(recipient, provider_message.into_bytes(), lane) + } }) .await .into_inner(); diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 430b4322da..7613a32019 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -54,8 +54,8 @@ impl MixnetResponseListener { async fn on_message(&self, reconstructed_message: ReconstructedMessage) { let raw_message = reconstructed_message.message; - if reconstructed_message.reply_surb.is_some() { - warn!("this message had a surb - we didn't do anything with it"); + if reconstructed_message.sender_tag.is_some() { + warn!("this message was sent anonymously - it couldn't have come from the service provider"); } let response = match Message::try_from_bytes(&raw_message) { diff --git a/clients/socks5/src/socks/mod.rs b/clients/socks5/src/socks/mod.rs index 2acdaa615b..16603839ff 100644 --- a/clients/socks5/src/socks/mod.rs +++ b/clients/socks5/src/socks/mod.rs @@ -5,7 +5,7 @@ use std::convert::TryFrom; use self::types::SocksProxyError; pub mod authentication; -mod client; +pub(crate) mod client; pub(crate) mod mixnet_responses; mod request; pub mod server; diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index e31c8d2b0b..0bba59ee0b 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -3,6 +3,7 @@ use crate::error::Socks5ClientError; use super::{ authentication::Authenticator, client::SocksClient, mixnet_responses::MixnetResponseListener, }; +use crate::socks::client; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, @@ -21,6 +22,7 @@ pub struct SphinxSocksServer { listening_address: SocketAddr, service_provider: Recipient, self_address: Recipient, + client_config: client::Config, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, } @@ -33,6 +35,7 @@ impl SphinxSocksServer { service_provider: Recipient, self_address: Recipient, lane_queue_lengths: LaneQueueLengths, + client_config: client::Config, shutdown: ShutdownListener, ) -> Self { // hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can @@ -44,6 +47,7 @@ impl SphinxSocksServer { listening_address: format!("{}:{}", ip, port).parse().unwrap(), service_provider, self_address, + client_config, lane_queue_lengths, shutdown, } @@ -86,6 +90,7 @@ impl SphinxSocksServer { tokio::select! { Ok((stream, _remote)) = listener.accept() => { let mut client = SocksClient::new( + self.client_config, stream, self.authenticator.clone(), input_sender.clone(), diff --git a/clients/webassembly/js-example/index.js b/clients/webassembly/js-example/index.js index 4a28435219..f0890bf12e 100644 --- a/clients/webassembly/js-example/index.js +++ b/clients/webassembly/js-example/index.js @@ -65,7 +65,6 @@ async function main() { * * Message and recipient are taken from the values in the user interface. * - * @param {Client} nymClient the nym client to use for message sending */ async function sendMessageTo() { const message = document.getElementById('message').value; @@ -96,10 +95,13 @@ function displaySend(message) { /** * Display received text messages in the browser. Colour them green. * - * @param {string} message + * @param {Uint8Array} raw */ -function displayReceived(message) { - const content = message; +function displayReceived(raw, sender_tag) { + const content = new TextDecoder().decode(raw); + if (sender_tag !== undefined) { + console.log("this message also contained some surbs from", sender_tag) + } let timestamp = new Date().toISOString().substr(11, 12); let receivedDiv = document.createElement('div'); @@ -116,7 +118,7 @@ function displayReceived(message) { /** * Display the nymClient's sender address in the user interface * - * @param {Client} nymClient + * @param {String} address */ function displaySenderAddress(address) { document.getElementById('sender').value = address; diff --git a/clients/webassembly/js-example/worker.js b/clients/webassembly/js-example/worker.js index 0cbf2fe4d6..da066d6ea2 100644 --- a/clients/webassembly/js-example/worker.js +++ b/clients/webassembly/js-example/worker.js @@ -17,38 +17,7 @@ importScripts('nym_client_wasm.js'); console.log('Initializing worker'); // wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts` -const { default_debug, get_gateway, NymClient, set_panic_hook, Config } = wasm_bindgen; - -class ClientWrapper { - constructor(config, onMessageHandler) { - this.rustClient = new NymClient(config); - this.rustClient.set_on_message(onMessageHandler); - this.rustClient.set_on_gateway_connect(this.onConnect); - } - - selfAddress = () => { - return this.rustClient.self_address(); - }; - - onConnect = () => { - console.log('Established (and authenticated) gateway connection!'); - }; - - start = async () => { - // this is current limitation of wasm in rust - for async methods you can't take self by reference... - // I'm trying to figure out if I can somehow hack my way around it, but for time being you have to re-assign - // the object (it's the same one) - this.rustClient = await this.rustClient.start(); - }; - - sendMessage = async (recipient, message) => { - this.rustClient = await this.rustClient.send_message(recipient, message); - }; - - sendBinaryMessage = async (recipient, message) => { - this.rustClient = await this.rustClient.send_binary_message(recipient, message); - }; -} +const { default_debug, NymClientBuilder, set_panic_hook, Config, GatewayEndpointConfig } = wasm_bindgen; let client = null; @@ -62,11 +31,12 @@ async function main() { set_panic_hook(); // validator server we will use to get topology from - const validator = 'https://validator.nymtech.net/api'; //"http://localhost:8081"; - const preferredGateway = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM'; - - const gatewayEndpoint = await get_gateway(validator, preferredGateway); - gatewayEndpoint.gateway_listener = "wss://gateway1.nymtech.net:443"; // this is needed if we want it to work on the web. However this gateway is a v1 gateway, we will need to change for v2 once we get there + const validator = 'https://qwerty-validator-api.qa.nymte.ch/api'; + + const gatewayId = 'EVupP2tRUeZo5Y6RpBHAbm8kSntpgNyZNL6yCr7BDEoG'; + const gatewayOwner = 'n1rmlew3euapuq7rs4s4j9apv00whrsazr764kl7'; + const gatewayListener = 'ws://176.58.120.72:9000'; + const gatewayEndpoint = new GatewayEndpointConfig(gatewayId, gatewayOwner, gatewayListener) // only really useful if you want to adjust some settings like traffic rate // (if not needed you can just pass a null) @@ -94,12 +64,17 @@ async function main() { }; console.log('Instantiating WASM client...'); - client = new ClientWrapper(config, onMessageHandler); + + let clientBuilder = new NymClientBuilder(config, onMessageHandler) console.log('Web worker creating WASM client...'); - await client.start(); + let local_client = await clientBuilder.start_client(); console.log('WASM client running!'); - - const selfAddress = client.rustClient.self_address(); + + const selfAddress = local_client.self_address(); + + // set the global (I guess we don't have to anymore?) + client = local_client; + console.log(`Client address is ${selfAddress}`); self.postMessage({ kind: 'Ready', @@ -114,7 +89,8 @@ async function main() { switch (event.data.kind) { case 'SendMessage': { const { message, recipient } = event.data.args; - await client.sendMessage(message, recipient); + let uint8Array = new TextEncoder().encode(message); + await client.send_regular_message(uint8Array, recipient); } } } diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs index 66c6ceefe7..4740a02b2a 100644 --- a/clients/webassembly/src/client/config.rs +++ b/clients/webassembly/src/client/config.rs @@ -106,6 +106,34 @@ pub struct Debug { /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. pub use_extended_packet_size: bool, + + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + pub maximum_reply_surb_waiting_period_ms: u64, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + pub maximum_reply_surb_age_ms: u64, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + pub maximum_reply_key_age_ms: u64, } impl From for ConfigDebug { @@ -135,6 +163,16 @@ impl From for ConfigDebug { disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, use_extended_packet_size, + minimum_reply_surb_storage_threshold: debug.minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: debug.maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: debug.minimum_reply_surb_request_size, + maximum_reply_surb_request_size: debug.maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: debug.maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period: Duration::from_millis( + debug.maximum_reply_surb_waiting_period_ms, + ), + maximum_reply_surb_age: Duration::from_millis(debug.maximum_reply_surb_age_ms), + maximum_reply_key_age: Duration::from_millis(debug.maximum_reply_key_age_ms), } } } @@ -157,6 +195,16 @@ impl From for Debug { disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, use_extended_packet_size: debug.use_extended_packet_size.is_some(), + minimum_reply_surb_storage_threshold: debug.minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: debug.maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: debug.minimum_reply_surb_request_size, + maximum_reply_surb_request_size: debug.maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: debug.maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period_ms: debug + .maximum_reply_surb_waiting_period + .as_millis() as u64, + maximum_reply_surb_age_ms: debug.maximum_reply_surb_age.as_millis() as u64, + maximum_reply_key_age_ms: debug.maximum_reply_key_age.as_millis() as u64, } } } diff --git a/clients/webassembly/src/client/helpers.rs b/clients/webassembly/src/client/helpers.rs new file mode 100644 index 0000000000..4349fd5f78 --- /dev/null +++ b/clients/webassembly/src/client/helpers.rs @@ -0,0 +1,30 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use client_core::client::base_client::ClientInput; +use client_core::client::inbound_messages::InputMessage; +use js_sys::Promise; +use std::sync::Arc; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::future_to_promise; + +// defining helper trait as we could directly call the method on the wrapper +pub(crate) trait InputSender { + fn send_message(&self, message: InputMessage) -> Promise; +} + +impl InputSender for Arc { + fn send_message(&self, message: InputMessage) -> Promise { + let this = Arc::clone(self); + future_to_promise(async move { + match this.input_sender.send(message).await { + Ok(_) => Ok(JsValue::null()), + Err(_) => { + let js_error = + js_sys::Error::new("InputMessageReceiver has stopped receiving!"); + Err(JsValue::from(js_error)) + } + } + }) + } +} diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 68a40cf99b..9c3aefe8c6 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,59 +2,65 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; +use crate::client::helpers::InputSender; use crate::client::response_pusher::ResponsePusher; use client_connections::TransmissionLane; use client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; +use client_core::client::replies::reply_storage::browser_backend; use client_core::client::{inbound_messages::InputMessage, key_manager::KeyManager}; -use crypto::asymmetric::identity; +use gateway_client::bandwidth::BandwidthController; +use js_sys::Promise; use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use rand::rngs::OsRng; +use std::sync::Arc; use task::ShutdownNotifier; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; use wasm_utils::{console_error, console_log}; pub mod config; +mod helpers; mod response_pusher; #[wasm_bindgen] pub struct NymClient { - config: Config, - - /// KeyManager object containing smart pointers to all relevant keys used by the client. - // due to disgusting workaround I had to wrap the key_manager in an Option - // so that the interface wouldn't change (i.e. both `start` and `new` would still return a `NymClient`) - key_manager: Option, - self_address: Option, - - // TODO: this should be stored somewhere persistently - // received_keys: HashSet, - /// Channel used for transforming 'raw' messages into sphinx packets and sending them - /// through the mix network. - client_input: Option, - - // callbacks - on_message: Option, - on_binary_message: Option, - on_gateway_connect: Option, + self_address: String, + client_input: Arc, // even though we don't use graceful shutdowns, other components rely on existence of this struct // and if it's dropped, everything will start going offline - _shutdown: Option, + _shutdown: ShutdownNotifier, } #[wasm_bindgen] -impl NymClient { +pub struct NymClientBuilder { + config: Config, + + /// KeyManager object containing smart pointers to all relevant keys used by the client. + key_manager: KeyManager, + + reply_surb_storage_backend: browser_backend::Backend, + + on_message: js_sys::Function, + + // unimplemented: + bandwidth_controller: Option, + disabled_credentials: bool, +} + +#[wasm_bindgen] +impl NymClientBuilder { #[wasm_bindgen(constructor)] - pub fn new(config: Config) -> Self { - Self { + pub fn new(config: Config, on_message: js_sys::Function) -> Self { + //, key_manager: Option) { + NymClientBuilder { + reply_surb_storage_backend: Self::setup_reply_surb_storage_backend(&config), config, - key_manager: Some(Self::setup_key_manager()), - on_message: None, - on_binary_message: None, - on_gateway_connect: None, - client_input: None, - self_address: None, - _shutdown: None, + key_manager: Self::setup_key_manager(), + on_message, + bandwidth_controller: None, + disabled_credentials: true, } } @@ -69,117 +75,154 @@ impl NymClient { KeyManager::new(&mut rng) } - pub fn set_on_message(&mut self, on_message: js_sys::Function) { - self.on_message = Some(on_message); - } - - pub fn set_on_binary_message(&mut self, on_binary_message: js_sys::Function) { - self.on_binary_message = Some(on_binary_message); - } - - pub fn set_on_gateway_connect(&mut self, on_connect: js_sys::Function) { - self.on_gateway_connect = Some(on_connect) - } - - fn as_mix_recipient(&self) -> Recipient { - // another disgusting (and hopefully temporary) workaround - let key_manager_ref = self - .key_manager - .as_ref() - .expect("attempting to call 'as_mix_recipient' after 'start'"); - - Recipient::new( - *key_manager_ref.identity_keypair().public_key(), - *key_manager_ref.encryption_keypair().public_key(), - identity::PublicKey::from_base58_string(&self.config.gateway_endpoint.gateway_id) - .expect("no gateway has been selected"), + // don't get too excited about the name, under the hood it's just a big fat placeholder + // with no persistence + fn setup_reply_surb_storage_backend(config: &Config) -> browser_backend::Backend { + browser_backend::Backend::new( + config.debug.minimum_reply_surb_storage_threshold, + config.debug.maximum_reply_surb_storage_threshold, ) } + fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) { + ResponsePusher::new(client_output, on_message).start() + } + + pub async fn start_client(self) -> Promise { + future_to_promise(async move { + console_log!("Starting the wasm client"); + + let base_builder = BaseClientBuilder::new( + &self.config.gateway_endpoint, + &self.config.debug, + self.key_manager, + self.bandwidth_controller, + self.reply_surb_storage_backend, + self.disabled_credentials, + vec![self.config.validator_api_url.clone()], + ); + + let self_address = base_builder.as_mix_recipient().to_string(); + let mut started_client = match base_builder.start_base().await { + Ok(base_client) => base_client, + Err(err) => { + let error_msg = format!("failed to start the base client components - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + return Err(JsValue::from(js_error)); + } + }; + + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + Self::start_reconstructed_pusher(client_output, self.on_message); + + Ok(JsValue::from(NymClient { + self_address, + client_input: Arc::new(client_input), + _shutdown: started_client.shutdown_notifier, + })) + }) + } +} + +#[wasm_bindgen] +impl NymClient { pub fn self_address(&self) -> String { - if let Some(address) = &self.self_address { - address.clone() - } else { - self.as_mix_recipient().to_string() + self.self_address.clone() + } + + fn parse_recipient(recipient: &str) -> Result { + match Recipient::try_from_base58_string(recipient) { + Ok(recipient) => Ok(recipient), + Err(err) => { + let error_msg = format!("{recipient} is not a valid Nym network recipient - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + Err(JsValue::from(js_error)) + } } } - // Right now it's impossible to have async exported functions to take `&mut self` rather than mut self - // TODO: try Rc> approach? - pub async fn send_message(self, message: String, recipient: String) -> Self { - console_log!("Sending {} to {}", message, recipient); - - let message_bytes = message.into_bytes(); - self.send_binary_message(message_bytes, recipient).await + fn parse_sender_tag(tag: &str) -> Result { + match AnonymousSenderTag::try_from_base58_string(tag) { + Ok(tag) => Ok(tag), + Err(err) => { + let error_msg = format!("{tag} is not a valid Nym AnonymousSenderTag - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + Err(JsValue::from(js_error)) + } + } } - pub async fn send_binary_message(self, message: Vec, recipient: String) -> Self { - console_log!("Sending {} bytes to {}", message.len(), recipient); - - let recipient = Recipient::try_from_base58_string(recipient).unwrap(); - let lane = TransmissionLane::General; - - let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - - self.client_input - .as_ref() - .expect("start method was not called before!") - .input_sender - .send(input_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - - self - } - - fn start_reconstructed_pusher( - client_output: ClientOutput, - on_message: Option, - on_binary_message: Option, - ) { - ResponsePusher::new(client_output, on_message, on_binary_message).start() - } - - pub async fn start(mut self) -> NymClient { - console_log!("Starting the wasm client"); - - let base_builder = BaseClientBuilder::new( - &self.config.gateway_endpoint, - &self.config.debug, - self.key_manager.take().unwrap(), - None, - true, - vec![self.config.validator_api_url.clone()], + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant + pub fn send_regular_message(&self, message: Vec, recipient: String) -> Promise { + console_log!( + "Attempting to send {:.2} kiB message to {recipient}", + message.len() as f64 / 1024.0 ); - self.self_address = Some(base_builder.as_mix_recipient().to_string()); - let mut started_client = match base_builder.start_base().await { - Ok(base_client) => base_client, - Err(err) => { - console_error!("failed to start base client components - {}", err); - // proper error handling is left here as an exercise for the reader (hi Mark : )) - panic!("failed to start base client components - {err}") - } + let recipient = match Self::parse_recipient(&recipient) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), }; - match self.on_gateway_connect.as_ref() { - Some(callback) => { - callback - .call0(&JsValue::null()) - .expect("on connect callback failed!"); - } - None => console_log!("Gateway connection established - no callback specified"), + let lane = TransmissionLane::General; + + let input_msg = InputMessage::new_regular(recipient, message, lane); + self.client_input.send_message(input_msg) + } + + /// Creates a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + pub fn send_anonymous_message( + &self, + message: Vec, + recipient: String, + reply_surbs: u32, + ) -> Promise { + console_log!( + "Attempting to anonymously send {:.2} kiB message to {recipient} while attaching {reply_surbs} replySURBs.", + message.len() as f64 / 1024.0 + ); + + let recipient = match Self::parse_recipient(&recipient) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), }; + let lane = TransmissionLane::General; - // those should be moved to a completely different struct, but I don't want to break compatibility for now - let client_input = started_client.client_input.register_producer(); - let client_output = started_client.client_output.register_consumer(); + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); + self.client_input.send_message(input_msg) + } - let on_message = self.on_message.take(); - let on_binary_message = self.on_binary_message.take(); - Self::start_reconstructed_pusher(client_output, on_message, on_binary_message); - self.client_input = Some(client_input); - self._shutdown = Some(started_client.shutdown_notifier); + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + pub fn send_reply(&self, message: Vec, recipient_tag: String) -> Promise { + console_log!( + "Attempting to send {:.2} kiB reply message to {recipient_tag}", + message.len() as f64 / 1024.0 + ); - self + let sender_tag = match Self::parse_sender_tag(&recipient_tag) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), + }; + let lane = TransmissionLane::General; + + let input_msg = InputMessage::new_reply(sender_tag, message, lane); + self.client_input.send_message(input_msg) } } diff --git a/clients/webassembly/src/client/response_pusher.rs b/clients/webassembly/src/client/response_pusher.rs index c5717a9bc3..fbe04c66ed 100644 --- a/clients/webassembly/src/client/response_pusher.rs +++ b/clients/webassembly/src/client/response_pusher.rs @@ -5,27 +5,18 @@ use client_core::client::base_client::ClientOutput; use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; use futures::channel::mpsc; use futures::StreamExt; +use js_sys::Uint8Array; use wasm_bindgen::JsValue; use wasm_bindgen_futures::spawn_local; -use wasm_utils::console_log; +use wasm_utils::console_error; pub(crate) struct ResponsePusher { reconstructed_receiver: ReconstructedMessagesReceiver, - on_message: Option, - on_binary_message: Option, + on_message: js_sys::Function, } impl ResponsePusher { - pub(crate) fn new( - client_output: ClientOutput, - on_message: Option, - on_binary_message: Option, - ) -> Self { - if on_message.is_none() && on_binary_message.is_none() { - // exercise for the reader : ) - panic!("neither 'on_message' nor 'on_binary_message' was set!") - } - + pub(crate) fn new(client_output: ClientOutput, on_message: js_sys::Function) -> Self { // register our output let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); @@ -40,7 +31,6 @@ impl ResponsePusher { ResponsePusher { reconstructed_receiver, on_message, - on_binary_message, } } @@ -49,23 +39,20 @@ impl ResponsePusher { let this = JsValue::null(); while let Some(reconstructed) = self.reconstructed_receiver.next().await { - for msg in reconstructed { - if let Some(ref callback_binary) = self.on_binary_message { - let arg1 = serde_wasm_bindgen::to_value(&msg.message).unwrap(); - callback_binary - .call1(&this, &arg1) - .expect("on binary message failed!"); - } - if let Some(ref callback) = self.on_message { - if msg.reply_surb.is_some() { - console_log!("the received message contained a reply-surb that we do not know how to handle (yet)") - } - let stringified = String::from_utf8_lossy(&msg.message).into_owned(); - let arg1 = serde_wasm_bindgen::to_value(&stringified).unwrap(); - callback.call1(&this, &arg1).expect("on message failed!"); - } + for reconstructed_msg in reconstructed { + let (msg, tag) = reconstructed_msg.into_inner(); + + let msg_slice: &[u8] = &msg; + let array = Uint8Array::from(msg_slice); + let arg1 = JsValue::from(array); + let arg2 = JsValue::from(tag); + self.on_message + .call2(&this, &arg1, &arg2) + .expect("on binary message failed!"); } } + + console_error!("we stopped receiving reconstructed messages!") }) } } diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index 515938ed54..f86270da63 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -10,9 +10,11 @@ pub type ConnectionId = u64; #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub enum TransmissionLane { General, - Reply, + // we need to treat surb-related requests and responses at higher priority + // so that the rest of underlying communication could actually continue + ReplySurbRequest, + AdditionalReplySurbs, Retransmission, - Control, ConnectionId(ConnectionId), } diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index d1ea2abf1e..8f1de308d3 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -57,6 +57,7 @@ version = "0.4" [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils] path = "../../wasm-utils" +features = ["websocket"] # only import it in wasm. Prefer proper tokio timer in non-wasm [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer] diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index a731186424..e1c2654b80 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -410,7 +410,7 @@ impl GatewayClient { match gateway_protocol { None => { warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in 1.2.0 we will have to return a hard error here + // note: in +1.2.0 we will have to return a hard error here Ok(()) } Some(v) if v != PROTOCOL_VERSION => { diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 8671a7363b..ac209f0920 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -16,7 +16,7 @@ pub mod client; pub mod error; pub mod packet_router; pub mod socket_state; -#[cfg(feature = "wasm")] +#[cfg(target_arch = "wasm32")] mod wasm_storage; /// Helper method for reading from websocket stream. Helps to flatten the structure. diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 6a218f1fa2..b5746ea06c 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -1,20 +1,12 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + use crate::{validator_api, ValidatorClientError}; use coconut_dkg_common::types::NodeIndex; -#[cfg(feature = "nymd-client")] -use coconut_dkg_common::{ - dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, -}; -#[cfg(feature = "nymd-client")] -use coconut_interface::Base58; use coconut_interface::VerificationKey; -use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -use mixnet_contract_common::{IdentityKey, MixId}; -#[cfg(feature = "nymd-client")] -use std::str::FromStr; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; @@ -28,15 +20,24 @@ use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient #[cfg(feature = "nymd-client")] use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] +use coconut_dkg_common::{ + dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, +}; +#[cfg(feature = "nymd-client")] +use coconut_interface::Base58; +#[cfg(feature = "nymd-client")] use cw3::ProposalResponse; #[cfg(feature = "nymd-client")] use mixnet_contract_common::{ + families::{Family, FamilyHead}, mixnode::MixNodeBond, pending_events::{PendingEpochEvent, PendingIntervalEvent}, - Delegation, RewardedSetNodeStatus, UnbondedMixnode, + Delegation, IdentityKey, RewardedSetNodeStatus, UnbondedMixnode, }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; +#[cfg(feature = "nymd-client")] +use std::str::FromStr; use url::Url; #[cfg(feature = "nymd-client")] use validator_api_requests::models::MixNodeBondAnnotated; diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 918e001fec..aaadfe0171 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -161,7 +161,7 @@ impl SphinxPacketProcessor { ) -> Result { match packet { ProcessedPacket::ForwardHop(packet, address, delay) => { - self.process_forward_hop(packet, address, delay, packet_mode) + self.process_forward_hop(*packet, address, delay, packet_mode) } // right now there's no use for the surb_id included in the header - probably it should get removed from the // sphinx all together? diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index f0f24ac665..d9d81aecd4 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } rand_distr = "0.3" +thiserror = "1.0.37" nymsphinx-acknowledgements = { path = "acknowledgements" } nymsphinx-addressing = { path = "addressing" } diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index 22bd4bc468..67e863d1f6 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -72,6 +72,10 @@ impl SurbAck { PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN } + pub fn expected_total_delay(&self) -> Delay { + self.expected_total_delay + } + pub fn prepare_for_sending(self) -> (Delay, Vec) { // SURB_FIRST_HOP || SURB_ACK let surb_bytes: Vec<_> = self diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index a67849b128..7a85486e4f 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys nymsphinx-types = { path = "../types" } # we need to be able to refer to some types defined inside sphinx crate serde = "1.0" # implementing serialization/deserialization for some types, like `Recipient` +thiserror = "1.0.37" [dev-dependencies] rand = "0.7" diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index 819f272efd..fd7235a9b7 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -10,6 +10,7 @@ use nymsphinx_types::Destination; use serde::de::{Error as SerdeError, Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{self, Formatter}; +use thiserror::Error; // Not entirely sure whether this is the correct place for those, but let's see how it's going // to work out @@ -19,46 +20,25 @@ const CLIENT_ENCRYPTION_KEY_SIZE: usize = encryption::PUBLIC_KEY_SIZE; pub type ClientIdentity = identity::PublicKey; const CLIENT_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH; -#[derive(Debug)] +pub type RecipientBytes = [u8; Recipient::LEN]; + +#[derive(Debug, Error)] pub enum RecipientFormattingError { - MalformedRecipientError, + #[error("recipient is malformed - {reason} ")] + MalformedRecipientError { reason: String }, + + #[error("recipient's identity key is malformed: {0}")] MalformedIdentityError(identity::Ed25519RecoveryError), - MalformedEncryptionKeyError(encryption::KeyRecoveryError), + + #[error("recipient's encryption key is malformed: {0}")] + MalformedEncryptionKeyError(#[from] encryption::KeyRecoveryError), + + #[error("recipient gateway's identity key is malformed: {0}")] MalformedGatewayError(identity::Ed25519RecoveryError), } -impl fmt::Display for RecipientFormattingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { - match self { - RecipientFormattingError::MalformedRecipientError => { - write!(f, "recipient is malformed") - } - RecipientFormattingError::MalformedIdentityError(id_err) => { - write!(f, "recipient's identity key is malformed: {}", id_err) - } - RecipientFormattingError::MalformedEncryptionKeyError(enc_err) => { - write!(f, "recipient's encryption key is malformed: {}", enc_err) - } - RecipientFormattingError::MalformedGatewayError(id_err) => write!( - f, - "recipient gateway's identity key is malformed: {}", - id_err - ), - } - } -} - -// since we have Debug and Display might as well slap Error on top of it too -impl std::error::Error for RecipientFormattingError {} - -impl From for RecipientFormattingError { - fn from(err: encryption::KeyRecoveryError) -> Self { - RecipientFormattingError::MalformedEncryptionKeyError(err) - } -} - // TODO: this should a different home... somewhere, but where? -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Recipient { client_identity: ClientIdentity, client_encryption_key: ClientEncryptionKey, @@ -154,7 +134,7 @@ impl Recipient { &self.gateway } - pub fn to_bytes(self) -> [u8; Self::LEN] { + pub fn to_bytes(self) -> RecipientBytes { let mut out = [0u8; Self::LEN]; out[..CLIENT_IDENTITY_SIZE].copy_from_slice(&self.client_identity.to_bytes()); out[CLIENT_IDENTITY_SIZE..CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE] @@ -165,7 +145,7 @@ impl Recipient { out } - pub fn try_from_bytes(bytes: [u8; Self::LEN]) -> Result { + pub fn try_from_bytes(bytes: RecipientBytes) -> Result { let identity_bytes = &bytes[..CLIENT_IDENTITY_SIZE]; let enc_key_bytes = &bytes[CLIENT_IDENTITY_SIZE..CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE]; @@ -196,14 +176,20 @@ impl Recipient { let string_address = full_address.into(); let split: Vec<_> = string_address.split('@').collect(); if split.len() != 2 { - return Err(RecipientFormattingError::MalformedRecipientError); + return Err(RecipientFormattingError::MalformedRecipientError { + reason: "the string address does not contain exactly a single '@' character" + .to_string(), + }); } let client_half = split[0]; let gateway_half = split[1]; let split_client: Vec<_> = client_half.split('.').collect(); if split_client.len() != 2 { - return Err(RecipientFormattingError::MalformedRecipientError); + return Err(RecipientFormattingError::MalformedRecipientError { + reason: "the string address does not contain exactly a single '.' character" + .to_string(), + }); } let client_identity = match ClientIdentity::from_base58_string(split_client[0]) { diff --git a/common/nymsphinx/addressing/src/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs index a01f7202bd..cf43625c5a 100644 --- a/common/nymsphinx/addressing/src/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -5,6 +5,7 @@ use crypto::asymmetric::identity; use nymsphinx_types::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; use std::convert::{TryFrom, TryInto}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use thiserror::Error; // Not entirely sure whether this is the correct place for those, but let's see how it's going // to work out @@ -20,10 +21,23 @@ pub const NODE_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH; /// In this case it's an ipv6 socket address (with version prefix) pub const MAX_NODE_ADDRESS_UNPADDED_LEN: usize = 19; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum NymNodeRoutingAddressError { - InsufficientNumberOfBytesAvailableError, - InvalidIpVersion, + #[error("Attempted to deserialize NymNodeRoutingAddress without providing any bytes")] + NoBytesProvided, + + #[error("Provided insufficient amount of few bytes to deserialize a valid NymNodeRoutingAddress for IPv{protocol_version} variant. Received {received} and required {required}")] + TooFewBytesProvided { + protocol_version: u8, + received: usize, + required: usize, + }, + + #[error("{received} is not a valid version of the Internet Protocol (IP). Expected either '4' or '6'")] + InvalidIpVersion { received: u8 }, + + #[error("Could not serialize NymNodeRoutingAddress into NodeAddressBytes as that requires using at least {required} bytes and only {NODE_ADDRESS_LENGTH} are available")] + TooSmallBytesRepresentation { required: usize }, } /// Current representation of Node routing information used in Nym system. @@ -84,28 +98,39 @@ impl NymNodeRoutingAddress { /// Tries to recover `Self` from a bytes slice. /// Does not care if it's zero-padded or not. pub fn try_from_bytes(b: &[u8]) -> Result { - // the bare minimum to represent `Self` is 7 bytes (for the shorter V4 version) - if b.len() < 7 { - return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); + if b.is_empty() { + return Err(NymNodeRoutingAddressError::NoBytesProvided); } let ip_version = b[0]; - let port: u16 = u16::from_be_bytes([b[1], b[2]]); let ip = match ip_version { - 4 => IpAddr::V4(Ipv4Addr::new(b[3], b[4], b[5], b[6])), + 4 => { + if b.len() < 7 { + return Err(NymNodeRoutingAddressError::TooFewBytesProvided { + protocol_version: 4, + received: b.len(), + required: 7, + }); + } + IpAddr::V4(Ipv4Addr::new(b[3], b[4], b[5], b[6])) + } 6 => { if b.len() < 19 { - return Err( - NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError, - ); + return Err(NymNodeRoutingAddressError::TooFewBytesProvided { + protocol_version: 6, + received: b.len(), + required: 19, + }); } let mut address_octets = [0u8; 16]; address_octets.copy_from_slice(&b[3..19]); IpAddr::V6(Ipv6Addr::from(address_octets)) } - _ => return Err(NymNodeRoutingAddressError::InvalidIpVersion), + v => return Err(NymNodeRoutingAddressError::InvalidIpVersion { received: v }), }; + let port: u16 = u16::from_be_bytes([b[1], b[2]]); + Ok(Self(SocketAddr::new(ip, port))) } @@ -148,7 +173,9 @@ impl TryInto for NymNodeRoutingAddress { fn try_into(self) -> Result { // first check if we have enough bytes to represent `self`: if self.bytes_min_len() > NODE_ADDRESS_LENGTH { - return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); + return Err(NymNodeRoutingAddressError::TooSmallBytesRepresentation { + required: self.bytes_min_len(), + }); } let padded_address = self.as_zero_padded_bytes(NODE_ADDRESS_LENGTH); diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index 246f6de509..5ddb069e19 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -7,12 +7,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = {version = "0.7.3", features = ["wasm-bindgen"]} +rand = { version = "0.7.3", features = ["wasm-bindgen"] } bs58 = "0.4" serde = "1.0" +thiserror = "1" crypto = { path = "../../crypto" } nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } nymsphinx-types = { path = "../types" } topology = { path = "../../topology" } + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] +version = "0.2.83" \ No newline at end of file diff --git a/common/nymsphinx/anonymous-replies/src/encryption_key.rs b/common/nymsphinx/anonymous-replies/src/encryption_key.rs index b52b8cea60..7a6689f4a7 100644 --- a/common/nymsphinx/anonymous-replies/src/encryption_key.rs +++ b/common/nymsphinx/anonymous-replies/src/encryption_key.rs @@ -16,7 +16,7 @@ pub type EncryptionKeyDigest = pub type SurbEncryptionKeySize = ::KeySize; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct SurbEncryptionKey(CipherKey); #[derive(Debug)] diff --git a/common/nymsphinx/anonymous-replies/src/lib.rs b/common/nymsphinx/anonymous-replies/src/lib.rs index ce93c4099b..3e48d0ea1b 100644 --- a/common/nymsphinx/anonymous-replies/src/lib.rs +++ b/common/nymsphinx/anonymous-replies/src/lib.rs @@ -3,6 +3,7 @@ pub mod encryption_key; pub mod reply_surb; +pub mod requests; pub use encryption_key::{SurbEncryptionKey, SurbEncryptionKeySize}; pub use reply_surb::{ReplySurb, ReplySurbError}; diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index b8b8ec04cf..1fb29b2910 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::encryption_key::{SurbEncryptionKey, SurbEncryptionKeyError, SurbEncryptionKeySize}; @@ -14,44 +14,22 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; use std::fmt::{self, Formatter}; use std::time; +use thiserror::Error; use topology::{NymTopology, NymTopologyError}; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ReplySurbError { + #[error("tried to use reply SURB with an unpadded message")] UnpaddedMessageError, - MalformedStringError(bs58::decode::Error), - RecoveryError(SphinxError), - InvalidEncryptionKeyData(SurbEncryptionKeyError), -} -impl fmt::Display for ReplySurbError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { - match self { - ReplySurbError::UnpaddedMessageError => { - write!(f, "tried to use reply SURB with an unpadded message") - } - ReplySurbError::MalformedStringError(decode_err) => { - write!(f, "reply SURB is incorrectly formatted: {}", decode_err) - } - ReplySurbError::RecoveryError(sphinx_err) => { - write!(f, "failed to recover reply SURB from bytes: {}", sphinx_err) - } - ReplySurbError::InvalidEncryptionKeyData(surb_key_err) => write!( - f, - "failed to recover reply SURB encryption key from bytes: {}", - surb_key_err - ), - } - } -} + #[error("reply SURB is incorrectly formatted: {0}")] + MalformedStringError(#[from] bs58::decode::Error), -// since we have Debug and Display might as well slap Error on top of it too -impl std::error::Error for ReplySurbError {} + #[error("failed to recover reply SURB from bytes: {0}")] + RecoveryError(#[from] SphinxError), -impl From for ReplySurbError { - fn from(err: SurbEncryptionKeyError) -> Self { - ReplySurbError::InvalidEncryptionKeyData(err) - } + #[error("failed to recover reply SURB encryption key from bytes: {0}")] + InvalidEncryptionKeyData(#[from] SurbEncryptionKeyError), } #[derive(Debug)] @@ -157,6 +135,8 @@ impl ReplySurb { } pub fn from_bytes(bytes: &[u8]) -> Result { + // TODO: introduce bound checks to guard us against out of bound reads + let encryption_key = SurbEncryptionKey::try_from_bytes(&bytes[..SurbEncryptionKeySize::USIZE])?; @@ -189,21 +169,22 @@ impl ReplySurb { // - surb-ack // - key digest // - encrypted plaintext with padding to constant length - pub fn apply_surb( + pub fn apply_surb>( self, - message: &[u8], + message: M, packet_size: Option, ) -> Result<(SphinxPacket, NymNodeRoutingAddress), ReplySurbError> { let packet_size = packet_size.unwrap_or_default(); - if message.len() != packet_size.plaintext_size() { + let message_bytes = message.as_ref(); + if message_bytes.len() != packet_size.plaintext_size() { return Err(ReplySurbError::UnpaddedMessageError); } // this can realistically only fail on too long messages and we just checked for that let (packet, first_hop) = self .surb - .use_surb(message, packet_size.payload_size()) + .use_surb(message_bytes, packet_size.payload_size()) .expect("this error indicates inconsistent message length checking - it shouldn't have happened!"); let first_hop_address = NymNodeRoutingAddress::try_from(first_hop).unwrap(); diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests.rs new file mode 100644 index 0000000000..686f65aa79 --- /dev/null +++ b/common/nymsphinx/anonymous-replies/src/requests.rs @@ -0,0 +1,476 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ReplySurb, ReplySurbError}; +use nymsphinx_addressing::clients::{Recipient, RecipientFormattingError}; +use rand::{CryptoRng, RngCore}; +use std::fmt::{Display, Formatter}; +use std::mem; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +pub const SENDER_TAG_SIZE: usize = 16; + +#[derive(Debug, Error)] +pub enum InvalidAnonymousSenderTagRepresentation { + #[error("Failed to decode the base58-encoded string - {0}")] + MalformedString(#[from] bs58::decode::Error), + + #[error( + "Decoded AnonymousSenderTag has invalid length. Expected {expected}, but got {received}" + )] + InvalidLength { received: usize, expected: usize }, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub struct AnonymousSenderTag([u8; SENDER_TAG_SIZE]); + +impl From<[u8; SENDER_TAG_SIZE]> for AnonymousSenderTag { + fn from(bytes: [u8; SENDER_TAG_SIZE]) -> Self { + AnonymousSenderTag(bytes) + } +} + +impl Display for AnonymousSenderTag { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base58_string()) + } +} + +impl AnonymousSenderTag { + pub fn new_random(rng: &mut R) -> Self { + let mut bytes = [0u8; SENDER_TAG_SIZE]; + rng.fill_bytes(&mut bytes); + AnonymousSenderTag(bytes) + } + + pub fn to_bytes(&self) -> [u8; SENDER_TAG_SIZE] { + self.0 + } + + pub fn from_bytes(bytes: [u8; SENDER_TAG_SIZE]) -> Self { + AnonymousSenderTag(bytes) + } + + pub fn to_base58_string(self) -> String { + bs58::encode(self.to_bytes()).into_string() + } + + pub fn try_from_base58_string>( + val: I, + ) -> Result { + let bytes = bs58::decode(val).into_vec()?; + if bytes.len() != SENDER_TAG_SIZE { + return Err(InvalidAnonymousSenderTagRepresentation::InvalidLength { + received: bytes.len(), + expected: SENDER_TAG_SIZE, + }); + } + + // the unwrap here is fine as we just asserted the bytes are of exactly SENDER_TAG_SIZE length + let byte_array: [u8; SENDER_TAG_SIZE] = bytes.try_into().unwrap(); + Ok(AnonymousSenderTag::from_bytes(byte_array)) + } +} + +#[derive(Debug, Error)] +pub enum InvalidReplyRequestError { + #[error("Did not provide sufficient number of bytes to deserialize a valid request")] + RequestTooShortToDeserialize, + + #[error("{received} is not a valid content tag for a repliable message")] + InvalidRepliableContentTag { received: u8 }, + + #[error("{received} is not a valid content tag for a reply message")] + InvalidReplyContentTag { received: u8 }, + + #[error("failed to deserialize recipient information - {0}")] + MalformedRecipient(#[from] RecipientFormattingError), + + #[error("failed to deserialize replySURB - {0}")] + MalformedReplySurb(#[from] ReplySurbError), +} + +#[derive(Debug)] +pub struct RepliableMessage { + pub sender_tag: AnonymousSenderTag, + pub content: RepliableMessageContent, +} + +impl Display for RepliableMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.content { + RepliableMessageContent::Data { + message, + reply_surbs, + } => write!( + f, + "repliable {:.2} kiB data message with {} reply surbs attached from {}", + message.len() as f64 / 1024.0, + reply_surbs.len(), + self.sender_tag, + ), + RepliableMessageContent::AdditionalSurbs { reply_surbs } => write!( + f, + "repliable additional surbs message ({} reply surbs attached) from {}", + reply_surbs.len(), + self.sender_tag, + ), + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + write!( + f, + "repliable heartbeat message ({} reply surbs attached) from {}", + additional_reply_surbs.len(), + self.sender_tag, + ) + } + } + } +} + +impl RepliableMessage { + pub fn new_data( + data: Vec, + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + ) -> Self { + RepliableMessage { + sender_tag, + content: RepliableMessageContent::Data { + message: data, + reply_surbs, + }, + } + } + + pub fn new_additional_surbs( + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + ) -> Self { + RepliableMessage { + sender_tag, + content: RepliableMessageContent::AdditionalSurbs { reply_surbs }, + } + } + + pub fn into_bytes(self) -> Vec { + let content_tag = self.content.tag(); + + self.sender_tag + .to_bytes() + .into_iter() + .chain(std::iter::once(content_tag as u8)) + .chain(self.content.into_bytes()) + .collect() + } + + pub fn try_from_bytes( + bytes: &[u8], + num_mix_hops: u8, + ) -> Result { + if bytes.len() < SENDER_TAG_SIZE + 1 { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let sender_tag = + AnonymousSenderTag::from_bytes(bytes[..SENDER_TAG_SIZE].try_into().unwrap()); + let content_tag = RepliableMessageContentTag::try_from(bytes[SENDER_TAG_SIZE])?; + + let content = RepliableMessageContent::try_from_bytes( + &bytes[SENDER_TAG_SIZE + 1..], + num_mix_hops, + content_tag, + )?; + + Ok(RepliableMessage { + sender_tag, + content, + }) + } +} + +// this recovery code is shared between all variants containing reply surbs +fn recover_reply_surbs( + bytes: &[u8], + num_mix_hops: u8, +) -> Result<(Vec, usize), InvalidReplyRequestError> { + let mut consumed = mem::size_of::(); + if bytes.len() < consumed { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let surb_size = ReplySurb::serialized_len(num_mix_hops); + if bytes[consumed..].len() < num_surbs as usize * surb_size { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let mut reply_surbs = Vec::with_capacity(num_surbs as usize); + for _ in 0..num_surbs as usize { + let surb_bytes = &bytes[consumed..consumed + surb_size]; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + reply_surbs.push(reply_surb); + + consumed += surb_size; + } + + Ok((reply_surbs, consumed)) +} + +#[repr(u8)] +enum RepliableMessageContentTag { + Data = 0, + AdditionalSurbs = 1, + Heartbeat = 2, +} + +impl TryFrom for RepliableMessageContentTag { + type Error = InvalidReplyRequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (RepliableMessageContentTag::Data as u8) => Ok(Self::Data), + _ if value == (RepliableMessageContentTag::AdditionalSurbs as u8) => { + Ok(Self::AdditionalSurbs) + } + _ if value == (RepliableMessageContentTag::Heartbeat as u8) => Ok(Self::Heartbeat), + val => Err(InvalidReplyRequestError::InvalidRepliableContentTag { received: val }), + } + } +} + +// sent by original sender that initialised the communication that knows address of the remote +#[derive(Debug)] +pub enum RepliableMessageContent { + Data { + message: Vec, + reply_surbs: Vec, + }, + AdditionalSurbs { + reply_surbs: Vec, + }, + Heartbeat { + additional_reply_surbs: Vec, + }, +} + +impl RepliableMessageContent { + pub fn into_bytes(self) -> Vec { + match self { + RepliableMessageContent::Data { + message, + reply_surbs, + } => { + let num_surbs = reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) + .chain(message.into_iter()) + .collect() + } + RepliableMessageContent::AdditionalSurbs { reply_surbs } => { + let num_surbs = reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) + .collect() + } + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + let num_surbs = additional_reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain( + additional_reply_surbs + .into_iter() + .flat_map(|s| s.to_bytes()), + ) + .collect() + } + } + } + + fn try_from_bytes( + bytes: &[u8], + num_mix_hops: u8, + tag: RepliableMessageContentTag, + ) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let (reply_surbs, n) = recover_reply_surbs(bytes, num_mix_hops)?; + + match tag { + RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data { + message: bytes[n..].to_vec(), + reply_surbs, + }), + RepliableMessageContentTag::AdditionalSurbs => { + Ok(RepliableMessageContent::AdditionalSurbs { reply_surbs }) + } + RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat { + additional_reply_surbs: reply_surbs, + }), + } + } + + fn tag(&self) -> RepliableMessageContentTag { + match self { + RepliableMessageContent::Data { .. } => RepliableMessageContentTag::Data, + RepliableMessageContent::AdditionalSurbs { .. } => { + RepliableMessageContentTag::AdditionalSurbs + } + RepliableMessageContent::Heartbeat { .. } => RepliableMessageContentTag::Heartbeat, + } + } +} + +// sent by the remote party who does **NOT** know the original sender's identity +#[derive(Debug)] +pub struct ReplyMessage { + pub content: ReplyMessageContent, +} + +impl Display for ReplyMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.content { + ReplyMessageContent::Data { message } => write!( + f, + "{:.2} kiB reply data message", + message.len() as f64 / 1024.0 + ), + ReplyMessageContent::SurbRequest { recipient, amount } => write!( + f, + "request for {amount} additional reply SURBs from {recipient}", + ), + } + } +} + +impl ReplyMessage { + pub fn new_data_message(message: Vec) -> Self { + ReplyMessage { + content: ReplyMessageContent::Data { message }, + } + } + + pub fn new_surb_request_message(recipient: Recipient, amount: u32) -> Self { + ReplyMessage { + content: ReplyMessageContent::SurbRequest { + recipient: Box::new(recipient), + amount, + }, + } + } + + pub fn into_bytes(self) -> Vec { + let content_tag = self.content.tag(); + + std::iter::once(content_tag as u8) + .chain(self.content.into_bytes()) + .collect() + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let tag = ReplyMessageContentTag::try_from(bytes[0])?; + let content = ReplyMessageContent::try_from_bytes(&bytes[1..], tag)?; + + Ok(ReplyMessage { content }) + } +} + +#[repr(u8)] +enum ReplyMessageContentTag { + Data = 0, + SurbRequest = 1, +} + +impl TryFrom for ReplyMessageContentTag { + type Error = InvalidReplyRequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (ReplyMessageContentTag::Data as u8) => Ok(Self::Data), + _ if value == (ReplyMessageContentTag::SurbRequest as u8) => Ok(Self::SurbRequest), + val => Err(InvalidReplyRequestError::InvalidReplyContentTag { received: val }), + } + } +} + +#[derive(Debug)] +pub enum ReplyMessageContent { + // TODO: later allow to request surbs whilst sending data + Data { + message: Vec, + }, + SurbRequest { + recipient: Box, + amount: u32, + }, +} + +impl ReplyMessageContent { + pub fn into_bytes(self) -> Vec { + match self { + ReplyMessageContent::Data { message } => message, + ReplyMessageContent::SurbRequest { recipient, amount } => recipient + .to_bytes() + .into_iter() + .chain(amount.to_be_bytes().into_iter()) + .collect(), + } + } + + fn try_from_bytes( + bytes: &[u8], + tag: ReplyMessageContentTag, + ) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + match tag { + ReplyMessageContentTag::Data => Ok(ReplyMessageContent::Data { + message: bytes.to_vec(), + }), + ReplyMessageContentTag::SurbRequest => { + if bytes.len() != Recipient::LEN + std::mem::size_of::() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&bytes[..Recipient::LEN]); + + Ok(ReplyMessageContent::SurbRequest { + recipient: Box::new(Recipient::try_from_bytes(recipient_bytes)?), + amount: u32::from_be_bytes([ + bytes[Recipient::LEN], + bytes[Recipient::LEN + 1], + bytes[Recipient::LEN + 2], + bytes[Recipient::LEN + 3], + ]), + }) + } + } + } + + fn tag(&self) -> ReplyMessageContentTag { + match self { + ReplyMessageContent::Data { .. } => ReplyMessageContentTag::Data, + ReplyMessageContent::SurbRequest { .. } => ReplyMessageContentTag::SurbRequest, + } + } +} diff --git a/common/nymsphinx/chunking/Cargo.toml b/common/nymsphinx/chunking/Cargo.toml index 5f47ab5358..3d6bacab61 100644 --- a/common/nymsphinx/chunking/Cargo.toml +++ b/common/nymsphinx/chunking/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] log = "0.4.8" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 58c1baa78a..3f102282dd 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -1,12 +1,10 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::set::generate_set_id; use crate::ChunkingError; use nymsphinx_params::{SerializedFragmentIdentifier, FRAG_ID_LEN}; -use rand::Rng; use std::convert::TryInto; -use std::fmt::{self, Formatter}; +use std::fmt::{self, Debug, Formatter}; // Personal reflection: In hindsight I've spent too much time on relatively too little // gain here, as even though I might have saved couple of bytes per packet, the gain @@ -60,7 +58,7 @@ pub const COVER_FRAG_ID: FragmentIdentifier = FragmentIdentifier { /// and u8 position of the `Fragment` in the set. // TODO: this should really be redesigned, especially how cover and reply messages are really // "abusing" this. They should work with it natively instead. -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct FragmentIdentifier { set_id: i32, fragment_position: u8, @@ -77,20 +75,6 @@ impl fmt::Display for FragmentIdentifier { } impl FragmentIdentifier { - // I really dislike how 'hacky' this function seems - // refer to: https://github.com/nymtech/nym/issues/294 for further discussion - pub fn new_reply(rng: &mut R) -> Self { - FragmentIdentifier { - set_id: generate_set_id(rng), - fragment_position: 0, - } - } - - // and this one - pub fn is_reply(self) -> bool { - self.set_id > 0 && self.fragment_position == 0 - } - pub fn to_bytes(self) -> SerializedFragmentIdentifier { debug_assert_eq!(FRAG_ID_LEN, 5); @@ -110,7 +94,7 @@ impl FragmentIdentifier { let set_id = i32::from_be_bytes([b[0], b[1], b[2], b[3]]); // set_id == 0 is valid for COVER_FRAG_ID and replies if set_id < 0 { - return Err(ChunkingError::MalformedFragmentIdentifier); + return Err(ChunkingError::MalformedFragmentIdentifier { received: set_id }); } Ok(FragmentIdentifier { @@ -124,12 +108,22 @@ impl FragmentIdentifier { /// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. /// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional /// header used to reconstruct the message after being received. -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone)] pub struct Fragment { header: FragmentHeader, payload: Vec, } +// manual implementation to hide detailed payload that we don't care about +impl Debug for Fragment { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Fragment") + .field("header", &self.header) + .field("payload length", &self.payload.len()) + .finish() + } +} + impl Fragment { /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata @@ -153,24 +147,42 @@ impl Fragment { // check for whether payload has expected length, which depend on whether fragment is linked // and if it's the only one or the last one in the set (then lower bound is removed) + let max_linked_len = linked_fragment_payload_max_len(max_plaintext_size); + let max_unlinked_len = unlinked_fragment_payload_max_len(max_plaintext_size); + if previous_fragments_set_id.is_some() { if total_fragments > 1 { - if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_linked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_linked_len, + }); } - } else if payload.len() > linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + } else if payload.len() > max_linked_len { + return Err(ChunkingError::TooLongPayloadLengthError { + received: payload.len(), + expected_at_most: max_linked_len, + }); } } else if next_fragments_set_id.is_some() { - if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_linked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_linked_len, + }); } } else if total_fragments != current_fragment { - if payload.len() != unlinked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_unlinked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_unlinked_len, + }); } - } else if payload.len() > unlinked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + } else if payload.len() > max_unlinked_len { + return Err(ChunkingError::TooLongPayloadLengthError { + received: payload.len(), + expected_at_most: max_unlinked_len, + }); } Ok(Fragment { @@ -347,7 +359,10 @@ impl FragmentHeader { fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { // header needs to be at least 7 bytes long if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); + return Err(ChunkingError::TooShortFragmentHeader { + received: b.len(), + expected: UNLINKED_FRAGMENTED_HEADER_LEN, + }); } let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); // sanity check for the fragmentation flag @@ -370,7 +385,10 @@ impl FragmentHeader { let read_bytes = if b[6] != 0 { // there's linking ID supposedly attached, make sure we have enough bytes to parse if b.len() < LINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); + return Err(ChunkingError::TooShortFragmentHeader { + received: b.len(), + expected: LINKED_FRAGMENTED_HEADER_LEN, + }); } let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 62a246955b..60316edb15 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -3,6 +3,7 @@ use crate::fragment::{linked_fragment_payload_max_len, unlinked_fragment_payload_max_len}; pub use set::split_into_sets; +use thiserror::Error; // Future consideration: currently in a lot of places, the payloads have randomised content // which is not a perfect testing strategy as it might not detect some edge cases I never would @@ -45,18 +46,26 @@ pub mod set; /// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` /// are further explained in the respective files. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Error)] pub enum ChunkingError { - InvalidPayloadLengthError, - TooBigMessageToSplit, + #[error("Received payload is too long. Got {received}, expected {expected}")] + InvalidPayloadLengthError { received: usize, expected: usize }, + + #[error("Received payload is too long. Got {received}, expected at most {expected_at_most}")] + TooLongPayloadLengthError { + received: usize, + expected_at_most: usize, + }, + + // this should really be split into multiple variants to provide better error information + #[error("Provided header was malformed or contained self-contradicting fields")] MalformedHeaderError, - NoValidProvidersError, - NoValidRoutesAvailableError, - InvalidTopologyError, - TooShortFragmentData, - MalformedFragmentData, - UnexpectedFragmentCount, - MalformedFragmentIdentifier, + + #[error("Received too few bytes to deserialize fragment header. Got {received}, expected {expected}")] + TooShortFragmentHeader { received: usize, expected: usize }, + + #[error("Received fragment identifier ({received}) is not a valid value!")] + MalformedFragmentIdentifier { received: i32 }, } /// Returns number of fragments the message will be split to as well as number of available @@ -94,7 +103,7 @@ pub fn number_of_required_fragments( // we know for sure that all fragments in all but last set are definitely full // (last one has single 'linked' fragment) - let without_last = (n - 1) * (u8::max_value() as usize); + let without_last = (n - 1) * (u8::MAX as usize); let linked_fragments_without_last = (2 * n - 2) - 1; let unlinked_fragments_without_last = without_last - linked_fragments_without_last; diff --git a/common/nymsphinx/cover/Cargo.toml b/common/nymsphinx/cover/Cargo.toml index 6fd629ade7..064ebf1298 100644 --- a/common/nymsphinx/cover/Cargo.toml +++ b/common/nymsphinx/cover/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" crypto = { path = "../../crypto" } nymsphinx-acknowledgements = { path = "../acknowledgements" } diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index aaaff5a9b5..2ce9454d5c 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -6,7 +6,7 @@ use crypto::symmetric::stream_cipher; use nymsphinx_acknowledgements::surb_ack::SurbAck; use nymsphinx_acknowledgements::AckKey; use nymsphinx_addressing::clients::Recipient; -use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_chunking::fragment::COVER_FRAG_ID; use nymsphinx_forwarding::packet::MixPacket; use nymsphinx_params::packet_sizes::PacketSize; @@ -18,35 +18,18 @@ use nymsphinx_types::{delays, Error as SphinxError}; use rand::{CryptoRng, RngCore}; use std::convert::TryFrom; use std::time; +use thiserror::Error; use topology::{NymTopology, NymTopologyError}; pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum CoverMessageError { - NoValidProvidersError, - InvalidTopologyError, - SphinxError(SphinxError), - InvalidFirstMixAddress, -} + #[error("Could not construct cover message due to invalid topology - {0}")] + InvalidTopologyError(#[from] NymTopologyError), -impl From for CoverMessageError { - fn from(err: SphinxError) -> Self { - CoverMessageError::SphinxError(err) - } -} - -impl From for CoverMessageError { - fn from(_: NymNodeRoutingAddressError) -> Self { - use CoverMessageError::*; - InvalidFirstMixAddress - } -} - -impl From for CoverMessageError { - fn from(_: NymTopologyError) -> Self { - CoverMessageError::InvalidTopologyError - } + #[error("Could not construct a valid sphinx packet - {0}")] + SphinxError(#[from] SphinxError), } pub fn generate_loop_cover_surb_ack( diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 53bd77c3b7..5794a88544 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -178,7 +178,7 @@ mod packet_encoding { ]; SphinxPacketBuilder::new() .with_payload_size(size.payload_size()) - .build_packet(b"foomp".to_vec(), &route, &destination, &delays) + .build_packet(b"foomp", &route, &destination, &delays) .unwrap() } diff --git a/common/nymsphinx/src/lib.rs b/common/nymsphinx/src/lib.rs index e3e7c51fac..bbc69c2682 100644 --- a/common/nymsphinx/src/lib.rs +++ b/common/nymsphinx/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod message; pub mod preparer; pub mod receiver; pub mod utils; @@ -16,3 +17,6 @@ pub use nymsphinx_forwarding as forwarding; pub use nymsphinx_framing as framing; pub use nymsphinx_params as params; pub use nymsphinx_types::*; + +// TEMP UNTIL FURTHER REFACTORING +pub use preparer::payload::NymsphinxPayloadBuilder; diff --git a/common/nymsphinx/src/message.rs b/common/nymsphinx/src/message.rs new file mode 100644 index 0000000000..f9b32daa35 --- /dev/null +++ b/common/nymsphinx/src/message.rs @@ -0,0 +1,238 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::chunking; +use crypto::asymmetric::encryption; +use crypto::Digest; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; +use nymsphinx_anonymous_replies::requests::{ + InvalidReplyRequestError, RepliableMessage, RepliableMessageContent, ReplyMessage, + ReplyMessageContent, +}; +use nymsphinx_chunking::fragment::Fragment; +use nymsphinx_params::{PacketSize, ReplySurbKeyDigestAlgorithm}; +use rand::Rng; +use std::fmt::{Display, Formatter}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymMessageError { + #[error("{received} is not a valid type tag for a NymMessage")] + InvalidMessageType { received: u8 }, + + #[error(transparent)] + InvalidReplyRequest(#[from] InvalidReplyRequestError), + + #[error("The received message seems to have incorrect zero padding (no '1' byte found)")] + InvalidMessagePadding, + + #[error("Received empty message for deserialization")] + EmptyMessage, +} + +#[repr(u8)] +enum NymMessageType { + Plain = 0, + Repliable = 1, + Reply = 2, +} + +impl TryFrom for NymMessageType { + type Error = NymMessageError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (NymMessageType::Plain as u8) => Ok(Self::Plain), + _ if value == (NymMessageType::Repliable as u8) => Ok(Self::Repliable), + _ if value == (NymMessageType::Reply as u8) => Ok(Self::Reply), + val => Err(NymMessageError::InvalidMessageType { received: val }), + } + } +} + +pub type PlainMessage = Vec; + +#[derive(Debug)] +pub enum NymMessage { + Plain(PlainMessage), + Repliable(RepliableMessage), + Reply(ReplyMessage), +} + +impl Display for NymMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + NymMessage::Plain(plain_message) => write!( + f, + "plain {:.2} kiB message", + plain_message.len() as f64 / 1024.0 + ), + NymMessage::Repliable(repliable_message) => repliable_message.fmt(f), + NymMessage::Reply(reply_message) => reply_message.fmt(f), + } + } +} + +impl NymMessage { + pub fn new_additional_surbs_request(recipient: Recipient, amount: u32) -> Self { + NymMessage::Reply(ReplyMessage { + content: ReplyMessageContent::SurbRequest { + recipient: Box::new(recipient), + amount, + }, + }) + } + + pub fn new_plain(msg: Vec) -> Self { + NymMessage::Plain(msg) + } + + pub fn new_repliable(msg: RepliableMessage) -> Self { + NymMessage::Repliable(msg) + } + + pub fn new_reply(msg: ReplyMessage) -> Self { + NymMessage::Reply(msg) + } + + pub fn is_reply_surb_request(&self) -> bool { + match self { + NymMessage::Reply(reply_msg) => { + matches!(reply_msg.content, ReplyMessageContent::SurbRequest { .. }) + } + _ => false, + } + } + + pub fn into_inner_data(self) -> Vec { + match self { + NymMessage::Plain(data) => data, + NymMessage::Repliable(repliable) => match repliable.content { + RepliableMessageContent::Data { message, .. } => message, + _ => Vec::new(), + }, + NymMessage::Reply(reply) => match reply.content { + ReplyMessageContent::Data { message } => message, + _ => Vec::new(), + }, + } + } + + fn typ(&self) -> NymMessageType { + match self { + NymMessage::Plain(_) => NymMessageType::Plain, + NymMessage::Repliable(_) => NymMessageType::Repliable, + NymMessage::Reply(_) => NymMessageType::Reply, + } + } + + fn inner_bytes(self) -> Vec { + match self { + NymMessage::Plain(msg) => msg, + NymMessage::Repliable(msg) => msg.into_bytes(), + NymMessage::Reply(msg) => msg.into_bytes(), + } + } + + // the message is in the format of: + // typ || msg + fn into_bytes(self) -> Vec { + let typ = self.typ(); + + std::iter::once(typ as u8) + .chain(self.inner_bytes()) + .collect() + } + + fn try_from_bytes(bytes: &[u8], num_mix_hops: u8) -> Result { + if bytes.is_empty() { + return Err(NymMessageError::EmptyMessage); + } + + let typ_tag = NymMessageType::try_from(bytes[0])?; + match typ_tag { + NymMessageType::Plain => Ok(NymMessage::Plain(bytes[1..].to_vec())), + NymMessageType::Repliable => Ok(NymMessage::Repliable( + RepliableMessage::try_from_bytes(&bytes[1..], num_mix_hops)?, + )), + NymMessageType::Reply => Ok(NymMessage::Reply(ReplyMessage::try_from_bytes( + &bytes[1..], + )?)), + } + } + + /// Length of plaintext (from the sphinx point of view) data that is available per sphinx + /// packet. + pub fn available_plaintext_per_packet(&self, packet_size: PacketSize) -> usize { + let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); + + let variant_overhead = match self { + // each plain or repliable packet attaches an ephemeral public key so that the recipient + // could perform diffie-hellman with its own keys followed by a kdf to re-derive + // the packet encryption key + NymMessage::Plain(_) | NymMessage::Repliable(_) => encryption::PUBLIC_KEY_SIZE, + // each reply attaches the digest of the encryption key so that the recipient could + // lookup correct key for decryption, + NymMessage::Reply(_) => ReplySurbKeyDigestAlgorithm::output_size(), + }; + + packet_size.plaintext_size() - ack_overhead - variant_overhead + } + + /// Pads the message so that after it gets chunked, it will occupy exactly N sphinx packets. + /// Produces new_message = message || 1 || 0000.... + pub fn pad_to_full_packet_lengths(self, plaintext_per_packet: usize) -> PaddedMessage { + let bytes = self.into_bytes(); + + // 1 is added as there will always have to be at least a single byte of padding (1) added + // to be able to later distinguish the actual padding from the underlying message + let (_, space_left) = + chunking::number_of_required_fragments(bytes.len() + 1, plaintext_per_packet); + + bytes + .into_iter() + .chain(std::iter::once(1u8)) + .chain(std::iter::repeat(0u8).take(space_left)) + .collect::>() + .into() + } +} + +pub struct PaddedMessage(Vec); + +impl PaddedMessage { + pub fn new_reconstructed(bytes: Vec) -> Self { + PaddedMessage(bytes) + } + + /// Splits the padded message into [`Fragment`] that when serialized are going to become + /// sphinx packet payloads. + pub fn split_into_fragments( + self, + rng: &mut R, + plaintext_per_packet: usize, + ) -> Vec { + chunking::split_into_sets(rng, &self.0, plaintext_per_packet) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } + + // reverse of NymMessage::pad_to_full_packet_lengths + pub fn remove_padding(self, num_mix_hops: u8) -> Result { + // we are looking for first occurrence of 1 in the tail and we get its index + if let Some(padding_end) = self.0.iter().rposition(|b| *b == 1) { + // and now we only take bytes until that point (but not including it) + NymMessage::try_from_bytes(&self.0[..padding_end], num_mix_hops) + } else { + Err(NymMessageError::InvalidMessagePadding) + } + } +} + +impl From> for PaddedMessage { + fn from(bytes: Vec) -> Self { + PaddedMessage(bytes) + } +} diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index d91a7c85a5..19f68e30bc 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -1,24 +1,17 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::chunking; -use crypto::asymmetric::encryption; -use crypto::shared_key::new_ephemeral_shared_key; -use crypto::symmetric::stream_cipher; -use crypto::Digest; +use crate::message::NymMessage; +use crate::NymsphinxPayloadBuilder; use nymsphinx_acknowledgements::surb_ack::SurbAck; use nymsphinx_acknowledgements::AckKey; use nymsphinx_addressing::clients::Recipient; -use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; -use nymsphinx_anonymous_replies::encryption_key::SurbEncryptionKey; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_anonymous_replies::reply_surb::ReplySurb; use nymsphinx_chunking::fragment::{Fragment, FragmentIdentifier}; use nymsphinx_forwarding::packet::MixPacket; use nymsphinx_params::packet_sizes::PacketSize; -use nymsphinx_params::{ - PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, - ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS, -}; +use nymsphinx_params::DEFAULT_NUM_MIX_HOPS; use nymsphinx_types::builder::SphinxPacketBuilder; use nymsphinx_types::{delays, Delay}; use rand::{CryptoRng, Rng}; @@ -26,6 +19,8 @@ use std::convert::TryFrom; use std::time::Duration; use topology::{NymTopology, NymTopologyError}; +pub(crate) mod payload; + /// Represents fully packed and prepared [`Fragment`] that can be sent through the mix network. pub struct PreparedFragment { /// Indicates the total expected round-trip time, i.e. delay from the sending of this message @@ -36,27 +31,17 @@ pub struct PreparedFragment { /// address of the node to which the message should be sent, the actual 'chunk' of the message /// going through the mix network and also the 'mode' of the packet, i.e. VPN or Mix. pub mix_packet: MixPacket, -} -#[derive(Debug)] -pub enum PreparationError { - TopologyError(NymTopologyError), - TooLongReplyMessageError, -} - -impl From for PreparationError { - fn from(err: NymTopologyError) -> Self { - PreparationError::TopologyError(err) - } + /// Identifier to uniquely identify a fragment. + pub fragment_identifier: FragmentIdentifier, } /// Prepares the message that is to be sent through the mix network by attaching /// an optional reply-SURB, padding it to appropriate length, encrypting its content, /// and chunking into appropriate size [`Fragment`]s. -// #[cfg_attr(not(target_arch = "wasm32"), derive(Clone))] #[derive(Clone)] #[must_use] -pub struct MessagePreparer { +pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. rng: R, @@ -115,86 +100,70 @@ where self.sender_address = sender_address; } - /// Length of plaintext (from the sphinx point of view) data that is available per sphinx - /// packet. - fn available_plaintext_per_packet(&self) -> usize { - // we need to put first hop's destination alongside the actual ack data - // TODO: a possible optimization way down the line: currently we're always assuming that - // the addresses will have `MAX_NODE_ADDRESS_UNPADDED_LEN`, i.e. be ipv6. In most cases - // they're actually going to be ipv4 hence wasting few bytes every packet. - // To fully utilise all available space, I guess first we'd need to generate routes for ACKs - // and only then perform the chunking with `available_plaintext_size` being called per chunk. - // However this will probably introduce bunch of complexity - // for relatively not a lot of gain, so it shouldn't be done just yet. - let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); - let ephemeral_public_key_overhead = encryption::PUBLIC_KEY_SIZE; - - self.packet_size.plaintext_size() - ack_overhead - ephemeral_public_key_overhead - } - - /// Pads the message so that after it gets chunked, it will occupy exactly N sphinx packets. - /// Produces new_message = message || 1 || 0000.... - fn pad_message(&self, message: Vec) -> Vec { - // 1 is added as there will always have to be at least a single byte of padding (1) added - // to be able to later distinguish the actual padding from the underlying message - let (_, space_left) = chunking::number_of_required_fragments( - message.len() + 1, - self.available_plaintext_per_packet(), - ); - - message - .into_iter() - .chain(std::iter::once(1u8)) - .chain(std::iter::repeat(0u8).take(space_left)) - .collect() - } - - /// Attaches reply-SURB to the message alongside the reply key. - /// Results in: - /// new_message = 0 || message - /// OR - /// new_message = 1 || REPLY_KEY || REPLY_SURB || message - fn optionally_attach_reply_surb( + pub fn generate_reply_surbs( &mut self, - message: Vec, - should_attach: bool, + amount: usize, topology: &NymTopology, - ) -> Result<(Vec, Option), PreparationError> { - if should_attach { + ) -> Result, NymTopologyError> { + let mut reply_surbs = Vec::with_capacity(amount); + for _ in 0..amount { let reply_surb = ReplySurb::construct( &mut self.rng, &self.sender_address, self.average_packet_delay, topology, )?; - - let reply_key = reply_surb.encryption_key(); - // if there's a reply surb, the message takes form of `1 || REPLY_KEY || REPLY_SURB || MSG` - Ok(( - std::iter::once(true as u8) - .chain(reply_surb.to_bytes().iter().cloned()) - .chain(message.into_iter()) - .collect(), - Some(reply_key.clone()), - )) - } else { - // but if there's no reply surb, the message takes form of `0 || MSG` - Ok(( - std::iter::once(false as u8) - .chain(message.into_iter()) - .collect(), - None, - )) + reply_surbs.push(reply_surb) } + + Ok(reply_surbs) } - /// Splits the message into [`Fragment`] that are going to be put later put into sphinx packets. - fn split_message(&mut self, message: Vec) -> Vec { - let plaintext_per_packet = self.available_plaintext_per_packet(); - chunking::split_into_sets(&mut self.rng, &message, plaintext_per_packet) - .into_iter() - .flat_map(|fragment_set| fragment_set.into_iter()) - .collect() + /// The procedure is as follows: + /// For each fragment: + /// - compute SURB_ACK + /// - generate (x, g^x) + /// - obtain key k from the reply-surb which was computed as follows: + /// k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) ) + /// - compute v_b = AES-128-CTR(k, serialized_fragment) + /// - compute vk_b = H(k) || v_b + /// - compute sphinx_plaintext = SURB_ACK || H(k) || v_b + /// - compute sphinx_packet by applying the reply surb on the sphinx_plaintext + pub fn prepare_reply_chunk_for_sending( + &mut self, + fragment: Fragment, + topology: &NymTopology, + ack_key: &AckKey, + reply_surb: ReplySurb, + ) -> Result { + // this is not going to be accurate by any means. but that's the best estimation we can do + let expected_forward_delay = Delay::new_from_millis( + (self.average_packet_delay.as_millis() * self.num_mix_hops as u128) as u64, + ); + + let fragment_identifier = fragment.fragment_identifier(); + + // create an ack + let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?; + let ack_delay = surb_ack.expected_total_delay(); + + let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack) + .build_reply(reply_surb.encryption_key()); + + // the unwrap here is fine as the failures can only originate from attempting to use invalid payload lenghts + // and we just very carefully constructed a (presumably) valid one + let (sphinx_packet, first_hop_address) = reply_surb + .apply_surb(packet_payload, Some(self.packet_size)) + .unwrap(); + + Ok(PreparedFragment { + // the round-trip delay is the sum of delays of all hops on the forward route as + // well as the total delay of the ack packet. + // we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution + total_delay: expected_forward_delay + ack_delay, + mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()), + fragment_identifier, + }) } /// Tries to convert this [`Fragment`] into a [`SphinxPacket`] that can be sent through the Nym mix-network, @@ -221,46 +190,14 @@ where ack_key: &AckKey, packet_recipient: &Recipient, ) -> Result { + let fragment_identifier = fragment.fragment_identifier(); + // create an ack - let (ack_delay, surb_ack_bytes) = self - .generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)? - .prepare_for_sending(); + let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?; + let ack_delay = surb_ack.expected_total_delay(); - // TODO: - // TODO: - // TODO: - // TODO: - // TODO: ASK @AP AND @DH WHETHER THOSE KEYS CAN/SHOULD ALSO BE REUSED IN VPN MODE!! - // TODO: - // TODO: - // TODO: - // TODO: - - // create keys for 'payload' encryption - let (ephemeral_keypair, shared_key) = - new_ephemeral_shared_key::( - &mut self.rng, - packet_recipient.encryption_key(), - ); - - // serialize fragment and encrypt its content - let mut chunk_data = fragment.into_bytes(); - - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt_in_place::( - &shared_key, - &zero_iv, - &mut chunk_data, - ); - - // combine it together as follows: - // SURB_ACK_FIRST_HOP || SURB_ACK_DATA || EPHEMERAL_KEY || CHUNK_DATA - // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) - let packet_payload: Vec<_> = surb_ack_bytes - .into_iter() - .chain(ephemeral_keypair.public_key().to_bytes().iter().cloned()) - .chain(chunk_data.into_iter()) - .collect(); + let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack) + .build_regular(&mut self.rng, packet_recipient.encryption_key()); // generate pseudorandom route for the packet let route = topology.random_route_to_gateway( @@ -290,6 +227,7 @@ where // note that the last hop of the packet is a gateway that does not do any delays total_delay: delays.iter().take(delays.len() - 1).sum::() + ack_delay, mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()), + fragment_identifier, }) } @@ -310,113 +248,12 @@ where ) } - /// Attaches an optional reply-surb and correct padding to the underlying message - /// and splits it into [`Fragment`] that can be later packed into sphinx packets to be - /// sent through the mix network. - pub fn prepare_and_split_message( - &mut self, - message: Vec, - with_reply_surb: bool, - topology: &NymTopology, - ) -> Result<(Vec, Option), PreparationError> { - let (message, reply_key) = - self.optionally_attach_reply_surb(message, with_reply_surb, topology)?; + pub fn pad_and_split_message(&mut self, message: NymMessage) -> Vec { + let plaintext_per_packet = message.available_plaintext_per_packet(self.packet_size); - let message = self.pad_message(message); - - Ok((self.split_message(message), reply_key)) - } - - // TODO: perhaps the return type could somehow be combined with [`PreparedFragment`] ? - pub async fn prepare_reply_for_use( - &mut self, - message: Vec, - reply_surb: ReplySurb, - topology: &NymTopology, - ack_key: &AckKey, - ) -> Result<(MixPacket, FragmentIdentifier), PreparationError> { - // there's no chunking in reply-surbs so there's a hard limit on message, - // we also need to put the key digest into the message (same size as ephemeral key) - // and need 1 byte to indicate padding length (this is not the case for 'normal' messages - // as there the padding is added for the whole message) - // so before doing any processing, let's see if we have enough space for it all - let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); - if message.len() - > self.packet_size.plaintext_size() - - ack_overhead - - ReplySurbKeyDigestAlgorithm::output_size() - - 1 - { - return Err(PreparationError::TooLongReplyMessageError); - } - - let reply_id = FragmentIdentifier::new_reply(&mut self.rng); - - // create an ack - // even though it won't be used for retransmission, it must be present so that - // gateways could not distinguish reply packets from normal messages due to lack of said acks - // note: the ack delay is irrelevant since we do not know the delay of actual surb - let (_, surb_ack_bytes) = self - .generate_surb_ack(reply_id, topology, ack_key)? - .prepare_for_sending(); - - let zero_pad_len = self.packet_size.plaintext_size() - - message.len() - - ack_overhead - - ReplySurbKeyDigestAlgorithm::output_size() - - 1; - - // create reply message that will reach the recipient: - let mut reply_content: Vec<_> = message - .into_iter() - .chain(std::iter::once(1)) - .chain(std::iter::repeat(0).take(zero_pad_len)) - .collect(); - - // encrypt the reply message - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt_in_place::( - reply_surb.encryption_key().inner(), - &zero_iv, - &mut reply_content, - ); - - // combine it together as follows: - // SURB_ACK_FIRST_HOP || SURB_ACK_DATA || KEY_DIGEST || E (REPLY_MESSAGE || 1 || 0*) - // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) - let packet_payload: Vec<_> = surb_ack_bytes - .into_iter() - .chain(reply_surb.encryption_key().compute_digest().iter().copied()) - .chain(reply_content.into_iter()) - .collect(); - - // finally put it all inside a sphinx packet - // this can only fail if packet payload has incorrect size, but if it does, it means - // there's a bug in the above code - let (packet, first_hop) = reply_surb - .apply_surb(&packet_payload, Some(self.packet_size)) - .unwrap(); - - Ok(( - MixPacket::new(first_hop, packet, Default::default()), - reply_id, - )) - } - - #[allow(dead_code)] - #[cfg(test)] - pub(crate) fn test_fixture() -> MessagePreparer { - let rng = rand::rngs::OsRng; - let dummy_address = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); - - MessagePreparer { - rng, - packet_size: Default::default(), - sender_address: dummy_address, - average_packet_delay: Default::default(), - average_ack_delay: Default::default(), - num_mix_hops: DEFAULT_NUM_MIX_HOPS, - } + message + .pad_to_full_packet_lengths(plaintext_per_packet) + .split_into_fragments(&mut self.rng, plaintext_per_packet) } } diff --git a/common/nymsphinx/src/preparer/payload.rs b/common/nymsphinx/src/preparer/payload.rs new file mode 100644 index 0000000000..94acde03c8 --- /dev/null +++ b/common/nymsphinx/src/preparer/payload.rs @@ -0,0 +1,97 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crypto::aes::cipher::{KeyIvInit, StreamCipher}; +use crypto::asymmetric::encryption; +use crypto::shared_key::new_ephemeral_shared_key; +use crypto::symmetric::stream_cipher; +use crypto::symmetric::stream_cipher::CipherKey; +use nymsphinx_acknowledgements::surb_ack::SurbAck; +use nymsphinx_anonymous_replies::SurbEncryptionKey; +use nymsphinx_chunking::fragment::Fragment; +use nymsphinx_params::{ + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, +}; +use rand::{CryptoRng, RngCore}; + +pub struct NymsphinxPayloadBuilder { + fragment: Fragment, + surb_ack: SurbAck, +} + +impl NymsphinxPayloadBuilder { + pub fn new(fragment: Fragment, surb_ack: SurbAck) -> Self { + NymsphinxPayloadBuilder { fragment, surb_ack } + } + + fn build( + self, + packet_encryption_key: &CipherKey, + variant_data: impl IntoIterator, + ) -> NymsphinxPayload + where + C: StreamCipher + KeyIvInit, + { + let (_, surb_ack_bytes) = self.surb_ack.prepare_for_sending(); + + let mut fragment_data = self.fragment.into_bytes(); + stream_cipher::encrypt_in_place::( + packet_encryption_key, + &stream_cipher::zero_iv::(), + &mut fragment_data, + ); + + // combines all the data as follows: + // SURB_ACK || VARIANT_SPECIFIC_DATA || CHUNK_DATA + // where variant-specific data is as follows: + // for replies it would be the digest of the encryption key used + // for 'regular' messages it would be the public component used in DH later used in the KDF + NymsphinxPayload( + surb_ack_bytes + .into_iter() + .chain(variant_data.into_iter()) + .chain(fragment_data.into_iter()) + .collect(), + ) + } + + pub fn build_reply(self, packet_encryption_key: &SurbEncryptionKey) -> NymsphinxPayload { + let key_digest = packet_encryption_key.compute_digest(); + self.build::( + packet_encryption_key.inner(), + key_digest.into_iter(), + ) + } + + pub fn build_regular( + self, + rng: &mut R, + recipient_encryption_key: &encryption::PublicKey, + ) -> NymsphinxPayload + where + R: RngCore + CryptoRng, + { + // create keys for 'payload' encryption + let (ephemeral_keypair, shared_key) = new_ephemeral_shared_key::< + PacketEncryptionAlgorithm, + PacketHkdfAlgorithm, + _, + >(rng, recipient_encryption_key); + + self.build::( + &shared_key, + ephemeral_keypair.public_key().to_bytes(), + ) + } +} + +// the actual byte data that will be put into the sphinx packet paylaod. +// no more transformations are going to happen to it +// TODO: use that fact for some better compile time assertions +pub struct NymsphinxPayload(Vec); + +impl AsRef<[u8]> for NymsphinxPayload { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 0b6a8e96dd..453f632f76 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -1,13 +1,22 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::message::{NymMessage, NymMessageError, PaddedMessage, PlainMessage}; +use crypto::aes::cipher::{KeyIvInit, StreamCipher}; use crypto::asymmetric::encryption; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use nymsphinx_anonymous_replies::reply_surb::{ReplySurb, ReplySurbError}; +use crypto::symmetric::stream_cipher::CipherKey; +use nymsphinx_anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx_anonymous_replies::SurbEncryptionKey; use nymsphinx_chunking::fragment::Fragment; use nymsphinx_chunking::reconstruction::MessageReconstructor; -use nymsphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, DEFAULT_NUM_MIX_HOPS}; +use nymsphinx_chunking::ChunkingError; +use nymsphinx_params::{ + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, + DEFAULT_NUM_MIX_HOPS, +}; +use thiserror::Error; // TODO: should this live in this file? #[derive(Debug)] @@ -15,31 +24,53 @@ pub struct ReconstructedMessage { /// The actual plaintext message that was received. pub message: Vec, - /// Optional ReplySURB to allow for an anonymous reply to the sender. - pub reply_surb: Option, + /// Optional ephemeral sender tag indicating pseudo-identity of the party who sent us the message + /// (alongside any reply SURBs) + pub sender_tag: Option, } -#[derive(Debug)] +impl From for (Vec, Option) { + fn from(msg: ReconstructedMessage) -> Self { + (msg.message, msg.sender_tag) + } +} + +impl ReconstructedMessage { + pub fn new(message: Vec, sender_tag: AnonymousSenderTag) -> Self { + Self { + message, + sender_tag: Some(sender_tag), + } + } + + pub fn into_inner(self) -> (Vec, Option) { + self.into() + } +} + +impl From for ReconstructedMessage { + fn from(message: PlainMessage) -> Self { + ReconstructedMessage { + message, + sender_tag: None, + } + } +} + +#[derive(Debug, Error)] pub enum MessageRecoveryError { - InvalidSurbPrefixError, - MalformedSurbError(ReplySurbError), - InvalidRemoteEphemeralKey(encryption::KeyRecoveryError), - MalformedFragmentError, - InvalidMessagePaddingError, - MalformedReconstructedMessage(Vec), - TooShortMessageError, -} + #[error("Recovered remote x25519 public key is invalid - {0}")] + InvalidRemoteEphemeralKey(#[from] encryption::KeyRecoveryError), -impl From for MessageRecoveryError { - fn from(err: ReplySurbError) -> Self { - MessageRecoveryError::MalformedSurbError(err) - } -} + #[error("The reconstructed message was malformed - {source}")] + MalformedReconstructedMessage { + #[source] + source: NymMessageError, + used_sets: Vec, + }, -impl From for MessageRecoveryError { - fn from(err: encryption::KeyRecoveryError) -> Self { - MessageRecoveryError::InvalidRemoteEphemeralKey(err) - } + #[error("Failed to recover message fragment - {0}")] + FragmentRecoveryError(#[from] ChunkingError), } pub struct MessageReceiver { @@ -64,36 +95,34 @@ impl MessageReceiver { self } - /// Parses the message to strip and optionally recover reply SURB. - fn recover_reply_surb_from_message( - &self, - message: &mut Vec, - ) -> Result, MessageRecoveryError> { - match message[0] { - n if n == false as u8 => { - message.remove(0); - Ok(None) - } - n if n == true as u8 => { - let surb_len: usize = ReplySurb::serialized_len(self.num_mix_hops); - // note the extra +1 (due to 0/1 message prefix) - let surb_bytes = &message[1..1 + surb_len]; - let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + fn decrypt_raw_message(&self, message: &mut [u8], key: &CipherKey) + where + C: StreamCipher + KeyIvInit, + { + let zero_iv = stream_cipher::zero_iv::(); + stream_cipher::decrypt_in_place::(key, &zero_iv, message) + } - *message = message.drain(1 + surb_len..).collect(); - Ok(Some(reply_surb)) - } - _ => Err(MessageRecoveryError::InvalidSurbPrefixError), - } + /// Given raw fragment data, **WITH KEY DIGEST PREFIX ALREADY REMOVED!!**, uses looked up + /// key to decrypt fragment data + pub fn recover_plaintext_from_reply( + &self, + reply_ciphertext: &mut [u8], + reply_key: SurbEncryptionKey, + ) { + self.decrypt_raw_message::( + reply_ciphertext, + reply_key.inner(), + ) } /// Given raw fragment data, recovers the remote ephemeral key, recomputes shared secret, /// uses it to decrypt fragment data - pub fn recover_plaintext( + pub fn recover_plaintext_from_regular_packet<'a>( &self, local_key: &encryption::PrivateKey, - mut raw_enc_frag: Vec, - ) -> Result, MessageRecoveryError> { + raw_enc_frag: &'a mut [u8], + ) -> Result<&'a mut [u8], MessageRecoveryError> { // 1. recover remote encryption key let remote_key_bytes = &raw_enc_frag[..encryption::PUBLIC_KEY_SIZE]; let remote_ephemeral_key = encryption::PublicKey::from_bytes(remote_key_bytes)?; @@ -105,33 +134,17 @@ impl MessageReceiver { ); // 3. decrypt fragment data - let fragment_bytes = &mut raw_enc_frag[encryption::PUBLIC_KEY_SIZE..]; + let fragment_ciphertext = &mut raw_enc_frag[encryption::PUBLIC_KEY_SIZE..]; - let zero_iv = stream_cipher::zero_iv::(); - Ok(stream_cipher::decrypt::( - &encryption_key, - &zero_iv, - fragment_bytes, - )) + self.decrypt_raw_message::(fragment_ciphertext, &encryption_key); + let fragment_data = fragment_ciphertext; + + Ok(fragment_data) } /// Given fragment data recovers [`Fragment`] itself. pub fn recover_fragment(&self, frag_data: &[u8]) -> Result { - Fragment::try_from_bytes(frag_data) - .map_err(|_| MessageRecoveryError::MalformedFragmentError) - } - - /// Removes the zero padding from the message that was initially included to ensure same length - /// sphinx payloads. - pub fn remove_padding(message: &mut Vec) -> Result<(), MessageRecoveryError> { - // we are looking for first occurrence of 1 in the tail and we get its index - if let Some(i) = message.iter().rposition(|b| *b == 1) { - // and now we only take bytes until that point (but not including it) - *message = message.drain(..i).collect(); - Ok(()) - } else { - Err(MessageRecoveryError::InvalidMessagePaddingError) - } + Ok(Fragment::try_from_bytes(frag_data)?) } /// Inserts given [`Fragment`] into the reconstructor. @@ -144,30 +157,15 @@ impl MessageReceiver { pub fn insert_new_fragment( &mut self, fragment: Fragment, - ) -> Result)>, MessageRecoveryError> { - if let Some((mut message, used_sets)) = self.reconstructor.insert_new_fragment(fragment) { - // Split message into plaintext and reply-SURB - let reply_surb = match self.recover_reply_surb_from_message(&mut message) { - Ok(reply_surb) => reply_surb, - Err(_) => { - return Err(MessageRecoveryError::MalformedReconstructedMessage( - used_sets, - )); - } - }; - - // Finally, remove the zero padding from the message - Self::remove_padding(&mut message).map_err(|_| { - MessageRecoveryError::MalformedReconstructedMessage(used_sets.clone()) - })?; - - Ok(Some(( - ReconstructedMessage { - message, - reply_surb, - }, - used_sets, - ))) + ) -> Result)>, MessageRecoveryError> { + if let Some((message, used_sets)) = self.reconstructor.insert_new_fragment(fragment) { + match PaddedMessage::new_reconstructed(message).remove_padding(self.num_mix_hops) { + Ok(message) => Ok(Some((message, used_sets))), + Err(err) => Err(MessageRecoveryError::MalformedReconstructedMessage { + source: err, + used_sets, + }), + } } else { Ok(None) } @@ -188,16 +186,14 @@ mod message_receiver { use super::*; use crypto::asymmetric::identity; use mixnet_contract_common::Layer; - use nymsphinx_addressing::clients::Recipient; - use rand::rngs::OsRng; use std::collections::HashMap; - use std::time::Duration; use topology::{gateway, mix, NymTopology}; // TODO: is it somehow maybe possible to move it to `topology` and have if conditionally // available to other modules? /// Returns a hardcoded, valid instance of [`NymTopology`] that is to be used in /// tests requiring instance of topology. + #[allow(dead_code)] fn topology_fixture() -> NymTopology { let mut mixes = HashMap::new(); mixes.insert( @@ -282,41 +278,4 @@ mod message_receiver { }], ) } - - #[test] - fn correctly_splits_message_into_plaintext_and_surb() { - let message_receiver: MessageReceiver = Default::default(); - - // the actual 'correctness' of the underlying message doesn't matter for this test - let message = vec![42; 100]; - let dummy_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML").unwrap(); - let average_delay = Duration::from_millis(500); - let topology = topology_fixture(); - - let reply_surb = - ReplySurb::construct(&mut OsRng, &dummy_recipient, average_delay, &topology).unwrap(); - - let reply_surb_bytes = reply_surb.to_bytes(); - - // this is not exactly what is 'received' but rather after "some" processing, however, - // this is the expected argument to the function - let mut received_without_surb: Vec<_> = - std::iter::once(0).chain(message.iter().cloned()).collect(); - - let reply_surb = message_receiver - .recover_reply_surb_from_message(&mut received_without_surb) - .unwrap(); - assert_eq!(received_without_surb, message); - assert!(reply_surb.is_none()); - - let mut received_with_surb: Vec<_> = std::iter::once(1) - .chain(reply_surb_bytes.iter().cloned()) - .chain(message.iter().cloned()) - .collect(); - let reply_surb = message_receiver - .recover_reply_surb_from_message(&mut received_with_surb) - .unwrap(); - assert_eq!(received_with_surb, message); - assert_eq!(reply_surb_bytes, reply_surb.unwrap().to_bytes()); - } } diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml index 34a916bbf5..353de2b1b2 100644 --- a/common/nymsphinx/types/Cargo.toml +++ b/common/nymsphinx/types/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sphinx = { git = "https://github.com/nymtech/sphinx", rev="c494250f2a78bed33a618d470792418eee932859" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="e05a1992522ed0afd3c6fcac160313ffc9bb306a" } #sphinx = { path = "../../../../sphinx"} diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index f6921e362a..3148f6cd84 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -15,6 +15,18 @@ pub enum RequestFlag { Send = 1, } +impl TryFrom for RequestFlag { + type Error = RequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), + _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), + _ => Err(RequestError::UnknownRequestFlag), + } + } +} + #[derive(Debug, Error)] pub enum RequestError { #[error("not enough bytes to recover the length of the address")] @@ -45,23 +57,12 @@ impl RequestError { } } -impl TryFrom for RequestFlag { - type Error = RequestError; - - fn try_from(value: u8) -> Result { - match value { - _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), - _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), - _ => Err(RequestError::UnknownRequestFlag), - } - } -} - #[derive(Debug)] pub struct ConnectRequest { + // TODO: is connection_id redundant now? pub conn_id: ConnectionId, pub remote_addr: RemoteAddress, - pub return_address: Recipient, + pub return_address: Option, } /// A request from a SOCKS5 client that a Nym Socks5 service provider should @@ -82,7 +83,7 @@ impl Request { pub fn new_connect( conn_id: ConnectionId, remote_addr: RemoteAddress, - return_address: Recipient, + return_address: Option, ) -> Request { Request::Connect(Box::new(ConnectRequest { conn_id, @@ -99,12 +100,13 @@ impl Request { /// Deserialize the request type, connection id, destination address and port, /// and the request body from bytes. /// - /// Serialized bytes looks like this: - /// - /// -------------------------------------------------------------------------------------- - /// request_flag | connection_id | address_length | remote_address_bytes | request_data | - /// 1 | 8 | 2 | address_length | ... | - /// -------------------------------------------------------------------------------------- + // TODO: this was already inaccurate + // /// Serialized bytes looks like this: + // /// + // /// -------------------------------------------------------------------------------------- + // /// request_flag | connection_id | address_length | remote_address_bytes | request_data | + // /// 1 | 8 | 2 | address_length | ... | + // /// -------------------------------------------------------------------------------------- /// /// The request_flag tells us whether this is a new connection request (`new_connect`), /// an already-established connection we should send up (`new_send`), or @@ -144,14 +146,20 @@ impl Request { // just a temporary reference to mid-slice for ease of use let recipient_data_bytes = &connect_request_bytes[address_end..]; - if recipient_data_bytes.len() != Recipient::LEN { - return Err(RequestError::ReturnAddressTooShort); - } + let return_address = if recipient_data_bytes.is_empty() { + None + } else { + if recipient_data_bytes.len() != Recipient::LEN { + return Err(RequestError::ReturnAddressTooShort); + } - let mut return_bytes = [0u8; Recipient::LEN]; - return_bytes.copy_from_slice(&recipient_data_bytes[..Recipient::LEN]); - let return_address = Recipient::try_from_bytes(return_bytes) - .map_err(RequestError::MalformedReturnAddress)?; + let mut return_bytes = [0u8; Recipient::LEN]; + return_bytes.copy_from_slice(&recipient_data_bytes[..Recipient::LEN]); + Some( + Recipient::try_from_bytes(return_bytes) + .map_err(RequestError::MalformedReturnAddress)?, + ) + }; Ok(Request::new_connect( connection_id, @@ -161,7 +169,7 @@ impl Request { } RequestFlag::Send => { let local_closed = b[9] != 0; - let data = b[10..].as_ref().to_vec(); + let data = b[10..].to_vec(); Ok(Request::Send(connection_id, data, local_closed)) } @@ -178,15 +186,19 @@ impl Request { let remote_address_bytes = req.remote_addr.into_bytes(); let remote_address_bytes_len = remote_address_bytes.len() as u16; - std::iter::once(RequestFlag::Connect as u8) - .chain(req.conn_id.to_be_bytes().iter().cloned()) - .chain(remote_address_bytes_len.to_be_bytes().iter().cloned()) - .chain(remote_address_bytes.into_iter()) - .chain(req.return_address.to_bytes().iter().cloned()) - .collect() + let iter = std::iter::once(RequestFlag::Connect as u8) + .chain(req.conn_id.to_be_bytes().into_iter()) + .chain(remote_address_bytes_len.to_be_bytes().into_iter()) + .chain(remote_address_bytes.into_iter()); + + if let Some(return_address) = req.return_address { + iter.chain(return_address.to_bytes().into_iter()).collect() + } else { + iter.collect() + } } Request::Send(conn_id, data, local_closed) => std::iter::once(RequestFlag::Send as u8) - .chain(conn_id.to_be_bytes().iter().cloned()) + .chain(conn_id.to_be_bytes().into_iter()) .chain(std::iter::once(local_closed as u8)) .chain(data.into_iter()) .collect(), @@ -324,7 +336,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes_prefix .iter() .cloned() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); assert!(Request::try_from_bytes(&request_bytes) .unwrap_err() @@ -361,7 +373,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes .into_iter() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); let request = Request::try_from_bytes(&request_bytes).unwrap(); @@ -370,7 +382,7 @@ mod request_deserialization_tests { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( - req.return_address.to_bytes().to_vec(), + req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } @@ -408,7 +420,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes .into_iter() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); let request = Request::try_from_bytes(&request_bytes).unwrap(); @@ -417,7 +429,7 @@ mod request_deserialization_tests { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( - req.return_address.to_bytes().to_vec(), + req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index f2a8a899fd..2996aeb639 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" bs58 = "0.4" log = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" ## internal crypto = { path = "../crypto" } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 2e9cde725b..ff3ba55c83 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::filter::VersionFilterable; @@ -14,19 +14,31 @@ use std::fmt::{self, Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::str::FromStr; +use thiserror::Error; pub mod filter; pub mod gateway; pub mod mix; -#[derive(Debug)] +#[derive(Debug, Clone, Error)] pub enum NymTopologyError { - InvalidMixLayerError, - MissingLayerError(Vec), - NonExistentGatewayError, + #[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")] + EmptyNetworkTopology, - InvalidNumberOfHopsError, - NoMixesOnLayerAvailable(MixLayer), + #[error("The provided network topology has no gateways available")] + NoGatewaysAvailable, + + #[error("The provided network topology has no mixnodes available")] + NoMixnodesAvailable, + + #[error("Gateway with identity key {identity_key} doesn't exist")] + NonExistentGatewayError { identity_key: String }, + + #[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")] + InvalidNumberOfHopsError { available: usize, requested: usize }, + + #[error("No mixnodes available on layer {layer}")] + EmptyMixLayer { layer: MixLayer }, } #[derive(Debug, Clone)] @@ -134,7 +146,10 @@ impl NymTopology { use rand::seq::SliceRandom; if self.mixes.len() < num_mix_hops as usize { - return Err(NymTopologyError::InvalidNumberOfHopsError); + return Err(NymTopologyError::InvalidNumberOfHopsError { + available: self.mixes.len(), + requested: num_mix_hops as usize, + }); } let mut route = Vec::with_capacity(num_mix_hops as usize); @@ -144,13 +159,13 @@ impl NymTopology { let layer_mixes = self .mixes .get(&layer) - .ok_or(NymTopologyError::NoMixesOnLayerAvailable(layer))?; + .ok_or(NymTopologyError::EmptyMixLayer { layer })?; // choose a random mix from the above list // this can return a 'None' only if slice is empty let random_mix = layer_mixes .choose(rng) - .ok_or(NymTopologyError::NoMixesOnLayerAvailable(layer))?; + .ok_or(NymTopologyError::EmptyMixLayer { layer })?; route.push(random_mix.into()); } @@ -169,9 +184,11 @@ impl NymTopology { // I don't think there's a need for this RNG to be crypto-secure R: Rng + ?Sized, { - let gateway = self - .get_gateway(gateway_identity) - .ok_or(NymTopologyError::NonExistentGatewayError)?; + let gateway = self.get_gateway(gateway_identity).ok_or( + NymTopologyError::NonExistentGatewayError { + identity_key: gateway_identity.to_base58_string(), + }, + )?; Ok(self .random_mix_route(rng, num_mix_hops)? @@ -186,29 +203,39 @@ impl NymTopology { } /// Checks if a mixnet path can be constructed using the specified number of hops - pub fn can_construct_path_through(&self, num_mix_hops: u8) -> bool { - // if there are no gateways present, we can't do anything - if self.gateways.is_empty() { - return false; + pub fn ensure_can_construct_path_through( + &self, + num_mix_hops: u8, + ) -> Result<(), NymTopologyError> { + let mixnodes = self.mixes(); + // 1. is it completely empty? + if mixnodes.is_empty() && self.gateways().is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); } - // early termination - if self.mixes.is_empty() { - return false; + // 2. does it have any mixnode at all? + if mixnodes.is_empty() { + return Err(NymTopologyError::NoMixnodesAvailable); } - // make sure there's at least one mix per layer - for i in 1..=num_mix_hops { - match self.mixes.get(&i) { - None => return false, - Some(layer_entry) => { - if layer_entry.is_empty() { - return false; + // 3. does it have any gateways at all? + if self.gateways().is_empty() { + return Err(NymTopologyError::NoGatewaysAvailable); + } + + // 4. does it have a mixnode on each layer? + for layer in 1..=num_mix_hops { + match mixnodes.get(&layer) { + None => return Err(NymTopologyError::EmptyMixLayer { layer }), + Some(layer_nodes) => { + if layer_nodes.is_empty() { + return Err(NymTopologyError::EmptyMixLayer { layer }); } } } } - true + + Ok(()) } #[must_use] diff --git a/common/wasm-utils/Cargo.toml b/common/wasm-utils/Cargo.toml index 75021ddad9..be39421ce2 100644 --- a/common/wasm-utils/Cargo.toml +++ b/common/wasm-utils/Cargo.toml @@ -16,6 +16,7 @@ wasm-bindgen-futures = "0.4" [dependencies.tungstenite] version = "0.13" default-features = false +optional = true [dependencies.web-sys] version = "0.3" @@ -30,3 +31,9 @@ features = [ "WebSocket", "Window", ] +optional = true + +[features] +default = ["sleep"] +websocket = ["tungstenite", "web-sys"] +sleep = ["web-sys"] \ No newline at end of file diff --git a/common/wasm-utils/src/lib.rs b/common/wasm-utils/src/lib.rs index 94b8faae05..8e0205c6de 100644 --- a/common/wasm-utils/src/lib.rs +++ b/common/wasm-utils/src/lib.rs @@ -5,8 +5,8 @@ use js_sys::Promise; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; -use web_sys::window; +#[cfg(feature = "websocket")] pub mod websocket; // will cause messages to be written as if console.log("...") was called @@ -39,9 +39,10 @@ extern "C" { pub fn error(s: &str); } +#[cfg(feature = "sleep")] pub async fn sleep(ms: i32) -> Result<(), JsValue> { let promise = Promise::new(&mut |yes, _| { - let win = window().expect("no window available!"); + let win = web_sys::window().expect("no window available!"); win.set_timeout_with_callback_and_timeout_and_arguments_0(&yes, ms) .unwrap(); }); diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 7a23c13655..9e13fd2e69 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -350,7 +350,7 @@ where match client_protocol { None => { warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in 1.2.0 we will have to return a hard error here + // note: in +1.2.0 we will have to return a hard error here Ok(()) } Some(v) if v != PROTOCOL_VERSION => { diff --git a/mixnode/src/node/packet_delayforwarder.rs b/mixnode/src/node/packet_delayforwarder.rs index 9be8175bed..81e0303afd 100644 --- a/mixnode/src/node/packet_delayforwarder.rs +++ b/mixnode/src/node/packet_delayforwarder.rs @@ -194,7 +194,7 @@ mod tests { ]; SphinxPacketBuilder::new() .with_payload_size(size.payload_size()) - .build_packet(b"foomp".to_vec(), &route, &destination, &delays) + .build_packet(b"foomp", &route, &destination, &delays) .unwrap() } diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 1c6bd7ed3e..711edb17af 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -123,9 +123,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -165,6 +165,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "attohttpc" version = "0.22.0" @@ -632,9 +641,11 @@ dependencies = [ name = "client-core" version = "1.1.2" dependencies = [ + "async-trait", "client-connections", "config", "crypto", + "dashmap", "dirs", "futures", "gateway-client", @@ -648,10 +659,11 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "sled", + "sqlx 0.6.2", "tap", "task", "thiserror", + "time 0.3.17", "tokio", "tokio-stream", "topology", @@ -660,6 +672,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", + "wasm-utils", ] [[package]] @@ -940,7 +953,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" dependencies = [ - "crc-catalog", + "crc-catalog 1.1.1", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog 2.1.0", ] [[package]] @@ -949,6 +971,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + [[package]] name = "crc32fast" version = "1.3.2" @@ -964,7 +992,7 @@ version = "0.1.0" dependencies = [ "async-trait", "log", - "sqlx", + "sqlx 0.5.13", "thiserror", "tokio", ] @@ -991,20 +1019,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-epoch" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" -dependencies = [ - "autocfg 1.1.0", - "cfg-if", - "crossbeam-utils", - "memoffset", - "once_cell", - "scopeguard", -] - [[package]] name = "crossbeam-queue" version = "0.3.6" @@ -1127,7 +1141,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "sct", + "sct 0.6.1", ] [[package]] @@ -1287,6 +1301,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.3", +] + [[package]] name = "deflate" version = "0.7.20" @@ -1422,6 +1449,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dotenvy" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" + [[package]] name = "dtoa" version = "0.4.8" @@ -1717,16 +1750,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2303,6 +2326,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashlink" @@ -2313,6 +2339,15 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "headers" version = "0.3.8" @@ -2532,9 +2567,9 @@ dependencies = [ "hyper-rustls", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tower-service", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -2547,12 +2582,12 @@ dependencies = [ "futures-util", "hyper", "log", - "rustls", + "rustls 0.19.1", "rustls-native-certs", "tokio", - "tokio-rustls", - "webpki", - "webpki-roots", + "tokio-rustls 0.22.0", + "webpki 0.21.4", + "webpki-roots 0.21.1", ] [[package]] @@ -2786,9 +2821,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -3110,7 +3145,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -3303,15 +3338,6 @@ dependencies = [ "syn", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "nym-connect" version = "1.1.2" @@ -3421,6 +3447,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "rand_distr", + "thiserror", "tokio", "topology", ] @@ -3445,6 +3472,7 @@ dependencies = [ "crypto", "nymsphinx-types", "serde", + "thiserror", ] [[package]] @@ -3458,7 +3486,9 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", "topology", + "wasm-bindgen", ] [[package]] @@ -3470,6 +3500,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", ] [[package]] @@ -3484,6 +3515,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", "topology", ] @@ -4010,7 +4042,7 @@ dependencies = [ "indexmap", "line-wrap", "serde", - "time 0.3.14", + "time 0.3.17", "xml-rs", ] @@ -4580,8 +4612,20 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -4591,11 +4635,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ "openssl-probe", - "rustls", + "rustls 0.19.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.9" @@ -4680,6 +4733,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.2.1" @@ -5003,22 +5066,6 @@ dependencies = [ "autocfg 1.1.0", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "0.6.14" @@ -5083,7 +5130,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792418eee932859#c494250f2a78bed33a618d470792418eee932859" +source = "git+https://github.com/nymtech/sphinx?rev=e05a1992522ed0afd3c6fcac160313ffc9bb306a#e05a1992522ed0afd3c6fcac160313ffc9bb306a" dependencies = [ "aes 0.7.5", "arrayref", @@ -5139,14 +5186,35 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "sqlformat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + [[package]] name = "sqlx" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" dependencies = [ - "sqlx-core", - "sqlx-macros", + "sqlx-core 0.5.13", + "sqlx-macros 0.5.13", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core 0.6.2", + "sqlx-macros 0.6.2", ] [[package]] @@ -5156,11 +5224,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" dependencies = [ "ahash", - "atoi", + "atoi 0.4.0", "bitflags", "byteorder", "bytes", - "crc", + "crc 2.1.0", "crossbeam-queue", "either", "event-listener", @@ -5170,7 +5238,7 @@ dependencies = [ "futures-executor", "futures-intrusive", "futures-util", - "hashlink", + "hashlink 0.7.0", "hex", "indexmap", "itoa 1.0.3", @@ -5181,17 +5249,63 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.19.1", "sha2 0.10.6", "smallvec 1.9.0", - "sqlformat", - "sqlx-rt", + "sqlformat 0.1.8", + "sqlx-rt 0.5.13", "stringprep", "thiserror", "tokio-stream", "url", - "webpki", - "webpki-roots", + "webpki 0.21.4", + "webpki-roots 0.21.1", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash", + "atoi 1.0.0", + "bitflags", + "byteorder", + "bytes", + "crc 3.0.0", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink 0.8.1", + "hex", + "indexmap", + "itoa 1.0.3", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.20.7", + "rustls-pemfile", + "sha2 0.10.6", + "smallvec 1.9.0", + "sqlformat 0.2.0", + "sqlx-rt 0.6.2", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki-roots 0.22.5", ] [[package]] @@ -5207,8 +5321,27 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.6", - "sqlx-core", - "sqlx-rt", + "sqlx-core 0.5.13", + "sqlx-rt 0.5.13", + "syn", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.0", + "once_cell", + "proc-macro2", + "quote", + "sha2 0.10.6", + "sqlx-core 0.6.2", + "sqlx-rt 0.6.2", "syn", "url", ] @@ -5221,7 +5354,18 @@ checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls 0.23.4", ] [[package]] @@ -5486,7 +5630,7 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "url", "uuid 1.1.2", @@ -5533,7 +5677,7 @@ dependencies = [ "sha2 0.10.6", "tauri-utils", "thiserror", - "time 0.3.14", + "time 0.3.17", "uuid 1.1.2", "walkdir", ] @@ -5661,7 +5805,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.14", + "time 0.3.17", "zeroize", ] @@ -5694,7 +5838,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -5722,7 +5866,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tracing", "url", @@ -5804,27 +5948,37 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.3", - "libc", - "num_threads", + "js-sys", + "serde", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5866,9 +6020,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.7", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -5931,6 +6096,7 @@ dependencies = [ "nymsphinx-addressing", "nymsphinx-types", "rand 0.7.3", + "thiserror", "version-checker", ] @@ -6518,13 +6684,32 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +dependencies = [ + "webpki 0.22.0", ] [[package]] diff --git a/nym-connect/src-tauri/src/config/mod.rs b/nym-connect/src-tauri/src/config/mod.rs index ade4ac2d57..c27bf6b6b7 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -1,17 +1,14 @@ -use std::path::PathBuf; - -use std::sync::Arc; -use tap::TapFallible; -use tokio::sync::RwLock; - -use client_core::config::Config as BaseConfig; -use config_common::NymConfig; -use nym_socks5::client::config::Config as Socks5Config; - use crate::{ error::{BackendError, Result}, state::State, }; +use client_core::config::Config as BaseConfig; +use config_common::NymConfig; +use nym_socks5::client::config::Config as Socks5Config; +use std::path::PathBuf; +use std::sync::Arc; +use tap::TapFallible; +use tokio::sync::RwLock; static SOCKS5_CONFIG_ID: &str = "nym-connect"; @@ -130,7 +127,7 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str } // Setup gateway by either registering a new one, or reusing exiting keys - let gateway = client_core::init::setup_gateway::( + let gateway = client_core::init::setup_gateway::<_, Socks5Config, _>( register_gateway, Some(chosen_gateway_id), config.get_base(), diff --git a/nym-connect/src-tauri/src/error.rs b/nym-connect/src-tauri/src/error.rs index b2b44ee345..cb677aac68 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -1,3 +1,4 @@ +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; use serde::{Serialize, Serializer}; use thiserror::Error; @@ -33,7 +34,7 @@ pub enum BackendError { #[error("{source}")] ClientCoreError { #[from] - source: ClientCoreError, + source: ClientCoreError, }, #[error("Could not send disconnect signal to the SOCKS5 client")] diff --git a/service-providers/network-requester/src/connection.rs b/service-providers/network-requester/src/connection.rs index 95bdac06a3..f4ffaca992 100644 --- a/service-providers/network-requester/src/connection.rs +++ b/service-providers/network-requester/src/connection.rs @@ -1,8 +1,8 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::core::ReturnAddress; use client_connections::LaneQueueLengths; -use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::ConnectionReceiver; use proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; use socks5_requests::{ConnectionId, Message as Socks5Message, RemoteAddress, Response}; @@ -18,14 +18,14 @@ pub(crate) struct Connection { id: ConnectionId, address: RemoteAddress, conn: Option, - return_address: Recipient, + return_address: ReturnAddress, } impl Connection { pub(crate) async fn new( id: ConnectionId, address: RemoteAddress, - return_address: Recipient, + return_address: ReturnAddress, ) -> io::Result { let conn = TcpStream::connect(&address).await?; @@ -40,14 +40,14 @@ impl Connection { pub(crate) async fn run_proxy( &mut self, mix_receiver: ConnectionReceiver, - mix_sender: MixProxySender<(Socks5Message, Recipient)>, + mix_sender: MixProxySender<(Socks5Message, ReturnAddress)>, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let stream = self.conn.take().unwrap(); let remote_source_address = "???".to_string(); // we don't know ip address of requester let connection_id = self.id; - let recipient = self.return_address; + let return_address = self.return_address.clone(); let (stream, _) = ProxyRunner::new( stream, self.address.clone(), @@ -61,7 +61,7 @@ impl Connection { .run(move |conn_id, read_data, socket_closed| { ( Socks5Message::Response(Response::new(conn_id, read_data, socket_closed)), - recipient, + return_address.clone(), ) }) .await diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 40f9cefd25..34fb42e7c0 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -15,13 +15,15 @@ use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::receiver::ReconstructedMessage; use proxy_helpers::connection_controller::{ BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender, }; use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender}; use socks5_requests::{ - ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response, + ConnectRequest, ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, + Response, }; use statistics_common::collector::StatisticsSender; use std::path::PathBuf; @@ -41,6 +43,56 @@ pub struct ServiceProvider { stats_provider_addr: Option, } +// TODO: move elsewhere after things settle +#[derive(Debug, Clone)] +pub enum ReturnAddress { + Known(Box), + Anonymous(AnonymousSenderTag), +} + +impl From for ReturnAddress { + fn from(recipient: Recipient) -> Self { + ReturnAddress::Known(Box::new(recipient)) + } +} + +impl From for ReturnAddress { + fn from(sender_tag: AnonymousSenderTag) -> Self { + ReturnAddress::Anonymous(sender_tag) + } +} + +impl ReturnAddress { + fn new( + explicit_return_address: Option, + implicit_tag: Option, + ) -> Option { + // if somehow we received both, always prefer the explicit address since it's way easier to use + if let Some(recipient) = explicit_return_address { + return Some(ReturnAddress::Known(Box::new(recipient))); + } + if let Some(sender_tag) = implicit_tag { + return Some(ReturnAddress::Anonymous(sender_tag)); + } + None + } + + fn send_back_to(self, message: Vec, connection_id: u64) -> ClientRequest { + match self { + ReturnAddress::Known(recipient) => ClientRequest::Send { + recipient: *recipient, + message, + connection_id: Some(connection_id), + }, + ReturnAddress::Anonymous(sender_tag) => ClientRequest::Reply { + message, + sender_tag, + connection_id: Some(connection_id), + }, + } + } +} + impl ServiceProvider { pub fn new( listening_address: String, @@ -72,13 +124,12 @@ impl ServiceProvider { /// via the `websocket_writer`. async fn mixnet_response_listener( mut websocket_writer: SplitSink, - mut mix_reader: MixProxyReader<(Socks5Message, Recipient)>, + mut mix_reader: MixProxyReader<(Socks5Message, ReturnAddress)>, stats_collector: Option, mut client_connection_rx: ConnectionCommandReceiver, ) { loop { tokio::select! { - // TODO: wire SURBs in here once they're available socks5_msg = mix_reader.recv() => { if let Some((msg, return_address)) = socks5_msg { if let Some(stats_collector) = stats_collector.as_ref() { @@ -95,15 +146,10 @@ impl ServiceProvider { .processed(remote_addr, msg.size() as u32); } } - let conn_id = msg.conn_id(); // make 'request' to native-websocket client - let response_message = ClientRequest::Send { - recipient: return_address, - message: msg.into_bytes(), - with_reply_surb: false, - connection_id: Some(conn_id), - }; + let conn_id = msg.conn_id(); + let response_message = return_address.send_back_to(msg.into_bytes(), conn_id); let message = Message::Binary(response_message.serialize()); websocket_writer.send(message).await.unwrap(); @@ -179,7 +225,7 @@ impl ServiceProvider { let received = match deserialized_message { ServerResponse::Received(received) => received, - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::LaneQueueLength { lane, queue_length } => { Self::handle_lane_queue_length_response( &lane_queue_lengths, lane, @@ -200,33 +246,34 @@ impl ServiceProvider { async fn start_proxy( conn_id: ConnectionId, remote_addr: String, - return_address: Recipient, + return_address: ReturnAddress, controller_sender: ControllerSender, - mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, ReturnAddress)>, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { - let mut conn = match Connection::new(conn_id, remote_addr.clone(), return_address).await { - Ok(conn) => conn, - Err(err) => { - error!( - "error while connecting to {:?} ! - {:?}", - remote_addr.clone(), - err - ); + let mut conn = + match Connection::new(conn_id, remote_addr.clone(), return_address.clone()).await { + Ok(conn) => conn, + Err(err) => { + error!( + "error while connecting to {:?} ! - {:?}", + remote_addr.clone(), + err + ); - // inform the remote that the connection is closed before it even was established - mix_input_sender - .send(( - Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), - return_address, - )) - .await - .expect("InputMessageReceiver has stopped receiving!"); + // inform the remote that the connection is closed before it even was established + mix_input_sender + .send(( + Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), + return_address, + )) + .await + .expect("InputMessageReceiver has stopped receiving!"); - return; - } - }; + return; + } + }; // Connect implies it's a fresh connection - register it with our controller let (mix_sender, mix_receiver) = mpsc::unbounded(); @@ -262,13 +309,25 @@ impl ServiceProvider { async fn handle_proxy_connect( &mut self, controller_sender: &mut ControllerSender, - mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, ReturnAddress)>, lane_queue_lengths: LaneQueueLengths, - conn_id: ConnectionId, - remote_addr: String, - return_address: Recipient, + sender_tag: Option, + connect_req: Box, shutdown: ShutdownListener, ) { + let return_address = match ReturnAddress::new(connect_req.return_address, sender_tag) { + Some(address) => address, + None => { + log::warn!( + "attempted to start connection with no way of returning data back to the sender" + ); + return; + } + }; + + let remote_addr = connect_req.remote_addr; + let conn_id = connect_req.conn_id; + if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) { let log_msg = format!("Domain {:?} failed filter check", remote_addr); log::info!("{}", log_msg); @@ -315,23 +374,24 @@ impl ServiceProvider { async fn handle_proxy_message( &mut self, - raw_request: &[u8], + message: ReconstructedMessage, controller_sender: &mut ControllerSender, - mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, ReturnAddress)>, lane_queue_lengths: LaneQueueLengths, stats_collector: Option, shutdown: ShutdownListener, ) { - let deserialized_msg = match Socks5Message::try_from_bytes(raw_request) { + let deserialized_msg = match Socks5Message::try_from_bytes(&message.message) { Ok(msg) => msg, Err(err) => { - error!("Failed to deserialized received message! - {}", err); + error!("Failed to deserialized received message! - {err}"); return; } }; match deserialized_msg { Socks5Message::Request(deserialized_request) => match deserialized_request { Request::Connect(req) => { + // TODO: stats might be invalid if connection fails to start if let Some(stats_collector) = stats_collector { stats_collector .connected_services @@ -343,9 +403,8 @@ impl ServiceProvider { controller_sender, mix_input_sender, lane_queue_lengths, - req.conn_id, - req.remote_addr, - req.return_address, + message.sender_tag, + req, shutdown, ) .await @@ -383,7 +442,7 @@ impl ServiceProvider { // channels responsible for managing messages that are to be sent to the mix network. The receiver is // going to be used by `mixnet_response_listener` let (mix_input_sender, mix_input_receiver) = - tokio::sync::mpsc::channel::<(Socks5Message, Recipient)>(1); + tokio::sync::mpsc::channel::<(Socks5Message, ReturnAddress)>(1); // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). let shutdown = task::ShutdownNotifier::default(); @@ -450,11 +509,8 @@ impl ServiceProvider { return Err(NetworkRequesterError::ConnectionClosed); }; - let raw_message = received.message; - // TODO: here be potential SURB (i.e. received.reply_SURB) - self.handle_proxy_message( - &raw_message, + received, &mut controller_sender, &mix_input_sender, shared_lane_queue_lengths.clone(), diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index ac43252c33..73e6918ea5 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; +use crate::core::ReturnAddress; use nymsphinx::addressing::clients::Recipient; use ordered_buffer::OrderedMessageSender; use socks5_requests::{ConnectionId, Message as Socks5Message, RemoteAddress, Request}; @@ -77,13 +78,13 @@ pub struct ServiceStatisticsCollector { pub(crate) response_stats_data: Arc>, pub(crate) connected_services: Arc>>, stats_provider_addr: Recipient, - mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, ReturnAddress)>, } impl ServiceStatisticsCollector { pub async fn new( stats_provider_addr: Option, - mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, ReturnAddress)>, ) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -173,12 +174,12 @@ impl StatisticsCollector for ServiceStatisticsCollector { "{}:{}", DEFAULT_STATISTICS_SERVICE_ADDRESS, DEFAULT_STATISTICS_SERVICE_PORT ), - self.stats_provider_addr, + Some(self.stats_provider_addr), ); self.mix_input_sender .send(( Socks5Message::Request(connect_req), - self.stats_provider_addr, + self.stats_provider_addr.into(), )) .await .expect("MixProxyReader has stopped receiving!"); @@ -188,7 +189,10 @@ impl StatisticsCollector for ServiceStatisticsCollector { let ordered_msg = message_sender.wrap_message(msg).into_bytes(); let send_req = Request::new_send(conn_id, ordered_msg, true); self.mix_input_sender - .send((Socks5Message::Request(send_req), self.stats_provider_addr)) + .send(( + Socks5Message::Request(send_req), + self.stats_provider_addr.into(), + )) .await .expect("MixProxyReader has stopped receiving!"); diff --git a/validator-api/src/network_monitor/chunker.rs b/validator-api/src/network_monitor/chunker.rs index ec0da08871..0caaba0811 100644 --- a/validator-api/src/network_monitor/chunker.rs +++ b/validator-api/src/network_monitor/chunker.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nymsphinx::forwarding::packet::MixPacket; +use nymsphinx::message::NymMessage; use nymsphinx::{ acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer, }; @@ -52,10 +53,9 @@ impl Chunker { ) -> Vec { let ack_key: AckKey = AckKey::new(&mut self.rng); - let (split_message, _reply_keys) = self + let split_message = self .message_preparer - .prepare_and_split_message(message, false, topology) - .expect("failed to split the message"); + .pad_and_split_message(NymMessage::new_plain(message)); let mut mix_packets = Vec::with_capacity(split_message.len()); for message_chunk in split_message { diff --git a/validator-api/src/network_monitor/monitor/processor.rs b/validator-api/src/network_monitor/monitor/processor.rs index 28ef3ae900..7b0649a49d 100644 --- a/validator-api/src/network_monitor/monitor/processor.rs +++ b/validator-api/src/network_monitor/monitor/processor.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::network_monitor::gateways_reader::GatewayMessages; @@ -68,27 +68,30 @@ struct ReceivedProcessorInner { } impl ReceivedProcessorInner { - fn on_message(&mut self, message: Vec) -> Result<(), ProcessingError> { + fn on_message(&mut self, mut message: Vec) -> Result<(), ProcessingError> { // if the nonce is none it means the packet was received during the 'waiting' for the // next test run if self.test_nonce.is_none() { return Err(ProcessingError::ReceivedOutsideTestRun); } - let encrypted_bytes = self + let plaintext = self .message_receiver - .recover_plaintext(self.client_encryption_keypair.private_key(), message) + .recover_plaintext_from_regular_packet( + self.client_encryption_keypair.private_key(), + &mut message, + ) .map_err(|_| ProcessingError::MalformedPacketReceived)?; let fragment = self .message_receiver - .recover_fragment(&encrypted_bytes) + .recover_fragment(plaintext) .map_err(|_| ProcessingError::MalformedPacketReceived)?; let (recovered, _) = self .message_receiver .insert_new_fragment(fragment) .map_err(|_| ProcessingError::MalformedPacketReceived)? .ok_or(ProcessingError::NonTestPacketReceived)?; // if it's a test packet it MUST BE reconstructed with single fragment - let test_packet = TestPacket::try_from_bytes(&recovered.message) + let test_packet = TestPacket::try_from_bytes(&recovered.into_inner_data()) .map_err(|_| ProcessingError::MalformedPacketReceived)?; // we know nonce is NOT none