diff --git a/.gitignore b/.gitignore index 74a82c4e13..0e2a851081 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target .env /.vscode/settings.json +sample-configs/validator-config.toml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 7041ae8d65..bf149680d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,5 +11,5 @@ before_script: - rustup component add rustfmt script: - cargo build - - cargo test + - cargo test -- --test-threads=1 - cargo fmt -- --check \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f60174ed7..8a65d4c2ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [Unreleased](https://github.com/nymtech/nym/tree/HEAD) + +[Full Changelog](https://github.com/nymtech/nym/compare/v0.4.1...HEAD) + +**Closed issues:** + +- COMPILE: Could not compile project using Cargo [\#118](https://github.com/nymtech/nym/issues/118) +- Wherever unbounded mpsc channel is used, prefer unbounded\_send\(\) over send\(\).await [\#90](https://github.com/nymtech/nym/issues/90) +- Add a `Send` method in nym-client [\#81](https://github.com/nymtech/nym/issues/81) +- Start on Tendermint integration [\#79](https://github.com/nymtech/nym/issues/79) +- Ditch DummyKeyPair [\#75](https://github.com/nymtech/nym/issues/75) +- Replace args with proper config files [\#69](https://github.com/nymtech/nym/issues/69) +- Fix incorrectly used Arcs [\#47](https://github.com/nymtech/nym/issues/47) +- nym-mixnode mandatory host option [\#26](https://github.com/nymtech/nym/issues/26) +- Create config struct for mixnode \(possibly also for client\) [\#21](https://github.com/nymtech/nym/issues/21) +- Reuse TCP socket connection between client and mixnodes [\#20](https://github.com/nymtech/nym/issues/20) +- Once implementation is available, wherever appropriate, replace `futures::lock::Mutex` with `futures::lock::RwLock` [\#9](https://github.com/nymtech/nym/issues/9) +- Check if RwLock on MixProcessingData is still needed [\#8](https://github.com/nymtech/nym/issues/8) +- Reuse TCP socket connection between mixnodes and providers [\#3](https://github.com/nymtech/nym/issues/3) +- Persistent socket connection with other mixes [\#2](https://github.com/nymtech/nym/issues/2) + +**Merged pull requests:** + +- Feature/client refactoring [\#128](https://github.com/nymtech/nym/pull/128) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/provider refactoring [\#125](https://github.com/nymtech/nym/pull/125) ([jstuczyn](https://github.com/jstuczyn)) +- all: fixing mis-spelling [\#123](https://github.com/nymtech/nym/pull/123) ([futurechimp](https://github.com/futurechimp)) +- Feature/further clippy fixes [\#121](https://github.com/nymtech/nym/pull/121) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/tokio tungstenite dependency fix [\#120](https://github.com/nymtech/nym/pull/120) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/config files cleanup [\#119](https://github.com/nymtech/nym/pull/119) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/config files [\#117](https://github.com/nymtech/nym/pull/117) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/un genericize keys [\#111](https://github.com/nymtech/nym/pull/111) ([futurechimp](https://github.com/futurechimp)) +- Feature/abci [\#110](https://github.com/nymtech/nym/pull/110) ([futurechimp](https://github.com/futurechimp)) +- Simplified the use of generics on identity keypair by using output types [\#109](https://github.com/nymtech/nym/pull/109) ([jstuczyn](https://github.com/jstuczyn)) + +## [v0.4.1](https://github.com/nymtech/nym/tree/v0.4.1) (2020-01-29) + +[Full Changelog](https://github.com/nymtech/nym/compare/v0.4.0...v0.4.1) + +**Closed issues:** + +- Change healthcheck to run on provided topology rather than pull one itself [\#95](https://github.com/nymtech/nym/issues/95) + +**Merged pull requests:** + +- Bugfix/healthcheck on provided topology [\#108](https://github.com/nymtech/nym/pull/108) ([jstuczyn](https://github.com/jstuczyn)) + ## [v0.4.0](https://github.com/nymtech/nym/tree/v0.4.0) (2020-01-28) [Full Changelog](https://github.com/nymtech/nym/compare/v0.4.0-rc.2...v0.4.0) diff --git a/Cargo.lock b/Cargo.lock index dbf0d16af9..994a105603 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,22 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "abci" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfb90d266d6ca61cea4d155dd5033c9516495f4f74f82faf1ca5c4feeb82577" +dependencies = [ + "byteorder", + "bytes 0.4.12", + "env_logger 0.7.1", + "futures 0.3.4", + "integer-encoding", + "log", + "protobuf", + "protobuf-codegen-pure", + "tokio 0.1.22", +] + [[package]] name = "addressing" version = "0.1.0" @@ -50,9 +67,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.6" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" +checksum = "d5e63fd144e18ba274ae7095c0197a870a7b9468abc801dd62f190d80817d2ec" dependencies = [ "memchr", ] @@ -86,14 +103,26 @@ checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] name = "assert-json-diff" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9881d306dee755eccf052d652b774a6b2861e86b4772f555262130e58e4f81d2" +checksum = "9c356497fd3417158bcb318266ac83c391219ca3a5fa659049f42e0041ab57d6" dependencies = [ + "extend", "serde", "serde_json", ] +[[package]] +name = "async-trait" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "750b1c38a1dfadd108da0f01c08f4cdc7ff1bb39b325f9c82cc972361780a6e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atty" version = "0.2.14" @@ -119,9 +148,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.42" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b1549d804b6c73f4817df2ba073709e96e426f12987127c48e6745568c350b" +checksum = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" dependencies = [ "backtrace-sys", "cfg-if", @@ -239,9 +268,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "byteorder" -version = "1.3.2" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "bytes" @@ -340,15 +369,24 @@ dependencies = [ [[package]] name = "colored" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8815e2ab78f3a59928fc32e141fbeece88320a240e43f47b2fd64ea3a88a5b3d" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" dependencies = [ "atty", "lazy_static", "winapi 0.3.8", ] +[[package]] +name = "config" +version = "0.1.0" +dependencies = [ + "handlebars", + "serde", + "toml", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -410,54 +448,47 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3aa945d63861bfe624b55d153a39684da1e8c0bc8fba932f7ee3a3c16cea3ca" +checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285" dependencies = [ "crossbeam-epoch", - "crossbeam-utils 0.7.0", + "crossbeam-utils", + "maybe-uninit", ] [[package]] name = "crossbeam-epoch" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5064ebdbf05ce3cb95e45c8b086f72263f4166b29b97f6baff7ef7fe047b55ac" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ - "autocfg 0.1.7", + "autocfg 1.0.0", "cfg-if", - "crossbeam-utils 0.7.0", + "crossbeam-utils", "lazy_static", + "maybe-uninit", "memoffset", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.1.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b" -dependencies = [ - "crossbeam-utils 0.6.6", -] - -[[package]] -name = "crossbeam-utils" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +checksum = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db" dependencies = [ "cfg-if", - "lazy_static", + "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "autocfg 0.1.7", + "autocfg 1.0.0", "cfg-if", "lazy_static", ] @@ -565,9 +596,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dtoa" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea57b42383d091c85abcc2706240b94ab2a8fa1fc81c10ff23c4de06e2a90b5e" +checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" [[package]] name = "either" @@ -598,12 +629,37 @@ dependencies = [ ] [[package]] -name = "error-chain" -version = "0.12.1" +name = "env_logger" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab49e9dcb602294bc42f9a7dfc9bc6e936fca4418ea300dbfb84fe16de0b7d9" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ - "version_check 0.1.5", + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "error-chain" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" +dependencies = [ + "version_check", +] + +[[package]] +name = "extend" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9db393664b0e6c6230a14115e7e798f80b70f54038dc21165db24c6b7f28fc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -697,9 +753,9 @@ checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" [[package]] name = "futures" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f16056ecbb57525ff698bb955162d0cd03bee84e6241c27ff75c08d8ca5987" +checksum = "5c329ae8753502fb44ae4fc2b622fa2a94652c41e795143765ba0927f92ab780" dependencies = [ "futures-channel", "futures-core", @@ -712,9 +768,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcae98ca17d102fd8a3603727b9259fcf7fa4239b603d2142926189bc8999b86" +checksum = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" dependencies = [ "futures-core", "futures-sink", @@ -722,9 +778,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79564c427afefab1dfb3298535b21eda083ef7935b4f0ecbfcb121f0aec10866" +checksum = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" [[package]] name = "futures-cpupool" @@ -738,9 +794,9 @@ dependencies = [ [[package]] name = "futures-executor" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e274736563f686a837a0568b478bdabfeaec2dca794b5649b04e2fe1627c231" +checksum = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" dependencies = [ "futures-core", "futures-task", @@ -749,15 +805,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e676577d229e70952ab25f3945795ba5b16d63ca794ca9d2c860e5595d20b5ff" +checksum = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" [[package]] name = "futures-macro" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e7c56c15537adb4f76d0b7a76ad131cb4d2f4f32d3b0bcabcbe1c7c5e87764" +checksum = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -767,21 +823,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171be33efae63c2d59e6dbba34186fe0d6394fb378069a76dfd80fdcffd43c16" +checksum = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6" [[package]] name = "futures-task" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bae52d6b29cf440e298856fec3965ee6fa71b06aa7495178615953fd669e5f9" +checksum = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" [[package]] name = "futures-util" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d66274fb76985d3c62c886d1da7ac4c0903a8c9f754e8fe0f35a6a6cc39e76" +checksum = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" dependencies = [ "futures-channel", "futures-core", @@ -839,7 +895,7 @@ dependencies = [ "bytes 0.4.12", "fnv", "futures 0.1.29", - "http", + "http 0.1.21", "indexmap", "log", "slab", @@ -847,6 +903,20 @@ dependencies = [ "tokio-io", ] +[[package]] +name = "handlebars" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba758d094d31274eb49d15da6f326b96bf3185239a6359bf684f3d5321148900" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error", + "serde", + "serde_json", +] + [[package]] name = "healthcheck" version = "0.1.0" @@ -854,7 +924,7 @@ dependencies = [ "addressing", "crypto", "directory-client", - "futures 0.3.1", + "futures 0.3.4", "itertools", "log", "mix-client", @@ -866,15 +936,15 @@ dependencies = [ "serde_derive", "sfw-provider-requests", "sphinx", - "tokio 0.2.10", + "tokio 0.2.12", "topology", ] [[package]] name = "hermit-abi" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" +checksum = "1010591b26bbfe835e9faeabeb11866061cc7dcebffd56ad7d0942d0e61aefd8" dependencies = [ "libc", ] @@ -910,6 +980,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b708cc7f06493459026f53b9a61a7a121a5d1ec6238dee58ea4941132b30156b" +dependencies = [ + "bytes 0.5.4", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.1.0" @@ -918,7 +999,7 @@ checksum = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" dependencies = [ "bytes 0.4.12", "futures 0.1.29", - "http", + "http 0.1.21", "tokio-buf", ] @@ -947,7 +1028,7 @@ dependencies = [ "futures 0.1.29", "futures-cpupool", "h2", - "http", + "http 0.1.21", "http-body", "httparse", "iovec", @@ -1004,20 +1085,29 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b54058f0a6ff80b6803da8faf8997cde53872b38f4023728f6830b06cd3c0dc" +checksum = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" dependencies = [ "autocfg 1.0.0", ] [[package]] name = "input_buffer" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1b822cc844905551931d6f81608ed5f50a79c1078a4e2b4d42dbc7c1eedfbf" +checksum = "19a8a95243d5a0398cae618ec29477c6e3cb631152be5c19481f80bc71559754" dependencies = [ - "bytes 0.4.12", + "bytes 0.5.4", +] + +[[package]] +name = "integer-encoding" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553a280155f89ff1c798520ccf877d1bc9571314164e998f8a7b5fe2d84cb45c" +dependencies = [ + "async-trait", ] [[package]] @@ -1046,9 +1136,9 @@ checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" [[package]] name = "jobserver" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b06c1b455f1cf4269a8cfc320ab930a810e2375a42af5075eb8a8b36405ce0" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" dependencies = [ "libc", ] @@ -1077,9 +1167,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.66" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" [[package]] name = "libgit2-sys" @@ -1135,6 +1225,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matches" version = "0.1.8" @@ -1149,9 +1245,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.3.0" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" [[package]] name = "memoffset" @@ -1180,9 +1276,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3f74f726ae935c3f514300cc6773a0c9492abc5e972d42ba0c0ebb88757625" +checksum = "aa679ff6578b1cddee93d7e82e263b94a575e0bfced07284eb0c037c1d2416a5" dependencies = [ "adler32", ] @@ -1261,15 +1357,15 @@ dependencies = [ "rand 0.7.3", "rand_distr", "sphinx", - "tokio 0.2.10", + "tokio 0.2.12", "topology", ] [[package]] name = "mockito" -version = "0.22.0" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e524e85ea7c80559354217a6470c14abc2411802a9996aed1821559b9e28e3c" +checksum = "ae82e6bad452dd42b0f4437414eae3c8c27b958a55dc6c198e351042c4e3024e" dependencies = [ "assert-json-diff", "colored", @@ -1277,12 +1373,21 @@ dependencies = [ "httparse", "lazy_static", "log", - "percent-encoding 1.0.1", + "percent-encoding 2.1.0", "rand 0.7.3", "regex", "serde_json", ] +[[package]] +name = "multi-tcp-client" +version = "0.1.0" +dependencies = [ + "futures 0.3.4", + "log", + "tokio 0.2.12", +] + [[package]] name = "native-tls" version = "0.2.3" @@ -1343,21 +1448,23 @@ dependencies = [ [[package]] name = "nym-client" -version = "0.4.1" +version = "0.5.0-rc.1" dependencies = [ "addressing", "bs58", "built", "clap", + "config", "crypto", "curve25519-dalek", "directory-client", "dirs", "dotenv", - "futures 0.3.1", + "futures 0.3.4", "healthcheck", "log", "mix-client", + "multi-tcp-client", "pem", "pemstore", "pretty_env_logger", @@ -1367,44 +1474,54 @@ dependencies = [ "serde_json", "sfw-provider-requests", "sphinx", - "tokio 0.2.10", + "tempfile", + "tokio 0.2.12", "tokio-tungstenite", "topology", - "tungstenite", ] [[package]] name = "nym-mixnode" -version = "0.4.1" +version = "0.5.0-rc.1" dependencies = [ "addressing", "bs58", "built", "clap", + "config", + "crypto", "curve25519-dalek", "directory-client", + "dirs", "dotenv", - "futures 0.3.1", + "futures 0.3.4", "log", + "multi-tcp-client", + "pemstore", "pretty_env_logger", + "serde", "sphinx", - "tokio 0.2.10", + "tempfile", + "tokio 0.2.12", ] [[package]] name = "nym-sfw-provider" -version = "0.4.1" +version = "0.5.0-rc.1" dependencies = [ "bs58", "built", "clap", + "config", "crypto", "curve25519-dalek", "directory-client", + "dirs", "dotenv", - "futures 0.3.1", + "futures 0.3.4", "hmac", "log", + "pemstore", "pretty_env_logger", "rand 0.7.3", "serde", @@ -1412,26 +1529,30 @@ dependencies = [ "sfw-provider-requests", "sha2", "sphinx", - "tokio 0.2.10", + "tempfile", + "tokio 0.2.12", ] [[package]] name = "nym-validator" -version = "0.4.1" +version = "0.5.0-rc.1" dependencies = [ + "abci", "built", + "byteorder", "clap", + "config", "crypto", "directory-client", + "dirs", "dotenv", - "futures 0.3.1", + "futures 0.3.4", "healthcheck", "log", "pretty_env_logger", "serde", - "serde_derive", - "tokio 0.2.10", - "toml", + "tempfile", + "tokio 0.2.12", "topology", ] @@ -1443,9 +1564,9 @@ checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" [[package]] name = "openssl" -version = "0.10.26" +version = "0.10.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3cc5799d98e1088141b8e01ff760112bbd9f19d850c124500566ca6901a585" +checksum = "973293749822d7dd6370d6da1e523b0d1db19f06c459134c658b2a4261378b52" dependencies = [ "bitflags", "cfg-if", @@ -1463,11 +1584,11 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-sys" -version = "0.9.53" +version = "0.9.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465d16ae7fc0e313318f7de5cecf57b2fbe7511fd213978b457e1c96ff46736f" +checksum = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" dependencies = [ - "autocfg 0.1.7", + "autocfg 1.0.0", "cc", "libc", "pkg-config", @@ -1534,19 +1655,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] -name = "pin-project" -version = "0.4.7" +name = "pest" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75fca1c4ff21f60ca2d37b80d72b63dab823a9d19d3cda3a81d18bc03f0ba8c5" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e5277315f6b4f27e0e6744feb5d5ba1891e7164871033d3c8344c6783b349a" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] +name = "pin-project" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6544cd4e4ecace61075a6ec78074beeef98d58aa9a3d07d053d993b2946a90d6" +checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" dependencies = [ "proc-macro2", "quote", @@ -1584,10 +1748,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" dependencies = [ "chrono", - "env_logger", + "env_logger 0.6.2", "log", ] +[[package]] +name = "proc-macro-error" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "proc-macro-error-attr" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn", + "syn-mid", +] + [[package]] name = "proc-macro-hack" version = "0.5.11" @@ -1607,23 +1797,48 @@ checksum = "369a6ed065f249a159e06c45752c780bda2fb53c995718f9e484d08daa9eb42e" [[package]] name = "proc-macro2" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +checksum = "6c09721c6781493a2a492a96b5a5bf19b65917fe6728884e7c44dd0c60ca3435" dependencies = [ "unicode-xid", ] +[[package]] +name = "protobuf" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6563a657a014b771e7f69f06447d88d8fbb5a215ffc4cab724afb3acedcc7701" + +[[package]] +name = "protobuf-codegen" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f1bbc6db30d5d3e730b6e2326e9a64a75ca9c80d6427d6f054dc8cacc79d225" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protobuf-codegen-pure" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5473ffa23d2ea3b9046764f1a22149791967aad946b6cbd99601e720afc4d0" +dependencies = [ + "protobuf", + "protobuf-codegen", +] + [[package]] name = "provider-client" version = "0.1.0" dependencies = [ - "futures 0.3.1", + "futures 0.3.4", "log", "pretty_env_logger", "sfw-provider-requests", "sphinx", - "tokio 0.2.10", + "tokio 0.2.12", ] [[package]] @@ -1838,9 +2053,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87" +checksum = "322cf97724bea3ee221b78fe25ac9c46114ebb51747ad5babd51a2fc6a8235a8" dependencies = [ "aho-corasick", "memchr", @@ -1850,9 +2065,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90" +checksum = "b28dfe3fe9badec5dbf0a79a9cccad2cfc2ab5484bdb3e44cbd1ae8b3ba2be06" [[package]] name = "remove_dir_all" @@ -1876,7 +2091,7 @@ dependencies = [ "encoding_rs", "flate2", "futures 0.1.29", - "http", + "http 0.1.21", "hyper", "hyper-tls", "log", @@ -1906,7 +2121,7 @@ dependencies = [ "base64 0.11.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils 0.7.0", + "crossbeam-utils", ] [[package]] @@ -1924,6 +2139,17 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ryu" version = "1.0.2" @@ -1932,9 +2158,9 @@ checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" [[package]] name = "schannel" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021" +checksum = "507a9e6e8ffe0a4e0ebb9a10293e62fdf7657c06f1b8bb07a8fcf697d2abf295" dependencies = [ "lazy_static", "winapi 0.3.8", @@ -1942,9 +2168,9 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" @@ -2004,9 +2230,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.45" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab8f15f15d6c41a154c1b128a22f2dfabe350ef53c40953d84e36155c91192b" +checksum = "9371ade75d4c2d6cb154141b9752cf3781ec9c05e0e5cf35060e1e70ee7b9c25" dependencies = [ "itoa", "ryu", @@ -2029,6 +2255,7 @@ dependencies = [ name = "sfw-provider-requests" version = "0.1.0" dependencies = [ + "bs58", "sphinx", ] @@ -2083,9 +2310,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e59e0c9fa00817912ae6e4e6e3c4fe04455e75699d06eedc7d85917ed8e8f4" +checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" [[package]] name = "socket2" @@ -2102,7 +2329,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=8424f4b0933b4a7f64ae828b2edefc5ba43a9e79#8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" +source = "git+https://github.com/nymtech/sphinx?rev=23f9c89b257ee0936e70afd682e9ed6a62e89eee#23f9c89b257ee0936e70afd682e9ed6a62e89eee" dependencies = [ "aes-ctr", "arrayref", @@ -2159,15 +2386,26 @@ checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" [[package]] name = "syn" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6f3550d8dff9ef7dc34d384ac6f107e5d31c8f57d9f28e0081503f547ac8f5" +checksum = "123bd9499cfb380418d509322d7a6d52e5315f064fe4b3ad18a53d6b92c07859" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] +[[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "synstructure" version = "0.12.3" @@ -2242,6 +2480,7 @@ dependencies = [ "futures 0.1.29", "mio", "num_cpus", + "tokio-codec", "tokio-current-thread", "tokio-executor", "tokio-io", @@ -2253,9 +2492,9 @@ dependencies = [ [[package]] name = "tokio" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fc73332507b971a5010664991a441b5ee0de92017f5a0e8b00fd684573045b" +checksum = "b34bee1facdc352fba10c9c58b654e6ecb6a2250167772bf86071f7c5f2f5061" dependencies = [ "bytes 0.5.4", "fnv", @@ -2287,10 +2526,21 @@ dependencies = [ ] [[package]] -name = "tokio-current-thread" -version = "0.1.6" +name = "tokio-codec" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16217cad7f1b840c5a97dfb3c43b0c871fef423a6e8d2118c604e843662a443" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.29", + "tokio-io", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" dependencies = [ "futures 0.1.29", "tokio-executor", @@ -2298,19 +2548,19 @@ dependencies = [ [[package]] name = "tokio-executor" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6df436c42b0c3330a82d855d2ef017cd793090ad550a6bc2184f4b933532ab" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" dependencies = [ - "crossbeam-utils 0.6.6", + "crossbeam-utils", "futures 0.1.29", ] [[package]] name = "tokio-io" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" dependencies = [ "bytes 0.4.12", "futures 0.1.29", @@ -2319,21 +2569,22 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a61f268a3db2acee8dcab514efc813dc6dbe8a00e86076f935f94304b59a7a" +checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" dependencies = [ + "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-reactor" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6732fe6b53c8d11178dcb77ac6d9682af27fc6d4cb87789449152e5377377146" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" dependencies = [ - "crossbeam-utils 0.6.6", + "crossbeam-utils", "futures 0.1.29", "lazy_static", "log", @@ -2348,9 +2599,9 @@ dependencies = [ [[package]] name = "tokio-sync" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06554cce1ae4a50f42fba8023918afa931413aded705b560e29600ccf7c6d76" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" dependencies = [ "fnv", "futures 0.1.29", @@ -2358,9 +2609,9 @@ dependencies = [ [[package]] name = "tokio-tcp" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d14b10654be682ac43efee27401d792507e30fd8d26389e1da3b185de2e4119" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" dependencies = [ "bytes 0.4.12", "futures 0.1.29", @@ -2372,13 +2623,13 @@ dependencies = [ [[package]] name = "tokio-threadpool" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c32ffea4827978e9aa392d2f743d973c1dfa3730a2ed3f22ce1e6984da848c" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" dependencies = [ "crossbeam-deque", "crossbeam-queue", - "crossbeam-utils 0.6.6", + "crossbeam-utils", "futures 0.1.29", "lazy_static", "log", @@ -2389,11 +2640,11 @@ dependencies = [ [[package]] name = "tokio-timer" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1739638e364e558128461fc1ad84d997702c8e31c2e6b18fb99842268199e827" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" dependencies = [ - "crossbeam-utils 0.6.6", + "crossbeam-utils", "futures 0.1.29", "slab", "tokio-executor", @@ -2401,13 +2652,14 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.10.0" -source = "git+https://github.com/snapview/tokio-tungstenite?rev=308d9680c0e59dd1e8651659a775c05df937934e#308d9680c0e59dd1e8651659a775c05df937934e" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b8fe88007ebc363512449868d7da4389c9400072a3f666f212c7280082882a" dependencies = [ - "futures 0.3.1", + "futures 0.3.4", "log", "pin-project", - "tokio 0.2.10", + "tokio 0.2.12", "tungstenite", ] @@ -2452,18 +2704,17 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0c2bd5aeb7dcd2bb32e472c8872759308495e5eccc942e929a513cd8d36110" +checksum = "cfea31758bf674f990918962e8e5f07071a3161bd7c4138ed23e416e1ac4264e" dependencies = [ "base64 0.11.0", "byteorder", - "bytes 0.4.12", - "http", + "bytes 0.5.4", + "http 0.2.0", "httparse", "input_buffer", "log", - "native-tls", "rand 0.7.3", "sha-1", "url 2.1.1", @@ -2476,13 +2727,19 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" +[[package]] +name = "ucd-trie" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f00ed7be0c1ff1e24f46c3d2af4859f7e863672ba3a6e92e7cff702bf9f06c2" + [[package]] name = "unicase" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.1", + "version_check", ] [[package]] @@ -2500,7 +2757,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" dependencies = [ - "smallvec 1.1.0", + "smallvec 1.2.0", ] [[package]] @@ -2579,12 +2836,6 @@ dependencies = [ "semver", ] -[[package]] -name = "version_check" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" - [[package]] name = "version_check" version = "0.9.1" diff --git a/Cargo.toml b/Cargo.toml index 22e94b3a0a..071c877bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ panic = "abort" members = [ "common/clients/directory-client", "common/clients/mix-client", + "common/clients/multi-tcp-client", "common/clients/provider-client", "common/clients/validator-client", "common/addressing", + "common/config", "common/crypto", "common/healthcheck", "common/pemstore", diff --git a/common/clients/directory-client/Cargo.toml b/common/clients/directory-client/Cargo.toml index a9e30636e0..a38459dcd2 100644 --- a/common/clients/directory-client/Cargo.toml +++ b/common/clients/directory-client/Cargo.toml @@ -16,4 +16,4 @@ serde = { version = "1.0.104", features = ["derive"] } topology = {path = "../../topology"} [dev-dependencies] -mockito = "0.22.0" +mockito = "0.23.0" diff --git a/common/clients/directory-client/src/lib.rs b/common/clients/directory-client/src/lib.rs index db641b952c..3acc52411d 100644 --- a/common/clients/directory-client/src/lib.rs +++ b/common/clients/directory-client/src/lib.rs @@ -54,7 +54,7 @@ impl DirectoryClient for Client { let presence_mix_nodes_post: PresenceMixNodesPost = PresenceMixNodesPost::new(config.base_url.clone()); let presence_providers_post: PresenceProvidersPost = - PresenceProvidersPost::new(config.base_url.clone()); + PresenceProvidersPost::new(config.base_url); Client { health_check, metrics_mixes, diff --git a/common/clients/directory-client/src/presence/mod.rs b/common/clients/directory-client/src/presence/mod.rs index b595953758..99d23ca130 100644 --- a/common/clients/directory-client/src/presence/mod.rs +++ b/common/clients/directory-client/src/presence/mod.rs @@ -29,11 +29,10 @@ impl NymTopology for Topology { }; let directory = Client::new(directory_config); - let topology = directory + directory .presence_topology .get() - .expect("Failed to retrieve network topology."); - topology + .expect("Failed to retrieve network topology.") } fn new_from_nodes( diff --git a/common/clients/mix-client/Cargo.toml b/common/clients/mix-client/Cargo.toml index ae7091c62c..40ddb9306d 100644 --- a/common/clients/mix-client/Cargo.toml +++ b/common/clients/mix-client/Cargo.toml @@ -18,6 +18,6 @@ addressing = {path = "../../addressing"} topology = {path = "../../topology"} ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } # sphinx = { path = "../../../../sphinx"} diff --git a/common/clients/mix-client/src/lib.rs b/common/clients/mix-client/src/lib.rs index fd883a54a4..2a754034ed 100644 --- a/common/clients/mix-client/src/lib.rs +++ b/common/clients/mix-client/src/lib.rs @@ -9,6 +9,7 @@ pub mod poisson; pub struct MixClient {} impl MixClient { + #[allow(clippy::new_without_default)] pub fn new() -> MixClient { MixClient {} } diff --git a/common/clients/mix-client/src/packet.rs b/common/clients/mix-client/src/packet.rs index f1ee832651..6b25afd6b1 100644 --- a/common/clients/mix-client/src/packet.rs +++ b/common/clients/mix-client/src/packet.rs @@ -3,10 +3,10 @@ use addressing::AddressTypeError; use sphinx::route::{Destination, DestinationAddressBytes, SURBIdentifier}; use sphinx::SphinxPacket; use std::net::SocketAddr; +use std::time; use topology::{NymTopology, NymTopologyError}; pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; -pub const LOOP_COVER_MESSAGE_AVERAGE_DELAY: f64 = 2.0; #[derive(Debug)] pub enum SphinxPacketEncapsulationError { @@ -39,29 +39,49 @@ impl From for SphinxPacketEncapsulationError { } } +#[deprecated(note = "please use loop_cover_message_route instead")] pub fn loop_cover_message( our_address: DestinationAddressBytes, surb_id: SURBIdentifier, topology: &T, + average_delay: time::Duration, ) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { let destination = Destination::new(our_address, surb_id); + #[allow(deprecated)] encapsulate_message( destination, LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), topology, - LOOP_COVER_MESSAGE_AVERAGE_DELAY, + average_delay, ) } +pub fn loop_cover_message_route( + our_address: DestinationAddressBytes, + surb_id: SURBIdentifier, + route: Vec, + average_delay: time::Duration, +) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { + let destination = Destination::new(our_address, surb_id); + + encapsulate_message_route( + destination, + LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), + route, + average_delay, + ) +} + +#[deprecated(note = "please use encapsulate_message_route instead")] pub fn encapsulate_message( recipient: Destination, message: Vec, topology: &T, - average_delay: f64, + average_delay: time::Duration, ) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { let mut providers = topology.providers(); - if providers.len() == 0 { + if providers.is_empty() { return Err(SphinxPacketEncapsulationError::NoValidProvidersError); } // unwrap is fine here as we asserted there is at least single provider @@ -69,7 +89,7 @@ pub fn encapsulate_message( let route = topology.route_to(provider)?; - let delays = sphinx::header::delays::generate(route.len(), average_delay); + let delays = sphinx::header::delays::generate_from_average_duration(route.len(), average_delay); // build the packet let packet = sphinx::SphinxPacket::new(message, &route[..], &recipient, &delays)?; @@ -80,3 +100,20 @@ pub fn encapsulate_message( Ok((first_node_address, packet)) } + +pub fn encapsulate_message_route( + recipient: Destination, + message: Vec, + route: Vec, + average_delay: time::Duration, +) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { + let delays = sphinx::header::delays::generate_from_average_duration(route.len(), average_delay); + + // build the packet + let packet = sphinx::SphinxPacket::new(message, &route[..], &recipient, &delays)?; + + let first_node_address = + addressing::socket_address_from_encoded_bytes(route.first().unwrap().address.to_bytes())?; + + Ok((first_node_address, packet)) +} diff --git a/common/clients/mix-client/src/poisson.rs b/common/clients/mix-client/src/poisson.rs index 6474d3c65b..14aaf7a9d4 100644 --- a/common/clients/mix-client/src/poisson.rs +++ b/common/clients/mix-client/src/poisson.rs @@ -1,9 +1,10 @@ use rand_distr::{Distribution, Exp}; +use std::time; -pub fn sample(average_delay: f64) -> f64 { +pub fn sample(average_duration: time::Duration) -> time::Duration { // this is our internal code used by our traffic streams // the error is only thrown if average delay is less than 0, which will never happen // so call to unwrap is perfectly safe here - let exp = Exp::new(1.0 / average_delay).unwrap(); - exp.sample(&mut rand::thread_rng()) + let exp = Exp::new(1.0 / average_duration.as_nanos() as f64).unwrap(); + time::Duration::from_nanos(exp.sample(&mut rand::thread_rng()).round() as u64) } diff --git a/common/clients/multi-tcp-client/Cargo.toml b/common/clients/multi-tcp-client/Cargo.toml new file mode 100644 index 0000000000..4887808519 --- /dev/null +++ b/common/clients/multi-tcp-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "multi-tcp-client" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = "0.3" +log = "0.4.8" +tokio = { version = "0.2", features = ["full"] } diff --git a/common/clients/multi-tcp-client/src/connection_manager/mod.rs b/common/clients/multi-tcp-client/src/connection_manager/mod.rs new file mode 100644 index 0000000000..bbd0f8bcd7 --- /dev/null +++ b/common/clients/multi-tcp-client/src/connection_manager/mod.rs @@ -0,0 +1,94 @@ +use crate::connection_manager::reconnector::ConnectionReconnector; +use crate::connection_manager::writer::ConnectionWriter; +use futures::task::Poll; +use futures::AsyncWriteExt; +use log::*; +use std::io; +use std::net::SocketAddr; +use std::time::Duration; + +mod reconnector; +mod writer; + +enum ConnectionState<'a> { + Writing(ConnectionWriter), + Reconnecting(ConnectionReconnector<'a>), +} + +pub(crate) struct ConnectionManager<'a> { + address: SocketAddr, + + maximum_reconnection_backoff: Duration, + reconnection_backoff: Duration, + + state: ConnectionState<'a>, +} + +impl<'a> ConnectionManager<'a> { + pub(crate) async fn new( + address: SocketAddr, + reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, + ) -> ConnectionManager<'a> { + // based on initial connection we will either have a writer or a reconnector + let state = match tokio::net::TcpStream::connect(address).await { + Ok(conn) => ConnectionState::Writing(ConnectionWriter::new(conn)), + Err(e) => { + warn!( + "failed to establish initial connection to {} ({}). Going into reconnection mode", + address, e + ); + ConnectionState::Reconnecting(ConnectionReconnector::new( + address, + reconnection_backoff, + maximum_reconnection_backoff, + )) + } + }; + + ConnectionManager { + address, + maximum_reconnection_backoff, + reconnection_backoff, + state, + } + } + + pub(crate) async fn send(&mut self, msg: &[u8]) -> io::Result<()> { + if let ConnectionState::Reconnecting(conn_reconnector) = &mut self.state { + // do a single poll rather than await for future to completely resolve + let new_connection = match futures::poll!(conn_reconnector) { + Poll::Pending => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "connection is broken - reconnection is in progress", + )) + } + Poll::Ready(conn) => conn, + }; + + debug!("Managed to reconnect to {}!", self.address); + self.state = ConnectionState::Writing(ConnectionWriter::new(new_connection)); + } + + // we must be in writing state if we are here, either by being here from beginning or just + // transitioning from reconnecting + if let ConnectionState::Writing(conn_writer) = &mut self.state { + return match conn_writer.write_all(msg).await { + // if we failed to write to connection we should reconnect + // TODO: is this true? can we fail to write to a connection while it still remains open and valid? + Ok(_) => Ok(()), + Err(e) => { + trace!("Creating connection reconnector!"); + self.state = ConnectionState::Reconnecting(ConnectionReconnector::new( + self.address, + self.reconnection_backoff, + self.maximum_reconnection_backoff, + )); + Err(e) + } + }; + }; + unreachable!() + } +} diff --git a/common/clients/multi-tcp-client/src/connection_manager/reconnector.rs b/common/clients/multi-tcp-client/src/connection_manager/reconnector.rs new file mode 100644 index 0000000000..7c87113d52 --- /dev/null +++ b/common/clients/multi-tcp-client/src/connection_manager/reconnector.rs @@ -0,0 +1,78 @@ +use futures::future::BoxFuture; +use futures::FutureExt; +use log::*; +use std::future::Future; +use std::io; +use std::net::SocketAddr; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; + +pub(crate) struct ConnectionReconnector<'a> { + address: SocketAddr, + connection: BoxFuture<'a, io::Result>, + + current_retry_attempt: u32, + + current_backoff_delay: tokio::time::Delay, + maximum_reconnection_backoff: Duration, + + initial_reconnection_backoff: Duration, +} + +impl<'a> ConnectionReconnector<'a> { + pub(crate) fn new( + address: SocketAddr, + initial_reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, + ) -> ConnectionReconnector<'a> { + ConnectionReconnector { + address, + connection: tokio::net::TcpStream::connect(address).boxed(), + current_backoff_delay: tokio::time::delay_for(Duration::new(0, 0)), // if we can re-establish connection on first try without any backoff that's perfect + current_retry_attempt: 0, + maximum_reconnection_backoff, + initial_reconnection_backoff, + } + } +} + +impl<'a> Future for ConnectionReconnector<'a> { + type Output = tokio::net::TcpStream; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + // see if we are still in exponential backoff + if Pin::new(&mut self.current_backoff_delay) + .poll(cx) + .is_pending() + { + return Poll::Pending; + }; + + // see if we managed to resolve the connection yet + match Pin::new(&mut self.connection).poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => { + warn!( + "we failed to re-establish connection to {} - {:?} (attempt {})", + self.address, e, self.current_retry_attempt + ); + self.current_retry_attempt += 1; + + // we failed to re-establish connection - continue exponential backoff + let next_delay = std::cmp::min( + self.maximum_reconnection_backoff, + 2_u32.pow(self.current_retry_attempt) * self.initial_reconnection_backoff, + ); + + self.current_backoff_delay + .reset(tokio::time::Instant::now() + next_delay); + + self.connection = tokio::net::TcpStream::connect(self.address).boxed(); + + Poll::Pending + } + Poll::Ready(Ok(conn)) => Poll::Ready(conn), + } + } +} diff --git a/common/clients/multi-tcp-client/src/connection_manager/writer.rs b/common/clients/multi-tcp-client/src/connection_manager/writer.rs new file mode 100644 index 0000000000..32d76694f3 --- /dev/null +++ b/common/clients/multi-tcp-client/src/connection_manager/writer.rs @@ -0,0 +1,46 @@ +use futures::task::{Context, Poll}; +use futures::AsyncWrite; +use std::io; +use std::pin::Pin; +use tokio::prelude::*; + +pub(crate) struct ConnectionWriter { + connection: tokio::net::TcpStream, +} + +impl ConnectionWriter { + pub(crate) fn new(connection: tokio::net::TcpStream) -> Self { + ConnectionWriter { connection } + } +} + +impl AsyncWrite for ConnectionWriter { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + use tokio::io::AsyncWrite; + + let mut read_buf = [0; 1]; + match Pin::new(&mut self.connection).poll_read(cx, &mut read_buf) { + // at least try the obvious check for if connection is definitely down + // TODO: can we do anything else? + Poll::Ready(Ok(n)) if n == 0 => Poll::Ready(Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "trying to write to closed connection", + ))), + _ => Pin::new(&mut self.connection).poll_write(cx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use tokio::io::AsyncWrite; + Pin::new(&mut self.connection).poll_flush(cx) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + use tokio::io::AsyncWrite; + Pin::new(&mut self.connection).poll_shutdown(cx) + } +} diff --git a/common/clients/multi-tcp-client/src/lib.rs b/common/clients/multi-tcp-client/src/lib.rs new file mode 100644 index 0000000000..34b64e77a5 --- /dev/null +++ b/common/clients/multi-tcp-client/src/lib.rs @@ -0,0 +1,226 @@ +use crate::connection_manager::ConnectionManager; +use log::*; +use std::collections::HashMap; +use std::io; +use std::net::SocketAddr; +use std::time::Duration; + +mod connection_manager; + +pub struct Config { + initial_endpoints: Vec, + initial_reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, +} + +impl Config { + pub fn new( + initial_endpoints: Vec, + initial_reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, + ) -> Self { + Config { + initial_endpoints, + initial_reconnection_backoff, + maximum_reconnection_backoff, + } + } +} + +pub struct Client<'a> { + connections_managers: HashMap>, + maximum_reconnection_backoff: Duration, + initial_reconnection_backoff: Duration, +} + +impl<'a> Client<'a> { + pub async fn new(config: Config) -> Client<'a> { + let mut connections_managers = HashMap::new(); + for initial_endpoint in config.initial_endpoints { + connections_managers.insert( + initial_endpoint, + ConnectionManager::new( + initial_endpoint, + config.initial_reconnection_backoff, + config.maximum_reconnection_backoff, + ) + .await, + ); + } + + Client { + connections_managers, + initial_reconnection_backoff: config.maximum_reconnection_backoff, + maximum_reconnection_backoff: config.initial_reconnection_backoff, + } + } + + pub async fn send(&mut self, address: SocketAddr, message: &[u8]) -> io::Result<()> { + if !self.connections_managers.contains_key(&address) { + info!( + "There is no existing connection to {:?} - it will be established now", + address + ); + + // TODO: now we're blocking to establish TCP connection this need to be changed + // so that other connections could progress + let new_manager = ConnectionManager::new( + address, + self.initial_reconnection_backoff, + self.maximum_reconnection_backoff, + ) + .await; + + self.connections_managers.insert(address, new_manager); + } + + // to optimize later by using channels and separate tokio tasks for each connection handler + // because right now say we want to write to addresses A and B - + // We have to wait until we're done dealing with A before we can do anything with B + self.connections_managers + .get_mut(&address) + .unwrap() + .send(&message) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str; + use std::time; + use tokio::prelude::*; + + const SERVER_MSG_LEN: usize = 16; + const CLOSE_MESSAGE: [u8; SERVER_MSG_LEN] = [0; SERVER_MSG_LEN]; + + struct DummyServer { + received_buf: Vec>, + listener: tokio::net::TcpListener, + } + + impl DummyServer { + async fn new(address: SocketAddr) -> Self { + DummyServer { + received_buf: Vec::new(), + listener: tokio::net::TcpListener::bind(address).await.unwrap(), + } + } + + fn get_received(&self) -> Vec> { + self.received_buf.clone() + } + + // this is only used in tests so slightly higher logging levels are fine + async fn listen_until(mut self, close_message: &[u8]) -> Self { + let (mut socket, _) = self.listener.accept().await.unwrap(); + loop { + let mut buf = [0u8; SERVER_MSG_LEN]; + match socket.read(&mut buf).await { + Ok(n) if n == 0 => { + info!("Remote connection closed"); + return self; + } + Ok(n) => { + info!("received ({}) - {:?}", n, str::from_utf8(buf[..n].as_ref())); + + if buf[..n].as_ref() == close_message { + info!("closing..."); + socket.shutdown(std::net::Shutdown::Both).unwrap(); + return self; + } else { + self.received_buf.push(buf[..n].to_vec()); + } + } + Err(e) => { + panic!("failed to read from socket; err = {:?}", e); + } + }; + } + } + } + + #[test] + fn client_reconnects_to_server_after_it_went_down() { + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let addr = "127.0.0.1:6000".parse().unwrap(); + let reconnection_backoff = Duration::from_secs(1); + let client_config = + Config::new(vec![addr], reconnection_backoff, 10 * reconnection_backoff); + + let messages_to_send = vec![[1u8; SERVER_MSG_LEN], [2; SERVER_MSG_LEN]]; + + let dummy_server = rt.block_on(DummyServer::new(addr)); + let finished_dummy_server_future = rt.spawn(dummy_server.listen_until(&CLOSE_MESSAGE)); + + let mut c = rt.block_on(Client::new(client_config)); + + for msg in &messages_to_send { + rt.block_on(c.send(addr, msg)).unwrap(); + } + + // kill server + rt.block_on(c.send(addr, &CLOSE_MESSAGE)).unwrap(); + let received_messages = rt + .block_on(finished_dummy_server_future) + .unwrap() + .get_received(); + + assert_eq!(received_messages, messages_to_send); + + // try to send - go into reconnection + let post_kill_message = [3u8; SERVER_MSG_LEN]; + + // we are trying to send to killed server + assert!(rt.block_on(c.send(addr, &post_kill_message)).is_err()); + + let new_dummy_server = rt.block_on(DummyServer::new(addr)); + let new_server_future = rt.spawn(new_dummy_server.listen_until(&CLOSE_MESSAGE)); + + // keep sending after we leave reconnection backoff and reconnect + loop { + if rt.block_on(c.send(addr, &post_kill_message)).is_ok() { + break; + } + rt.block_on( + async move { tokio::time::delay_for(time::Duration::from_millis(50)).await }, + ); + } + + // kill the server to ensure it actually got the message + rt.block_on(c.send(addr, &CLOSE_MESSAGE)).unwrap(); + let new_received_messages = rt.block_on(new_server_future).unwrap().get_received(); + assert_eq!(post_kill_message.to_vec(), new_received_messages[0]); + } + + #[test] + fn server_receives_all_sent_messages_when_up() { + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let addr = "127.0.0.1:6001".parse().unwrap(); + let reconnection_backoff = Duration::from_secs(2); + let client_config = + Config::new(vec![addr], reconnection_backoff, 10 * reconnection_backoff); + + let messages_to_send = vec![[1u8; SERVER_MSG_LEN], [2; SERVER_MSG_LEN]]; + + let dummy_server = rt.block_on(DummyServer::new(addr)); + let finished_dummy_server_future = rt.spawn(dummy_server.listen_until(&CLOSE_MESSAGE)); + + let mut c = rt.block_on(Client::new(client_config)); + + for msg in &messages_to_send { + rt.block_on(c.send(addr, msg)).unwrap(); + } + + rt.block_on(c.send(addr, &CLOSE_MESSAGE)).unwrap(); + + // the server future should have already been resolved + let received_messages = rt + .block_on(finished_dummy_server_future) + .unwrap() + .get_received(); + + assert_eq!(received_messages, messages_to_send); + } +} diff --git a/common/clients/provider-client/Cargo.toml b/common/clients/provider-client/Cargo.toml index 786b3c2a29..f5ecafe9cc 100644 --- a/common/clients/provider-client/Cargo.toml +++ b/common/clients/provider-client/Cargo.toml @@ -16,4 +16,4 @@ tokio = { version = "0.2", features = ["full"] } sfw-provider-requests = { path = "../../../sfw-provider/sfw-provider-requests" } ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } diff --git a/common/clients/provider-client/src/lib.rs b/common/clients/provider-client/src/lib.rs index e24f893ac9..7191941a93 100644 --- a/common/clients/provider-client/src/lib.rs +++ b/common/clients/provider-client/src/lib.rs @@ -82,14 +82,14 @@ impl ProviderClient { } pub async fn retrieve_messages(&self) -> Result>, ProviderClientError> { - let auth_token = match self.auth_token { - Some(token) => token, + let auth_token = match self.auth_token.as_ref() { + Some(token) => token.clone(), None => { return Err(ProviderClientError::EmptyAuthTokenError); } }; - let pull_request = PullRequest::new(self.our_address, auth_token); + let pull_request = PullRequest::new(self.our_address.clone(), auth_token); let bytes = pull_request.to_bytes(); let response = self.send_request(bytes).await?; @@ -103,7 +103,7 @@ impl ProviderClient { return Err(ProviderClientError::ClientAlreadyRegisteredError); } - let register_request = RegisterRequest::new(self.our_address); + let register_request = RegisterRequest::new(self.our_address.clone()); let bytes = register_request.to_bytes(); let response = self.send_request(bytes).await?; diff --git a/common/config/Cargo.toml b/common/config/Cargo.toml new file mode 100644 index 0000000000..d955cd93d8 --- /dev/null +++ b/common/config/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "config" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +handlebars = "3.0.1" +serde = { version = "1.0.104", features = ["derive"] } +toml = "0.5.6" diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs new file mode 100644 index 0000000000..f5c8c0c032 --- /dev/null +++ b/common/config/src/lib.rs @@ -0,0 +1,64 @@ +use handlebars::Handlebars; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::path::PathBuf; +use std::{fs, io}; + +pub trait NymConfig: Default + Serialize + DeserializeOwned { + fn template() -> &'static str; + + fn config_file_name() -> String; + + fn default_root_directory() -> PathBuf; + + // default, most probable, implementations; can be easily overridden where required + fn default_config_directory(id: Option<&str>) -> PathBuf { + Self::default_root_directory() + .join(id.unwrap_or("")) + .join("config") + } + + fn default_data_directory(id: Option<&str>) -> PathBuf { + Self::default_root_directory() + .join(id.unwrap_or("")) + .join("data") + } + + fn root_directory(&self) -> PathBuf; + fn config_directory(&self) -> PathBuf; + fn data_directory(&self) -> PathBuf; + + fn save_to_file(&self, custom_location: Option) -> io::Result<()> { + let reg = Handlebars::new(); + // it's whoever is implementing the trait responsibility to make sure you can execute your own template on your data + let templated_config = reg.render_template(Self::template(), self).unwrap(); + + // make sure the whole directory structure actually exists + match custom_location.clone() { + Some(loc) => { + if let Some(parent_dir) = loc.parent() { + fs::create_dir_all(parent_dir) + } else { + Ok(()) + } + } + None => fs::create_dir_all(self.config_directory()), + }?; + + fs::write( + custom_location + .unwrap_or_else(|| self.config_directory().join(Self::config_file_name())), + templated_config, + ) + } + + fn load_from_file(custom_location: Option, id: Option<&str>) -> io::Result { + let config_contents = fs::read_to_string( + custom_location + .unwrap_or_else(|| Self::default_config_directory(id).join("config.toml")), + )?; + + toml::from_str(&config_contents) + .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) + } +} diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 9cda304ed6..f2a29d0ec2 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -15,4 +15,4 @@ rand = "0.7.2" rand_os = "0.1" ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } \ No newline at end of file +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } \ No newline at end of file diff --git a/common/crypto/src/encryption/mod.rs b/common/crypto/src/encryption/mod.rs index b36ddf1af7..03ad7de8d1 100644 --- a/common/crypto/src/encryption/mod.rs +++ b/common/crypto/src/encryption/mod.rs @@ -1,39 +1,138 @@ -use crate::PemStorable; +use crate::{PemStorableKey, PemStorableKeyPair}; +use curve25519_dalek::montgomery::MontgomeryPoint; +use curve25519_dalek::scalar::Scalar; -pub mod x25519; +// TODO: ensure this is a proper name for this considering we are not implementing entire DH here -pub trait MixnetEncryptionKeyPair -where - Priv: MixnetEncryptionPrivateKey, - Pub: MixnetEncryptionPublicKey, -{ - fn new() -> Self; - fn private_key(&self) -> &Priv; - fn public_key(&self) -> &Pub; - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self; +const CURVE_GENERATOR: MontgomeryPoint = curve25519_dalek::constants::X25519_BASEPOINT; - // TODO: encryption related methods +pub struct KeyPair { + pub(crate) private_key: PrivateKey, + pub(crate) public_key: PublicKey, } -pub trait MixnetEncryptionPublicKey: - Sized + PemStorable + for<'a> From<&'a ::PrivateKeyMaterial> -{ - // we need to couple public and private keys together - type PrivateKeyMaterial: MixnetEncryptionPrivateKey; +impl KeyPair { + pub fn new() -> Self { + let mut rng = rand_os::OsRng::new().unwrap(); + let private_key_value = Scalar::random(&mut rng); + let public_key_value = CURVE_GENERATOR * private_key_value; - fn to_bytes(&self) -> Vec; - fn from_bytes(b: &[u8]) -> Self; -} - -pub trait MixnetEncryptionPrivateKey: Sized + PemStorable { - // we need to couple public and private keys together - type PublicKeyMaterial: MixnetEncryptionPublicKey; - - /// Returns the associated public key - fn public_key(&self) -> Self::PublicKeyMaterial { - self.into() + KeyPair { + private_key: PrivateKey(private_key_value), + public_key: PublicKey(public_key_value), + } } - fn to_bytes(&self) -> Vec; - fn from_bytes(b: &[u8]) -> Self; + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { + KeyPair { + private_key: PrivateKey::from_bytes(priv_bytes), + public_key: PublicKey::from_bytes(pub_bytes), + } + } +} + +impl Default for KeyPair { + fn default() -> Self { + KeyPair::new() + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = PrivateKey; + type PublicPemKey = PublicKey; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PrivateKey(pub Scalar); + +impl PrivateKey { + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + pub fn from_bytes(b: &[u8]) -> Self { + let mut bytes = [0; 32]; + bytes.copy_from_slice(&b[..]); + // due to trait restriction we have no choice but to panic if this fails + let key = Scalar::from_canonical_bytes(bytes).unwrap(); + Self(key) + } + + pub fn inner(&self) -> Scalar { + self.0 + } +} + +impl PemStorableKey for PrivateKey { + fn pem_type(&self) -> String { + String::from("X25519 PRIVATE KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PublicKey(pub MontgomeryPoint); + +impl<'a> From<&'a PrivateKey> for PublicKey { + fn from(pk: &'a PrivateKey) -> Self { + PublicKey(CURVE_GENERATOR * pk.0) + } +} + +impl PublicKey { + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + pub fn from_bytes(b: &[u8]) -> Self { + let mut bytes = [0; 32]; + bytes.copy_from_slice(&b[..]); + let key = MontgomeryPoint(bytes); + Self(key) + } + + pub fn inner(&self) -> MontgomeryPoint { + self.0 + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string(val: String) -> Self { + Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) + } +} + +impl PemStorableKey for PublicKey { + fn pem_type(&self) -> String { + String::from("X25519 PUBLIC KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } } diff --git a/common/crypto/src/encryption/x25519.rs b/common/crypto/src/encryption/x25519.rs deleted file mode 100644 index 44d86783da..0000000000 --- a/common/crypto/src/encryption/x25519.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::encryption::{ - MixnetEncryptionKeyPair, MixnetEncryptionPrivateKey, MixnetEncryptionPublicKey, -}; -use crate::PemStorable; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; - -// TODO: ensure this is a proper name for this considering we are not implementing entire DH here - -const CURVE_GENERATOR: MontgomeryPoint = curve25519_dalek::constants::X25519_BASEPOINT; - -pub struct KeyPair { - pub(crate) private_key: PrivateKey, - pub(crate) public_key: PublicKey, -} - -impl MixnetEncryptionKeyPair for KeyPair { - fn new() -> Self { - let mut rng = rand_os::OsRng::new().unwrap(); - let private_key_value = Scalar::random(&mut rng); - let public_key_value = CURVE_GENERATOR * private_key_value; - - KeyPair { - private_key: PrivateKey(private_key_value), - public_key: PublicKey(public_key_value), - } - } - - fn private_key(&self) -> &PrivateKey { - &self.private_key - } - - fn public_key(&self) -> &PublicKey { - &self.public_key - } - - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - KeyPair { - private_key: PrivateKey::from_bytes(priv_bytes), - public_key: PublicKey::from_bytes(pub_bytes), - } - } -} - -// COPY IS DERIVED ONLY TEMPORARILY UNTIL https://github.com/nymtech/nym/issues/47 is fixed -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct PrivateKey(pub Scalar); - -impl MixnetEncryptionPrivateKey for PrivateKey { - type PublicKeyMaterial = PublicKey; - - fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - // due to trait restriction we have no choice but to panic if this fails - let key = Scalar::from_canonical_bytes(bytes).unwrap(); - Self(key) - } -} - -impl PemStorable for PrivateKey { - fn pem_type(&self) -> String { - String::from("X25519 PRIVATE KEY") - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PublicKey(pub MontgomeryPoint); - -impl<'a> From<&'a PrivateKey> for PublicKey { - fn from(pk: &'a PrivateKey) -> Self { - PublicKey(CURVE_GENERATOR * pk.0) - } -} - -impl MixnetEncryptionPublicKey for PublicKey { - type PrivateKeyMaterial = PrivateKey; - - fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - let key = MontgomeryPoint(bytes); - Self(key) - } -} - -impl PemStorable for PublicKey { - fn pem_type(&self) -> String { - String::from("X25519 PUBLIC KEY") - } -} diff --git a/common/crypto/src/identity/mod.rs b/common/crypto/src/identity/mod.rs index cf39000e5d..9b7a396671 100644 --- a/common/crypto/src/identity/mod.rs +++ b/common/crypto/src/identity/mod.rs @@ -1,162 +1,138 @@ -use crate::encryption::{ - MixnetEncryptionKeyPair, MixnetEncryptionPrivateKey, MixnetEncryptionPublicKey, -}; -use crate::{encryption, PemStorable}; +use crate::{encryption, PemStorableKey, PemStorableKeyPair}; use bs58; use curve25519_dalek::scalar::Scalar; use sphinx::route::DestinationAddressBytes; -pub trait MixnetIdentityKeyPair: Clone -where - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, -{ - fn new() -> Self; - fn private_key(&self) -> &Priv; - fn public_key(&self) -> &Pub; - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self; - - // TODO: signing related methods -} - -pub trait MixnetIdentityPublicKey: - Sized - + PemStorable - + Clone - + for<'a> From<&'a ::PrivateKeyMaterial> -{ - // we need to couple public and private keys together - type PrivateKeyMaterial: MixnetIdentityPrivateKey; - - fn derive_address(&self) -> DestinationAddressBytes; - fn to_bytes(&self) -> Vec; - fn from_bytes(b: &[u8]) -> Self; -} - -pub trait MixnetIdentityPrivateKey: Sized + PemStorable + Clone { - // we need to couple public and private keys together - type PublicKeyMaterial: MixnetIdentityPublicKey; - - /// Returns the associated public key - fn public_key(&self) -> Self::PublicKeyMaterial { - self.into() - } - - fn to_bytes(&self) -> Vec; - fn from_bytes(b: &[u8]) -> Self; -} - -// same for validator - // for time being define a dummy identity using x25519 encryption keys (as we've done so far) // and replace it with proper keys, like ed25519 later on #[derive(Clone)] -pub struct DummyMixIdentityKeyPair { - pub private_key: DummyMixIdentityPrivateKey, - pub public_key: DummyMixIdentityPublicKey, +pub struct MixIdentityKeyPair { + pub private_key: MixIdentityPrivateKey, + pub public_key: MixIdentityPublicKey, } -impl MixnetIdentityKeyPair - for DummyMixIdentityKeyPair -{ - fn new() -> Self { - let keypair = encryption::x25519::KeyPair::new(); - DummyMixIdentityKeyPair { - private_key: DummyMixIdentityPrivateKey(keypair.private_key), - public_key: DummyMixIdentityPublicKey(keypair.public_key), +impl MixIdentityKeyPair { + pub fn new() -> Self { + let keypair = encryption::KeyPair::new(); + MixIdentityKeyPair { + private_key: MixIdentityPrivateKey(keypair.private_key), + public_key: MixIdentityPublicKey(keypair.public_key), } } - fn private_key(&self) -> &DummyMixIdentityPrivateKey { + pub fn private_key(&self) -> &MixIdentityPrivateKey { &self.private_key } - fn public_key(&self) -> &DummyMixIdentityPublicKey { + pub fn public_key(&self) -> &MixIdentityPublicKey { &self.public_key } - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - DummyMixIdentityKeyPair { - private_key: DummyMixIdentityPrivateKey::from_bytes(priv_bytes), - public_key: DummyMixIdentityPublicKey::from_bytes(pub_bytes), + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { + MixIdentityKeyPair { + private_key: MixIdentityPrivateKey::from_bytes(priv_bytes), + public_key: MixIdentityPublicKey::from_bytes(pub_bytes), } } } +impl Default for MixIdentityKeyPair { + fn default() -> Self { + MixIdentityKeyPair::new() + } +} + +impl PemStorableKeyPair for MixIdentityKeyPair { + type PrivatePemKey = MixIdentityPrivateKey; + type PublicPemKey = MixIdentityPublicKey; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + #[derive(Debug, Clone, Eq, PartialEq)] -pub struct DummyMixIdentityPublicKey(encryption::x25519::PublicKey); +pub struct MixIdentityPublicKey(encryption::PublicKey); -impl MixnetIdentityPublicKey for DummyMixIdentityPublicKey { - type PrivateKeyMaterial = DummyMixIdentityPrivateKey; - - fn derive_address(&self) -> DestinationAddressBytes { +impl MixIdentityPublicKey { + pub fn derive_address(&self) -> DestinationAddressBytes { let mut temporary_address = [0u8; 32]; let public_key_bytes = self.to_bytes(); temporary_address.copy_from_slice(&public_key_bytes[..]); - temporary_address + DestinationAddressBytes::from_bytes(temporary_address) } - fn to_bytes(&self) -> Vec { + pub fn to_bytes(&self) -> Vec { self.0.to_bytes() } - fn from_bytes(b: &[u8]) -> Self { - Self(encryption::x25519::PublicKey::from_bytes(b)) + pub fn from_bytes(b: &[u8]) -> Self { + Self(encryption::PublicKey::from_bytes(b)) } -} -impl PemStorable for DummyMixIdentityPublicKey { - fn pem_type(&self) -> String { - format!("DUMMY KEY BASED ON {}", self.0.pem_type()) - } -} - -impl DummyMixIdentityPublicKey { pub fn to_base58_string(&self) -> String { bs58::encode(&self.to_bytes()).into_string() } - #[allow(dead_code)] - fn from_base58_string(val: String) -> Self { + pub fn from_base58_string(val: String) -> Self { Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) } } -// COPY IS DERIVED ONLY TEMPORARILY UNTIL https://github.com/nymtech/nym/issues/47 is fixed -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct DummyMixIdentityPrivateKey(pub encryption::x25519::PrivateKey); +impl PemStorableKey for MixIdentityPublicKey { + fn pem_type(&self) -> String { + format!("DUMMY KEY BASED ON {}", self.0.pem_type()) + } -impl<'a> From<&'a DummyMixIdentityPrivateKey> for DummyMixIdentityPublicKey { - fn from(pk: &'a DummyMixIdentityPrivateKey) -> Self { - let private_ref = &pk.0; - let public: encryption::x25519::PublicKey = private_ref.into(); - DummyMixIdentityPublicKey(public) + fn to_bytes(&self) -> Vec { + self.to_bytes() } } -impl MixnetIdentityPrivateKey for DummyMixIdentityPrivateKey { - type PublicKeyMaterial = DummyMixIdentityPublicKey; +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MixIdentityPrivateKey(pub encryption::PrivateKey); - fn to_bytes(&self) -> Vec { +impl<'a> From<&'a MixIdentityPrivateKey> for MixIdentityPublicKey { + fn from(pk: &'a MixIdentityPrivateKey) -> Self { + let private_ref = &pk.0; + let public: encryption::PublicKey = private_ref.into(); + MixIdentityPublicKey(public) + } +} + +impl MixIdentityPrivateKey { + pub fn to_bytes(&self) -> Vec { self.0.to_bytes() } - fn from_bytes(b: &[u8]) -> Self { - Self(encryption::x25519::PrivateKey::from_bytes(b)) + pub fn from_bytes(b: &[u8]) -> Self { + Self(encryption::PrivateKey::from_bytes(b)) } } // TODO: this will be implemented differently by using the proper trait -impl DummyMixIdentityPrivateKey { - pub fn as_scalar(self) -> Scalar { - let encryption_key = self.0; +impl MixIdentityPrivateKey { + pub fn as_scalar(&self) -> Scalar { + let encryption_key = &self.0; encryption_key.0 } } -impl PemStorable for DummyMixIdentityPrivateKey { +impl PemStorableKey for MixIdentityPrivateKey { fn pem_type(&self) -> String { format!("DUMMY KEY BASED ON {}", self.0.pem_type()) } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } } diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index e1f5f2d17c..bb8a61b20c 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -1,9 +1,22 @@ pub mod encryption; pub mod identity; -// TODO: this trait will need to be moved elsewhere, probably to some 'persistence' crate -// but since it will need to be used by all identities, it's not really appropriate if it lived in nym-client +// TODO: ideally those trait should be moved to 'pemstore' crate, however, that would cause +// circular dependency. The best solution would be to remove dependency on 'crypto' from +// pemstore by using either dynamic dispatch or generics - perhaps this should be done +// at some point during one of refactors. -pub trait PemStorable { +pub trait PemStorableKey { fn pem_type(&self) -> String; + fn to_bytes(&self) -> Vec; +} + +pub trait PemStorableKeyPair { + type PrivatePemKey: PemStorableKey; + type PublicPemKey: PemStorableKey; + + fn private_key(&self) -> &Self::PrivatePemKey; + fn public_key(&self) -> &Self::PublicPemKey; + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self; } diff --git a/common/healthcheck/Cargo.toml b/common/healthcheck/Cargo.toml index f972b56af1..b8f7212792 100644 --- a/common/healthcheck/Cargo.toml +++ b/common/healthcheck/Cargo.toml @@ -27,7 +27,7 @@ sfw-provider-requests = { path = "../../sfw-provider/sfw-provider-requests" } topology = {path = "../topology" } ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } # sphinx = { path = "../../../sphinx"} [dev-dependencies] diff --git a/common/healthcheck/src/lib.rs b/common/healthcheck/src/lib.rs index 7cd97692db..04196db3ed 100644 --- a/common/healthcheck/src/lib.rs +++ b/common/healthcheck/src/lib.rs @@ -1,10 +1,7 @@ use crate::result::HealthCheckResult; -use crypto::identity::{MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey}; -use directory_client::requests::presence_topology_get::PresenceTopologyGetRequester; -use directory_client::DirectoryClient; -use log::{debug, error, info, trace}; +use crypto::identity::MixIdentityKeyPair; +use log::trace; use std::fmt::{Error, Formatter}; -use std::marker::PhantomData; use std::time::Duration; use topology::{NymTopology, NymTopologyError}; @@ -36,38 +33,22 @@ impl From for HealthCheckerError { } } -pub struct HealthChecker -where - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, -{ +pub struct HealthChecker { num_test_packets: usize, resolution_timeout: Duration, - identity_keypair: IDPair, - - _phantom_private: PhantomData, - _phantom_public: PhantomData, + identity_keypair: MixIdentityKeyPair, } -impl HealthChecker -where - IDPair: crypto::identity::MixnetIdentityKeyPair, - Priv: crypto::identity::MixnetIdentityPrivateKey, - Pub: crypto::identity::MixnetIdentityPublicKey, -{ +impl HealthChecker { pub fn new( - resolution_timeout_f64: f64, + resolution_timeout: Duration, num_test_packets: usize, - identity_keypair: IDPair, + identity_keypair: MixIdentityKeyPair, ) -> Self { HealthChecker { - resolution_timeout: Duration::from_secs_f64(resolution_timeout_f64), + resolution_timeout, num_test_packets, identity_keypair, - - _phantom_private: PhantomData, - _phantom_public: PhantomData, } } diff --git a/common/healthcheck/src/path_check.rs b/common/healthcheck/src/path_check.rs index b1d2617ad8..3833dd5c8e 100644 --- a/common/healthcheck/src/path_check.rs +++ b/common/healthcheck/src/path_check.rs @@ -1,4 +1,4 @@ -use crypto::identity::{MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey}; +use crypto::identity::MixIdentityKeyPair; use itertools::Itertools; use log::{debug, error, info, trace, warn}; use mix_client::MixClient; @@ -27,22 +27,18 @@ pub(crate) struct PathChecker { } impl PathChecker { - pub(crate) async fn new( + pub(crate) async fn new( providers: Vec, - identity_keys: &IDPair, + identity_keys: &MixIdentityKeyPair, check_id: [u8; 16], - ) -> Self - where - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, - { + ) -> Self { let mut provider_clients = HashMap::new(); let address = identity_keys.public_key().derive_address(); for provider in providers { - let mut provider_client = ProviderClient::new(provider.client_listener, address, None); + let mut provider_client = + ProviderClient::new(provider.client_listener, address.clone(), None); let insertion_result = match provider_client.register().await { Ok(token) => { debug!("registered at provider {}", provider.pub_key); @@ -78,7 +74,7 @@ impl PathChecker { // iteration is used to distinguish packets sent through the same path (as the healthcheck // may try to send say 10 packets through given path) - fn unique_path_key(path: &Vec, check_id: [u8; 16], iteration: u8) -> Vec { + fn unique_path_key(path: &[SphinxNode], check_id: [u8; 16], iteration: u8) -> Vec { check_id .iter() .cloned() @@ -178,8 +174,8 @@ impl PathChecker { self.update_path_statuses(provider_messages); } - pub(crate) async fn send_test_packet(&mut self, path: &Vec, iteration: u8) { - if path.len() == 0 { + pub(crate) async fn send_test_packet(&mut self, path: &[SphinxNode], iteration: u8) { + if path.is_empty() { warn!("trying to send test packet through an empty path!"); return; } @@ -226,7 +222,7 @@ impl PathChecker { let first_node_client = self .layer_one_clients .entry(first_node_key) - .or_insert(Some(mix_client::MixClient::new())); + .or_insert_with(|| Some(mix_client::MixClient::new())); if first_node_client.is_none() { debug!("we can ignore this path as layer one mix is inaccessible"); @@ -243,7 +239,7 @@ impl PathChecker { // we already checked for 'None' case let first_node_client = first_node_client.as_ref().unwrap(); - let delays: Vec<_> = path.iter().map(|_| Delay::new(0)).collect(); + let delays: Vec<_> = path.iter().map(|_| Delay::new_from_nanos(0)).collect(); // all of the data used to create the packet was created by us let packet = sphinx::SphinxPacket::new( @@ -257,7 +253,7 @@ impl PathChecker { debug!("sending test packet to {}", first_node_address); match first_node_client.send(packet, first_node_address).await { Err(err) => { - info!("failed to send packet to {} - {}", first_node_address, err); + debug!("failed to send packet to {} - {}", first_node_address, err); if self .paths_status .insert(path_identifier, PathStatus::Unhealthy) diff --git a/common/healthcheck/src/result.rs b/common/healthcheck/src/result.rs index 9eda0885f5..2db51ddc3a 100644 --- a/common/healthcheck/src/result.rs +++ b/common/healthcheck/src/result.rs @@ -1,7 +1,7 @@ use crate::path_check::{PathChecker, PathStatus}; use crate::score::NodeScore; -use crypto::identity::{MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey}; -use log::{debug, error, info, warn}; +use crypto::identity::MixIdentityKeyPair; +use log::{debug, error, warn}; use rand_os::rand_core::RngCore; use sphinx::route::NodeAddressBytes; use std::collections::HashMap; @@ -16,7 +16,7 @@ impl std::fmt::Display for HealthCheckResult { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { write!(f, "NETWORK HEALTH\n==============\n")?; for score in self.0.iter() { - write!(f, "{}\n", score)? + writeln!(f, "{}", score)? } Ok(()) } @@ -34,12 +34,8 @@ impl HealthCheckResult { let health = mixes .into_iter() - .map(|node| NodeScore::from_mixnode(node)) - .chain( - providers - .into_iter() - .map(|node| NodeScore::from_provider(node)), - ) + .map(NodeScore::from_mixnode) + .chain(providers.into_iter().map(NodeScore::from_provider)) .collect(); HealthCheckResult(health) @@ -102,18 +98,12 @@ impl HealthCheckResult { id } - pub async fn calculate( + pub async fn calculate( topology: &T, iterations: usize, resolution_timeout: Duration, - identity_keys: &IDPair, - ) -> Self - where - T: NymTopology, - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, - { + identity_keys: &MixIdentityKeyPair, + ) -> Self { // currently healthchecker supports only up to 255 iterations - if we somehow // find we need more, it's relatively easy change assert!(iterations <= 255); @@ -150,7 +140,7 @@ impl HealthCheckResult { } } - info!( + debug!( "waiting {:?} for pending requests to resolve", resolution_timeout ); diff --git a/common/healthcheck/src/score.rs b/common/healthcheck/src/score.rs index 7262d4ee0d..9bdbf74bce 100644 --- a/common/healthcheck/src/score.rs +++ b/common/healthcheck/src/score.rs @@ -37,6 +37,7 @@ pub(crate) struct NodeScore { impl Ord for NodeScore { // order by: version, layer, sent, received, pubkey; ignore addresses + #[allow(clippy::comparison_chain)] fn cmp(&self, other: &Self) -> Ordering { if self.typ > other.typ { return Ordering::Greater; @@ -110,7 +111,7 @@ impl NodeScore { pub_key: NodeAddressBytes::from_base58_string(node.pub_key), addresses: vec![node.mixnet_listener, node.client_listener], version: node.version, - layer: format!("provider"), + layer: "provider".to_string(), packets_sent: 0, packets_received: 0, } diff --git a/common/pemstore/src/pemstore.rs b/common/pemstore/src/pemstore.rs index 9c7a914d02..f6059b3a06 100644 --- a/common/pemstore/src/pemstore.rs +++ b/common/pemstore/src/pemstore.rs @@ -1,40 +1,35 @@ use crate::pathfinder::PathFinder; +use crypto::identity::MixIdentityKeyPair; +use crypto::PemStorableKey; +use crypto::{encryption, PemStorableKeyPair}; +use log::info; use pem::{encode, parse, Pem}; use std::fs::File; use std::io; use std::io::prelude::*; use std::path::PathBuf; -#[allow(dead_code)] -pub fn read_mix_encryption_keypair_from_disk(_id: String) -> crypto::encryption::x25519::KeyPair { - unimplemented!() -} - pub struct PemStore { + #[allow(dead_code)] config_dir: PathBuf, - private_mix_key: PathBuf, - public_mix_key: PathBuf, + private_mix_key_file: PathBuf, + public_mix_key_file: PathBuf, } impl PemStore { pub fn new(pathfinder: P) -> PemStore { PemStore { config_dir: pathfinder.config_dir(), - private_mix_key: pathfinder.private_identity_key(), - public_mix_key: pathfinder.public_identity_key(), + private_mix_key_file: pathfinder.private_identity_key(), + public_mix_key_file: pathfinder.public_identity_key(), } } - pub fn read_identity(&self) -> io::Result - where - IDPair: crypto::identity::MixnetIdentityKeyPair, - Priv: crypto::identity::MixnetIdentityPrivateKey, - Pub: crypto::identity::MixnetIdentityPublicKey, - { - let private_pem = self.read_pem_file(self.private_mix_key.clone())?; - let public_pem = self.read_pem_file(self.public_mix_key.clone())?; + pub fn read_keypair(&self) -> io::Result { + let private_pem = self.read_pem_file(self.private_mix_key_file.clone())?; + let public_pem = self.read_pem_file(self.public_mix_key_file.clone())?; - let key_pair = IDPair::from_bytes(&private_pem.contents, &public_pem.contents); + let key_pair = T::from_bytes(&private_pem.contents, &public_pem.contents); if key_pair.private_key().pem_type() != private_pem.tag { return Err(io::Error::new( @@ -53,39 +48,62 @@ impl PemStore { Ok(key_pair) } + pub fn read_encryption(&self) -> io::Result { + self.read_keypair() + } + + pub fn read_identity(&self) -> io::Result { + self.read_keypair() + } + fn read_pem_file(&self, filepath: PathBuf) -> io::Result { let mut pem_bytes = File::open(filepath)?; let mut buf = Vec::new(); pem_bytes.read_to_end(&mut buf)?; parse(&buf).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } - // This should be refactored and made more generic for when we have other kinds of - // KeyPairs that we want to persist (e.g. validator keypairs, or keys for - // signing vs encryption). However, for the moment, it does the job. - pub fn write_identity(&self, key_pair: IDPair) -> io::Result<()> - where - IDPair: crypto::identity::MixnetIdentityKeyPair, - Priv: crypto::identity::MixnetIdentityPrivateKey, - Pub: crypto::identity::MixnetIdentityPublicKey, - { - std::fs::create_dir_all(self.config_dir.clone())?; + fn write_keypair(&self, key_pair: impl PemStorableKeyPair) -> io::Result<()> { let private_key = key_pair.private_key(); let public_key = key_pair.public_key(); + self.write_pem_file( - self.private_mix_key.clone(), + self.private_mix_key_file.clone(), private_key.to_bytes(), private_key.pem_type(), )?; + info!( + "Written private key to {:?}", + self.private_mix_key_file.clone() + ); self.write_pem_file( - self.public_mix_key.clone(), + self.public_mix_key_file.clone(), public_key.to_bytes(), public_key.pem_type(), )?; + info!( + "Written public key to {:?}", + self.public_mix_key_file.clone() + ); Ok(()) } + // This should be refactored and made more generic for when we have other kinds of + // KeyPairs that we want to persist (e.g. validator keypairs, or keys for + // signing vs encryption). However, for the moment, it does the job. + pub fn write_identity(&self, key_pair: MixIdentityKeyPair) -> io::Result<()> { + self.write_keypair(key_pair) + } + + pub fn write_encryption_keys(&self, key_pair: encryption::KeyPair) -> io::Result<()> { + self.write_keypair(key_pair) + } + fn write_pem_file(&self, filepath: PathBuf, data: Vec, tag: String) -> io::Result<()> { + // ensure the whole directory structure exists + if let Some(parent_dir) = filepath.parent() { + std::fs::create_dir_all(parent_dir)?; + } let pem = Pem { tag, contents: data, diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index c3c23e733d..b414460c82 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -19,5 +19,5 @@ addressing = {path = "../addressing"} version-checker = {path = "../version-checker" } ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } # sphinx = { path = "../../../sphinx"} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 3067b30b47..eb81cdfd42 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -10,7 +10,9 @@ mod filter; pub mod mix; pub mod provider; -pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync { +// TODO: Figure out why 'Clone' was required to have 'TopologyAccessor' working +// even though it only contains an Arc +pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { fn new(directory_server: String) -> Self; fn new_from_nodes( mix_nodes: Vec, @@ -30,7 +32,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync { } highest_layer = max(highest_layer, mix.layer); - let layer_nodes = layered_topology.entry(mix.layer).or_insert(Vec::new()); + let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new); layer_nodes.push(mix); } @@ -40,12 +42,12 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync { if !layered_topology.contains_key(&layer) { missing_layers.push(layer); } - if layered_topology[&layer].len() == 0 { + if layered_topology[&layer].is_empty() { missing_layers.push(layer); } } - if missing_layers.len() > 0 { + if !missing_layers.is_empty() { return Err(NymTopologyError::MissingLayerError(missing_layers)); } @@ -112,10 +114,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync { } fn can_construct_path_through(&self) -> bool { - match self.make_layered_topology() { - Ok(_) => true, - Err(_) => false, - } + self.make_layered_topology().is_ok() } } diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 4685d6416e..ff1ec5b08e 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-mixnode" -version = "0.4.1" +version = "0.5.0-rc.1" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" @@ -11,18 +11,31 @@ edition = "2018" bs58 = "0.3.0" clap = "2.33.0" curve25519-dalek = "1.2.3" +dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3.1" log = "0.4" pretty_env_logger = "0.3" +serde = { version = "1.0.104", features = ["derive"] } tokio = { version = "0.2", features = ["full"] } ## internal addressing = {path = "../common/addressing" } +config = {path = "../common/config"} +crypto = {path = "../common/crypto"} directory-client = { path = "../common/clients/directory-client" } +multi-tcp-client = { path = "../common/clients/multi-tcp-client" } +pemstore = {path = "../common/pemstore"} ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } [build-dependencies] built = "0.3.2" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +qa = [] +local = [] \ No newline at end of file diff --git a/mixnode/src/built_info.rs b/mixnode/src/built_info.rs new file mode 100644 index 0000000000..d11fb6389f --- /dev/null +++ b/mixnode/src/built_info.rs @@ -0,0 +1,2 @@ +// The file has been placed there by the build script. +include!(concat!(env!("OUT_DIR"), "/built.rs")); diff --git a/mixnode/src/commands/init.rs b/mixnode/src/commands/init.rs new file mode 100644 index 0000000000..4c8a1243a5 --- /dev/null +++ b/mixnode/src/commands/init.rs @@ -0,0 +1,82 @@ +use crate::commands::override_config; +use crate::config::persistence::pathfinder::MixNodePathfinder; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::encryption; +use pemstore::pemstore::PemStore; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("init") + .about("Initialise the mixnode") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnode we want to create config for.") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("layer") + .long("layer") + .help("The mixnet layer of this particular node") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("host") + .long("host") + .help("The custom host on which the mixnode will be running") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("port") + .long("port") + .help("The port on which the mixnode will be listening") + .takes_value(true), + ) + .arg( + Arg::with_name("announce-host") + .long("announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("announce-port") + .long("announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the node is sending presence and metrics to") + .takes_value(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + println!("Initialising mixnode {}...", id); + + let layer = matches.value_of("layer").unwrap().parse().unwrap(); + let mut config = crate::config::Config::new(id, layer); + + config = override_config(config, matches); + + let sphinx_keys = encryption::KeyPair::new(); + let pathfinder = MixNodePathfinder::new_from_config(&config); + let pem_store = PemStore::new(pathfinder); + pem_store + .write_encryption_keys(sphinx_keys) + .expect("Failed to save sphinx keys"); + println!("Saved mixnet sphinx keypair"); + + let config_save_location = config.get_config_file_save_location(); + config + .save_to_file(None) + .expect("Failed to save the config file"); + println!("Saved configuration file to {:?}", config_save_location); + + println!("Mixnode configuration completed.\n\n\n") +} diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs new file mode 100644 index 0000000000..b7c45f5bd5 --- /dev/null +++ b/mixnode/src/commands/mod.rs @@ -0,0 +1,40 @@ +use crate::config::Config; +use clap::ArgMatches; + +pub mod init; +pub mod run; + +pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { + if let Some(host) = matches.value_of("host") { + config = config.with_listening_host(host); + } + + if let Some(port) = matches.value_of("port").map(|port| port.parse::()) { + if let Err(err) = port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_listening_port(port.unwrap()); + } + + if let Some(directory) = matches.value_of("directory") { + config = config.with_custom_directory(directory); + } + + if let Some(announce_host) = matches.value_of("announce-host") { + config = config.with_announce_host(announce_host); + } + + if let Some(announce_port) = matches + .value_of("announce-port") + .map(|port| port.parse::()) + { + if let Err(err) = announce_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_announce_port(announce_port.unwrap()); + } + + config +} diff --git a/mixnode/src/commands/run.rs b/mixnode/src/commands/run.rs new file mode 100644 index 0000000000..42baaffe01 --- /dev/null +++ b/mixnode/src/commands/run.rs @@ -0,0 +1,112 @@ +use crate::commands::override_config; +use crate::config::Config; +use crate::node::MixNode; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; + +pub fn command_args<'a, 'b>() -> App<'a, 'b> { + App::new("run") + .about("Starts the mixnode") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnode we want to run") + .takes_value(true) + .required(true), + ) + // the rest of arguments are optional, they are used to override settings in config file + .arg( + Arg::with_name("config") + .long("config") + .help("Custom path to the nym-mixnode configuration file") + .takes_value(true), + ) + .arg( + Arg::with_name("layer") + .long("layer") + .help("The mixnet layer of this particular node") + .takes_value(true), + ) + .arg( + Arg::with_name("host") + .long("host") + .help("The custom host on which the mixnode will be running") + .takes_value(true), + ) + .arg( + Arg::with_name("port") + .long("port") + .help("The port on which the mixnode will be listening") + .takes_value(true), + ) + .arg( + Arg::with_name("announce-host") + .long("announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("announce-port") + .long("announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the node is sending presence and metrics to") + .takes_value(true), + ) +} + +fn show_binding_warning(address: String) { + println!("\n##### WARNING #####"); + println!( + "\nYou are trying to bind to {} - you might not be accessible to other nodes\n\ + You can ignore this warning if you're running setup on a local network \n\ + or have set a custom 'announce-host'", + address + ); + println!("\n##### WARNING #####\n"); +} + +fn special_addresses() -> Vec<&'static str> { + vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + + println!("Starting mixnode {}...", id); + + let mut config = + Config::load_from_file(matches.value_of("config").map(|path| path.into()), Some(id)) + .expect("Failed to load config file"); + + config = override_config(config, matches); + + let listening_ip_string = config.get_listening_address().ip().to_string(); + if special_addresses().contains(&listening_ip_string.as_ref()) { + show_binding_warning(listening_ip_string); + } + + println!( + "Directory server [presence]: {}", + config.get_presence_directory_server() + ); + println!( + "Directory server [metrics]: {}", + config.get_metrics_directory_server() + ); + + println!( + "Listening for incoming packets on {}", + config.get_listening_address() + ); + println!( + "Announcing the following socket address: {}", + config.get_announce_address() + ); + + MixNode::new(config).run(); +} diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs new file mode 100644 index 0000000000..c53b917210 --- /dev/null +++ b/mixnode/src/config/mod.rs @@ -0,0 +1,362 @@ +use crate::config::template::config_template; +use config::NymConfig; +use log::*; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::str::FromStr; +use std::time; + +pub mod persistence; +mod template; + +// 'MIXNODE' +const DEFAULT_LISTENING_PORT: u16 = 1789; + +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_PRESENCE_SENDING_DELAY: u64 = 3000; +const DEFAULT_METRICS_SENDING_DELAY: u64 = 3000; +const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: u64 = 10_000; // 10s +const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: u64 = 300_000; // 5min + +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + mixnode: MixNode, + + #[serde(default)] + logging: Logging, + #[serde(default)] + debug: Debug, +} + +impl NymConfig for Config { + fn template() -> &'static str { + config_template() + } + + fn config_file_name() -> String { + "config.toml".to_string() + } + + fn default_root_directory() -> PathBuf { + dirs::home_dir() + .expect("Failed to evaluate $HOME value") + .join(".nym") + .join("mixnodes") + } + + fn root_directory(&self) -> PathBuf { + self.mixnode.nym_root_directory.clone() + } + + fn config_directory(&self) -> PathBuf { + self.mixnode + .nym_root_directory + .join(&self.mixnode.id) + .join("config") + } + + fn data_directory(&self) -> PathBuf { + self.mixnode + .nym_root_directory + .join(&self.mixnode.id) + .join("data") + } +} + +impl Config { + pub fn new>(id: S, layer: u64) -> Self { + Config::default().with_id(id).with_layer(layer) + } + + // builder methods + pub fn with_id>(mut self, id: S) -> Self { + let id = id.into(); + if self.mixnode.private_sphinx_key_file.as_os_str().is_empty() { + self.mixnode.private_sphinx_key_file = + self::MixNode::default_private_sphinx_key_file(&id); + } + if self.mixnode.public_sphinx_key_file.as_os_str().is_empty() { + self.mixnode.public_sphinx_key_file = + self::MixNode::default_public_sphinx_key_file(&id); + } + self.mixnode.id = id; + self + } + + pub fn with_layer(mut self, layer: u64) -> Self { + self.mixnode.layer = layer; + self + } + + // if you want to use distinct servers for metrics and presence + // you need to do so in the config.toml file. + pub fn with_custom_directory>(mut self, directory_server: S) -> Self { + let directory_server_string = directory_server.into(); + self.debug.presence_directory_server = directory_server_string.clone(); + self.debug.metrics_directory_server = directory_server_string; + self + } + + pub fn with_listening_host>(mut self, host: S) -> Self { + // see if the provided `host` is just an ip address or ip:port + let host = host.into(); + + // is it ip:port? + match SocketAddr::from_str(host.as_ref()) { + Ok(socket_addr) => { + self.mixnode.listening_address = socket_addr; + self + } + // try just for ip + Err(_) => match IpAddr::from_str(host.as_ref()) { + Ok(ip_addr) => { + self.mixnode.listening_address.set_ip(ip_addr); + self + } + Err(_) => { + error!( + "failed to make any changes to config - invalid host {}", + host + ); + self + } + }, + } + } + + pub fn with_listening_port(mut self, port: u16) -> Self { + self.mixnode.listening_address.set_port(port); + self + } + + pub fn with_announce_host>(mut self, host: S) -> Self { + // this is slightly more complicated as we store announce information as String, + // since it might not necessarily be a valid SocketAddr (say `nymtech.net:8080` is a valid + // announce address, yet invalid SocketAddr` + + // first lets see if we received host:port or just host part of an address + let host = host.into(); + let split_host: Vec<_> = host.split(':').collect(); + match split_host.len() { + 1 => { + // we provided only 'host' part so we are going to reuse existing port + self.mixnode.announce_address = + format!("{}:{}", host, self.mixnode.listening_address.port()); + self + } + 2 => { + // we provided 'host:port' so just put the whole thing there + self.mixnode.announce_address = host; + self + } + _ => { + // we provided something completely invalid, so don't try to parse it + error!( + "failed to make any changes to config - invalid announce host {}", + host + ); + self + } + } + } + + pub fn with_announce_port(mut self, port: u16) -> Self { + let current_host: Vec<_> = self.mixnode.announce_address.split(':').collect(); + debug_assert_eq!(current_host.len(), 2); + self.mixnode.announce_address = format!("{}:{}", current_host[0], port); + self + } + + // getters + pub fn get_config_file_save_location(&self) -> PathBuf { + self.config_directory().join(Self::config_file_name()) + } + + pub fn get_private_sphinx_key_file(&self) -> PathBuf { + self.mixnode.private_sphinx_key_file.clone() + } + + pub fn get_public_sphinx_key_file(&self) -> PathBuf { + self.mixnode.public_sphinx_key_file.clone() + } + + pub fn get_presence_directory_server(&self) -> String { + self.debug.presence_directory_server.clone() + } + + pub fn get_presence_sending_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.presence_sending_delay) + } + + pub fn get_metrics_directory_server(&self) -> String { + self.debug.metrics_directory_server.clone() + } + + pub fn get_metrics_sending_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.metrics_sending_delay) + } + + pub fn get_layer(&self) -> u64 { + self.mixnode.layer + } + + pub fn get_listening_address(&self) -> SocketAddr { + self.mixnode.listening_address + } + + pub fn get_announce_address(&self) -> String { + self.mixnode.announce_address.clone() + } + + pub fn get_packet_forwarding_initial_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_initial_backoff) + } + + pub fn get_packet_forwarding_maximum_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_maximum_backoff) + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixNode { + /// ID specifies the human readable ID of this particular mixnode. + id: String, + + /// Layer of this particular mixnode determining its position in the network. + layer: u64, + + /// Socket address to which this mixnode will bind to and will be listening for packets. + listening_address: SocketAddr, + + /// Optional address announced to the directory server for the clients to connect to. + /// It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + /// later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. + /// Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` + /// are valid announce addresses, while the later will default to whatever port is used for + /// `listening_address`. + announce_address: String, + + /// Path to file containing private sphinx key. + private_sphinx_key_file: PathBuf, + + /// Path to file containing public sphinx key. + public_sphinx_key_file: PathBuf, + + /// nym_home_directory specifies absolute path to the home nym MixNodes directory. + /// It is expected to use default value and hence .toml file should not redefine this field. + nym_root_directory: PathBuf, +} + +impl MixNode { + fn default_private_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_sphinx.pem") + } + + fn default_public_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_sphinx.pem") + } +} + +impl Default for MixNode { + fn default() -> Self { + MixNode { + id: "".to_string(), + layer: 0, + listening_address: format!("0.0.0.0:{}", DEFAULT_LISTENING_PORT) + .parse() + .unwrap(), + announce_address: format!("127.0.0.1:{}", DEFAULT_LISTENING_PORT), + private_sphinx_key_file: Default::default(), + public_sphinx_key_file: Default::default(), + nym_root_directory: Config::default_root_directory(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Logging {} + +impl Default for Logging { + fn default() -> Self { + Logging {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + // The idea of additional 'directory servers' is to let mixes report their presence + // and metrics to separate places + /// Directory server to which the server will be reporting their presence data. + presence_directory_server: String, + + /// Delay between each subsequent presence data being sent. + /// The provided value is interpreted as milliseconds. + presence_sending_delay: u64, + + /// Directory server to which the server will be reporting their metrics data. + metrics_directory_server: String, + + /// Delay between each subsequent metrics data being sent. + /// The provided value is interpreted as milliseconds. + metrics_sending_delay: u64, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_initial_backoff: u64, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_maximum_backoff: u64, +} + +impl Debug { + fn default_directory_server() -> String { + #[cfg(feature = "qa")] + return "https://qa-directory.nymtech.net".to_string(); + #[cfg(feature = "local")] + return "http://localhost:8080".to_string(); + + "https://directory.nymtech.net".to_string() + } +} + +impl Default for Debug { + fn default() -> Self { + Debug { + presence_directory_server: Self::default_directory_server(), + presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, + metrics_directory_server: Self::default_directory_server(), + metrics_sending_delay: DEFAULT_METRICS_SENDING_DELAY, + packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + } + } +} + +#[cfg(test)] +mod mixnode_config { + use super::*; + + #[test] + fn after_saving_default_config_the_loaded_one_is_identical() { + // need to figure out how to do something similar but without touching the disk + // or the file system at all... + let temp_location = tempfile::tempdir().unwrap().path().join("config.toml"); + let default_config = Config::default().with_id("foomp".to_string()); + default_config + .save_to_file(Some(temp_location.clone())) + .unwrap(); + + let loaded_config = Config::load_from_file(Some(temp_location), None).unwrap(); + + assert_eq!(default_config, loaded_config); + } +} diff --git a/nym-client/src/config/persistance/mod.rs b/mixnode/src/config/persistence/mod.rs similarity index 100% rename from nym-client/src/config/persistance/mod.rs rename to mixnode/src/config/persistence/mod.rs diff --git a/mixnode/src/config/persistence/pathfinder.rs b/mixnode/src/config/persistence/pathfinder.rs new file mode 100644 index 0000000000..a0a7627bf1 --- /dev/null +++ b/mixnode/src/config/persistence/pathfinder.rs @@ -0,0 +1,44 @@ +use crate::config::Config; +use pemstore::pathfinder::PathFinder; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct MixNodePathfinder { + pub config_dir: PathBuf, + pub private_sphinx_key: PathBuf, + pub public_sphinx_key: PathBuf, +} + +impl MixNodePathfinder { + pub fn new_from_config(config: &Config) -> Self { + MixNodePathfinder { + config_dir: config.get_config_file_save_location(), + private_sphinx_key: config.get_private_sphinx_key_file(), + public_sphinx_key: config.get_public_sphinx_key_file(), + } + } +} + +impl PathFinder for MixNodePathfinder { + fn config_dir(&self) -> PathBuf { + self.config_dir.clone() + } + + fn private_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.private_sphinx_key.clone() + } + + fn public_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.public_sphinx_key.clone() + } + + fn private_encryption_key(&self) -> Option { + Some(self.private_sphinx_key.clone()) + } + + fn public_encryption_key(&self) -> Option { + Some(self.public_sphinx_key.clone()) + } +} diff --git a/mixnode/src/config/template.rs b/mixnode/src/config/template.rs new file mode 100644 index 0000000000..78ec91fa02 --- /dev/null +++ b/mixnode/src/config/template.rs @@ -0,0 +1,81 @@ +pub(crate) fn config_template() -> &'static str { + // While using normal toml marshalling would have been way simpler with less overhead, + // I think it's useful to have comments attached to the saved config file to explain behaviour of + // particular fields. + // Note: any changes to the template must be reflected in the appropriate structs in mod.rs. + r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base mixnode config options ##### + +[mixnode] +# Human readable ID of this particular mixnode. +id = "{{ mixnode.id }}" + +# Layer of this particular mixnode determining its position in the network. +layer = {{ mixnode.layer }} + +# Socket address to which this mixnode will bind to and will be listening for packets. +listening_address = "{{ mixnode.listening_address }}" + +# Path to file containing private identity key. +private_sphinx_key_file = "{{ mixnode.private_sphinx_key_file }}" + +# Path to file containing public sphinx key. +public_sphinx_key_file = "{{ mixnode.public_sphinx_key_file }}" + +##### additional mixnode config options ##### + +# Optional address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. +# Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` +# are valid announce addresses, while the later will default to whatever port is used for +# `listening_address`. +announce_address = "{{ mixnode.announce_address }}" + +##### advanced configuration options ##### + +# Absolute path to the home Nym Clients directory. +nym_root_directory = "{{ mixnode.nym_root_directory }}" + + +##### logging configuration options ##### + +[logging] + +# TODO + + +##### debug configuration options ##### +# The following options should not be modified unless you know EXACTLY what you are doing +# as if set incorrectly, they may impact your anonymity. + +[debug] + +# Directory server to which the server will be reporting their presence data. +presence_directory_server = "{{ debug.presence_directory_server}}" + +# Delay between each subsequent presence data being sent. +# The provided value is interpreted as milliseconds. +presence_sending_delay = {{ debug.presence_sending_delay }} + +# Directory server to which the server will be reporting their metrics data. +metrics_directory_server = "{{ debug.metrics_directory_server }}" + +# Delay between each subsequent metrics data being sent. +# The provided value is interpreted as milliseconds. +metrics_sending_delay = {{ debug.metrics_sending_delay }} + +# Initial value of an exponential backoff to reconnect to dropped TCP connection when +# forwarding sphinx packets. +# The provided value is interpreted as milliseconds. +packet_forwarding_initial_backoff = {{ debug.packet_forwarding_initial_backoff }} + +# Maximum value of an exponential backoff to reconnect to dropped TCP connection when +# forwarding sphinx packets. +# The provided value is interpreted as milliseconds. +packet_forwarding_maximum_backoff = {{ debug.packet_forwarding_maximum_backoff }} +"# +} diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index f16f460dd3..994f74f23d 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -1,82 +1,37 @@ -use clap::{App, Arg, ArgMatches, SubCommand}; -use log::*; -use std::process; +use clap::{App, ArgMatches}; -mod mix_peer; +pub mod built_info; +mod commands; +mod config; mod node; fn main() { dotenv::dotenv().ok(); pretty_env_logger::init(); + println!("{}", banner()); + let arg_matches = App::new("Nym Mixnode") .version(built_info::PKG_VERSION) .author("Nymtech") .about("Implementation of the Loopix-based Mixnode") - .subcommand( - SubCommand::with_name("run") - .about("Starts the mixnode") - .arg( - Arg::with_name("host") - .long("host") - .help("The custom host on which the mixnode will be running") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("port") - .long("port") - .help("The port on which the mixnode will be listening") - .takes_value(true), - ) - .arg( - Arg::with_name("layer") - .long("layer") - .help("The mixnet layer of this particular node") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("announce_host") - .long("announce-host") - .help("The host that will be reported to the directory server") - .takes_value(true) - ) - .arg( - Arg::with_name("announce_port") - .long("announce-port") - .help("The port that will be reported to the directory server") - .takes_value(true) - ) - .arg( - Arg::with_name("directory") - .long("directory") - .help("Address of the directory server the node is sending presence and metrics to") - .takes_value(true), - ), - ) + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) .get_matches(); - if let Err(e) = execute(arg_matches) { - error!("{}", e); - process::exit(1); - } + execute(arg_matches); } -pub mod built_info { - // The file has been placed there by the build script. - include!(concat!(env!("OUT_DIR"), "/built.rs")); -} - -fn execute(matches: ArgMatches) -> Result<(), String> { +fn execute(matches: ArgMatches) { match matches.subcommand() { - ("run", Some(m)) => Ok(node::runner::start(m)), - _ => Err(usage()), + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), } } -fn usage() -> String { - banner() + "usage: --help to see available options.\n\n" +fn usage() -> &'static str { + "usage: --help to see available options.\n\n" } fn banner() -> String { diff --git a/mixnode/src/mix_peer.rs b/mixnode/src/mix_peer.rs deleted file mode 100644 index 8a8579a780..0000000000 --- a/mixnode/src/mix_peer.rs +++ /dev/null @@ -1,46 +0,0 @@ -use addressing; -use addressing::AddressTypeError; -use sphinx::route::NodeAddressBytes; -use std::error::Error; -use std::net::SocketAddr; -use tokio::prelude::*; - -#[derive(Debug)] -pub struct MixPeer { - connection: SocketAddr, -} - -#[derive(Debug)] -pub enum MixPeerError { - InvalidAddressError, -} - -impl From for MixPeerError { - fn from(_: AddressTypeError) -> Self { - use MixPeerError::*; - - InvalidAddressError - } -} - -impl MixPeer { - // note that very soon `next_hop_address` will be changed to `next_hop_metadata` - pub fn new(next_hop_address: NodeAddressBytes) -> Result { - let next_hop_socket_address = - addressing::socket_address_from_encoded_bytes(next_hop_address.to_bytes())?; - Ok(MixPeer { - connection: next_hop_socket_address, - }) - } - - pub async fn send(&self, bytes: Vec) -> Result<(), Box> { - let next_hop_address = self.connection.clone(); - let mut stream = tokio::net::TcpStream::connect(next_hop_address).await?; - stream.write_all(&bytes).await?; - Ok(()) - } - - pub fn to_string(&self) -> String { - self.connection.to_string() - } -} diff --git a/mixnode/src/node/listener.rs b/mixnode/src/node/listener.rs new file mode 100644 index 0000000000..0409a1754d --- /dev/null +++ b/mixnode/src/node/listener.rs @@ -0,0 +1,101 @@ +use crate::node::packet_processing::{MixProcessingResult, PacketProcessor}; +use futures::channel::mpsc; +use log::*; +use std::io; +use std::net::SocketAddr; +use tokio::prelude::*; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +async fn process_received_packet( + packet_data: [u8; sphinx::PACKET_SIZE], + packet_processor: PacketProcessor, + forwarding_channel: mpsc::UnboundedSender<(SocketAddr, Vec)>, +) { + // all processing incl. delay was done, the only thing left is to forward it + match packet_processor.process_sphinx_packet(packet_data).await { + Err(e) => debug!("We failed to process received sphinx packet - {:?}", e), + Ok(res) => match res { + MixProcessingResult::ForwardHop(hop_address, hop_data) => { + // send our data to tcp client for forwarding. If forwarding fails, then it fails, + // it's not like we can do anything about it + // + // in unbounded_send() failed it means that the receiver channel was disconnected + // and hence something weird must have happened without a way of recovering + forwarding_channel + .unbounded_send((hop_address, hop_data)) + .unwrap(); + packet_processor.report_sent(hop_address); + } + MixProcessingResult::LoopMessage => { + warn!("Somehow processed a loop cover message that we haven't implemented yet!") + } + }, + } +} + +async fn process_socket_connection( + mut socket: tokio::net::TcpStream, + packet_processor: PacketProcessor, + forwarding_channel: mpsc::UnboundedSender<(SocketAddr, Vec)>, +) { + let mut buf = [0u8; sphinx::PACKET_SIZE]; + loop { + match socket.read(&mut buf).await { + // socket closed + Ok(n) if n == 0 => { + trace!("Remote connection closed."); + return; + } + Ok(n) => { + if n != sphinx::PACKET_SIZE { + warn!("read data of different length than expected sphinx packet size - {} (expected {})", n, sphinx::PACKET_SIZE); + continue; + } + + // we must be able to handle multiple packets from same connection independently + tokio::spawn(process_received_packet( + buf, + // note: processing_data is relatively cheap (and safe) to clone - + // it contains arc to private key and metrics reporter (which is just + // a single mpsc unbounded sender) + packet_processor.clone(), + forwarding_channel.clone(), + )) + } + Err(e) => { + warn!( + "failed to read from socket. Closing the connection; err = {:?}", + e + ); + return; + } + }; + } +} + +pub(crate) fn run_socket_listener( + handle: &Handle, + addr: SocketAddr, + packet_processor: PacketProcessor, + forwarding_channel: mpsc::UnboundedSender<(SocketAddr, Vec)>, +) -> JoinHandle> { + let handle_clone = handle.clone(); + handle.spawn(async move { + let mut listener = tokio::net::TcpListener::bind(addr).await?; + loop { + let (socket, _) = listener.accept().await?; + + let thread_packet_processor = packet_processor.clone(); + let forwarding_channel_clone = forwarding_channel.clone(); + handle_clone.spawn(async move { + process_socket_connection( + socket, + thread_packet_processor, + forwarding_channel_clone, + ) + .await; + }); + } + }) +} diff --git a/mixnode/src/node/metrics.rs b/mixnode/src/node/metrics.rs index adc1d74e48..cf8e52e942 100644 --- a/mixnode/src/node/metrics.rs +++ b/mixnode/src/node/metrics.rs @@ -8,60 +8,49 @@ use log::{debug, error}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; -const METRICS_INTERVAL: u64 = 3; +pub(crate) enum MetricEvent { + Sent(String), + Received, +} -#[derive(Debug)] -pub struct MetricsReporter { +#[derive(Debug, Clone)] +// Note: you should NEVER create more than a single instance of this using 'new()'. +// You should always use .clone() to create additional instances +struct MixMetrics { + inner: Arc>, +} + +struct MixMetricsInner { received: u64, sent: HashMap, } -impl MetricsReporter { +impl MixMetrics { pub(crate) fn new() -> Self { - MetricsReporter { - received: 0, - sent: HashMap::new(), + MixMetrics { + inner: Arc::new(Mutex::new(MixMetricsInner { + received: 0, + sent: HashMap::new(), + })), } } - pub(crate) fn add_arc_mutex(self) -> Arc> { - Arc::new(Mutex::new(self)) - } - - async fn increment_received_metrics(metrics: Arc>) { - let mut unlocked = metrics.lock().await; + async fn increment_received_metrics(&mut self) { + let mut unlocked = self.inner.lock().await; unlocked.received += 1; } - pub(crate) async fn run_received_metrics_control( - metrics: Arc>, - mut rx: mpsc::Receiver<()>, - ) { - while let Some(_) = rx.next().await { - MetricsReporter::increment_received_metrics(metrics.clone()).await; - } - } - - async fn increment_sent_metrics(metrics: Arc>, sent_to: String) { - let mut unlocked = metrics.lock().await; - let receiver_count = unlocked.sent.entry(sent_to).or_insert(0); + async fn increment_sent_metrics(&mut self, destination: String) { + let mut unlocked = self.inner.lock().await; + let receiver_count = unlocked.sent.entry(destination).or_insert(0); *receiver_count += 1; } - pub(crate) async fn run_sent_metrics_control( - metrics: Arc>, - mut rx: mpsc::Receiver, - ) { - while let Some(sent_metric) = rx.next().await { - MetricsReporter::increment_sent_metrics(metrics.clone(), sent_metric).await; - } - } - - async fn acquire_and_reset_metrics( - metrics: Arc>, - ) -> (u64, HashMap) { - let mut unlocked = metrics.lock().await; + async fn acquire_and_reset_metrics(&mut self) -> (u64, HashMap) { + let mut unlocked = self.inner.lock().await; let received = unlocked.received; let sent = std::mem::replace(&mut unlocked.sent, HashMap::new()); @@ -69,27 +58,138 @@ impl MetricsReporter { (received, sent) } +} - pub(crate) async fn run_metrics_sender( - metrics: Arc>, - cfg: directory_client::Config, - pub_key_str: String, - ) { - let delay_duration = Duration::from_secs(METRICS_INTERVAL); - let directory_client = directory_client::Client::new(cfg); - loop { - tokio::time::delay_for(delay_duration).await; - let (received, sent) = - MetricsReporter::acquire_and_reset_metrics(metrics.clone()).await; +struct MetricsReceiver { + metrics: MixMetrics, + metrics_rx: mpsc::UnboundedReceiver, +} - match directory_client.metrics_post.post(&MixMetric { - pub_key: pub_key_str.clone(), - received, - sent, - }) { - Err(err) => error!("failed to send metrics - {:?}", err), - Ok(_) => debug!("sent metrics information"), - } +impl MetricsReceiver { + fn new(metrics: MixMetrics, metrics_rx: mpsc::UnboundedReceiver) -> Self { + MetricsReceiver { + metrics, + metrics_rx, } } + + fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + while let Some(metrics_data) = self.metrics_rx.next().await { + match metrics_data { + MetricEvent::Received => self.metrics.increment_received_metrics().await, + MetricEvent::Sent(destination) => { + self.metrics.increment_sent_metrics(destination).await + } + } + } + }) + } +} + +struct MetricsSender { + metrics: MixMetrics, + directory_client: directory_client::Client, + pub_key_str: String, + sending_delay: Duration, +} + +impl MetricsSender { + fn new( + metrics: MixMetrics, + directory_server: String, + pub_key_str: String, + sending_delay: Duration, + ) -> Self { + MetricsSender { + metrics, + directory_client: directory_client::Client::new(directory_client::Config::new( + directory_server, + )), + pub_key_str, + sending_delay, + } + } + + fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + loop { + tokio::time::delay_for(self.sending_delay).await; + let (received, sent) = self.metrics.acquire_and_reset_metrics().await; + + match self.directory_client.metrics_post.post(&MixMetric { + pub_key: self.pub_key_str.clone(), + received, + sent, + }) { + Err(err) => error!("failed to send metrics - {:?}", err), + Ok(_) => debug!("sent metrics information"), + } + } + }) + } +} + +#[derive(Clone)] +pub struct MetricsReporter { + metrics_tx: mpsc::UnboundedSender, +} + +impl MetricsReporter { + pub(crate) fn new(metrics_tx: mpsc::UnboundedSender) -> Self { + MetricsReporter { metrics_tx } + } + + pub(crate) fn report_sent(&self, destination: String) { + // in unbounded_send() failed it means that the receiver channel was disconnected + // and hence something weird must have happened without a way of recovering + self.metrics_tx + .unbounded_send(MetricEvent::Sent(destination)) + .unwrap() + } + + pub(crate) fn report_received(&self) { + // in unbounded_send() failed it means that the receiver channel was disconnected + // and hence something weird must have happened without a way of recovering + self.metrics_tx + .unbounded_send(MetricEvent::Received) + .unwrap() + } +} + +// basically an easy single entry point to start all metrics related tasks +pub struct MetricsController { + receiver: MetricsReceiver, + reporter: MetricsReporter, + sender: MetricsSender, +} + +impl MetricsController { + pub(crate) fn new( + directory_server: String, + pub_key_str: String, + sending_delay: Duration, + ) -> Self { + let (metrics_tx, metrics_rx) = mpsc::unbounded(); + let shared_metrics = MixMetrics::new(); + + MetricsController { + sender: MetricsSender::new( + shared_metrics.clone(), + directory_server, + pub_key_str, + sending_delay, + ), + receiver: MetricsReceiver::new(shared_metrics, metrics_rx), + reporter: MetricsReporter::new(metrics_tx), + } + } + + // reporter is how node is going to be accessing the metrics data + pub(crate) fn start(self, handle: &Handle) -> MetricsReporter { + // TODO: should we do anything with JoinHandle(s) returned by start methods? + self.receiver.start(handle); + self.sender.start(handle); + self.reporter + } } diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 0432dec413..c73aade059 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -1,292 +1,118 @@ -use crate::mix_peer::MixPeer; -use crate::node; -use crate::node::metrics::MetricsReporter; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; +use crate::config::persistence::pathfinder::MixNodePathfinder; +use crate::config::Config; +use crate::node::packet_processing::PacketProcessor; +use crypto::encryption; use futures::channel::mpsc; -use futures::lock::Mutex; -use futures::SinkExt; use log::*; -use sphinx::header::delays::Delay as SphinxDelay; -use sphinx::{ProcessedPacket, SphinxPacket}; +use pemstore::pemstore::PemStore; use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; -use tokio::prelude::*; use tokio::runtime::Runtime; +mod listener; mod metrics; +mod packet_forwarding; +pub(crate) mod packet_processing; mod presence; -pub mod runner; - -pub struct Config { - announce_address: String, - directory_server: String, - layer: usize, - public_key: MontgomeryPoint, - secret_key: Scalar, - socket_address: SocketAddr, -} - -impl Config { - pub fn public_key_string(&self) -> String { - let key_bytes = self.public_key.to_bytes().to_vec(); - bs58::encode(&key_bytes).into_string() - } -} - -#[derive(Debug)] -pub enum MixProcessingError { - SphinxRecoveryError, - ReceivedFinalHopError, - SphinxProcessingError, - InvalidHopAddress, -} - -impl From for MixProcessingError { - // for time being just have a single error instance for all possible results of sphinx::ProcessingError - fn from(_: sphinx::ProcessingError) -> Self { - use MixProcessingError::*; - - SphinxRecoveryError - } -} - -struct ForwardingData { - packet: SphinxPacket, - delay: SphinxDelay, - recipient: MixPeer, - sent_metrics_tx: mpsc::Sender, -} - -// TODO: this will need to be changed if MixPeer will live longer than our Forwarding Data -impl ForwardingData { - fn new( - packet: SphinxPacket, - delay: SphinxDelay, - recipient: MixPeer, - sent_metrics_tx: mpsc::Sender, - ) -> Self { - ForwardingData { - packet, - delay, - recipient, - sent_metrics_tx, - } - } -} - -// ProcessingData defines all data required to correctly unwrap sphinx packets -struct ProcessingData { - secret_key: Scalar, - received_metrics_tx: mpsc::Sender<()>, - sent_metrics_tx: mpsc::Sender, -} - -impl ProcessingData { - fn new( - secret_key: Scalar, - received_metrics_tx: mpsc::Sender<()>, - sent_metrics_tx: mpsc::Sender, - ) -> Self { - ProcessingData { - secret_key, - received_metrics_tx, - sent_metrics_tx, - } - } - - fn add_arc_mutex(self) -> Arc> { - Arc::new(Mutex::new(self)) - } -} - -struct PacketProcessor; - -impl PacketProcessor { - pub async fn process_sphinx_data_packet( - packet_data: &[u8], - processing_data: Arc>, - ) -> Result { - // we received something resembling a sphinx packet, report it! - let processing_data = processing_data.lock().await; - let mut received_metrics_tx = processing_data.received_metrics_tx.clone(); - - // if unwrap failed it means our metrics reporter died, so we should exit application and - // force restart - if received_metrics_tx.send(()).await.is_err() { - error!("failed to send metrics data to the controller - the underlying thread probably died!"); - std::process::exit(1); - } - - let packet = SphinxPacket::from_bytes(packet_data.to_vec())?; - let (next_packet, next_hop_address, delay) = - match packet.process(processing_data.secret_key) { - Ok(ProcessedPacket::ProcessedPacketForwardHop(packet, address, delay)) => { - (packet, address, delay) - } - Ok(_) => return Err(MixProcessingError::ReceivedFinalHopError), - Err(e) => { - warn!("Failed to unwrap Sphinx packet: {:?}", e); - return Err(MixProcessingError::SphinxProcessingError); - } - }; - - let next_mix = match MixPeer::new(next_hop_address) { - Ok(next_mix) => next_mix, - Err(_) => return Err(MixProcessingError::InvalidHopAddress), - }; - - let fwd_data = ForwardingData::new( - next_packet, - delay, - next_mix, - processing_data.sent_metrics_tx.clone(), - ); - Ok(fwd_data) - } - - async fn wait_and_forward(mut forwarding_data: ForwardingData) { - let delay_duration = Duration::from_nanos(forwarding_data.delay.get_value()); - tokio::time::delay_for(delay_duration).await; - - if forwarding_data - .sent_metrics_tx - .send(forwarding_data.recipient.to_string()) - .await - .is_err() - { - error!("failed to send metrics data to the controller - the underlying thread probably died!"); - std::process::exit(1); - } - - trace!("RECIPIENT: {:?}", forwarding_data.recipient); - match forwarding_data - .recipient - .send(forwarding_data.packet.to_bytes()) - .await - { - Ok(()) => (), - Err(e) => { - warn!( - "failed to write bytes to next mix peer. err = {:?}", - e.to_string() - ); - } - } - } -} // the MixNode will live for whole duration of this program pub struct MixNode { - directory_server: String, - network_address: SocketAddr, - public_key: MontgomeryPoint, - secret_key: Scalar, - // TODO: use it later to enforce forward travel - // layer: usize, + runtime: Runtime, + config: Config, + sphinx_keypair: encryption::KeyPair, } impl MixNode { - pub fn new(config: &Config) -> Self { + fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { + let sphinx_keypair = PemStore::new(MixNodePathfinder::new_from_config(&config_file)) + .read_encryption() + .expect("Failed to read stored sphinx key files"); + println!( + "Public encryption key: {}\nFor time being, it is identical to identity keys", + sphinx_keypair.public_key().to_base58_string() + ); + sphinx_keypair + } + + pub fn new(config: Config) -> Self { + let sphinx_keypair = Self::load_sphinx_keys(&config); + MixNode { - directory_server: config.directory_server.clone(), - network_address: config.socket_address, - secret_key: config.secret_key, - public_key: config.public_key, - // layer: config.layer, + runtime: Runtime::new().unwrap(), + config, + sphinx_keypair, } } - async fn process_socket_connection( - mut socket: tokio::net::TcpStream, - processing_data: Arc>, + fn start_presence_notifier(&self) { + info!("Starting presence notifier..."); + let notifier_config = presence::NotifierConfig::new( + self.config.get_presence_directory_server(), + self.config.get_announce_address(), + self.sphinx_keypair.public_key().to_base58_string(), + self.config.get_layer(), + self.config.get_presence_sending_delay(), + ); + presence::Notifier::new(notifier_config).start(self.runtime.handle()); + } + + fn start_metrics_reporter(&self) -> metrics::MetricsReporter { + info!("Starting metrics reporter..."); + metrics::MetricsController::new( + self.config.get_metrics_directory_server(), + self.sphinx_keypair.public_key().to_base58_string(), + self.config.get_metrics_sending_delay(), + ) + .start(self.runtime.handle()) + } + + fn start_socket_listener( + &self, + metrics_reporter: metrics::MetricsReporter, + forwarding_channel: mpsc::UnboundedSender<(SocketAddr, Vec)>, ) { - let mut buf = [0u8; sphinx::PACKET_SIZE]; + info!("Starting socket listener..."); + // this is the only location where our private key is going to be copied + // it will be held in memory owned by `MixNode` and inside an Arc of `PacketProcessor` + let packet_processor = + PacketProcessor::new(self.sphinx_keypair.private_key().clone(), metrics_reporter); - // In a loop, read data from the socket and write the data back. - loop { - match socket.read(&mut buf).await { - // socket closed - Ok(n) if n == 0 => { - trace!("Remote connection closed."); - return; - } - Ok(_) => { - let fwd_data = match PacketProcessor::process_sphinx_data_packet( - buf.as_ref(), - processing_data.clone(), - ) - .await - { - Ok(fwd_data) => fwd_data, - Err(e) => { - warn!("failed to process sphinx packet: {:?}", e); - return; - } - }; - PacketProcessor::wait_and_forward(fwd_data).await; - } - Err(e) => { - warn!("failed to read from socket; err = {:?}", e); - return; - } - }; - - // Write the some data back - if let Err(e) = socket.write_all(b"foomp").await { - warn!("failed to write reply to socket; err = {:?}", e); - return; - } - } + listener::run_socket_listener( + self.runtime.handle(), + self.config.get_listening_address(), + packet_processor, + forwarding_channel, + ); } - pub fn start(&self, config: node::Config) -> Result<(), Box> { - // Create the runtime, probably later move it to MixNode itself? - let mut rt = Runtime::new()?; + fn start_packet_forwarder(&mut self) -> mpsc::UnboundedSender<(SocketAddr, Vec)> { + info!("Starting packet forwarder..."); - let (received_tx, received_rx) = mpsc::channel(1024); - let (sent_tx, sent_rx) = mpsc::channel(1024); + // this can later be replaced with topology information + let initial_addresses = vec![]; + self.runtime + .block_on(packet_forwarding::PacketForwarder::new( + initial_addresses, + self.config.get_packet_forwarding_initial_backoff(), + self.config.get_packet_forwarding_maximum_backoff(), + )) + .start(self.runtime.handle()) + } - let directory_cfg = directory_client::Config { - base_url: self.directory_server.clone(), - }; - let pub_key_str = bs58::encode(&self.public_key.to_bytes().to_vec()).into_string(); + pub fn run(&mut self) { + let forwarding_channel = self.start_packet_forwarder(); + let metrics_reporter = self.start_metrics_reporter(); + self.start_socket_listener(metrics_reporter, forwarding_channel); + self.start_presence_notifier(); - rt.spawn({ - let presence_notifier = presence::Notifier::new(&config); - presence_notifier.run() - }); + if let Err(e) = self.runtime.block_on(tokio::signal::ctrl_c()) { + error!( + "There was an error while capturing SIGINT - {:?}. We will terminate regardless", + e + ); + } - let metrics = MetricsReporter::new().add_arc_mutex(); - rt.spawn(MetricsReporter::run_received_metrics_control( - metrics.clone(), - received_rx, - )); - rt.spawn(MetricsReporter::run_sent_metrics_control( - metrics.clone(), - sent_rx, - )); - rt.spawn(MetricsReporter::run_metrics_sender( - metrics, - directory_cfg, - pub_key_str, - )); - - // Spawn the root task - rt.block_on(async { - let mut listener = tokio::net::TcpListener::bind(self.network_address).await?; - let processing_data = - ProcessingData::new(self.secret_key, received_tx, sent_tx).add_arc_mutex(); - - loop { - let (socket, _) = listener.accept().await?; - - let thread_processing_data = processing_data.clone(); - tokio::spawn(async move { - MixNode::process_socket_connection(socket, thread_processing_data).await; - }); - } - }) + println!( + "Received SIGINT - the mixnode will terminate now (threads are not YET nicely stopped)" + ); } } diff --git a/mixnode/src/node/packet_forwarding.rs b/mixnode/src/node/packet_forwarding.rs new file mode 100644 index 0000000000..8335322081 --- /dev/null +++ b/mixnode/src/node/packet_forwarding.rs @@ -0,0 +1,48 @@ +use futures::channel::mpsc; +use futures::StreamExt; +use log::*; +use std::net::SocketAddr; +use std::time::Duration; +use tokio::runtime::Handle; + +pub(crate) struct PacketForwarder<'a> { + tcp_client: multi_tcp_client::Client<'a>, + conn_tx: mpsc::UnboundedSender<(SocketAddr, Vec)>, + conn_rx: mpsc::UnboundedReceiver<(SocketAddr, Vec)>, +} + +impl PacketForwarder<'static> { + pub(crate) async fn new( + initial_endpoints: Vec, + initial_reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, + ) -> PacketForwarder<'static> { + let tcp_client_config = multi_tcp_client::Config::new( + initial_endpoints, + initial_reconnection_backoff, + maximum_reconnection_backoff, + ); + + let (conn_tx, conn_rx) = mpsc::unbounded(); + + PacketForwarder { + tcp_client: multi_tcp_client::Client::new(tcp_client_config).await, + conn_tx, + conn_rx, + } + } + + pub(crate) fn start(mut self, handle: &Handle) -> mpsc::UnboundedSender<(SocketAddr, Vec)> { + // TODO: what to do with the lost JoinHandle? + let sender_channel = self.conn_tx.clone(); + handle.spawn(async move { + while let Some((address, packet)) = self.conn_rx.next().await { + match self.tcp_client.send(address, &packet).await { + Err(e) => warn!("Failed to forward packet to {:?} - {:?}", address, e), + Ok(_) => trace!("Forwarded packet to {:?}", address), + } + } + }); + sender_channel + } +} diff --git a/mixnode/src/node/packet_processing.rs b/mixnode/src/node/packet_processing.rs new file mode 100644 index 0000000000..ec6187e9aa --- /dev/null +++ b/mixnode/src/node/packet_processing.rs @@ -0,0 +1,109 @@ +use crate::node::metrics; +use addressing::AddressTypeError; +use crypto::encryption; +use log::*; +use sphinx::header::delays::Delay as SphinxDelay; +use sphinx::route::NodeAddressBytes; +use sphinx::{ProcessedPacket, SphinxPacket}; +use std::net::SocketAddr; +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Debug)] +pub enum MixProcessingError { + SphinxRecoveryError, + ReceivedFinalHopError, + SphinxProcessingError, + InvalidHopAddress, +} + +pub enum MixProcessingResult { + ForwardHop(SocketAddr, Vec), + #[allow(dead_code)] + LoopMessage, +} + +impl From for MixProcessingError { + // for time being just have a single error instance for all possible results of sphinx::ProcessingError + fn from(_: sphinx::ProcessingError) -> Self { + use MixProcessingError::*; + + SphinxRecoveryError + } +} + +impl From for MixProcessingError { + fn from(_: AddressTypeError) -> Self { + use MixProcessingError::*; + + InvalidHopAddress + } +} + +// PacketProcessor contains all data required to correctly unwrap and forward sphinx packets +#[derive(Clone)] +pub struct PacketProcessor { + secret_key: Arc, + metrics_reporter: metrics::MetricsReporter, +} + +impl PacketProcessor { + pub(crate) fn new( + secret_key: encryption::PrivateKey, + metrics_reporter: metrics::MetricsReporter, + ) -> Self { + PacketProcessor { + secret_key: Arc::new(secret_key), + metrics_reporter, + } + } + + pub(crate) fn report_sent(&self, addr: SocketAddr) { + self.metrics_reporter.report_sent(addr.to_string()) + } + + async fn process_forward_hop( + &self, + packet: SphinxPacket, + forward_address: NodeAddressBytes, + delay: SphinxDelay, + ) -> Result { + let next_hop_address = + addressing::socket_address_from_encoded_bytes(forward_address.to_bytes())?; + + // Delay packet for as long as required + tokio::time::delay_for(delay.to_duration()).await; + + Ok(MixProcessingResult::ForwardHop( + next_hop_address, + packet.to_bytes(), + )) + } + + pub(crate) async fn process_sphinx_packet( + &self, + raw_packet_data: [u8; sphinx::PACKET_SIZE], + ) -> Result { + // we received something resembling a sphinx packet, report it! + self.metrics_reporter.report_received(); + + let packet = SphinxPacket::from_bytes(&raw_packet_data)?; + + match packet.process(self.secret_key.deref().inner()) { + Ok(ProcessedPacket::ProcessedPacketForwardHop(packet, address, delay)) => { + self.process_forward_hop(packet, address, delay).await + } + Ok(ProcessedPacket::ProcessedPacketFinalHop(_, _, _)) => { + warn!("Received a loop cover message that we haven't implemented yet!"); + Err(MixProcessingError::ReceivedFinalHopError) + } + Err(e) => { + warn!("Failed to unwrap Sphinx packet: {:?}", e); + Err(MixProcessingError::SphinxProcessingError) + } + } + } +} + +// TODO: the test that definitely needs to be written is as follows: +// we are stuck trying to write to mix A, can we still forward just fine to mix B? diff --git a/mixnode/src/node/presence.rs b/mixnode/src/node/presence.rs index b017bacdea..bc851641a4 100644 --- a/mixnode/src/node/presence.rs +++ b/mixnode/src/node/presence.rs @@ -1,47 +1,77 @@ -use crate::node; +use crate::built_info; use directory_client::presence::mixnodes::MixNodePresence; use directory_client::requests::presence_mixnodes_post::PresenceMixNodesPoster; use directory_client::DirectoryClient; use log::{debug, error}; use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +pub struct NotifierConfig { + directory_server: String, + announce_host: String, + pub_key_string: String, + layer: u64, + sending_delay: Duration, +} + +impl NotifierConfig { + pub fn new( + directory_server: String, + announce_host: String, + pub_key_string: String, + layer: u64, + sending_delay: Duration, + ) -> Self { + NotifierConfig { + directory_server, + announce_host, + pub_key_string, + layer, + sending_delay, + } + } +} pub struct Notifier { - pub net_client: directory_client::Client, + net_client: directory_client::Client, presence: MixNodePresence, + sending_delay: Duration, } impl Notifier { - pub fn new(node_config: &node::Config) -> Notifier { - let config = directory_client::Config { - base_url: node_config.directory_server.clone(), + pub fn new(config: NotifierConfig) -> Notifier { + let directory_client_cfg = directory_client::Config { + base_url: config.directory_server, }; - let net_client = directory_client::Client::new(config); + let net_client = directory_client::Client::new(directory_client_cfg); let presence = MixNodePresence { - host: node_config.announce_address.clone(), - pub_key: node_config.public_key_string(), - layer: node_config.layer as u64, + host: config.announce_host, + pub_key: config.pub_key_string, + layer: config.layer, last_seen: 0, - version: env!("CARGO_PKG_VERSION").to_string(), + version: built_info::PKG_VERSION.to_string(), }; Notifier { net_client, presence, + sending_delay: config.sending_delay, } } - pub fn notify(&self) { + fn notify(&self) { match self.net_client.presence_mix_nodes_post.post(&self.presence) { Err(err) => error!("failed to send presence - {:?}", err), Ok(_) => debug!("sent presence information"), } } - pub async fn run(self) { - let delay_duration = Duration::from_secs(5); - - loop { - self.notify(); - tokio::time::delay_for(delay_duration).await; - } + pub fn start(self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + loop { + self.notify(); + tokio::time::delay_for(self.sending_delay).await; + } + }) } } diff --git a/mixnode/src/node/runner.rs b/mixnode/src/node/runner.rs deleted file mode 100644 index a403e02036..0000000000 --- a/mixnode/src/node/runner.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::banner; -use crate::node; -use crate::node::MixNode; -use clap::ArgMatches; -use std::net::ToSocketAddrs; - -fn print_binding_warning(address: &str) { - println!("\n##### WARNING #####"); - println!( - "\nYou are trying to bind to {} - you might not be accessible to other nodes", - address - ); - println!("\n##### WARNING #####\n"); -} - -pub fn start(matches: &ArgMatches) { - println!("{}", banner()); - println!("Starting mixnode..."); - - let config = new_config(matches); - println!("Public key: {}", config.public_key_string()); - println!("Directory server: {}", config.directory_server); - println!( - "Listening for incoming packets on {}", - config.socket_address - ); - println!( - "Announcing the following socket address: {}", - config.announce_address - ); - - let mix = MixNode::new(&config); - mix.start(config).unwrap(); -} - -fn new_config(matches: &ArgMatches) -> node::Config { - let host = matches.value_of("host").unwrap(); - if host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" { - print_binding_warning(host); - } - - let port = match matches.value_of("port").unwrap_or("1789").parse::() { - Ok(n) => n, - Err(err) => panic!("Invalid port value provided - {:?}", err), - }; - - let layer = match matches.value_of("layer").unwrap().parse::() { - Ok(n) => n, - Err(err) => panic!("Invalid layer value provided - {:?}", err), - }; - - let socket_address = (host, port) - .to_socket_addrs() - .expect("Failed to combine host and port") - .next() - .expect("Failed to extract the socket address from the iterator"); - - let announce_host = matches.value_of("announce_host").unwrap_or(host); - let announce_port = matches - .value_of("announce_port") - .map(|port| port.parse::().unwrap()) - .unwrap_or(port); - - let _ = (announce_host, announce_port) - .to_socket_addrs() - .expect("Failed to combine announce host and port") - .next() - .expect("Failed to extract the announce socket address from the iterator"); - - let announce_address = format!("{}:{}", announce_host, announce_port); - - let (secret_key, public_key) = sphinx::crypto::keygen(); - - let directory_server = matches - .value_of("directory") - .unwrap_or("https://directory.nymtech.net") - .to_string(); - - node::Config { - directory_server, - layer, - public_key, - socket_address, - announce_address, - secret_key, - } -} diff --git a/nym-client/Cargo.toml b/nym-client/Cargo.toml index e5b16383f7..0b5dd51e4a 100644 --- a/nym-client/Cargo.toml +++ b/nym-client/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-client" -version = "0.4.1" +version = "0.5.0-rc.1" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" @@ -25,25 +25,31 @@ reqwest = "0.9.22" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" tokio = { version = "0.2", features = ["full"] } -tungstenite = "0.9.2" +tokio-tungstenite = "0.10.1" ## internal addressing = {path = "../common/addressing" } +config = {path = "../common/config"} crypto = {path = "../common/crypto"} directory-client = { path = "../common/clients/directory-client" } healthcheck = { path = "../common/healthcheck" } mix-client = { path = "../common/clients/mix-client" } +multi-tcp-client = { path = "../common/clients/multi-tcp-client" } pemstore = {path = "../common/pemstore"} provider-client = { path = "../common/clients/provider-client" } sfw-provider-requests = { path = "../sfw-provider/sfw-provider-requests" } topology = {path = "../common/topology" } ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } # sphinx = { path = "../../sphinx"} -# putting this explicitly below everything and most likely, the next time we look into it, it will already have a proper release -tokio-tungstenite = { git = "https://github.com/snapview/tokio-tungstenite", rev="308d9680c0e59dd1e8651659a775c05df937934e" } - [build-dependencies] built = "0.3.2" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +qa = [] +local = [] \ No newline at end of file diff --git a/nym-client/src/client/cover_traffic_stream.rs b/nym-client/src/client/cover_traffic_stream.rs index c48c0c4801..c0e4e8dc57 100644 --- a/nym-client/src/client/cover_traffic_stream.rs +++ b/nym-client/src/client/cover_traffic_stream.rs @@ -1,37 +1,86 @@ -use crate::client::mix_traffic::MixMessage; -use crate::client::topology_control::TopologyInnerRef; -use crate::client::LOOP_COVER_AVERAGE_DELAY; -use futures::channel::mpsc; -use log::{error, info, trace, warn}; +use crate::client::mix_traffic::{MixMessage, MixMessageSender}; +use crate::client::topology_control::TopologyAccessor; +use futures::task::{Context, Poll}; +use futures::{Future, Stream, StreamExt}; +use log::*; use sphinx::route::Destination; +use std::pin::Pin; use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; +use tokio::time; use topology::NymTopology; -pub(crate) async fn start_loop_cover_traffic_stream( - tx: mpsc::UnboundedSender, +pub(crate) struct LoopCoverTrafficStream { + average_packet_delay: Duration, + average_cover_message_sending_delay: Duration, + next_delay: time::Delay, + mix_tx: MixMessageSender, our_info: Destination, - topology_ctrl_ref: TopologyInnerRef, -) { - info!("Starting loop cover traffic stream"); - loop { - trace!("next cover message!"); - let delay = mix_client::poisson::sample(LOOP_COVER_AVERAGE_DELAY); - let delay_duration = Duration::from_secs_f64(delay); - tokio::time::delay_for(delay_duration).await; + topology_access: TopologyAccessor, +} - let read_lock = topology_ctrl_ref.read().await; - let topology = match read_lock.topology.as_ref() { - None => { - warn!("No valid topology detected - won't send any loop cover message this time"); - continue; - } - Some(topology) => topology, +impl Stream for LoopCoverTrafficStream { + // Item is only used to indicate we should create a new message rather than actual cover message + // reason being to not introduce unnecessary complexity by having to keep state of topology + // mutex when trying to acquire it. So right now the Stream trait serves as a glorified timer. + // Perhaps this should be changed in the future. + type Item = (); + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // it is not yet time to return a message + if Pin::new(&mut self.next_delay).poll(cx).is_pending() { + return Poll::Pending; }; - let cover_message = match mix_client::packet::loop_cover_message( - our_info.address, - our_info.identifier, - topology, + // we know it's time to send a message, so let's prepare delay for the next one + // Get the `now` by looking at the current `delay` deadline + let now = self.next_delay.deadline(); + let next_poisson_delay = + mix_client::poisson::sample(self.average_cover_message_sending_delay); + + // The next interval value is `next_poisson_delay` after the one that just + // yielded. + let next = now + next_poisson_delay; + self.next_delay.reset(next); + + Poll::Ready(Some(())) + } +} + +impl LoopCoverTrafficStream { + pub(crate) fn new( + mix_tx: MixMessageSender, + our_info: Destination, + topology_access: TopologyAccessor, + average_cover_message_sending_delay: time::Duration, + average_packet_delay: time::Duration, + ) -> Self { + LoopCoverTrafficStream { + average_packet_delay, + average_cover_message_sending_delay, + next_delay: time::delay_for(Default::default()), + mix_tx, + our_info, + topology_access, + } + } + + async fn on_new_message(&mut self) { + trace!("next cover message!"); + let route = match self.topology_access.random_route().await { + None => { + warn!("No valid topology detected - won't send any loop cover message this time"); + return; + } + Some(route) => route, + }; + + let cover_message = match mix_client::packet::loop_cover_message_route( + self.our_info.address.clone(), + self.our_info.identifier, + route, + self.average_packet_delay, ) { Ok(message) => message, Err(err) => { @@ -39,7 +88,7 @@ pub(crate) async fn start_loop_cover_traffic_stream( "Somehow we managed to create an invalid cover message - {:?}", err ); - continue; + return; } }; @@ -47,7 +96,25 @@ pub(crate) async fn start_loop_cover_traffic_stream( // - we run out of memory // - the receiver channel is closed // in either case there's no recovery and we can only panic - tx.unbounded_send(MixMessage::new(cover_message.0, cover_message.1)) + self.mix_tx + .unbounded_send(MixMessage::new(cover_message.0, cover_message.1)) .unwrap(); } + + async fn run(&mut self) { + // we should set initial delay only when we actually start the stream + self.next_delay = time::delay_for(mix_client::poisson::sample( + self.average_cover_message_sending_delay, + )); + + while let Some(_) = self.next().await { + self.on_new_message().await; + } + } + + pub(crate) fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + self.run().await; + }) + } } diff --git a/nym-client/src/client/mix_traffic.rs b/nym-client/src/client/mix_traffic.rs index bfcd3ddfc6..c71a7c3be6 100644 --- a/nym-client/src/client/mix_traffic.rs +++ b/nym-client/src/client/mix_traffic.rs @@ -1,10 +1,15 @@ use futures::channel::mpsc; use futures::StreamExt; -use log::{debug, error, info, trace}; +use log::*; use sphinx::SphinxPacket; use std::net::SocketAddr; +use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; pub(crate) struct MixMessage(SocketAddr, SphinxPacket); +pub(crate) type MixMessageSender = mpsc::UnboundedSender; +pub(crate) type MixMessageReceiver = mpsc::UnboundedReceiver; impl MixMessage { pub(crate) fn new(address: SocketAddr, packet: SphinxPacket) -> Self { @@ -12,26 +17,57 @@ impl MixMessage { } } -pub(crate) struct MixTrafficController; +// TODO: put our TCP client here +pub(crate) struct MixTrafficController<'a> { + tcp_client: multi_tcp_client::Client<'a>, + mix_rx: MixMessageReceiver, +} -impl MixTrafficController { - pub(crate) async fn run(mut rx: mpsc::UnboundedReceiver) { - info!("Mix Traffic Controller started!"); - let mix_client = mix_client::MixClient::new(); - while let Some(mix_message) = rx.next().await { - debug!("Got a mix_message for {:?}", mix_message.0); - let send_res = mix_client.send(mix_message.1, mix_message.0).await; - match send_res { - Ok(_) => { - trace!("sent a mix message"); - } - // TODO: should there be some kind of threshold of failed messages - // that if reached, the application blows? - Err(e) => error!( - "We failed to send the message to {} :( - {:?}", - mix_message.0, e - ), - }; +impl MixTrafficController<'static> { + pub(crate) async fn new( + initial_endpoints: Vec, + initial_reconnection_backoff: Duration, + maximum_reconnection_backoff: Duration, + mix_rx: MixMessageReceiver, + ) -> Self { + let tcp_client_config = multi_tcp_client::Config::new( + initial_endpoints, + initial_reconnection_backoff, + maximum_reconnection_backoff, + ); + + MixTrafficController { + tcp_client: multi_tcp_client::Client::new(tcp_client_config).await, + mix_rx, } } + + async fn on_message(&mut self, mix_message: MixMessage) { + debug!("Got a mix_message for {:?}", mix_message.0); + match self + .tcp_client + .send(mix_message.0, &mix_message.1.to_bytes()) + .await + { + Ok(_) => trace!("sent a mix message"), + // TODO: should there be some kind of threshold of failed messages + // that if reached, the application blows? + Err(e) => error!( + "We failed to send the packet to {} - {:?}", + mix_message.0, e + ), + }; + } + + pub(crate) async fn run(&mut self) { + while let Some(mix_message) = self.mix_rx.next().await { + self.on_message(mix_message).await; + } + } + + pub(crate) fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + self.run().await; + }) + } } diff --git a/nym-client/src/client/mod.rs b/nym-client/src/client/mod.rs index 4d4e0da408..f4d5e5e347 100644 --- a/nym-client/src/client/mod.rs +++ b/nym-client/src/client/mod.rs @@ -1,16 +1,23 @@ -use crate::client::mix_traffic::MixTrafficController; -use crate::client::received_buffer::ReceivedMessagesBuffer; -use crate::client::topology_control::TopologyInnerRef; +use crate::client::cover_traffic_stream::LoopCoverTrafficStream; +use crate::client::mix_traffic::{MixMessageReceiver, MixMessageSender, MixTrafficController}; +use crate::client::provider_poller::{PolledMessagesReceiver, PolledMessagesSender}; +use crate::client::received_buffer::{ + ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, +}; +use crate::client::topology_control::{ + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, +}; +use crate::config::persistence::pathfinder::ClientPathfinder; +use crate::config::{Config, SocketType}; use crate::sockets::tcp; use crate::sockets::ws; -use crypto::identity::{MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey}; -use directory_client::presence::Topology; +use crypto::identity::MixIdentityKeyPair; +use directory_client::presence; use futures::channel::mpsc; -use futures::join; use log::*; +use pemstore::pemstore::PemStore; use sfw_provider_requests::AuthToken; use sphinx::route::Destination; -use std::marker::PhantomData; use std::net::SocketAddr; use tokio::runtime::Runtime; use topology::NymTopology; @@ -19,92 +26,260 @@ mod cover_traffic_stream; mod mix_traffic; mod provider_poller; mod real_traffic_stream; -pub mod received_buffer; -pub mod topology_control; +pub(crate) mod received_buffer; +pub(crate) mod topology_control; -// TODO: all of those constants should probably be moved to config file -const LOOP_COVER_AVERAGE_DELAY: f64 = 0.5; -// seconds -const MESSAGE_SENDING_AVERAGE_DELAY: f64 = 0.5; -// seconds; -const FETCH_MESSAGES_DELAY: f64 = 1.0; // seconds; +pub(crate) type InputMessageSender = mpsc::UnboundedSender; +pub(crate) type InputMessageReceiver = mpsc::UnboundedReceiver; -const TOPOLOGY_REFRESH_RATE: f64 = 10.0; // seconds - -pub enum SocketType { - TCP, - WebSocket, - None, -} - -pub struct NymClient -where - IDPair: MixnetIdentityKeyPair + Send + Sync, - Priv: MixnetIdentityPrivateKey + Send + Sync, - Pub: MixnetIdentityPublicKey + Send + Sync, -{ - keypair: IDPair, +pub struct NymClient { + runtime: Runtime, + config: Config, + identity_keypair: MixIdentityKeyPair, // to be used by "send" function or socket, etc - pub input_tx: mpsc::UnboundedSender, - - input_rx: mpsc::UnboundedReceiver, - socket_listening_address: SocketAddr, - directory: String, - auth_token: Option, - socket_type: SocketType, - - _phantom_private: PhantomData, - _phantom_public: PhantomData, + input_tx: Option, } #[derive(Debug)] -pub struct InputMessage(pub Destination, pub Vec); +// TODO: make fields private +pub(crate) struct InputMessage(pub Destination, pub Vec); -impl NymClient -where - IDPair: MixnetIdentityKeyPair + Send + Sync, - Priv: MixnetIdentityPrivateKey + Send + Sync, - Pub: MixnetIdentityPublicKey + Send + Sync, -{ - pub fn new( - keypair: IDPair, - socket_listening_address: SocketAddr, - directory: String, - auth_token: Option, - socket_type: SocketType, - ) -> Self { - let (input_tx, input_rx) = mpsc::unbounded::(); +impl NymClient { + fn load_identity_keys(config_file: &Config) -> MixIdentityKeyPair { + let identity_keypair = PemStore::new(ClientPathfinder::new_from_config(&config_file)) + .read_identity() + .expect("Failed to read stored identity key files"); + println!( + "Public identity key: {}\nFor time being, it is identical to address", + identity_keypair.public_key.to_base58_string() + ); + identity_keypair + } + + pub fn new(config: Config) -> Self { + let identity_keypair = Self::load_identity_keys(&config); NymClient { - keypair, - input_tx, - input_rx, - socket_listening_address, - directory, - auth_token, - socket_type, - _phantom_private: PhantomData, - _phantom_public: PhantomData, + runtime: Runtime::new().unwrap(), + config, + identity_keypair, + input_tx: None, } } + pub fn as_mix_destination(&self) -> Destination { + Destination::new( + self.identity_keypair.public_key().derive_address(), + // TODO: what with SURBs? + Default::default(), + ) + } + async fn get_provider_socket_address( - &self, - topology_ctrl_ref: TopologyInnerRef, + provider_id: String, + mut topology_accessor: TopologyAccessor, ) -> SocketAddr { - // this is temporary and assumes there exists only a single provider. - topology_ctrl_ref.read().await.topology.as_ref().unwrap() + topology_accessor.get_current_topology_clone().await.as_ref().expect("The current network topoloy is empty - are you using correct directory server?") .providers() - .first() - .expect("Could not get a provider from the initial network topology, are you using the right directory server?") + .iter() + .find(|provider| provider.pub_key == provider_id) + .unwrap_or_else( || panic!("Could not find provider with id {:?} - are you sure it is still online? Perhaps try to run `nym-client init` again to obtain a new provider", provider_id)) .client_listener } - pub fn start(self) -> Result<(), Box> { - info!("Starting nym client"); - let mut rt = Runtime::new()?; + // future constantly pumping loop cover traffic at some specified average rate + // the pumped traffic goes to the MixTrafficController + fn start_cover_traffic_stream( + &self, + topology_accessor: TopologyAccessor, + mix_tx: MixMessageSender, + ) { + info!("Starting loop cover traffic stream..."); + // we need to explicitly enter runtime due to "next_delay: time::delay_for(Default::default())" + // set in the constructor which HAS TO be called within context of a tokio runtime + self.runtime + .enter(|| { + LoopCoverTrafficStream::new( + mix_tx, + self.as_mix_destination(), + topology_accessor, + self.config.get_loop_cover_traffic_average_delay(), + self.config.get_average_packet_delay(), + ) + }) + .start(self.runtime.handle()); + } + fn start_real_traffic_stream( + &self, + topology_accessor: TopologyAccessor, + mix_tx: MixMessageSender, + input_rx: InputMessageReceiver, + ) { + info!("Starting real traffic stream..."); + // we need to explicitly enter runtime due to "next_delay: time::delay_for(Default::default())" + // set in the constructor which HAS TO be called within context of a tokio runtime + self.runtime + .enter(|| { + real_traffic_stream::OutQueueControl::new( + mix_tx, + input_rx, + self.as_mix_destination(), + topology_accessor, + self.config.get_average_packet_delay(), + self.config.get_message_sending_average_delay(), + ) + }) + .start(self.runtime.handle()); + } + + // 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, + query_receiver: ReceivedBufferRequestReceiver, + poller_receiver: PolledMessagesReceiver, + ) { + info!("Starting 'received messages buffer controller'..."); + ReceivedMessagesBufferController::new(query_receiver, poller_receiver) + .start(self.runtime.handle()) + } + + // future constantly trying to fetch any received messages from the provider + // the received messages are sent to ReceivedMessagesBuffer to be available to rest of the system + fn start_provider_poller( + &mut self, + topology_accessor: TopologyAccessor, + poller_input_tx: PolledMessagesSender, + ) { + info!("Starting provider poller..."); + // we already have our provider written in the config + let provider_id = self.config.get_provider_id(); + + let provider_client_listener_address = self.runtime.block_on( + Self::get_provider_socket_address(provider_id, topology_accessor), + ); + + let mut provider_poller = provider_poller::ProviderPoller::new( + poller_input_tx, + provider_client_listener_address, + self.identity_keypair.public_key().derive_address(), + self.config + .get_provider_auth_token() + .map(|str_token| AuthToken::try_from_base58_string(str_token).ok()) + .unwrap_or(None), + self.config.get_fetch_message_delay(), + ); + + if !provider_poller.is_registered() { + info!("Trying to perform initial provider registration..."); + self.runtime + .block_on(provider_poller.perform_initial_registration()) + .expect("Failed to perform initial provider registration"); + } + provider_poller.start(self.runtime.handle()); + } + + // future responsible for periodically polling directory server and updating + // the current global view of topology + fn start_topology_refresher( + &mut self, + topology_accessor: TopologyAccessor, + ) { + let healthcheck_keys = MixIdentityKeyPair::new(); + + let topology_refresher_config = TopologyRefresherConfig::new( + self.config.get_directory_server(), + self.config.get_topology_refresh_rate(), + healthcheck_keys, + self.config.get_topology_resolution_timeout(), + self.config.get_number_of_healthcheck_test_packets() as usize, + self.config.get_node_score_threshold(), + ); + let mut topology_refresher = + TopologyRefresher::new(topology_refresher_config, topology_accessor); + // before returning, block entire runtime to refresh the current network view so that any + // components depending on topology would see a non-empty view + info!("Obtaining initial network topology..."); + self.runtime.block_on(topology_refresher.refresh()); + info!("Starting topology refresher..."); + topology_refresher.start(self.runtime.handle()); + } + + // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) + fn start_mix_traffic_controller(&mut self, mix_rx: MixMessageReceiver) { + info!("Starting mix trafic controller..."); + // TODO: possible optimisation: set the initial endpoints to all known mixes from layer 1 + let initial_mix_endpoints = Vec::new(); + self.runtime + .block_on(MixTrafficController::new( + initial_mix_endpoints, + self.config.get_packet_forwarding_initial_backoff(), + self.config.get_packet_forwarding_maximum_backoff(), + mix_rx, + )) + .start(self.runtime.handle()); + } + + fn start_socket_listener( + &self, + topology_accessor: TopologyAccessor, + received_messages_buffer_output_tx: ReceivedBufferRequestSender, + input_tx: InputMessageSender, + ) { + match self.config.get_socket_type() { + SocketType::WebSocket => { + ws::start_websocket( + self.runtime.handle(), + self.config.get_listening_port(), + input_tx, + received_messages_buffer_output_tx, + self.identity_keypair.public_key().derive_address(), + topology_accessor, + ); + } + SocketType::TCP => { + tcp::start_tcpsocket( + self.runtime.handle(), + self.config.get_listening_port(), + input_tx, + received_messages_buffer_output_tx, + self.identity_keypair.public_key().derive_address(), + topology_accessor, + ); + } + SocketType::None => (), + } + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's entirely untested and there are absolutely no guarantees about it + pub fn send_message(&self, destination: Destination, message: Vec) { + self.input_tx + .as_ref() + .expect("start method was not called before!") + .unbounded_send(InputMessage(destination, message)) + .unwrap() + } + + /// blocking version of `start` method. Will run forever (or until SIGINT is sent) + pub fn run_forever(&mut self) { + self.start(); + if let Err(e) = self.runtime.block_on(tokio::signal::ctrl_c()) { + error!( + "There was an error while capturing SIGINT - {:?}. We will terminate regardless", + e + ); + } + + println!( + "Received SIGINT - the mixnode will terminate now (threads are not YET nicely stopped)" + ); + } + + pub fn start(&mut self) { + info!("Starting nym client"); // channels for inter-component communication // mix_tx is the transmitter for any component generating sphinx packets that are to be sent to the mixnet @@ -122,128 +297,27 @@ where let (received_messages_buffer_output_tx, received_messages_buffer_output_rx) = mpsc::unbounded(); - let self_address = self.keypair.public_key().derive_address(); + // channels responsible for controlling real messages + let (input_tx, input_rx) = mpsc::unbounded::(); - // generate same type of keys we have as our identity - let healthcheck_keys = IDPair::new(); - - // TODO: when we switch to our graph topology, we need to remember to change 'Topology' type - let topology_controller = - rt.block_on(topology_control::TopologyControl::::new( - self.directory.clone(), - TOPOLOGY_REFRESH_RATE, - healthcheck_keys, - )); - - let provider_client_listener_address = - rt.block_on(self.get_provider_socket_address(topology_controller.get_inner_ref())); - - let mut provider_poller = provider_poller::ProviderPoller::new( - poller_input_tx, - provider_client_listener_address, - self_address, - self.auth_token, + // TODO: when we switch to our graph topology, we need to remember to change 'presence::Topology' type + let shared_topology_accessor = TopologyAccessor::::new(); + // 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()); + self.start_received_messages_buffer_controller( + received_messages_buffer_output_rx, + poller_input_rx, ); - - // registration - if let Err(err) = rt.block_on(provider_poller.perform_initial_registration()) { - panic!("Failed to perform initial registration: {:?}", err); - }; - - // setup all of futures for the components running on the client - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - let received_messages_buffer_controllers_future = rt.spawn( - ReceivedMessagesBuffer::new() - .start_controllers(poller_input_rx, received_messages_buffer_output_rx), + self.start_provider_poller(shared_topology_accessor.clone(), poller_input_tx); + self.start_mix_traffic_controller(mix_rx); + self.start_cover_traffic_stream(shared_topology_accessor.clone(), mix_tx.clone()); + self.start_real_traffic_stream(shared_topology_accessor.clone(), mix_tx, input_rx); + self.start_socket_listener( + shared_topology_accessor, + received_messages_buffer_output_tx, + input_tx.clone(), ); - - // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) - let mix_traffic_future = rt.spawn(MixTrafficController::run(mix_rx)); - - // future constantly pumping loop cover traffic at some specified average rate - // the pumped traffic goes to the MixTrafficController - let loop_cover_traffic_future = - rt.spawn(cover_traffic_stream::start_loop_cover_traffic_stream( - mix_tx.clone(), - Destination::new(self_address, Default::default()), - topology_controller.get_inner_ref(), - )); - - // cloning arguments required by OutQueueControl; required due to move - let input_rx = self.input_rx; - let topology_ref = topology_controller.get_inner_ref(); - - // future constantly pumping traffic at some specified average rate - // if a real message is available on 'input_rx' that might have been received from say - // the websocket, the real message is used, otherwise a loop cover message is generated - // the pumped traffic goes to the MixTrafficController - let out_queue_control_future = rt.spawn(async move { - real_traffic_stream::OutQueueControl::new( - mix_tx, - input_rx, - Destination::new(self_address, Default::default()), - topology_ref, - ) - .run_out_queue_control() - .await - }); - - // future constantly trying to fetch any received messages from the provider - // the received messages are sent to ReceivedMessagesBuffer to be available to rest of the system - let provider_polling_future = rt.spawn(provider_poller.start_provider_polling()); - - // a temporary workaround for starting socket listener of specified type - // in the future the actual socket handler should start THIS client instead - match self.socket_type { - SocketType::WebSocket => { - rt.spawn(ws::start_websocket( - self.socket_listening_address, - self.input_tx, - received_messages_buffer_output_tx, - self_address, - topology_controller.get_inner_ref(), - )); - } - SocketType::TCP => { - rt.spawn(tcp::start_tcpsocket( - self.socket_listening_address, - self.input_tx, - received_messages_buffer_output_tx, - self_address, - topology_controller.get_inner_ref(), - )); - } - SocketType::None => (), - } - - // future responsible for periodically polling directory server and updating - // the current global view of topology - let topology_refresher_future = rt.spawn(topology_controller.run_refresher()); - - rt.block_on(async { - let future_results = join!( - received_messages_buffer_controllers_future, - mix_traffic_future, - loop_cover_traffic_future, - out_queue_control_future, - provider_polling_future, - topology_refresher_future, - ); - - assert!( - future_results.0.is_ok() - && future_results.1.is_ok() - && future_results.2.is_ok() - && future_results.3.is_ok() - && future_results.4.is_ok() - && future_results.5.is_ok() - ); - }); - - // this line in theory should never be reached as the runtime should be permanently blocked on traffic senders - error!("The client went kaput..."); - Ok(()) + self.input_tx = Some(input_tx); } } diff --git a/nym-client/src/client/provider_poller.rs b/nym-client/src/client/provider_poller.rs index 71ccd41fdd..24d329c9ae 100644 --- a/nym-client/src/client/provider_poller.rs +++ b/nym-client/src/client/provider_poller.rs @@ -1,13 +1,18 @@ -use crate::client::FETCH_MESSAGES_DELAY; use futures::channel::mpsc; -use log::{debug, error, info, trace, warn}; +use log::*; use provider_client::ProviderClientError; use sfw_provider_requests::AuthToken; use sphinx::route::DestinationAddressBytes; use std::net::SocketAddr; -use std::time::Duration; +use std::time; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +pub(crate) type PolledMessagesSender = mpsc::UnboundedSender>>; +pub(crate) type PolledMessagesReceiver = mpsc::UnboundedReceiver>>; pub(crate) struct ProviderPoller { + polling_rate: time::Duration, provider_client: provider_client::ProviderClient, poller_tx: mpsc::UnboundedSender>>, } @@ -18,6 +23,7 @@ impl ProviderPoller { provider_client_listener_address: SocketAddr, client_address: DestinationAddressBytes, auth_token: Option, + polling_rate: time::Duration, ) -> Self { ProviderPoller { provider_client: provider_client::ProviderClient::new( @@ -26,14 +32,19 @@ impl ProviderPoller { auth_token, ), poller_tx, + polling_rate, } } + pub(crate) fn is_registered(&self) -> bool { + self.provider_client.is_registered() + } + // This method is only temporary until registration is moved to `client init` pub(crate) async fn perform_initial_registration(&mut self) -> Result<(), ProviderClientError> { debug!("performing initial provider registration"); - if !self.provider_client.is_registered() { + if !self.is_registered() { let auth_token = match self.provider_client.register().await { // in this particular case we can ignore this error Err(ProviderClientError::ClientAlreadyRegisteredError) => return Ok(()), @@ -43,20 +54,17 @@ impl ProviderPoller { self.provider_client.update_token(auth_token) } else { - warn!("did not perform registration - we were already registered") + warn!("did not perform provider registration - we were already registered") } Ok(()) } pub(crate) async fn start_provider_polling(self) { - info!("Starting provider poller"); - let loop_message = &mix_client::packet::LOOP_COVER_MESSAGE_PAYLOAD.to_vec(); let dummy_message = &sfw_provider_requests::DUMMY_MESSAGE_CONTENT.to_vec(); - let delay_duration = Duration::from_secs_f64(FETCH_MESSAGES_DELAY); - let extended_delay_duration = Duration::from_secs_f64(FETCH_MESSAGES_DELAY * 10.0); + let extended_delay_duration = self.polling_rate * 10; loop { debug!("Polling provider..."); @@ -82,7 +90,11 @@ impl ProviderPoller { // in either case there's no recovery and we can only panic self.poller_tx.unbounded_send(good_messages).unwrap(); - tokio::time::delay_for(delay_duration).await; + tokio::time::delay_for(self.polling_rate).await; } } + + pub(crate) fn start(self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { self.start_provider_polling().await }) + } } diff --git a/nym-client/src/client/real_traffic_stream.rs b/nym-client/src/client/real_traffic_stream.rs index fc8069c12b..2e83196610 100644 --- a/nym-client/src/client/real_traffic_stream.rs +++ b/nym-client/src/client/real_traffic_stream.rs @@ -1,6 +1,6 @@ use crate::client::mix_traffic::MixMessage; -use crate::client::topology_control::TopologyInnerRef; -use crate::client::{InputMessage, MESSAGE_SENDING_AVERAGE_DELAY}; +use crate::client::topology_control::TopologyAccessor; +use crate::client::InputMessage; use futures::channel::mpsc; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; @@ -8,18 +8,19 @@ use log::{error, info, trace, warn}; use sphinx::route::Destination; use std::pin::Pin; use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; use tokio::time; use topology::NymTopology; -// have a rather low value for test sake -const AVERAGE_PACKET_DELAY: f64 = 0.1; - pub(crate) struct OutQueueControl { - delay: time::Delay, + average_packet_delay: Duration, + average_message_sending_delay: Duration, + next_delay: time::Delay, mix_tx: mpsc::UnboundedSender, input_rx: mpsc::UnboundedReceiver, our_info: Destination, - topology_ctrl_ref: TopologyInnerRef, + topology_access: TopologyAccessor, } pub(crate) enum StreamMessage { @@ -32,21 +33,19 @@ impl Stream for OutQueueControl { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { // it is not yet time to return a message - if Pin::new(&mut self.delay).poll(cx).is_pending() { + if Pin::new(&mut self.next_delay).poll(cx).is_pending() { return Poll::Pending; }; // we know it's time to send a message, so let's prepare delay for the next one // Get the `now` by looking at the current `delay` deadline - let now = self.delay.deadline(); - - let next_poisson_delay = - Duration::from_secs_f64(mix_client::poisson::sample(MESSAGE_SENDING_AVERAGE_DELAY)); + let now = self.next_delay.deadline(); + let next_poisson_delay = mix_client::poisson::sample(self.average_message_sending_delay); // The next interval value is `next_poisson_delay` after the one that just // yielded. let next = now + next_poisson_delay; - self.delay.reset(next); + self.next_delay.reset(next); // decide what kind of message to send match Pin::new(&mut self.input_rx).poll_next(cx) { @@ -63,70 +62,86 @@ impl Stream for OutQueueControl { } } -impl OutQueueControl { +impl OutQueueControl { pub(crate) fn new( mix_tx: mpsc::UnboundedSender, input_rx: mpsc::UnboundedReceiver, our_info: Destination, - topology: TopologyInnerRef, + topology_access: TopologyAccessor, + average_packet_delay: Duration, + average_message_sending_delay: Duration, ) -> Self { - let initial_delay = time::delay_for(Duration::from_secs_f64(MESSAGE_SENDING_AVERAGE_DELAY)); OutQueueControl { - delay: initial_delay, + average_packet_delay, + average_message_sending_delay, + next_delay: time::delay_for(Default::default()), mix_tx, input_rx, our_info, - topology_ctrl_ref: topology, + topology_access, } } + async fn on_message(&mut self, next_message: StreamMessage) { + trace!("created new message"); + let route = match self.topology_access.random_route().await { + None => { + warn!("No valid topology detected - won't send any real or loop message this time"); + // TODO: this creates a potential problem: we can lose real messages if we were + // unable to get topology, perhaps we should store them in some buffer? + return; + } + Some(route) => route, + }; + + let next_packet = match next_message { + StreamMessage::Cover => mix_client::packet::loop_cover_message_route( + self.our_info.address.clone(), + self.our_info.identifier, + route, + self.average_packet_delay, + ), + StreamMessage::Real(real_message) => mix_client::packet::encapsulate_message_route( + real_message.0, + real_message.1, + route, + self.average_packet_delay, + ), + }; + + let next_packet = match next_packet { + Ok(message) => message, + Err(err) => { + error!( + "Somehow we managed to create an invalid traffic message - {:?}", + err + ); + return; + } + }; + + // if this one fails, there's no retrying because it means that either: + // - we run out of memory + // - the receiver channel is closed + // in either case there's no recovery and we can only panic + self.mix_tx + .unbounded_send(MixMessage::new(next_packet.0, next_packet.1)) + .unwrap(); + } + pub(crate) async fn run_out_queue_control(mut self) { + // we should set initial delay only when we actually start the stream + self.next_delay = time::delay_for(mix_client::poisson::sample( + self.average_message_sending_delay, + )); + info!("starting out queue controller"); while let Some(next_message) = self.next().await { - trace!("created new message"); - let read_lock = self.topology_ctrl_ref.read().await; - let topology = match read_lock.topology.as_ref() { - None => { - warn!( - "No valid topology detected - won't send any loop cover message this time" - ); - continue; - } - Some(topology) => topology, - }; - - let next_packet = match next_message { - StreamMessage::Cover => mix_client::packet::loop_cover_message( - self.our_info.address, - self.our_info.identifier, - topology, - ), - StreamMessage::Real(real_message) => mix_client::packet::encapsulate_message( - real_message.0, - real_message.1, - topology, - AVERAGE_PACKET_DELAY, - ), - }; - - let next_packet = match next_packet { - Ok(message) => message, - Err(err) => { - error!( - "Somehow we managed to create an invalid traffic message - {:?}", - err - ); - continue; - } - }; - - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx - .unbounded_send(MixMessage::new(next_packet.0, next_packet.1)) - .unwrap(); + self.on_message(next_message).await; } } + + pub(crate) fn start(self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { self.run_out_queue_control().await }) + } } diff --git a/nym-client/src/client/received_buffer.rs b/nym-client/src/client/received_buffer.rs index a6df580c9f..123fc92882 100644 --- a/nym-client/src/client/received_buffer.rs +++ b/nym-client/src/client/received_buffer.rs @@ -1,89 +1,124 @@ +use crate::client::provider_poller::PolledMessagesReceiver; use futures::channel::{mpsc, oneshot}; -use futures::lock::Mutex as FMutex; +use futures::lock::Mutex; use futures::StreamExt; -use log::{error, info, trace}; +use log::*; use std::sync::Arc; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; -pub type BufferResponse = oneshot::Sender>>; +pub(crate) type ReceivedBufferResponse = oneshot::Sender>>; +pub(crate) type ReceivedBufferRequestSender = mpsc::UnboundedSender; +pub(crate) type ReceivedBufferRequestReceiver = mpsc::UnboundedReceiver; -pub(crate) struct ReceivedMessagesBuffer { - inner: Arc>, -} - -impl ReceivedMessagesBuffer { - pub(crate) fn new() -> Self { - ReceivedMessagesBuffer { - inner: Arc::new(FMutex::new(Inner::new())), - } - } - - pub(crate) async fn start_controllers( - self, - poller_rx: mpsc::UnboundedReceiver>>, // to receive new messages - query_receiver: mpsc::UnboundedReceiver, // to receive requests to acquire all stored messages - ) { - let input_controller_future = tokio::spawn(Self::run_poller_input_controller( - self.inner.clone(), - poller_rx, - )); - let output_controller_future = tokio::spawn(Self::run_query_output_controller( - self.inner, - query_receiver, - )); - - futures::future::select(input_controller_future, output_controller_future).await; - panic!("One of the received buffer controllers failed!") - } - - pub(crate) async fn run_poller_input_controller( - buf: Arc>, - mut poller_rx: mpsc::UnboundedReceiver>>, - ) { - info!("Started Received Messages Buffer Input Controller"); - - while let Some(new_messages) = poller_rx.next().await { - Inner::add_new_messages(&*buf, new_messages).await; - } - } - - pub(crate) async fn run_query_output_controller( - buf: Arc>, - mut query_receiver: mpsc::UnboundedReceiver, - ) { - info!("Started Received Messages Buffer Output Controller"); - - while let Some(request) = query_receiver.next().await { - let messages = Inner::acquire_and_empty(&*buf).await; - if let Err(failed_messages) = request.send(messages) { - error!( - "Failed to send the messages to the requester. Adding them back to the buffer" - ); - Inner::add_new_messages(&*buf, failed_messages).await; - } - } - } -} - -pub(crate) struct Inner { +struct ReceivedMessagesBufferInner { messages: Vec>, } -impl Inner { +#[derive(Debug, Clone)] +// Note: you should NEVER create more than a single instance of this using 'new()'. +// You should always use .clone() to create additional instances +struct ReceivedMessagesBuffer { + inner: Arc>, +} + +impl ReceivedMessagesBuffer { fn new() -> Self { - Inner { - messages: Vec::new(), + ReceivedMessagesBuffer { + inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { + messages: Vec::new(), + })), } } - async fn add_new_messages(buf: &FMutex, msgs: Vec>) { + async fn add_new_messages(&mut self, msgs: Vec>) { trace!("Adding new messages to the buffer! {:?}", msgs); - let mut unlocked = buf.lock().await; - unlocked.messages.extend(msgs); + self.inner.lock().await.messages.extend(msgs) } - async fn acquire_and_empty(buf: &FMutex) -> Vec> { + async fn acquire_and_empty(&mut self) -> Vec> { trace!("Emptying the buffer and returning all messages"); - let mut unlocked = buf.lock().await; - std::mem::replace(&mut unlocked.messages, Vec::new()) + let mut mutex_guard = self.inner.lock().await; + std::mem::replace(&mut mutex_guard.messages, Vec::new()) + } +} + +struct RequestReceiver { + received_buffer: ReceivedMessagesBuffer, + query_receiver: ReceivedBufferRequestReceiver, +} + +impl RequestReceiver { + fn new( + received_buffer: ReceivedMessagesBuffer, + query_receiver: ReceivedBufferRequestReceiver, + ) -> Self { + RequestReceiver { + received_buffer, + query_receiver, + } + } + + fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + while let Some(request) = self.query_receiver.next().await { + let messages = self.received_buffer.acquire_and_empty().await; + if let Err(failed_messages) = request.send(messages) { + error!( + "Failed to send the messages to the requester. Adding them back to the buffer" + ); + self.received_buffer.add_new_messages(failed_messages).await; + } + } + }) + } +} + +struct MessageReceiver { + received_buffer: ReceivedMessagesBuffer, + poller_receiver: PolledMessagesReceiver, +} + +impl MessageReceiver { + fn new( + received_buffer: ReceivedMessagesBuffer, + poller_receiver: PolledMessagesReceiver, + ) -> Self { + MessageReceiver { + received_buffer, + poller_receiver, + } + } + fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + while let Some(new_messages) = self.poller_receiver.next().await { + self.received_buffer.add_new_messages(new_messages).await; + } + }) + } +} + +pub(crate) struct ReceivedMessagesBufferController { + messsage_receiver: MessageReceiver, + request_receiver: RequestReceiver, +} + +impl ReceivedMessagesBufferController { + pub(crate) fn new( + query_receiver: ReceivedBufferRequestReceiver, + poller_receiver: PolledMessagesReceiver, + ) -> Self { + let received_buffer = ReceivedMessagesBuffer::new(); + + ReceivedMessagesBufferController { + messsage_receiver: MessageReceiver::new(received_buffer.clone(), poller_receiver), + request_receiver: RequestReceiver::new(received_buffer, query_receiver), + } + } + + pub(crate) fn start(self, handle: &Handle) { + // TODO: should we do anything with JoinHandle(s) returned by start methods? + self.messsage_receiver.start(handle); + self.request_receiver.start(handle); } } diff --git a/nym-client/src/client/topology_control.rs b/nym-client/src/client/topology_control.rs index 9833f81166..1b11641bb9 100644 --- a/nym-client/src/client/topology_control.rs +++ b/nym-client/src/client/topology_control.rs @@ -1,28 +1,67 @@ use crate::built_info; -use crypto::identity::{MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey}; +use crypto::identity::MixIdentityKeyPair; +use futures::lock::Mutex; use healthcheck::HealthChecker; -use log::{error, info, trace, warn}; +use log::*; use std::sync::Arc; +use std::time; use std::time::Duration; -use tokio::sync::RwLock as FRwLock; +use tokio::runtime::Handle; +// use tokio::sync::RwLock; +use tokio::task::JoinHandle; use topology::NymTopology; -const NODE_HEALTH_THRESHOLD: f64 = 0.0; +struct TopologyAccessorInner(Option); -// auxiliary type for ease of use -pub type TopologyInnerRef = Arc>>; +impl TopologyAccessorInner { + fn new() -> Self { + TopologyAccessorInner(None) + } -pub(crate) struct TopologyControl -where - T: NymTopology, - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, -{ - directory_server: String, - inner: Arc>>, - health_checker: HealthChecker, - refresh_rate: f64, + fn update(&mut self, new: Option) { + self.0 = new; + } +} + +#[derive(Clone, Debug)] +pub(crate) struct TopologyAccessor { + // TODO: this requires some actual benchmarking to determine if obtaining mutex is not going + // to cause some bottlenecking and whether perhaps RwLock would be better + inner: Arc>>, +} + +impl TopologyAccessor { + pub(crate) fn new() -> Self { + TopologyAccessor { + inner: Arc::new(Mutex::new(TopologyAccessorInner::new())), + } + } + + async fn update_global_topology(&mut self, new_topology: Option) { + self.inner.lock().await.update(new_topology); + } + + // Unless you absolutely need the entire topology, use `random_route` instead + pub(crate) async fn get_current_topology_clone(&mut self) -> Option { + self.inner.lock().await.0.clone() + } + + // this is a rather temporary solution as each client will have an associated provider + // currently that is not implemented yet and there only exists one provider in the network + pub(crate) async fn random_route(&mut self) -> Option> { + match &self.inner.lock().await.0 { + None => None, + Some(ref topology) => { + let mut providers = topology.providers(); + if providers.is_empty() { + return None; + } + // unwrap is fine here as we asserted there is at least single provider + let provider = providers.pop().unwrap().into(); + topology.route_to(provider).ok() + } + } + } } #[derive(Debug)] @@ -31,41 +70,62 @@ enum TopologyError { NoValidPathsError, } -impl TopologyControl -where - T: NymTopology, - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, -{ - pub(crate) async fn new( - directory_server: String, - refresh_rate: f64, - identity_keypair: IDPair, - ) -> Self { - // this is a temporary solution as the healthcheck will eventually be moved to validators - let health_checker = healthcheck::HealthChecker::new(5.0, 2, identity_keypair); +pub(crate) struct TopologyRefresherConfig { + directory_server: String, + refresh_rate: time::Duration, + identity_keypair: MixIdentityKeyPair, + resolution_timeout: time::Duration, + number_test_packets: usize, + node_score_threshold: f64, +} - let mut topology_control = TopologyControl { +impl TopologyRefresherConfig { + pub(crate) fn new( + directory_server: String, + refresh_rate: time::Duration, + identity_keypair: MixIdentityKeyPair, + resolution_timeout: time::Duration, + number_test_packets: usize, + node_score_threshold: f64, + ) -> Self { + TopologyRefresherConfig { directory_server, refresh_rate, - inner: Arc::new(FRwLock::new(Inner::new(None))), + identity_keypair, + resolution_timeout, + number_test_packets, + node_score_threshold, + } + } +} + +pub(crate) struct TopologyRefresher { + directory_server: String, + topology_accessor: TopologyAccessor, + health_checker: HealthChecker, + refresh_rate: Duration, + node_score_threshold: f64, +} + +impl TopologyRefresher { + pub(crate) fn new( + cfg: TopologyRefresherConfig, + topology_accessor: TopologyAccessor, + ) -> Self { + // this is a temporary solution as the healthcheck will eventually be moved to validators + let health_checker = healthcheck::HealthChecker::new( + cfg.resolution_timeout, + cfg.number_test_packets, + cfg.identity_keypair, + ); + + TopologyRefresher { + directory_server: cfg.directory_server, + topology_accessor, health_checker, - }; - - // best effort approach to try to get a valid topology after call to 'new' - let initial_topology = match topology_control.get_current_compatible_topology().await { - Ok(topology) => Some(topology), - Err(err) => { - error!("Initial topology is invalid - {:?}. Right now it will be impossible to send any packets through the mixnet!", err); - None - } - }; - topology_control - .update_global_topology(initial_topology) - .await; - - topology_control + refresh_rate: cfg.refresh_rate, + node_score_threshold: cfg.node_score_threshold, + } } async fn get_current_compatible_topology(&self) -> Result { @@ -89,7 +149,7 @@ where }; let healthy_topology = healthcheck_scores - .filter_topology_by_score(&version_filtered_topology, NODE_HEALTH_THRESHOLD); + .filter_topology_by_score(&version_filtered_topology, self.node_score_threshold); // make sure you can still send a packet through the network: if !healthy_topology.can_construct_path_through() { @@ -99,43 +159,27 @@ where Ok(healthy_topology) } - pub(crate) fn get_inner_ref(&self) -> Arc>> { - self.inner.clone() + pub(crate) async fn refresh(&mut self) { + trace!("Refreshing the topology"); + let new_topology = match self.get_current_compatible_topology().await { + Ok(topology) => Some(topology), + Err(err) => { + warn!("the obtained topology seems to be invalid - {:?}, it will be impossible to send packets through", err); + None + } + }; + + self.topology_accessor + .update_global_topology(new_topology) + .await; } - async fn update_global_topology(&mut self, new_topology: Option) { - // acquire write lock - let mut write_lock = self.inner.write().await; - write_lock.topology = new_topology; - } - - pub(crate) async fn run_refresher(mut self) { - info!("Starting topology refresher"); - let delay_duration = Duration::from_secs_f64(self.refresh_rate); - loop { - trace!("Refreshing the topology"); - let new_topology_res = self.get_current_compatible_topology().await; - - let new_topology = match new_topology_res { - Ok(topology) => Some(topology), - Err(err) => { - warn!("the obtained topology seems to be invalid - {:?}, it will be impossible to send packets through", err); - None - } - }; - - self.update_global_topology(new_topology).await; - tokio::time::delay_for(delay_duration).await; - } - } -} - -pub struct Inner { - pub topology: Option, -} - -impl Inner { - fn new(topology: Option) -> Self { - Inner { topology } + pub(crate) fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + loop { + self.refresh().await; + tokio::time::delay_for(self.refresh_rate).await; + } + }) } } diff --git a/nym-client/src/commands/init.rs b/nym-client/src/commands/init.rs index bc4403e6b9..e6285c3999 100644 --- a/nym-client/src/commands/init.rs +++ b/nym-client/src/commands/init.rs @@ -1,18 +1,146 @@ -use crate::config::persistance::pathfinder::ClientPathfinder; -use clap::ArgMatches; -use crypto::identity::MixnetIdentityKeyPair; +use crate::built_info; +use crate::commands::override_config; +use crate::config::persistence::pathfinder::ClientPathfinder; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::identity::MixIdentityKeyPair; +use directory_client::presence::Topology; use pemstore::pemstore::PemStore; +use sfw_provider_requests::AuthToken; +use sphinx::route::DestinationAddressBytes; +use topology::provider::Node; +use topology::NymTopology; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("init") + .about("Initialise a Nym client. Do this first!") + .arg(Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnet-client we want to create config for.") + .takes_value(true) + .required(true) + ) + .arg(Arg::with_name("provider") + .long("provider") + .help("Id of the provider we have preference to connect to. If left empty, a random provider will be chosen.") + .takes_value(true) + ) + .arg(Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the client is getting topology from") + .takes_value(true), + ) + .arg(Arg::with_name("socket-type") + .long("socket-type") + .help("Type of socket to use (TCP, WebSocket or None) in all subsequent runs") + .takes_value(true) + ) + .arg(Arg::with_name("port") + .short("p") + .long("port") + .help("Port for the socket (if applicable) to listen on in all subsequent runs") + .takes_value(true) + ) +} + +async fn try_provider_registrations( + providers: Vec, + our_address: DestinationAddressBytes, +) -> Option<(String, AuthToken)> { + // since the order of providers is non-deterministic we can just try to get a first 'working' provider + for provider in providers { + let provider_client = provider_client::ProviderClient::new( + provider.client_listener, + our_address.clone(), + None, + ); + let auth_token = provider_client.register().await; + if let Ok(token) = auth_token { + return Some((provider.pub_key, token)); + } + } + None +} + +// in the long run this will be provider specific and only really applicable to a +// relatively small subset of all providers +async fn choose_provider( + directory_server: String, + our_address: DestinationAddressBytes, +) -> (String, AuthToken) { + // TODO: once we change to graph topology this here will need to be updated! + let topology = Topology::new(directory_server.clone()); + let version_filtered_topology = topology.filter_node_versions( + built_info::PKG_VERSION, + built_info::PKG_VERSION, + built_info::PKG_VERSION, + ); + // don't care about health of the networks as mixes can go up and down any time, + // but DO care about providers + let providers = version_filtered_topology.providers(); + + // try to perform registration so that we wouldn't need to do it at startup + // + at the same time we'll know if we can actually talk with that provider + let registration_result = try_provider_registrations(providers, our_address).await; + match registration_result { + None => { + // while technically there's no issue client-side, it will be impossible to execute + // `nym-client run` as no provider is available so it might be best to not finalize + // the init and rely on users trying to init another time? + panic!( + "Currently there are no valid providers available on the network ({}). \ + Please try to run `init` again at later time or change your directory server", + directory_server + ) + } + Some((provider_id, auth_token)) => (provider_id, auth_token), + } +} pub fn execute(matches: &ArgMatches) { println!("Initialising client..."); - let id = matches.value_of("id").unwrap().to_string(); // required for now - let pathfinder = ClientPathfinder::new(id); + let id = matches.value_of("id").unwrap(); // required for now + let mut config = crate::config::Config::new(id); - println!("Writing keypairs to {:?}...", pathfinder.config_dir); - let mix_keys = crypto::identity::DummyMixIdentityKeyPair::new(); + config = override_config(config, matches); + + let mix_identity_keys = MixIdentityKeyPair::new(); + + // if there is no provider chosen, get a random-ish one from the topology + if config.get_provider_id().is_empty() { + let our_address = mix_identity_keys.public_key().derive_address(); + // TODO: is there perhaps a way to make it work without having to spawn entire runtime? + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let (provider_id, auth_token) = + rt.block_on(choose_provider(config.get_directory_server(), our_address)); + config = config + .with_provider_id(provider_id) + .with_provider_auth_token(auth_token); + } + + let pathfinder = ClientPathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); - pem_store.write_identity(mix_keys).unwrap(); + pem_store + .write_identity(mix_identity_keys) + .expect("Failed to save identity keys"); + println!("Saved mixnet identity keypair"); + let config_save_location = config.get_config_file_save_location(); + config + .save_to_file(None) + .expect("Failed to save the config file"); + println!("Saved configuration file to {:?}", config_save_location); + + println!( + "Unless overridden in all `nym-client run` we will be talking to the following provider: {}...", + config.get_provider_id(), + ); + if config.get_provider_auth_token().is_some() { + println!( + "using optional AuthToken: {:?}", + config.get_provider_auth_token().unwrap() + ) + } println!("Client configuration completed.\n\n\n") } diff --git a/nym-client/src/commands/mod.rs b/nym-client/src/commands/mod.rs index 1b0fe864f6..2908199326 100644 --- a/nym-client/src/commands/mod.rs +++ b/nym-client/src/commands/mod.rs @@ -1,3 +1,29 @@ +use crate::config::{Config, SocketType}; +use clap::ArgMatches; + pub mod init; -pub mod tcpsocket; -pub mod websocket; +pub mod run; + +pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { + if let Some(directory) = matches.value_of("directory") { + config = config.with_custom_directory(directory); + } + + if let Some(provider_id) = matches.value_of("provider") { + config = config.with_provider_id(provider_id); + } + + if let Some(socket_type) = matches.value_of("socket-type") { + config = config.with_socket(SocketType::from_string(socket_type)); + } + + if let Some(port) = matches.value_of("port").map(|port| port.parse::()) { + if let Err(err) = port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_port(port.unwrap()); + } + + config +} diff --git a/nym-client/src/commands/run.rs b/nym-client/src/commands/run.rs new file mode 100644 index 0000000000..9f59192bf5 --- /dev/null +++ b/nym-client/src/commands/run.rs @@ -0,0 +1,54 @@ +use crate::client::NymClient; +use crate::commands::override_config; +use crate::config::Config; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("run") + .about("Run the Nym client with provided configuration client optionally overriding set parameters") + .arg(Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnet-client we want to run.") + .takes_value(true) + .required(true) + ) + // the rest of arguments are optional, they are used to override settings in config file + .arg(Arg::with_name("config") + .long("config") + .help("Custom path to the nym-mixnet-client configuration file") + .takes_value(true) + ) + .arg(Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the client is getting topology from") + .takes_value(true), + ) + .arg(Arg::with_name("provider") + .long("provider") + .help("Id of the provider we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened") + .takes_value(true) + ) + .arg(Arg::with_name("socket-type") + .long("socket-type") + .help("Type of socket to use (TCP, WebSocket or None)") + .takes_value(true) + ) + .arg(Arg::with_name("port") + .short("p") + .long("port") + .help("Port for the socket (if applicable) to listen on") + .takes_value(true) + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + + let mut config = + Config::load_from_file(matches.value_of("config").map(|path| path.into()), Some(id)) + .expect("Failed to load config file"); + + config = override_config(config, matches); + NymClient::new(config).run_forever(); +} diff --git a/nym-client/src/commands/tcpsocket.rs b/nym-client/src/commands/tcpsocket.rs deleted file mode 100644 index 7a98a91681..0000000000 --- a/nym-client/src/commands/tcpsocket.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::client::{NymClient, SocketType}; -use crate::config::persistance::pathfinder::ClientPathfinder; -use clap::ArgMatches; -use crypto::identity::DummyMixIdentityKeyPair; -use pemstore::pemstore::PemStore; -use std::net::ToSocketAddrs; - -pub fn execute(matches: &ArgMatches) { - let id = matches.value_of("id").unwrap().to_string(); - let port = match matches.value_of("port").unwrap_or("9001").parse::() { - Ok(n) => n, - Err(err) => panic!("Invalid port value provided - {:?}", err), - }; - - let directory_server = matches - .value_of("directory") - .unwrap_or("https://directory.nymtech.net") - .to_string(); - - println!("Starting TCP socket on port: {:?}", port); - println!("Listening for messages..."); - - let socket_address = ("127.0.0.1", port) - .to_socket_addrs() - .expect("Failed to combine host and port") - .next() - .expect("Failed to extract the socket address from the iterator"); - - // TODO: currently we know we are reading the 'DummyMixIdentityKeyPair', but how to properly assert the type? - let keypair: DummyMixIdentityKeyPair = PemStore::new(ClientPathfinder::new(id)) - .read_identity() - .unwrap(); - // TODO: reading auth_token from disk (if exists); - - println!("Public key: {}", keypair.public_key.to_base58_string()); - - let auth_token = None; - let client = NymClient::new( - keypair, - socket_address.clone(), - directory_server, - auth_token, - SocketType::TCP, - ); - - client.start().unwrap(); -} diff --git a/nym-client/src/commands/websocket.rs b/nym-client/src/commands/websocket.rs deleted file mode 100644 index 3193c1ffde..0000000000 --- a/nym-client/src/commands/websocket.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::client::{NymClient, SocketType}; -use crate::config::persistance::pathfinder::ClientPathfinder; -use clap::ArgMatches; -use crypto::identity::DummyMixIdentityKeyPair; -use pemstore::pemstore::PemStore; -use std::net::ToSocketAddrs; - -pub fn execute(matches: &ArgMatches) { - let id = matches.value_of("id").unwrap().to_string(); - let port = match matches.value_of("port").unwrap_or("9001").parse::() { - Ok(n) => n, - Err(err) => panic!("Invalid port value provided - {:?}", err), - }; - - let directory_server = matches - .value_of("directory") - .unwrap_or("https://directory.nymtech.net") - .to_string(); - - println!("Starting websocket on port: {:?}", port); - println!("Listening for messages..."); - - let socket_address = ("127.0.0.1", port) - .to_socket_addrs() - .expect("Failed to combine host and port") - .next() - .expect("Failed to extract the socket address from the iterator"); - - // TODO: currently we know we are reading the 'DummyMixIdentityKeyPair', but how to properly assert the type? - let keypair: DummyMixIdentityKeyPair = PemStore::new(ClientPathfinder::new(id)) - .read_identity() - .unwrap(); - - // TODO: reading auth_token from disk (if exists); - - println!("Public key: {}", keypair.public_key.to_base58_string()); - - let auth_token = None; - let client = NymClient::new( - keypair, - socket_address, - directory_server, - auth_token, - SocketType::WebSocket, - ); - - client.start().unwrap(); -} diff --git a/nym-client/src/config/mod.rs b/nym-client/src/config/mod.rs index 4bf0f95209..207f5b222a 100644 --- a/nym-client/src/config/mod.rs +++ b/nym-client/src/config/mod.rs @@ -1 +1,417 @@ -pub mod persistance; +use crate::config::template::config_template; +use config::NymConfig; +use serde::{Deserialize, Deserializer, Serialize}; +use std::path::PathBuf; +use std::time; + +pub mod persistence; +mod template; + +// 'CLIENT' +const DEFAULT_LISTENING_PORT: u16 = 9001; + +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: u64 = 1000; +const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: u64 = 500; +const DEFAULT_AVERAGE_PACKET_DELAY: u64 = 200; +const DEFAULT_FETCH_MESSAGES_DELAY: u64 = 1000; +const DEFAULT_TOPOLOGY_REFRESH_RATE: u64 = 10_000; +const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: u64 = 5_000; +const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: u64 = 10_000; // 10s +const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: u64 = 300_000; // 5min + +const DEFAULT_NUMBER_OF_HEALTHCHECK_TEST_PACKETS: u64 = 2; +const DEFAULT_NODE_SCORE_THRESHOLD: f64 = 0.0; + +#[derive(Debug, Deserialize, PartialEq, Serialize, Clone, Copy)] +#[serde(deny_unknown_fields)] +pub enum SocketType { + TCP, + WebSocket, + None, +} + +impl SocketType { + pub fn from_string>(val: S) -> Self { + let mut upper = val.into(); + upper.make_ascii_uppercase(); + match upper.as_ref() { + "TCP" => SocketType::TCP, + "WEBSOCKET" | "WS" => SocketType::WebSocket, + _ => SocketType::None, + } + } +} + +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + client: Client, + socket: Socket, + + #[serde(default)] + logging: Logging, + #[serde(default)] + debug: Debug, +} + +impl NymConfig for Config { + fn template() -> &'static str { + config_template() + } + + fn config_file_name() -> String { + "config.toml".to_string() + } + + fn default_root_directory() -> PathBuf { + dirs::home_dir() + .expect("Failed to evaluate $HOME value") + .join(".nym") + .join("clients") + } + + fn root_directory(&self) -> PathBuf { + self.client.nym_root_directory.clone() + } + + fn config_directory(&self) -> PathBuf { + self.client + .nym_root_directory + .join(&self.client.id) + .join("config") + } + + fn data_directory(&self) -> PathBuf { + self.client + .nym_root_directory + .join(&self.client.id) + .join("data") + } +} + +impl Config { + pub fn new>(id: S) -> Self { + Config::default().with_id(id) + } + + // builder methods + pub fn with_id>(mut self, id: S) -> Self { + let id = id.into(); + if self.client.private_identity_key_file.as_os_str().is_empty() { + self.client.private_identity_key_file = + self::Client::default_private_identity_key_file(&id); + } + if self.client.public_identity_key_file.as_os_str().is_empty() { + self.client.public_identity_key_file = + self::Client::default_public_identity_key_file(&id); + } + self.client.id = id; + self + } + + pub fn with_provider_id>(mut self, id: S) -> Self { + self.client.provider_id = id.into(); + self + } + + pub fn with_provider_auth_token>(mut self, token: S) -> Self { + self.client.provider_authtoken = Some(token.into()); + self + } + + pub fn with_custom_directory>(mut self, directory_server: S) -> Self { + self.client.directory_server = directory_server.into(); + self + } + + pub fn with_socket(mut self, socket_type: SocketType) -> Self { + self.socket.socket_type = socket_type; + self + } + + pub fn with_port(mut self, port: u16) -> Self { + self.socket.listening_port = port; + self + } + + // getters + pub fn get_config_file_save_location(&self) -> PathBuf { + self.config_directory().join(Self::config_file_name()) + } + + pub fn get_private_identity_key_file(&self) -> PathBuf { + self.client.private_identity_key_file.clone() + } + + pub fn get_public_identity_key_file(&self) -> PathBuf { + self.client.public_identity_key_file.clone() + } + + pub fn get_directory_server(&self) -> String { + self.client.directory_server.clone() + } + + pub fn get_provider_id(&self) -> String { + self.client.provider_id.clone() + } + + pub fn get_provider_auth_token(&self) -> Option { + self.client.provider_authtoken.clone() + } + + pub fn get_socket_type(&self) -> SocketType { + self.socket.socket_type + } + + pub fn get_listening_port(&self) -> u16 { + self.socket.listening_port + } + + // Debug getters + pub fn get_average_packet_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.average_packet_delay) + } + + pub fn get_loop_cover_traffic_average_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.loop_cover_traffic_average_delay) + } + + pub fn get_fetch_message_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.fetch_message_delay) + } + + pub fn get_message_sending_average_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.message_sending_average_delay) + } + + pub fn get_rate_compliant_cover_messages_disabled(&self) -> bool { + self.debug.rate_compliant_cover_messages_disabled + } + + pub fn get_topology_refresh_rate(&self) -> time::Duration { + time::Duration::from_millis(self.debug.topology_refresh_rate) + } + + pub fn get_topology_resolution_timeout(&self) -> time::Duration { + time::Duration::from_millis(self.debug.topology_resolution_timeout) + } + + pub fn get_number_of_healthcheck_test_packets(&self) -> u64 { + self.debug.number_of_healthcheck_test_packets + } + + pub fn get_node_score_threshold(&self) -> f64 { + self.debug.node_score_threshold + } + + pub fn get_packet_forwarding_initial_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_initial_backoff) + } + + pub fn get_packet_forwarding_maximum_backoff(&self) -> time::Duration { + time::Duration::from_millis(self.debug.packet_forwarding_maximum_backoff) + } +} + +fn de_option_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(s)) + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Client { + /// ID specifies the human readable ID of this particular client. + id: String, + + /// URL to the directory server. + directory_server: String, + + /// Path to file containing private identity key. + private_identity_key_file: PathBuf, + + /// Path to file containing public identity key. + public_identity_key_file: PathBuf, + + /// provider_id specifies ID of the provider to which the client should send messages. + /// If initially omitted, a random provider will be chosen from the available topology. + provider_id: String, + + /// A provider specific, optional, base58 stringified authentication token used for + /// communication with particular provider. + #[serde(deserialize_with = "de_option_string")] + provider_authtoken: Option, + + /// 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, +} + +impl Default for Client { + fn default() -> Self { + // there must be explicit checks for whether id is not empty later + Client { + id: "".to_string(), + directory_server: Self::default_directory_server(), + private_identity_key_file: Default::default(), + public_identity_key_file: Default::default(), + provider_id: "".to_string(), + provider_authtoken: None, + nym_root_directory: Config::default_root_directory(), + } + } +} + +impl Client { + fn default_directory_server() -> String { + #[cfg(feature = "qa")] + return "https://qa-directory.nymtech.net".to_string(); + #[cfg(feature = "local")] + return "http://localhost:8080".to_string(); + + "https://directory.nymtech.net".to_string() + } + + fn default_private_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_identity.pem") + } + + fn default_public_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_identity.pem") + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Socket { + socket_type: SocketType, + listening_port: u16, +} + +impl Default for Socket { + fn default() -> Self { + Socket { + socket_type: SocketType::None, + listening_port: DEFAULT_LISTENING_PORT, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Logging {} + +impl Default for Logging { + fn default() -> Self { + Logging {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// The parameter of Poisson distribution determining how long, on average, + /// sent packet is going to be delayed at any given mix node. + /// So for a packet going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + /// The provided value is interpreted as milliseconds. + average_packet_delay: u64, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take for another loop cover traffic message to be sent. + /// The provided value is interpreted as milliseconds. + loop_cover_traffic_average_delay: u64, + + /// The uniform delay every which clients are querying the providers for received packets. + /// The provided value is interpreted as milliseconds. + fetch_message_delay: u64, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take another 'real traffic stream' message to be sent. + /// If no real packets are available and cover traffic is enabled, + /// a loop cover message is sent instead in order to preserve the rate. + /// The provided value is interpreted as milliseconds. + message_sending_average_delay: u64, + + /// Whether loop cover messages should be sent to respect message_sending_rate. + /// In the case of it being disabled and not having enough real traffic + /// waiting to be sent the actual sending rate is going be lower than the desired value + /// thus decreasing the anonymity. + rate_compliant_cover_messages_disabled: bool, + + /// The uniform delay every which clients are querying the directory server + /// to try to obtain a compatible network topology to send sphinx packets through. + /// The provided value is interpreted as milliseconds. + topology_refresh_rate: u64, + + /// During topology refresh, test packets are sent through every single possible network + /// path. This timeout determines waiting period until it is decided that the packet + /// did not reach its destination. + /// The provided value is interpreted as milliseconds. + topology_resolution_timeout: u64, + + /// How many packets should be sent through each path during the healthcheck + number_of_healthcheck_test_packets: u64, + + /// In the current healthcheck implementation, threshold indicating percentage of packets + /// node received during healthcheck. Node's score must be above that value to be + /// considered healthy. + node_score_threshold: f64, + + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_initial_backoff: u64, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + /// The provided value is interpreted as milliseconds. + packet_forwarding_maximum_backoff: u64, +} + +impl Default for Debug { + fn default() -> Self { + Debug { + average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, + fetch_message_delay: DEFAULT_FETCH_MESSAGES_DELAY, + message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, + rate_compliant_cover_messages_disabled: false, + topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, + topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, + number_of_healthcheck_test_packets: DEFAULT_NUMBER_OF_HEALTHCHECK_TEST_PACKETS, + node_score_threshold: DEFAULT_NODE_SCORE_THRESHOLD, + packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + } + } +} + +#[cfg(test)] +mod client_config { + use super::*; + + #[test] + fn after_saving_default_config_the_loaded_one_is_identical() { + // need to figure out how to do something similar but without touching the disk + // or the file system at all... + let temp_location = tempfile::tempdir().unwrap().path().join("config.toml"); + let default_config = Config::default().with_id("foomp".to_string()); + default_config + .save_to_file(Some(temp_location.clone())) + .unwrap(); + + let loaded_config = Config::load_from_file(Some(temp_location), None).unwrap(); + + assert_eq!(default_config, loaded_config); + } +} diff --git a/nym-client/src/config/persistence/mod.rs b/nym-client/src/config/persistence/mod.rs new file mode 100644 index 0000000000..5692e9bc8b --- /dev/null +++ b/nym-client/src/config/persistence/mod.rs @@ -0,0 +1 @@ +pub mod pathfinder; diff --git a/nym-client/src/config/persistance/pathfinder.rs b/nym-client/src/config/persistence/pathfinder.rs similarity index 73% rename from nym-client/src/config/persistance/pathfinder.rs rename to nym-client/src/config/persistence/pathfinder.rs index c71aab5b84..f4847bfcaf 100644 --- a/nym-client/src/config/persistance/pathfinder.rs +++ b/nym-client/src/config/persistence/pathfinder.rs @@ -1,6 +1,8 @@ +use crate::config::Config; use pemstore::pathfinder::PathFinder; use std::path::PathBuf; +#[derive(Debug)] pub struct ClientPathfinder { pub config_dir: PathBuf, pub private_mix_key: PathBuf, @@ -19,6 +21,14 @@ impl ClientPathfinder { public_mix_key, } } + + pub fn new_from_config(config: &Config) -> Self { + ClientPathfinder { + config_dir: config.get_config_file_save_location(), + private_mix_key: config.get_private_identity_key_file(), + public_mix_key: config.get_public_identity_key_file(), + } + } } impl PathFinder for ClientPathfinder { diff --git a/nym-client/src/config/template.rs b/nym-client/src/config/template.rs new file mode 100644 index 0000000000..c18730ed4b --- /dev/null +++ b/nym-client/src/config/template.rs @@ -0,0 +1,123 @@ +pub(crate) fn config_template() -> &'static str { + // While using normal toml marshalling would have been way simpler with less overhead, + // I think it's useful to have comments attached to the saved config file to explain behaviour of + // particular fields. + // Note: any changes to the template must be reflected in the appropriate structs in mod.rs. + r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base client config options ##### + +[client] +# Human readable ID of this particular client. +id = "{{ client.id }}" + +# URL to the directory server. +directory_server = "{{ client.directory_server }}" + +# Path to file containing private identity key. +private_identity_key_file = "{{ client.private_identity_key_file }}" + +# Path to file containing public identity key. +public_identity_key_file = "{{ client.public_identity_key_file }}" + +##### additional client config options ##### + +# ID of the provider from which the client should be fetching messages. +provider_id = "{{ client.provider_id }}" + +# A provider specific, optional, base58 stringified authentication token used for +# communication with particular provider. +provider_authtoken = "{{ client.provider_authtoken }}" + +##### advanced configuration options ##### + +# Absolute path to the home Nym Clients directory. +nym_root_directory = "{{ client.nym_root_directory }}" + + +##### socket config options ##### + +[socket] + +# allowed values are 'TCP', 'WebSocket' or 'None' +socket_type = "{{ socket.socket_type }}" + +# if applicable (for the case of 'TCP' or 'WebSocket'), the port on which the client +# will be listening for incoming requests +listening_port = {{ socket.listening_port }} + + +##### logging configuration options ##### + +[logging] + +# TODO + + +##### debug configuration options ##### +# The following options should not be modified unless you know EXACTLY what you are doing +# as if set incorrectly, they may impact your anonymity. + +[debug] + +# The parameter of Poisson distribution determining how long, on average, +# sent packet is going to be delayed at any given mix node. +# So for a packet going through three mix nodes, on average, it will take three times this value +# until the packet reaches its destination. +# The provided value is interpreted as milliseconds. +average_packet_delay = {{ debug.average_packet_delay }} + +# The parameter of Poisson distribution determining how long, on average, +# it is going to take for another loop cover traffic message to be sent. +# The provided value is interpreted as milliseconds. +loop_cover_traffic_average_delay = {{ debug.loop_cover_traffic_average_delay }} + +# The uniform delay every which clients are querying the providers for received packets. +# The provided value is interpreted as milliseconds. +fetch_message_delay = {{ debug.fetch_message_delay }} + +# The parameter of Poisson distribution determining how long, on average, +# it is going to take another 'real traffic stream' message to be sent. +# If no real packets are available and cover traffic is enabled, +# a loop cover message is sent instead in order to preserve the rate. +# The provided value is interpreted as milliseconds. +message_sending_average_delay = {{ debug.message_sending_average_delay }} + +# Whether loop cover messages should be sent to respect message_sending_rate. +# In the case of it being disabled and not having enough real traffic +# waiting to be sent the actual sending rate is going be lower than the desired value +# thus decreasing the anonymity. +rate_compliant_cover_messages_disabled = {{ debug.rate_compliant_cover_messages_disabled }} + +# The uniform delay every which clients are querying the directory server +# to try to obtain a compatible network topology to send sphinx packets through. +# The provided value is interpreted as milliseconds. +topology_refresh_rate = {{ debug.topology_refresh_rate }} + +# During topology refresh, test packets are sent through every single possible network +# path. This timeout determines waiting period until it is decided that the packet +# did not reach its destination. +# The provided value is interpreted as milliseconds. +topology_resolution_timeout = {{ debug.topology_resolution_timeout }} + +# How many packets should be sent through each path during the healthcheck +number_of_healthcheck_test_packets = {{ debug.number_of_healthcheck_test_packets }} + +# In the current healthcheck implementation, threshold indicating percentage of packets +# node received during healthcheck. Node's score must be above that value to be +# considered healthy. +node_score_threshold = {{ debug.node_score_threshold }} + +# Initial value of an exponential backoff to reconnect to dropped TCP connection when +# forwarding sphinx packets. +# The provided value is interpreted as milliseconds. +packet_forwarding_initial_backoff = {{ debug.packet_forwarding_initial_backoff }} + +# Maximum value of an exponential backoff to reconnect to dropped TCP connection when +# forwarding sphinx packets. +# The provided value is interpreted as milliseconds. +packet_forwarding_maximum_backoff = {{ debug.packet_forwarding_maximum_backoff }} +"# +} diff --git a/nym-client/src/main.rs b/nym-client/src/main.rs index 8160641828..c83c375beb 100644 --- a/nym-client/src/main.rs +++ b/nym-client/src/main.rs @@ -1,4 +1,4 @@ -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{App, ArgMatches}; pub mod built_info; pub mod client; @@ -10,72 +10,14 @@ fn main() { dotenv::dotenv().ok(); pretty_env_logger::init(); + println!("{}", banner()); + let arg_matches = App::new("Nym Client") .version(built_info::PKG_VERSION) .author("Nymtech") .about("Implementation of the Nym Client") - .subcommand( - SubCommand::with_name("init") - .about("Initialise a Nym client. Do this first!") - .arg(Arg::with_name("id") - .long("id") - .help("Id of the nym-mixnet-client we want to create config for.") - .takes_value(true) - .required(true) - ) - .arg(Arg::with_name("provider") - .long("provider") - .help("Id of the provider we have preference to connect to. If left empty, a random provider will be chosen.") - .takes_value(true) - ) - ) - .subcommand( - SubCommand::with_name("tcpsocket") - .about("Run Nym client that listens for bytes on a TCP socket") - .arg( - Arg::with_name("port") - .short("p") - .long("port") - .help("Port for TCP socket to listen on") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("directory") - .long("directory") - .help("Address of the directory server the client is getting topology from") - .takes_value(true), - ) - .arg(Arg::with_name("id") - .long("id") - .help("Id of the nym-mixnet-client we want to run.") - .takes_value(true) - .required(true) - ) - ) - .subcommand( - SubCommand::with_name("websocket") - .about("Run Nym client that listens on a websocket") - .arg( - Arg::with_name("port") - .short("p") - .long("port") - .help("Port for websocket to listen on") - .takes_value(true) - ) - .arg( - Arg::with_name("directory") - .long("directory") - .help("Address of the directory server the client is getting topology from") - .takes_value(true), - ) - .arg(Arg::with_name("id") - .long("id") - .help("Id of the nym-mixnet-client we want to run.") - .takes_value(true) - .required(true) - ) - ) + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) .get_matches(); execute(arg_matches); @@ -83,26 +25,14 @@ fn main() { fn execute(matches: ArgMatches) { match matches.subcommand() { - ("init", Some(m)) => { - println!("{}", banner()); - commands::init::execute(m); - } - ("tcpsocket", Some(m)) => { - println!("{}", banner()); - commands::tcpsocket::execute(m); - } - ("websocket", Some(m)) => { - println!("{}", banner()); - commands::websocket::execute(m); - } - _ => { - println!("{}", usage()); - } + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), } } -fn usage() -> String { - banner() + "usage: --help to see available options.\n\n" +fn usage() -> &'static str { + "usage: --help to see available options.\n\n" } fn banner() -> String { diff --git a/nym-client/src/sockets/tcp.rs b/nym-client/src/sockets/tcp.rs index 2b2b309070..abfe06b3c1 100644 --- a/nym-client/src/sockets/tcp.rs +++ b/nym-client/src/sockets/tcp.rs @@ -1,5 +1,5 @@ -use crate::client::received_buffer::BufferResponse; -use crate::client::topology_control::TopologyInnerRef; +use crate::client::received_buffer::ReceivedBufferResponse; +use crate::client::topology_control::TopologyAccessor; use crate::client::InputMessage; use futures::channel::{mpsc, oneshot}; use futures::future::FutureExt; @@ -11,6 +11,8 @@ use std::convert::TryFrom; use std::io; use std::net::SocketAddr; use tokio::prelude::*; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; use topology::NymTopology; const SEND_REQUEST_PREFIX: u8 = 1; @@ -84,7 +86,7 @@ fn parse_send_request(data: &[u8]) -> Result { Ok(ClientRequest::Send { message, - recipient_address, + recipient_address: DestinationAddressBytes::from_bytes(recipient_address), }) } @@ -101,8 +103,7 @@ impl ClientRequest { "too long message. Sent {} bytes while the maximum is {}", msg.len(), sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH - ) - .to_string(), + ), }; } let dummy_surb = [0; 16]; @@ -111,7 +112,9 @@ impl ClientRequest { ServerResponse::Send } - async fn handle_fetch(mut msg_query: mpsc::UnboundedSender) -> ServerResponse { + async fn handle_fetch( + mut msg_query: mpsc::UnboundedSender, + ) -> ServerResponse { trace!("handle_fetch called"); let (res_tx, res_rx) = oneshot::channel(); if msg_query.send(res_tx).await.is_err() { @@ -125,7 +128,7 @@ impl ClientRequest { Err(e) => { warn!("Failed to fetch client messages - {:?}", e); return ServerResponse::Error { - message: format!("Server failed to receive messages").to_string(), + message: "Server failed to receive messages".to_string(), }; } }; @@ -134,9 +137,10 @@ impl ClientRequest { ServerResponse::Fetch { messages } } - async fn handle_get_clients(topology: &TopologyInnerRef) -> ServerResponse { - let topology_data = &topology.read().await.topology; - match topology_data { + async fn handle_get_clients( + mut topology_accessor: TopologyAccessor, + ) -> ServerResponse { + match topology_accessor.get_current_topology_clone().await { Some(topology) => { let clients = topology .providers() @@ -154,7 +158,7 @@ impl ClientRequest { async fn handle_own_details(self_address_bytes: DestinationAddressBytes) -> ServerResponse { ServerResponse::OwnDetails { - address: self_address_bytes.to_vec(), + address: self_address_bytes.to_bytes().to_vec(), } } } @@ -229,7 +233,7 @@ async fn handle_connection( } ClientRequest::Fetch => ClientRequest::handle_fetch(request_handling_data.msg_query).await, ClientRequest::GetClients => { - ClientRequest::handle_get_clients(&request_handling_data.topology).await + ClientRequest::handle_get_clients(request_handling_data.topology_accessor).await } ClientRequest::OwnDetails => { ClientRequest::handle_own_details(request_handling_data.self_address).await @@ -241,17 +245,17 @@ async fn handle_connection( struct RequestHandlingData { msg_input: mpsc::UnboundedSender, - msg_query: mpsc::UnboundedSender, + msg_query: mpsc::UnboundedSender, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, + topology_accessor: TopologyAccessor, } async fn accept_connection( mut socket: tokio::net::TcpStream, msg_input: mpsc::UnboundedSender, - msg_query: mpsc::UnboundedSender, + msg_query: mpsc::UnboundedSender, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, + topology_accessor: TopologyAccessor, ) { let address = socket .peer_addr() @@ -272,7 +276,7 @@ async fn accept_connection( } Ok(n) => { let request_handling_data = RequestHandlingData { - topology: topology.clone(), + topology_accessor: topology_accessor.clone(), msg_input: msg_input.clone(), msg_query: msg_query.clone(), self_address: self_address.clone(), @@ -296,14 +300,16 @@ async fn accept_connection( } } -pub async fn start_tcpsocket( - address: SocketAddr, +pub(crate) async fn run_tcpsocket( + listening_port: u16, message_tx: mpsc::UnboundedSender, - received_messages_query_tx: mpsc::UnboundedSender, + received_messages_query_tx: mpsc::UnboundedSender, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, -) -> Result<(), TCPSocketError> { - let mut listener = tokio::net::TcpListener::bind(address).await?; + topology_accessor: TopologyAccessor, +) { + let address = SocketAddr::new("127.0.0.1".parse().unwrap(), listening_port); + info!("Starting tcp socket listener at {:?}", address); + let mut listener = tokio::net::TcpListener::bind(address).await.unwrap(); while let Ok((stream, _)) = listener.accept().await { // it's fine to be cloning the channel on all new connection, because in principle @@ -312,11 +318,28 @@ pub async fn start_tcpsocket( stream, message_tx.clone(), received_messages_query_tx.clone(), - self_address, - topology.clone(), + self_address.clone(), + topology_accessor.clone(), )); } - - error!("The tcpsocket went kaput..."); - Ok(()) +} + +pub(crate) fn start_tcpsocket( + handle: &Handle, + listening_port: u16, + message_tx: mpsc::UnboundedSender, + received_messages_query_tx: mpsc::UnboundedSender, + self_address: DestinationAddressBytes, + topology_accessor: TopologyAccessor, +) -> JoinHandle<()> { + handle.spawn(async move { + run_tcpsocket( + listening_port, + message_tx, + received_messages_query_tx, + self_address, + topology_accessor, + ) + .await; + }) } diff --git a/nym-client/src/sockets/ws.rs b/nym-client/src/sockets/ws.rs index 15dedb37c9..01bd6a111a 100644 --- a/nym-client/src/sockets/ws.rs +++ b/nym-client/src/sockets/ws.rs @@ -1,5 +1,5 @@ -use crate::client::received_buffer::BufferResponse; -use crate::client::topology_control::TopologyInnerRef; +use crate::client::received_buffer::ReceivedBufferResponse; +use crate::client::topology_control::TopologyAccessor; use crate::client::InputMessage; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; @@ -12,17 +12,19 @@ use sphinx::route::{Destination, DestinationAddressBytes}; use std::convert::TryFrom; use std::io; use std::net::SocketAddr; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use tokio_tungstenite::tungstenite::protocol::{CloseFrame, Message}; use topology::NymTopology; -use tungstenite::protocol::frame::coding::CloseCode; -use tungstenite::protocol::{CloseFrame, Message}; struct Connection { address: SocketAddr, msg_input: mpsc::UnboundedSender, - msg_query: mpsc::UnboundedSender, + msg_query: mpsc::UnboundedSender, rx: UnboundedReceiver, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, + topology_accessor: TopologyAccessor, tx: UnboundedSender, } @@ -49,8 +51,12 @@ impl Connection { ClientRequest::handle_send(message, recipient_address, self.msg_input.clone()).await } ClientRequest::Fetch => ClientRequest::handle_fetch(self.msg_query.clone()).await, - ClientRequest::GetClients => ClientRequest::handle_get_clients(&self.topology).await, - ClientRequest::OwnDetails => ClientRequest::handle_own_details(self.self_address).await, + ClientRequest::GetClients => { + ClientRequest::handle_get_clients(self.topology_accessor.clone()).await + } + ClientRequest::OwnDetails => { + ClientRequest::handle_own_details(self.self_address.clone()).await + } } } @@ -194,8 +200,7 @@ impl ClientRequest { "message too long. Sent {} bytes, but the maximum is {}", message_bytes.len(), sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH - ) - .to_string(), + ), }; } @@ -219,13 +224,18 @@ impl ClientRequest { let dummy_surb = [0; 16]; - let input_msg = InputMessage(Destination::new(address, dummy_surb), message_bytes); + let input_msg = InputMessage( + Destination::new(DestinationAddressBytes::from_bytes(address), dummy_surb), + message_bytes, + ); input_tx.send(input_msg).await.unwrap(); ServerResponse::Send } - async fn handle_fetch(mut msg_query: mpsc::UnboundedSender) -> ServerResponse { + async fn handle_fetch( + mut msg_query: mpsc::UnboundedSender, + ) -> ServerResponse { let (res_tx, res_rx) = oneshot::channel(); if msg_query.send(res_tx).await.is_err() { warn!("Failed to handle_fetch. msg_query.send() is an error."); @@ -239,7 +249,7 @@ impl ClientRequest { Err(e) => { warn!("Failed to fetch client messages - {:?}", e); return ServerResponse::Error { - message: format!("Server failed to receive messages").to_string(), + message: "Server failed to receive messages".to_string(), }; } }; @@ -261,9 +271,10 @@ impl ClientRequest { ServerResponse::Fetch { messages } } - async fn handle_get_clients(topology: &TopologyInnerRef) -> ServerResponse { - let topology_data = &topology.read().await.topology; - match topology_data { + async fn handle_get_clients( + mut topology_accessor: TopologyAccessor, + ) -> ServerResponse { + match topology_accessor.get_current_topology_clone().await { Some(topology) => { let clients = topology .providers() @@ -280,7 +291,7 @@ impl ClientRequest { } async fn handle_own_details(self_address_bytes: DestinationAddressBytes) -> ServerResponse { - let self_address = bs58::encode(&self_address_bytes).into_string(); + let self_address = self_address_bytes.to_base58_string(); ServerResponse::OwnDetails { address: self_address, } @@ -309,18 +320,22 @@ impl Into for ServerResponse { async fn accept_connection( stream: tokio::net::TcpStream, msg_input: mpsc::UnboundedSender, - msg_query: mpsc::UnboundedSender, + msg_query: mpsc::UnboundedSender, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, + topology_accessor: TopologyAccessor, ) { let address = stream .peer_addr() .expect("connected streams should have a peer address"); debug!("Peer address: {}", address); - let mut ws_stream = tokio_tungstenite::accept_async(stream) - .await - .expect("Error during the websocket handshake occurred"); + let mut ws_stream = match tokio_tungstenite::accept_async(stream).await { + Ok(ws_stream) => ws_stream, + Err(e) => { + error!("Error during the websocket handshake occurred - {}", e); + return; + } + }; // Create a channel for our stream, which other sockets will use to // send us messages. Then register our address with the stream to send @@ -331,7 +346,7 @@ async fn accept_connection( address, rx: msg_rx, tx: response_tx, - topology, + topology_accessor, msg_input, msg_query, self_address, @@ -349,10 +364,7 @@ async fn accept_connection( } }; - let mut should_close = false; - if message.is_close() { - should_close = true; - } + let should_close = message.is_close(); if let Err(err) = msg_tx.unbounded_send(message) { error!( @@ -386,14 +398,16 @@ async fn accept_connection( } } -pub async fn start_websocket( - address: SocketAddr, +pub(crate) async fn run_websocket( + listening_port: u16, message_tx: mpsc::UnboundedSender, - received_messages_query_tx: mpsc::UnboundedSender, + received_messages_query_tx: mpsc::UnboundedSender, self_address: DestinationAddressBytes, - topology: TopologyInnerRef, -) -> Result<(), WebSocketError> { - let mut listener = tokio::net::TcpListener::bind(address).await?; + topology_accessor: TopologyAccessor, +) { + let address = SocketAddr::new("127.0.0.1".parse().unwrap(), listening_port); + info!("Starting websocket listener at {:?}", address); + let mut listener = tokio::net::TcpListener::bind(address).await.unwrap(); while let Ok((stream, _)) = listener.accept().await { // it's fine to be cloning the channel on all new connection, because in principle @@ -402,11 +416,28 @@ pub async fn start_websocket( stream, message_tx.clone(), received_messages_query_tx.clone(), - self_address, - topology.clone(), + self_address.clone(), + topology_accessor.clone(), )); } - - error!("The websocket went kaput..."); - Ok(()) +} + +pub(crate) fn start_websocket( + handle: &Handle, + listening_port: u16, + message_tx: mpsc::UnboundedSender, + received_messages_query_tx: mpsc::UnboundedSender, + self_address: DestinationAddressBytes, + topology_accessor: TopologyAccessor, +) -> JoinHandle<()> { + handle.spawn(async move { + run_websocket( + listening_port, + message_tx, + received_messages_query_tx, + self_address, + topology_accessor, + ) + .await; + }) } diff --git a/sample-configs/validator-config.toml.sample b/sample-configs/validator-config.toml.sample index c60c223287..2997abea29 100644 --- a/sample-configs/validator-config.toml.sample +++ b/sample-configs/validator-config.toml.sample @@ -1,7 +1,7 @@ [healthcheck] #directory-server = "http://localhost:8080" -directory-server = "https://qa-directory.nymtech.net" +directory-server = "https://directory.nymtech.net" interval = 10.0 test-packets-per-node = 2 # in seconds resolution-timeout = 5 # in seconds \ No newline at end of file diff --git a/scripts/run_local_network.sh b/scripts/run_local_network.sh index fe2f949408..a890f2bc36 100755 --- a/scripts/run_local_network.sh +++ b/scripts/run_local_network.sh @@ -21,24 +21,32 @@ killall nym-mixnode killall nym-sfw-provider echo "Press CTRL-C to stop." +echo "Make sure you have started nym-directory !" -cargo build +cargo build --release --manifest-path nym-client/Cargo.toml --features=local +cargo build --release --manifest-path mixnode/Cargo.toml --features=local +cargo build --release --manifest-path sfw-provider/Cargo.toml --features=local MAX_LAYERS=3 NUMMIXES=${1:-3} # Set $NUMMIXES to default of 3, but allow the user to set other values if desired + +$PWD/target/release/nym-sfw-provider init --id provider-local --clients-host 127.0.0.1 --mix-host 127.0.0.1 --mix-port 4000 --mix-announce-port 4000 +$PWD/target/release/nym-sfw-provider run --id provider-local & + +sleep 1 + for (( j=0; j<$NUMMIXES; j++ )) # Note: to disable logging (or direct it to another output) modify the constant on top of mixnode or provider; # Will make it later either configurable by flags or config file. do let layer=j%MAX_LAYERS+1 - $PWD/target/debug/nym-mixnode run --port $((9980+$j)) --host "localhost" --layer $layer --directory http://localhost:8080 & + $PWD/target/release/nym-mixnode init --id mix-local$j --host 127.0.0.1 --port $((9980+$j)) --layer $layer --announce-host 127.0.0.1:$((9980+$j)) + $PWD/target/release/nym-mixnode run --id mix-local$j & sleep 1 done -sleep 1 -$PWD/target/debug/nym-sfw-provider run --clientHost "localhost" --mixHost "localhost" --mixPort 9997 --clientPort 9998 --directory http://localhost:8080 # trap call ctrl_c() trap ctrl_c SIGINT SIGTERM SIGTSTP diff --git a/sfw-provider/Cargo.toml b/sfw-provider/Cargo.toml index d2a7cf2c5c..debf9bbde4 100644 --- a/sfw-provider/Cargo.toml +++ b/sfw-provider/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-sfw-provider" -version = "0.4.1" +version = "0.5.0-rc.1" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" @@ -11,6 +11,7 @@ edition = "2018" bs58 = "0.3.0" clap = "2.33.0" curve25519-dalek = "1.2.3" +dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3.1" log = "0.4" @@ -24,11 +25,20 @@ hmac = "0.7.1" ## internal crypto = {path = "../common/crypto"} +config = {path = "../common/config"} directory-client = { path = "../common/clients/directory-client" } +pemstore = {path = "../common/pemstore"} sfw-provider-requests = { path = "./sfw-provider-requests" } ## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } [build-dependencies] built = "0.3.2" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +qa = [] +local = [] \ No newline at end of file diff --git a/sfw-provider/sfw-provider-requests/Cargo.toml b/sfw-provider/sfw-provider-requests/Cargo.toml index 12a8343bba..a34bae654e 100644 --- a/sfw-provider/sfw-provider-requests/Cargo.toml +++ b/sfw-provider/sfw-provider-requests/Cargo.toml @@ -7,4 +7,5 @@ edition = "2018" # 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="8424f4b0933b4a7f64ae828b2edefc5ba43a9e79" } +bs58 = "0.3.0" +sphinx = { git = "https://github.com/nymtech/sphinx", rev="23f9c89b257ee0936e70afd682e9ed6a62e89eee" } diff --git a/sfw-provider/sfw-provider-requests/src/lib.rs b/sfw-provider/sfw-provider-requests/src/lib.rs index 70e06feaad..7a4656e417 100644 --- a/sfw-provider/sfw-provider-requests/src/lib.rs +++ b/sfw-provider/sfw-provider-requests/src/lib.rs @@ -4,4 +4,113 @@ pub mod responses; pub const DUMMY_MESSAGE_CONTENT: &[u8] = b"[DUMMY MESSAGE] Wanting something does not give you the right to have it."; -pub type AuthToken = [u8; 32]; +// To be renamed to 'AuthToken' once it is safe to replace it +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct AuthToken([u8; 32]); + +#[derive(Debug)] +pub enum AuthTokenConversionError { + InvalidStringError, + StringOfInvalidLengthError, +} + +impl AuthToken { + pub fn from_bytes(bytes: [u8; 32]) -> Self { + AuthToken(bytes) + } + + pub fn to_bytes(&self) -> [u8; 32] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(_) => return Err(AuthTokenConversionError::InvalidStringError), + }; + + if decoded.len() != 32 { + return Err(AuthTokenConversionError::StringOfInvalidLengthError); + } + + let mut token = [0u8; 32]; + token.copy_from_slice(&decoded[..]); + Ok(AuthToken(token)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl Into for AuthToken { + fn into(self) -> String { + self.to_base58_string() + } +} + +#[cfg(test)] +mod auth_token_conversion { + use super::*; + + #[test] + fn it_is_possible_to_recover_it_from_valid_b58_string() { + let auth_token = AuthToken([42; 32]); + let auth_token_string = auth_token.to_base58_string(); + assert_eq!( + auth_token, + AuthToken::try_from_base58_string(auth_token_string).unwrap() + ) + } + + #[test] + fn it_is_possible_to_recover_it_from_valid_b58_str_ref() { + let auth_token = AuthToken([42; 32]); + let auth_token_string = auth_token.to_base58_string(); + let auth_token_str_ref: &str = &auth_token_string; + assert_eq!( + auth_token, + AuthToken::try_from_base58_string(auth_token_str_ref).unwrap() + ) + } + + #[test] + fn it_returns_error_on_b58_with_invalid_characters() { + let auth_token = AuthToken([42; 32]); + let auth_token_string = auth_token.to_base58_string(); + + let mut chars = auth_token_string.chars(); + let _consumed_first_char = chars.next().unwrap(); + + let invalid_chars_token = "=".to_string() + chars.as_str(); + assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) + } + + #[test] + fn it_returns_error_on_too_long_b58_string() { + let auth_token = AuthToken([42; 32]); + let mut auth_token_string = auth_token.to_base58_string(); + auth_token_string.push('f'); + + assert!(AuthToken::try_from_base58_string(auth_token_string).is_err()) + } + + #[test] + fn it_returns_error_on_too_short_b58_string() { + let auth_token = AuthToken([42; 32]); + let auth_token_string = auth_token.to_base58_string(); + + let mut chars = auth_token_string.chars(); + let _consumed_first_char = chars.next().unwrap(); + let _consumed_second_char = chars.next().unwrap(); + let invalid_chars_token = chars.as_str(); + + assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) + } +} diff --git a/sfw-provider/sfw-provider-requests/src/requests.rs b/sfw-provider/sfw-provider-requests/src/requests.rs index 3dba30c038..06ab0a8741 100644 --- a/sfw-provider/sfw-provider-requests/src/requests.rs +++ b/sfw-provider/sfw-provider-requests/src/requests.rs @@ -79,8 +79,8 @@ impl ProviderRequest for PullRequest { Self::get_prefix() .to_vec() .into_iter() - .chain(self.destination_address.iter().cloned()) - .chain(self.auth_token.iter().cloned()) + .chain(self.destination_address.to_bytes().iter().cloned()) + .chain(self.auth_token.0.iter().cloned()) .collect() } @@ -102,8 +102,8 @@ impl ProviderRequest for PullRequest { auth_token.copy_from_slice(&bytes[34..]); Ok(PullRequest { - auth_token, - destination_address, + auth_token: AuthToken::from_bytes(auth_token), + destination_address: DestinationAddressBytes::from_bytes(destination_address), }) } } @@ -130,7 +130,7 @@ impl ProviderRequest for RegisterRequest { Self::get_prefix() .to_vec() .into_iter() - .chain(self.destination_address.iter().cloned()) + .chain(self.destination_address.to_bytes().iter().cloned()) .collect() } @@ -149,7 +149,7 @@ impl ProviderRequest for RegisterRequest { destination_address.copy_from_slice(&bytes[2..]); Ok(RegisterRequest { - destination_address, + destination_address: DestinationAddressBytes::from_bytes(destination_address), }) } } @@ -160,34 +160,34 @@ mod creating_pull_request { #[test] fn it_is_possible_to_recover_it_from_bytes() { - let address = [ + let address = DestinationAddressBytes::from_bytes([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, - ]; + ]); let auth_token = [1u8; 32]; - let pull_request = PullRequest::new(address, auth_token); + let pull_request = PullRequest::new(address.clone(), AuthToken(auth_token)); let bytes = pull_request.to_bytes(); let recovered = PullRequest::from_bytes(&bytes).unwrap(); assert_eq!(address, recovered.destination_address); - assert_eq!(auth_token, recovered.auth_token); + assert_eq!(AuthToken(auth_token), recovered.auth_token); } #[test] fn it_is_possible_to_recover_it_from_bytes_with_enum_wrapper() { - let address = [ + let address = DestinationAddressBytes::from_bytes([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, - ]; + ]); let auth_token = [1u8; 32]; - let pull_request = PullRequest::new(address, auth_token); + let pull_request = PullRequest::new(address.clone(), AuthToken(auth_token)); let bytes = pull_request.to_bytes(); let recovered = ProviderRequests::from_bytes(&bytes).unwrap(); match recovered { ProviderRequests::PullMessages(req) => { assert_eq!(address, req.destination_address); - assert_eq!(auth_token, req.auth_token); + assert_eq!(AuthToken(auth_token), req.auth_token); } _ => panic!("expected to recover pull request!"), } @@ -200,11 +200,11 @@ mod creating_register_request { #[test] fn it_is_possible_to_recover_it_from_bytes() { - let address = [ + let address = DestinationAddressBytes::from_bytes([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, - ]; - let register_request = RegisterRequest::new(address); + ]); + let register_request = RegisterRequest::new(address.clone()); let bytes = register_request.to_bytes(); let recovered = RegisterRequest::from_bytes(&bytes).unwrap(); @@ -213,11 +213,11 @@ mod creating_register_request { #[test] fn it_is_possible_to_recover_it_from_bytes_with_enum_wrapper() { - let address = [ + let address = DestinationAddressBytes::from_bytes([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, - ]; - let register_request = RegisterRequest::new(address); + ]); + let register_request = RegisterRequest::new(address.clone()); let bytes = register_request.to_bytes(); let recovered = ProviderRequests::from_bytes(&bytes).unwrap(); diff --git a/sfw-provider/sfw-provider-requests/src/responses.rs b/sfw-provider/sfw-provider-requests/src/responses.rs index c15017c76d..47163ef022 100644 --- a/sfw-provider/sfw-provider-requests/src/responses.rs +++ b/sfw-provider/sfw-provider-requests/src/responses.rs @@ -26,6 +26,10 @@ pub struct RegisterResponse { pub auth_token: AuthToken, } +pub struct ErrorResponse { + pub message: String, +} + impl PullResponse { pub fn new(messages: Vec>) -> Self { PullResponse { messages } @@ -38,6 +42,14 @@ impl RegisterResponse { } } +impl ErrorResponse { + pub fn new>(message: S) -> Self { + ErrorResponse { + message: message.into(), + } + } +} + // TODO: This should go into some kind of utils module/crate fn read_be_u16(input: &mut &[u8]) -> u16 { let (int_bytes, rest) = input.split_at(std::mem::size_of::()); @@ -72,7 +84,7 @@ impl ProviderResponse for PullResponse { return Err(ProviderResponseError::UnmarshalErrorInvalidLength); } - let mut bytes_copy = bytes.clone(); + let mut bytes_copy = bytes; let num_msgs = read_be_u16(&mut bytes_copy); // can we read all lengths of messages? @@ -109,7 +121,7 @@ impl ProviderResponse for PullResponse { impl ProviderResponse for RegisterResponse { fn to_bytes(&self) -> Vec { - self.auth_token.to_vec() + self.auth_token.0.to_vec() } fn from_bytes(bytes: &[u8]) -> Result { @@ -117,13 +129,28 @@ impl ProviderResponse for RegisterResponse { 32 => { let mut auth_token = [0u8; 32]; auth_token.copy_from_slice(&bytes[..32]); - Ok(RegisterResponse { auth_token }) + Ok(RegisterResponse { + auth_token: AuthToken(auth_token), + }) } _ => Err(ProviderResponseError::UnmarshalErrorInvalidLength), } } } +impl ProviderResponse for ErrorResponse { + fn to_bytes(&self) -> Vec { + self.message.clone().into_bytes() + } + + fn from_bytes(bytes: &[u8]) -> Result { + match String::from_utf8(bytes.to_vec()) { + Err(_) => Err(ProviderResponseError::UnmarshalError), + Ok(message) => Ok(ErrorResponse { message }), + } + } +} + #[cfg(test)] mod creating_pull_response { use super::*; diff --git a/sfw-provider/src/built_info.rs b/sfw-provider/src/built_info.rs new file mode 100644 index 0000000000..d11fb6389f --- /dev/null +++ b/sfw-provider/src/built_info.rs @@ -0,0 +1,2 @@ +// The file has been placed there by the build script. +include!(concat!(env!("OUT_DIR"), "/built.rs")); diff --git a/sfw-provider/src/commands/init.rs b/sfw-provider/src/commands/init.rs new file mode 100644 index 0000000000..f87e189c51 --- /dev/null +++ b/sfw-provider/src/commands/init.rs @@ -0,0 +1,111 @@ +use crate::commands::override_config; +use crate::config::persistence::pathfinder::ProviderPathfinder; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::encryption; +use pemstore::pemstore::PemStore; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("init") + .about("Initialise the store and forward service provider") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the sfw-provider we want to create config for.") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("mix-host") + .long("mix-host") + .help("The custom host on which the service provider will be running for receiving sphinx packets") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("mix-port") + .long("mix-port") + .help("The port on which the service provider will be listening for sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-host") + .long("clients-host") + .help("The custom host on which the service provider will be running for receiving clients sfw-provider-requests") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("clients-port") + .long("clients-port") + .help("The port on which the service provider will be listening for clients sfw-provider-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-announce-host") + .long("mix-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-announce-port") + .long("mix-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-host") + .long("clients-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-port") + .long("clients-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("inboxes") + .long("inboxes") + .help("Directory with inboxes where all packets for the clients are stored") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-ledger") + .long("clients-ledger") + .help("[UNIMPLEMENTED] Ledger file containing registered clients") + .takes_value(true) + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the node is sending presence data to") + .takes_value(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + println!("Initialising sfw service provider {}...", id); + + let mut config = crate::config::Config::new(id); + + config = override_config(config, matches); + + let sphinx_keys = encryption::KeyPair::new(); + let pathfinder = ProviderPathfinder::new_from_config(&config); + let pem_store = PemStore::new(pathfinder); + pem_store + .write_encryption_keys(sphinx_keys) + .expect("Failed to save sphinx keys"); + println!("Saved mixnet sphinx keypair"); + + let config_save_location = config.get_config_file_save_location(); + config + .save_to_file(None) + .expect("Failed to save the config file"); + println!("Saved configuration file to {:?}", config_save_location); + + println!("Service provider configuration completed.\n\n\n") +} diff --git a/sfw-provider/src/commands/mod.rs b/sfw-provider/src/commands/mod.rs new file mode 100644 index 0000000000..95651ffe13 --- /dev/null +++ b/sfw-provider/src/commands/mod.rs @@ -0,0 +1,78 @@ +use crate::config::Config; +use clap::ArgMatches; + +pub mod init; +pub mod run; + +pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { + if let Some(mix_host) = matches.value_of("mix-host") { + config = config.with_mix_listening_host(mix_host); + } + + if let Some(mix_port) = matches.value_of("mix-port").map(|port| port.parse::()) { + if let Err(err) = mix_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_mix_listening_port(mix_port.unwrap()); + } + + if let Some(clients_host) = matches.value_of("clients-host") { + config = config.with_clients_listening_host(clients_host); + } + + if let Some(clients_port) = matches + .value_of("clients-port") + .map(|port| port.parse::()) + { + if let Err(err) = clients_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_clients_listening_port(clients_port.unwrap()); + } + + if let Some(mix_announce_host) = matches.value_of("mix-announce-host") { + config = config.with_mix_announce_host(mix_announce_host); + } + + if let Some(mix_announce_port) = matches + .value_of("mix-announce-port") + .map(|port| port.parse::()) + { + if let Err(err) = mix_announce_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_mix_announce_port(mix_announce_port.unwrap()); + } + + if let Some(clients_announce_host) = matches.value_of("clients-announce-host") { + config = config.with_clients_announce_host(clients_announce_host); + } + + if let Some(clients_announce_port) = matches + .value_of("clients-announce-port") + .map(|port| port.parse::()) + { + if let Err(err) = clients_announce_port { + // if port was overridden, it must be parsable + panic!("Invalid port value provided - {:?}", err); + } + config = config.with_clients_announce_port(clients_announce_port.unwrap()); + } + + if let Some(directory) = matches.value_of("directory") { + config = config.with_custom_directory(directory); + } + + if let Some(inboxes_dir) = matches.value_of("inboxes") { + config = config.with_custom_clients_inboxes(inboxes_dir); + } + + if let Some(clients_ledger) = matches.value_of("clients-ledger") { + config = config.with_custom_clients_ledger(clients_ledger); + } + + config +} diff --git a/sfw-provider/src/commands/run.rs b/sfw-provider/src/commands/run.rs new file mode 100644 index 0000000000..123398e6db --- /dev/null +++ b/sfw-provider/src/commands/run.rs @@ -0,0 +1,161 @@ +use crate::commands::override_config; +use crate::config::Config; +use crate::provider::ServiceProvider; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("run") + .about("Starts the service provider") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-mixnode we want to run") + .takes_value(true) + .required(true), + ) + // the rest of arguments are optional, they are used to override settings in config file + .arg( + Arg::with_name("config") + .long("config") + .help("Custom path to the nym-provider configuration file") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-host") + .long("mix-host") + .help("The custom host on which the service provider will be running for receiving sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-port") + .long("mix-port") + .help("The port on which the service provider will be listening for sphinx packets") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-host") + .long("clients-host") + .help("The custom host on which the service provider will be running for receiving clients sfw-provider-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-port") + .long("clients-port") + .help("The port on which the service provider will be listening for clients sfw-provider-requests") + .takes_value(true) + ) + .arg( + Arg::with_name("mix-announce-host") + .long("mix-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("mix-announce-port") + .long("mix-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-host") + .long("clients-announce-host") + .help("The host that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("clients-announce-port") + .long("clients-announce-port") + .help("The port that will be reported to the directory server") + .takes_value(true), + ) + .arg( + Arg::with_name("inboxes") + .long("inboxes") + .help("Directory with inboxes where all packets for the clients are stored") + .takes_value(true) + ) + .arg( + Arg::with_name("clients-ledger") + .long("clients-ledger") + .help("[UNIMPLEMENTED] Ledger file containing registered clients") + .takes_value(true) + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the node is sending presence data to") + .takes_value(true), + ) +} + +fn show_binding_warning(address: String) { + println!("\n##### WARNING #####"); + println!( + "\nYou are trying to bind to {} - you might not be accessible to other nodes\n\ + You can ignore this warning if you're running setup on a local network \n\ + or have set a custom 'announce-host'", + address + ); + println!("\n##### WARNING #####\n"); +} + +fn special_addresses() -> Vec<&'static str> { + vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + + println!("Starting sfw-provider {}...", id); + + let mut config = + Config::load_from_file(matches.value_of("config").map(|path| path.into()), Some(id)) + .expect("Failed to load config file"); + + config = override_config(config, matches); + + let mix_listening_ip_string = config.get_mix_listening_address().ip().to_string(); + if special_addresses().contains(&mix_listening_ip_string.as_ref()) { + show_binding_warning(mix_listening_ip_string); + } + + let clients_listening_ip_string = config.get_clients_listening_address().ip().to_string(); + if special_addresses().contains(&clients_listening_ip_string.as_ref()) { + show_binding_warning(clients_listening_ip_string); + } + + println!( + "Directory server [presence]: {}", + config.get_presence_directory_server() + ); + + println!( + "Listening for incoming sphinx packets on {}", + config.get_mix_listening_address() + ); + println!( + "Announcing the following socket address for sphinx packets: {}", + config.get_mix_announce_address() + ); + + println!( + "Listening for incoming clients packets on {}", + config.get_clients_listening_address() + ); + println!( + "Announcing the following socket address for clients packets: {}", + config.get_clients_announce_address() + ); + + println!( + "Inboxes directory is: {:?}", + config.get_clients_inboxes_dir() + ); + println!( + "[UNIMPLEMENTED] Registered client ledger is: {:?}", + config.get_clients_ledger_path() + ); + + ServiceProvider::new(config).run(); +} diff --git a/sfw-provider/src/config/mod.rs b/sfw-provider/src/config/mod.rs new file mode 100644 index 0000000000..7da7ed4a45 --- /dev/null +++ b/sfw-provider/src/config/mod.rs @@ -0,0 +1,497 @@ +use crate::config::template::config_template; +use config::NymConfig; +use log::*; +use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::str::FromStr; +use std::time; + +pub mod persistence; +mod template; + +// 'PROVIDER' +const DEFAULT_MIX_LISTENING_PORT: u16 = 1789; +const DEFAULT_CLIENT_LISTENING_PORT: u16 = 9000; +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_PRESENCE_SENDING_DELAY: u64 = 3000; + +const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; +const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: u16 = 5; + +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + provider: Provider, + + mixnet_endpoint: MixnetEndpoint, + + clients_endpoint: ClientsEndpoint, + + #[serde(default)] + logging: Logging, + #[serde(default)] + debug: Debug, +} + +impl NymConfig for Config { + fn template() -> &'static str { + config_template() + } + + fn config_file_name() -> String { + "config.toml".to_string() + } + + fn default_root_directory() -> PathBuf { + dirs::home_dir() + .expect("Failed to evaluate $HOME value") + .join(".nym") + .join("sfw-providers") + } + + fn root_directory(&self) -> PathBuf { + self.provider.nym_root_directory.clone() + } + + fn config_directory(&self) -> PathBuf { + self.provider + .nym_root_directory + .join(&self.provider.id) + .join("config") + } + + fn data_directory(&self) -> PathBuf { + self.provider + .nym_root_directory + .join(&self.provider.id) + .join("data") + } +} + +impl Config { + pub fn new>(id: S) -> Self { + Config::default().with_id(id) + } + + // builder methods + pub fn with_id>(mut self, id: S) -> Self { + let id = id.into(); + if self.provider.private_sphinx_key_file.as_os_str().is_empty() { + self.provider.private_sphinx_key_file = + self::Provider::default_private_sphinx_key_file(&id); + } + if self.provider.public_sphinx_key_file.as_os_str().is_empty() { + self.provider.public_sphinx_key_file = + self::Provider::default_public_sphinx_key_file(&id); + } + if self + .clients_endpoint + .inboxes_directory + .as_os_str() + .is_empty() + { + self.clients_endpoint.inboxes_directory = + self::ClientsEndpoint::default_inboxes_directory(&id); + } + if self.clients_endpoint.ledger_path.as_os_str().is_empty() { + self.clients_endpoint.ledger_path = self::ClientsEndpoint::default_ledger_path(&id); + } + + self.provider.id = id; + self + } + + pub fn with_custom_directory>(mut self, directory_server: S) -> Self { + self.debug.presence_directory_server = directory_server.into(); + self + } + + pub fn with_mix_listening_host>(mut self, host: S) -> Self { + // see if the provided `host` is just an ip address or ip:port + let host = host.into(); + + // is it ip:port? + match SocketAddr::from_str(host.as_ref()) { + Ok(socket_addr) => { + self.mixnet_endpoint.listening_address = socket_addr; + self + } + // try just for ip + Err(_) => match IpAddr::from_str(host.as_ref()) { + Ok(ip_addr) => { + self.mixnet_endpoint.listening_address.set_ip(ip_addr); + self + } + Err(_) => { + error!( + "failed to make any changes to config - invalid host {}", + host + ); + self + } + }, + } + } + + pub fn with_mix_listening_port(mut self, port: u16) -> Self { + self.mixnet_endpoint.listening_address.set_port(port); + self + } + + pub fn with_mix_announce_host>(mut self, host: S) -> Self { + // this is slightly more complicated as we store announce information as String, + // since it might not necessarily be a valid SocketAddr (say `nymtech.net:8080` is a valid + // announce address, yet invalid SocketAddr` + + // first lets see if we received host:port or just host part of an address + let host = host.into(); + let split_host: Vec<_> = host.split(':').collect(); + match split_host.len() { + 1 => { + // we provided only 'host' part so we are going to reuse existing port + self.mixnet_endpoint.announce_address = + format!("{}:{}", host, self.mixnet_endpoint.listening_address.port()); + self + } + 2 => { + // we provided 'host:port' so just put the whole thing there + self.mixnet_endpoint.announce_address = host; + self + } + _ => { + // we provided something completely invalid, so don't try to parse it + error!( + "failed to make any changes to config - invalid announce host {}", + host + ); + self + } + } + } + + pub fn with_mix_announce_port(mut self, port: u16) -> Self { + let current_host: Vec<_> = self.mixnet_endpoint.announce_address.split(':').collect(); + debug_assert_eq!(current_host.len(), 2); + self.mixnet_endpoint.announce_address = format!("{}:{}", current_host[0], port); + self + } + + pub fn with_clients_listening_host>(mut self, host: S) -> Self { + // see if the provided `host` is just an ip address or ip:port + let host = host.into(); + + // is it ip:port? + match SocketAddr::from_str(host.as_ref()) { + Ok(socket_addr) => { + self.clients_endpoint.listening_address = socket_addr; + self + } + // try just for ip + Err(_) => match IpAddr::from_str(host.as_ref()) { + Ok(ip_addr) => { + self.clients_endpoint.listening_address.set_ip(ip_addr); + self + } + Err(_) => { + error!( + "failed to make any changes to config - invalid host {}", + host + ); + self + } + }, + } + } + + pub fn with_clients_listening_port(mut self, port: u16) -> Self { + self.clients_endpoint.listening_address.set_port(port); + self + } + + pub fn with_clients_announce_host>(mut self, host: S) -> Self { + // this is slightly more complicated as we store announce information as String, + // since it might not necessarily be a valid SocketAddr (say `nymtech.net:8080` is a valid + // announce address, yet invalid SocketAddr` + + // first lets see if we received host:port or just host part of an address + let host = host.into(); + let split_host: Vec<_> = host.split(':').collect(); + match split_host.len() { + 1 => { + // we provided only 'host' part so we are going to reuse existing port + self.clients_endpoint.announce_address = format!( + "{}:{}", + host, + self.clients_endpoint.listening_address.port() + ); + self + } + 2 => { + // we provided 'host:port' so just put the whole thing there + self.clients_endpoint.announce_address = host; + self + } + _ => { + // we provided something completely invalid, so don't try to parse it + error!( + "failed to make any changes to config - invalid announce host {}", + host + ); + self + } + } + } + + pub fn with_clients_announce_port(mut self, port: u16) -> Self { + let current_host: Vec<_> = self.clients_endpoint.announce_address.split(':').collect(); + debug_assert_eq!(current_host.len(), 2); + self.clients_endpoint.announce_address = format!("{}:{}", current_host[0], port); + self + } + + pub fn with_custom_clients_inboxes>(mut self, inboxes_dir: S) -> Self { + self.clients_endpoint.inboxes_directory = PathBuf::from(inboxes_dir.into()); + self + } + + pub fn with_custom_clients_ledger>(mut self, ledger_path: S) -> Self { + self.clients_endpoint.ledger_path = PathBuf::from(ledger_path.into()); + self + } + + // getters + pub fn get_config_file_save_location(&self) -> PathBuf { + self.config_directory().join(Self::config_file_name()) + } + + pub fn get_private_sphinx_key_file(&self) -> PathBuf { + self.provider.private_sphinx_key_file.clone() + } + + pub fn get_public_sphinx_key_file(&self) -> PathBuf { + self.provider.public_sphinx_key_file.clone() + } + + pub fn get_presence_directory_server(&self) -> String { + self.debug.presence_directory_server.clone() + } + + pub fn get_presence_sending_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.presence_sending_delay) + } + + pub fn get_mix_listening_address(&self) -> SocketAddr { + self.mixnet_endpoint.listening_address + } + + pub fn get_mix_announce_address(&self) -> String { + self.mixnet_endpoint.announce_address.clone() + } + + pub fn get_clients_listening_address(&self) -> SocketAddr { + self.clients_endpoint.listening_address + } + + pub fn get_clients_announce_address(&self) -> String { + self.clients_endpoint.announce_address.clone() + } + + pub fn get_clients_inboxes_dir(&self) -> PathBuf { + self.clients_endpoint.inboxes_directory.clone() + } + + pub fn get_clients_ledger_path(&self) -> PathBuf { + self.clients_endpoint.ledger_path.clone() + } + + pub fn get_message_retrieval_limit(&self) -> u16 { + self.debug.message_retrieval_limit + } + + pub fn get_stored_messages_filename_length(&self) -> u16 { + self.debug.stored_messages_filename_length + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Provider { + /// ID specifies the human readable ID of this particular provider. + id: String, + + /// Path to file containing private sphinx key. + private_sphinx_key_file: PathBuf, + + /// Path to file containing public sphinx key. + public_sphinx_key_file: PathBuf, + + /// nym_home_directory specifies absolute path to the home nym service providers directory. + /// It is expected to use default value and hence .toml file should not redefine this field. + nym_root_directory: PathBuf, +} + +impl Provider { + fn default_private_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_sphinx.pem") + } + + fn default_public_sphinx_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_sphinx.pem") + } +} + +impl Default for Provider { + fn default() -> Self { + Provider { + id: "".to_string(), + private_sphinx_key_file: Default::default(), + public_sphinx_key_file: Default::default(), + nym_root_directory: Config::default_root_directory(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetEndpoint { + /// Socket address to which this service provider will bind to + /// and will be listening for sphinx packets coming from the mixnet. + listening_address: SocketAddr, + + /// Optional address announced to the directory server for the clients to connect to. + /// It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + /// later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. + /// Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` + /// are valid announce addresses, while the later will default to whatever port is used for + /// `listening_address`. + announce_address: String, +} + +impl Default for MixnetEndpoint { + fn default() -> Self { + MixnetEndpoint { + listening_address: format!("0.0.0.0:{}", DEFAULT_MIX_LISTENING_PORT) + .parse() + .unwrap(), + announce_address: format!("127.0.0.1:{}", DEFAULT_MIX_LISTENING_PORT), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ClientsEndpoint { + /// Socket address to which this service provider will bind to + /// and will be listening for data packets coming from the clients. + listening_address: SocketAddr, + + /// Optional address announced to the directory server for the clients to connect to. + /// It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + /// later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. + /// Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` + /// are valid announce addresses, while the later will default to whatever port is used for + /// `listening_address`. + announce_address: String, + + /// Path to the directory with clients inboxes containing messages stored for them. + inboxes_directory: PathBuf, + + /// [TODO: implement its storage] Full path to a file containing mapping of + /// client addresses to their access tokens. + ledger_path: PathBuf, +} + +impl ClientsEndpoint { + fn default_inboxes_directory(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("inboxes") + } + + fn default_ledger_path(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("client_ledger.dat") + } +} + +impl Default for ClientsEndpoint { + fn default() -> Self { + ClientsEndpoint { + listening_address: format!("0.0.0.0:{}", DEFAULT_CLIENT_LISTENING_PORT) + .parse() + .unwrap(), + announce_address: format!("127.0.0.1:{}", DEFAULT_CLIENT_LISTENING_PORT), + inboxes_directory: Default::default(), + ledger_path: Default::default(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Logging {} + +impl Default for Logging { + fn default() -> Self { + Logging {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Directory server to which the server will be reporting their presence data. + presence_directory_server: String, + + /// Delay between each subsequent presence data being sent. + presence_sending_delay: u64, + + /// Length of filenames for new client messages. + stored_messages_filename_length: u16, + + /// number of messages client gets on each request + /// if there are no real messages, dummy ones are create to always return + /// `message_retrieval_limit` total messages + message_retrieval_limit: u16, +} + +impl Debug { + fn default_directory_server() -> String { + #[cfg(feature = "qa")] + return "https://qa-directory.nymtech.net".to_string(); + #[cfg(feature = "local")] + return "http://localhost:8080".to_string(); + + "https://directory.nymtech.net".to_string() + } +} + +impl Default for Debug { + fn default() -> Self { + Debug { + presence_directory_server: Self::default_directory_server(), + presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, + stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, + message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[cfg(test)] +mod provider_config { + use super::*; + + #[test] + fn after_saving_default_config_the_loaded_one_is_identical() { + // need to figure out how to do something similar but without touching the disk + // or the file system at all... + let temp_location = tempfile::tempdir().unwrap().path().join("config.toml"); + let default_config = Config::default().with_id("foomp".to_string()); + default_config + .save_to_file(Some(temp_location.clone())) + .unwrap(); + + let loaded_config = Config::load_from_file(Some(temp_location), None).unwrap(); + + assert_eq!(default_config, loaded_config); + } +} diff --git a/sfw-provider/src/config/persistence/mod.rs b/sfw-provider/src/config/persistence/mod.rs new file mode 100644 index 0000000000..5692e9bc8b --- /dev/null +++ b/sfw-provider/src/config/persistence/mod.rs @@ -0,0 +1 @@ +pub mod pathfinder; diff --git a/sfw-provider/src/config/persistence/pathfinder.rs b/sfw-provider/src/config/persistence/pathfinder.rs new file mode 100644 index 0000000000..a86a967e4e --- /dev/null +++ b/sfw-provider/src/config/persistence/pathfinder.rs @@ -0,0 +1,44 @@ +use crate::config::Config; +use pemstore::pathfinder::PathFinder; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct ProviderPathfinder { + pub config_dir: PathBuf, + pub private_sphinx_key: PathBuf, + pub public_sphinx_key: PathBuf, +} + +impl ProviderPathfinder { + pub fn new_from_config(config: &Config) -> Self { + ProviderPathfinder { + config_dir: config.get_config_file_save_location(), + private_sphinx_key: config.get_private_sphinx_key_file(), + public_sphinx_key: config.get_public_sphinx_key_file(), + } + } +} + +impl PathFinder for ProviderPathfinder { + fn config_dir(&self) -> PathBuf { + self.config_dir.clone() + } + + fn private_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.private_sphinx_key.clone() + } + + fn public_identity_key(&self) -> PathBuf { + // TEMPORARILY USE SAME KEYS AS ENCRYPTION + self.public_sphinx_key.clone() + } + + fn private_encryption_key(&self) -> Option { + Some(self.private_sphinx_key.clone()) + } + + fn public_encryption_key(&self) -> Option { + Some(self.public_sphinx_key.clone()) + } +} diff --git a/sfw-provider/src/config/template.rs b/sfw-provider/src/config/template.rs new file mode 100644 index 0000000000..64208c8ab7 --- /dev/null +++ b/sfw-provider/src/config/template.rs @@ -0,0 +1,94 @@ +pub(crate) fn config_template() -> &'static str { + // While using normal toml marshalling would have been way simpler with less overhead, + // I think it's useful to have comments attached to the saved config file to explain behaviour of + // particular fields. + // Note: any changes to the template must be reflected in the appropriate structs in mod.rs. + r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base mixnode config options ##### + +[provider] +# Human readable ID of this particular service provider. +id = "{{ provider.id }}" + +# Path to file containing private sphinx key. +private_sphinx_key_file = "{{ provider.private_sphinx_key_file }}" + +# Path to file containing public sphinx key. +public_sphinx_key_file = "{{ provider.public_sphinx_key_file }}" + +# nym_home_directory specifies absolute path to the home nym service providers directory. +# It is expected to use default value and hence .toml file should not redefine this field. +nym_root_directory = "{{ provider.nym_root_directory }}" + + +##### Mixnet endpoint config options ##### + +[mixnet_endpoint] +# Socket address to which this service provider will bind to +# and will be listening for sphinx packets coming from the mixnet. +listening_address = "{{ mixnet_endpoint.listening_address }}" + +# Optional address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. +# Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` +# are valid announce addresses, while the later will default to whatever port is used for +# `listening_address`. +announce_address = "{{ mixnet_endpoint.announce_address }}" + + +#### Clients endpoint config options ##### + +[clients_endpoint] +# Socket address to which this service provider will bind to +# and will be listening for sphinx packets coming from the mixnet. +listening_address = "{{ clients_endpoint.listening_address }}" + +# Optional address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net:8080`. +# Additionally a custom port can be provided, so both `nymtech.net:8080` and `nymtech.net` +# are valid announce addresses, while the later will default to whatever port is used for +# `listening_address`. +announce_address = "{{ clients_endpoint.announce_address }}" + +# Path to the directory with clients inboxes containing messages stored for them. +inboxes_directory = "{{ clients_endpoint.inboxes_directory }}" + +# [TODO: implement its storage] Full path to a file containing mapping of +# client addresses to their access tokens. +ledger_path = "{{ clients_endpoint.ledger_path }}" + + +##### logging configuration options ##### + +[logging] + +# TODO + + +##### debug configuration options ##### +# The following options should not be modified unless you know EXACTLY what you are doing +# as if set incorrectly, they may impact your anonymity. + +[debug] + +# Directory server to which the server will be reporting their presence data. +presence_directory_server = "{{ debug.presence_directory_server}}" + +# Delay between each subsequent presence data being sent. +presence_sending_delay = {{ debug.presence_sending_delay }} + +# Length of filenames for new client messages. +stored_messages_filename_length = {{ debug.stored_messages_filename_length }} + +# number of messages client gets on each request +# if there are no real messages, dummy ones are create to always return +# `message_retrieval_limit` total messages +message_retrieval_limit = {{ debug.message_retrieval_limit }} + +"# +} diff --git a/sfw-provider/src/main.rs b/sfw-provider/src/main.rs index 6cd945a2c0..9f4d755644 100644 --- a/sfw-provider/src/main.rs +++ b/sfw-provider/src/main.rs @@ -1,178 +1,32 @@ -use crate::provider::ServiceProvider; -use clap::{App, Arg, ArgMatches, SubCommand}; -use crypto::identity::MixnetIdentityKeyPair; -use log::error; -use std::net::ToSocketAddrs; -use std::path::PathBuf; -use std::process; +use clap::{App, ArgMatches}; +pub mod built_info; +mod commands; +mod config; pub mod provider; fn main() { dotenv::dotenv().ok(); pretty_env_logger::init(); + println!("{}", banner()); + let arg_matches = App::new("Nym Service Provider") .version(built_info::PKG_VERSION) .author("Nymtech") .about("Implementation of the Loopix-based Service Provider") - .subcommand( - SubCommand::with_name("run") - .about("Starts the service provider") - .arg( - Arg::with_name("mixHost") - .long("mixHost") - .help("The custom host on which the service provider will be running for receiving sphinx packets") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("mixPort") - .long("mixPort") - .help("The port on which the service provider will be listening for sphinx packets") - .takes_value(true) - ) - .arg( - Arg::with_name("clientHost") - .long("clientHost") - .help("The custom host on which the service provider will be running for receiving client sfw-provider-requests") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("clientPort") - .long("clientPort") - .help("The port on which the service provider will be listening for client sfw-provider-requests") - .takes_value(true) - ) - .arg( - Arg::with_name("storeDir") - .short("s") - .long("storeDir") - .help("Directory storing all packets for the clients") - .takes_value(true) - ) - .arg( - Arg::with_name("registeredLedger") - .short("r") - .long("registeredLedger") - .help("Directory of the ledger of registered clients") - .takes_value(true) - ) - .arg( - Arg::with_name("directory") - .long("directory") - .help("Address of the directory server the node is sending presence and metrics to") - .takes_value(true), - ), - ) + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) .get_matches(); - if let Err(e) = execute(arg_matches) { - error!("{}", e); - process::exit(1); - } + execute(arg_matches); } -fn print_binding_warning(address: &str) { - println!("\n##### WARNING #####"); - println!( - "\nYou are trying to bind to {} - you might not be accessible to other nodes", - address - ); - println!("\n##### WARNING #####\n"); -} - -fn run(matches: &ArgMatches) { - println!("{}", banner()); - let config = new_config(matches); - let provider = ServiceProvider::new(config); - - provider.start().unwrap() -} - -fn new_config(matches: &ArgMatches) -> provider::Config { - let directory_server = matches - .value_of("directory") - .unwrap_or("https://directory.nymtech.net") - .to_string(); - - let mix_host = matches.value_of("mixHost").unwrap(); - let mix_port = match matches.value_of("mixPort").unwrap_or("8085").parse::() { - Ok(n) => n, - Err(err) => panic!("Invalid mix host port value provided - {:?}", err), - }; - - let client_host = matches.value_of("clientHost").unwrap(); - let client_port = match matches - .value_of("clientPort") - .unwrap_or("9000") - .parse::() - { - Ok(n) => n, - Err(err) => panic!("Invalid client port value provided - {:?}", err), - }; - - if mix_host == "localhost" || mix_host == "127.0.0.1" || mix_host == "0.0.0.0" { - print_binding_warning(mix_host); - } - - if client_host == "localhost" || client_host == "127.0.0.1" || client_host == "0.0.0.0" { - print_binding_warning(client_host); - } - - let key_pair = crypto::identity::DummyMixIdentityKeyPair::new(); // TODO: persist this so keypairs don't change every restart - let store_dir = PathBuf::from( - matches - .value_of("storeDir") - .unwrap_or("/tmp/nym-provider/inboxes"), - ); - let registered_client_ledger_dir = PathBuf::from( - matches - .value_of("registeredLedger") - .unwrap_or("/tmp/nym-provider/registered_clients"), - ); - - println!("store_dir is: {:?}", store_dir); - println!( - "registered_client_ledger_dir is: {:?}", - registered_client_ledger_dir - ); - - let mix_socket_address = (mix_host, mix_port) - .to_socket_addrs() - .expect("Failed to combine host and port") - .next() - .expect("Failed to extract the socket address from the iterator"); - - let client_socket_address = (client_host, client_port) - .to_socket_addrs() - .expect("Failed to combine host and port") - .next() - .expect("Failed to extract the socket address from the iterator"); - - println!("Listening for mixnet packets on {}", mix_socket_address); - println!("Listening for client requests on {}", client_socket_address); - - provider::Config { - mix_socket_address, - directory_server, - public_key: key_pair.public_key, - client_socket_address, - secret_key: key_pair.private_key, - store_dir: PathBuf::from(store_dir), - } -} - -pub mod built_info { - // The file has been placed there by the build script. - include!(concat!(env!("OUT_DIR"), "/built.rs")); -} - -fn execute(matches: ArgMatches) -> Result<(), String> { +fn execute(matches: ArgMatches) { match matches.subcommand() { - ("run", Some(m)) => Ok(run(m)), - _ => Err(usage()), + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), } } diff --git a/sfw-provider/src/provider/client_handling/ledger.rs b/sfw-provider/src/provider/client_handling/ledger.rs new file mode 100644 index 0000000000..a5dda44f36 --- /dev/null +++ b/sfw-provider/src/provider/client_handling/ledger.rs @@ -0,0 +1,74 @@ +use directory_client::presence::providers::MixProviderClient; +use futures::lock::Mutex; +use sfw_provider_requests::AuthToken; +use sphinx::route::DestinationAddressBytes; +use std::collections::HashMap; +use std::io; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Debug, Clone)] +// Note: you should NEVER create more than a single instance of this using 'new()'. +// You should always use .clone() to create additional instances +pub struct ClientLedger { + inner: Arc>, +} + +impl ClientLedger { + pub(crate) fn new() -> Self { + ClientLedger { + inner: Arc::new(Mutex::new(ClientLedgerInner(HashMap::new()))), + } + } + + pub(crate) async fn verify_token( + &self, + auth_token: &AuthToken, + client_address: &DestinationAddressBytes, + ) -> bool { + match self.inner.lock().await.0.get(client_address) { + None => false, + Some(expected_token) => expected_token == auth_token, + } + } + + pub(crate) async fn insert_token( + &mut self, + auth_token: AuthToken, + client_address: DestinationAddressBytes, + ) -> Option { + self.inner.lock().await.0.insert(client_address, auth_token) + } + + pub(crate) async fn remove_token( + &mut self, + client_address: &DestinationAddressBytes, + ) -> Option { + self.inner.lock().await.0.remove(client_address) + } + + pub(crate) async fn current_clients(&self) -> Vec { + self.inner + .lock() + .await + .0 + .keys() + .map(|client_address| client_address.to_base58_string()) + .map(|pub_key| MixProviderClient { pub_key }) + .collect() + } + + #[allow(dead_code)] + pub(crate) fn load(_file: PathBuf) -> Self { + // TODO: actual loading, + // temporarily just create a new one + Self::new() + } + + #[allow(dead_code)] + pub(crate) async fn save(&self, _file: PathBuf) -> io::Result<()> { + unimplemented!() + } +} + +struct ClientLedgerInner(HashMap); diff --git a/sfw-provider/src/provider/client_handling/listener.rs b/sfw-provider/src/provider/client_handling/listener.rs new file mode 100644 index 0000000000..260c947b07 --- /dev/null +++ b/sfw-provider/src/provider/client_handling/listener.rs @@ -0,0 +1,95 @@ +use crate::provider::client_handling::request_processing::{ + ClientProcessingResult, RequestProcessor, +}; +use log::*; +use sfw_provider_requests::responses::{ + ErrorResponse, ProviderResponse, PullResponse, RegisterResponse, +}; +use std::io; +use std::net::SocketAddr; +use tokio::prelude::*; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +async fn process_request( + socket: &mut tokio::net::TcpStream, + packet_data: &[u8], + request_processor: &mut RequestProcessor, +) { + match request_processor.process_client_request(packet_data).await { + Err(e) => { + warn!("We failed to process client request - {:?}", e); + let response_bytes = ErrorResponse::new(format!("{:?}", e)).to_bytes(); + if let Err(e) = socket.write_all(&response_bytes).await { + debug!("Failed to write response to the client - {:?}", e); + } + } + Ok(res) => match res { + ClientProcessingResult::RegisterResponse(auth_token) => { + let response_bytes = RegisterResponse::new(auth_token).to_bytes(); + if let Err(e) = socket.write_all(&response_bytes).await { + debug!("Failed to write response to the client - {:?}", e); + } + } + ClientProcessingResult::PullResponse(retrieved_messages) => { + let (messages, paths): (Vec<_>, Vec<_>) = retrieved_messages + .into_iter() + .map(|c| c.into_tuple()) + .unzip(); + let response_bytes = PullResponse::new(messages).to_bytes(); + if socket.write_all(&response_bytes).await.is_ok() { + // only delete stored messages if we managed to actually send the response + if let Err(e) = request_processor.delete_sent_messages(paths).await { + error!("Somehow failed to delete stored messages! - {:?}", e); + } + } + } + }, + } +} + +async fn process_socket_connection( + mut socket: tokio::net::TcpStream, + mut request_processor: RequestProcessor, +) { + let mut buf = [0u8; 1024]; + loop { + match socket.read(&mut buf).await { + // socket closed + Ok(n) if n == 0 => { + trace!("Remote connection closed."); + return; + } + // in here we do not really want to process multiple requests from the same + // client concurrently as then we might end up with really weird race conditions + // plus realistically it wouldn't really introduce any speed up + Ok(n) => process_request(&mut socket, &buf[0..n], &mut request_processor).await, + Err(e) => { + warn!( + "failed to read from socket. Closing the connection; err = {:?}", + e + ); + return; + } + }; + } +} + +pub(crate) fn run_client_socket_listener( + handle: &Handle, + addr: SocketAddr, + request_processor: RequestProcessor, +) -> JoinHandle> { + let handle_clone = handle.clone(); + handle.spawn(async move { + let mut listener = tokio::net::TcpListener::bind(addr).await?; + loop { + let (socket, _) = listener.accept().await?; + + let thread_request_processor = request_processor.clone(); + handle_clone.spawn(async move { + process_socket_connection(socket, thread_request_processor).await; + }); + } + }) +} diff --git a/sfw-provider/src/provider/client_handling/mod.rs b/sfw-provider/src/provider/client_handling/mod.rs index e3b9dcf86e..c7fdd30b23 100644 --- a/sfw-provider/src/provider/client_handling/mod.rs +++ b/sfw-provider/src/provider/client_handling/mod.rs @@ -1,274 +1,3 @@ -use crate::provider::storage::{ClientStorage, StoreError}; -use crate::provider::ClientLedger; -use crypto::identity::{DummyMixIdentityPrivateKey, MixnetIdentityPrivateKey}; -use futures::lock::Mutex as FMutex; -use hmac::{Hmac, Mac}; -use log::*; -use sfw_provider_requests::requests::{ - ProviderRequestError, ProviderRequests, PullRequest, RegisterRequest, -}; -use sfw_provider_requests::responses::{ProviderResponse, PullResponse, RegisterResponse}; -use sfw_provider_requests::AuthToken; -use sha2::Sha256; -use std::io; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -type HmacSha256 = Hmac; - -#[derive(Debug)] -pub enum ClientProcessingError { - ClientDoesntExistError, - StoreError, - InvalidRequest, - WrongToken, - IOError, -} - -impl From for ClientProcessingError { - fn from(_: ProviderRequestError) -> Self { - use ClientProcessingError::*; - - InvalidRequest - } -} - -impl From for ClientProcessingError { - fn from(e: StoreError) -> Self { - match e { - StoreError::ClientDoesntExistError => ClientProcessingError::ClientDoesntExistError, - _ => ClientProcessingError::StoreError, - } - } -} - -impl From for ClientProcessingError { - fn from(_: io::Error) -> Self { - use ClientProcessingError::*; - - IOError - } -} - -#[derive(Debug)] -pub(crate) struct ClientProcessingData { - store_dir: PathBuf, - registered_clients_ledger: Arc>, - secret_key: DummyMixIdentityPrivateKey, -} - -impl ClientProcessingData { - pub(crate) fn new( - store_dir: PathBuf, - registered_clients_ledger: Arc>, - secret_key: DummyMixIdentityPrivateKey, - ) -> Self { - ClientProcessingData { - store_dir, - registered_clients_ledger, - secret_key, - } - } - - pub(crate) fn add_arc(self) -> Arc { - Arc::new(self) - } -} - -pub(crate) struct ClientRequestProcessor; - -impl ClientRequestProcessor { - pub(crate) async fn process_client_request( - data: &[u8], - processing_data: Arc, - ) -> Result, ClientProcessingError> { - let client_request = ProviderRequests::from_bytes(&data)?; - trace!("Received the following request: {:?}", client_request); - match client_request { - ProviderRequests::Register(req) => Ok(ClientRequestProcessor::register_new_client( - req, - processing_data, - ) - .await? - .to_bytes()), - ProviderRequests::PullMessages(req) => Ok( - ClientRequestProcessor::process_pull_messages_request(req, processing_data) - .await? - .to_bytes(), - ), - } - } - - async fn process_pull_messages_request( - req: PullRequest, - processing_data: Arc, - ) -> Result { - // TODO: this lock is completely unnecessary as we're only reading the data. - // Wait for https://github.com/nymtech/nym-sfw-provider/issues/19 to resolve. - let unlocked_ledger = processing_data.registered_clients_ledger.lock().await; - - if unlocked_ledger.has_token(req.auth_token) { - // drop the mutex so that we could do IO without blocking others wanting to get the lock - drop(unlocked_ledger); - let retrieved_messages = ClientStorage::retrieve_client_files( - req.destination_address, - processing_data.store_dir.as_path(), - )?; - Ok(PullResponse::new(retrieved_messages)) - } else { - Err(ClientProcessingError::WrongToken) - } - } - - async fn register_new_client( - req: RegisterRequest, - processing_data: Arc, - ) -> Result { - debug!( - "Processing register new client request: {:?}", - req.destination_address - ); - let mut unlocked_ledger = processing_data.registered_clients_ledger.lock().await; - - let auth_token = ClientRequestProcessor::generate_new_auth_token( - req.destination_address.to_vec(), - processing_data.secret_key, - ); - if !unlocked_ledger.has_token(auth_token) { - unlocked_ledger.insert_token(auth_token, req.destination_address); - ClientRequestProcessor::create_storage_dir( - req.destination_address, - processing_data.store_dir.as_path(), - )?; - } - Ok(RegisterResponse::new(auth_token)) - } - - fn create_storage_dir( - client_address: sphinx::route::DestinationAddressBytes, - store_dir: &Path, - ) -> io::Result<()> { - let client_dir_name = bs58::encode(client_address).into_string(); - let full_store_dir = store_dir.join(client_dir_name); - std::fs::create_dir_all(full_store_dir) - } - - fn generate_new_auth_token(data: Vec, key: DummyMixIdentityPrivateKey) -> AuthToken { - // also note that `new_varkey` doesn't even have an execution branch returning an error - let mut auth_token_raw = HmacSha256::new_varkey(&key.to_bytes()) - .expect("HMAC should be able take key of any size"); - auth_token_raw.input(&data); - let mut auth_token = [0u8; 32]; - auth_token.copy_from_slice(&auth_token_raw.result().code().to_vec()); - auth_token - } -} - -#[cfg(test)] -mod register_new_client { - // use super::*; - - // TODO: those tests require being called in async context. we need to research how to test this stuff... - // #[test] - // fn registers_new_auth_token_for_each_new_client() { - // let req1 = RegisterRequest { - // destination_address: [1u8; 32], - // }; - // let registered_client_ledger = ClientLedger::new(); - // let store_dir = PathBuf::from("./foo/"); - // let key = Scalar::from_bytes_mod_order([1u8; 32]); - // let client_processing_data = ClientProcessingData::new(store_dir, registered_client_ledger, key).add_arc_futures_mutex(); - // - // - // // need to do async.... - // client_processing_data.lock().await; - // assert_eq!(0, registered_client_ledger.0.len()); - // ClientRequestProcessor::register_new_client( - // req1, - // client_processing_data.clone(), - // ); - // - // assert_eq!(1, registered_client_ledger.0.len()); - // - // let req2 = RegisterRequest { - // destination_address: [2u8; 32], - // }; - // ClientRequestProcessor::register_new_client( - // req2, - // client_processing_data, - // ); - // assert_eq!(2, registered_client_ledger.0.len()); - // } - // - // #[test] - // fn registers_given_token_only_once() { - // let req1 = RegisterRequest { - // destination_address: [1u8; 32], - // }; - // let registered_client_ledger = ClientLedger::new(); - // let store_dir = PathBuf::from("./foo/"); - // let key = Scalar::from_bytes_mod_order([1u8; 32]); - // let client_processing_data = ClientProcessingData::new(store_dir, registered_client_ledger, key).add_arc_futures_mutex(); - // - // ClientRequestProcessor::register_new_client( - // req1, - // client_processing_data.clone(), - // ); - // let req2 = RegisterRequest { - // destination_address: [1u8; 32], - // }; - // ClientRequestProcessor::register_new_client( - // req2, - // client_processing_data.clone(), - // ); - // - // client_processing_data.lock().await; - // - // assert_eq!(1, registered_client_ledger.0.len()) - // } -} - -#[cfg(test)] -mod create_storage_dir { - use super::*; - use sphinx::route::DestinationAddressBytes; - - #[test] - fn it_creates_a_correct_storage_directory() { - let client_address: DestinationAddressBytes = [1u8; 32]; - let store_dir = Path::new("/tmp/"); - ClientRequestProcessor::create_storage_dir(client_address, store_dir).unwrap(); - } -} - -#[cfg(test)] -mod generating_new_auth_token { - use super::*; - - #[test] - fn for_the_same_input_generates_the_same_auth_token() { - let data1 = vec![1u8; 55]; - let data2 = vec![1u8; 55]; - let key = DummyMixIdentityPrivateKey::from_bytes(&[1u8; 32]); - let token1 = ClientRequestProcessor::generate_new_auth_token(data1, key); - let token2 = ClientRequestProcessor::generate_new_auth_token(data2, key); - assert_eq!(token1, token2); - } - - #[test] - fn for_different_inputs_generates_different_auth_tokens() { - let data1 = vec![1u8; 55]; - let data2 = vec![2u8; 55]; - let key = DummyMixIdentityPrivateKey::from_bytes(&[1u8; 32]); - let token1 = ClientRequestProcessor::generate_new_auth_token(data1, key); - let token2 = ClientRequestProcessor::generate_new_auth_token(data2, key); - assert_ne!(token1, token2); - - let data1 = vec![1u8; 50]; - let data2 = vec![2u8; 55]; - let key = DummyMixIdentityPrivateKey::from_bytes(&[1u8; 32]); - let token1 = ClientRequestProcessor::generate_new_auth_token(data1, key); - let token2 = ClientRequestProcessor::generate_new_auth_token(data2, key); - assert_ne!(token1, token2); - } -} +pub(crate) mod ledger; +pub(crate) mod listener; +pub(crate) mod request_processing; diff --git a/sfw-provider/src/provider/client_handling/request_processing.rs b/sfw-provider/src/provider/client_handling/request_processing.rs new file mode 100644 index 0000000000..57707c1b0d --- /dev/null +++ b/sfw-provider/src/provider/client_handling/request_processing.rs @@ -0,0 +1,202 @@ +use crate::provider::client_handling::ledger::ClientLedger; +use crate::provider::storage::{ClientFile, ClientStorage}; +use crypto::encryption; +use hmac::{Hmac, Mac}; +use log::*; +use sfw_provider_requests::requests::{ + ProviderRequestError, ProviderRequests, PullRequest, RegisterRequest, +}; +use sfw_provider_requests::AuthToken; +use sha2::Sha256; +use sphinx::route::DestinationAddressBytes; +use std::io; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Debug)] +pub enum ClientProcessingResult { + PullResponse(Vec), + RegisterResponse(AuthToken), +} + +#[derive(Debug)] +pub enum ClientProcessingError { + InvalidRequest, + InvalidToken, + IOError(String), +} + +impl From for ClientProcessingError { + fn from(_: ProviderRequestError) -> Self { + use ClientProcessingError::*; + + InvalidRequest + } +} + +impl From for ClientProcessingError { + fn from(e: io::Error) -> Self { + use ClientProcessingError::*; + + IOError(e.to_string()) + } +} + +// PacketProcessor contains all data required to correctly process client requests +#[derive(Clone)] +pub struct RequestProcessor { + secret_key: Arc, + client_storage: ClientStorage, + client_ledger: ClientLedger, +} + +impl RequestProcessor { + pub(crate) fn new( + secret_key: encryption::PrivateKey, + client_storage: ClientStorage, + client_ledger: ClientLedger, + ) -> Self { + RequestProcessor { + secret_key: Arc::new(secret_key), + client_storage, + client_ledger, + } + } + + pub(crate) async fn process_client_request( + &mut self, + request_bytes: &[u8], + ) -> Result { + let client_request = ProviderRequests::from_bytes(request_bytes)?; + debug!("Received the following request: {:?}", client_request); + match client_request { + ProviderRequests::Register(req) => self.process_register_request(req).await, + ProviderRequests::PullMessages(req) => self.process_pull_request(req).await, + } + } + + pub(crate) async fn process_register_request( + &mut self, + req: RegisterRequest, + ) -> Result { + debug!( + "Processing register new client request: {:?}", + req.destination_address.to_base58_string() + ); + + let auth_token = self.generate_new_auth_token(req.destination_address.clone()); + if self + .client_ledger + .insert_token(auth_token.clone(), req.destination_address.clone()) + .await + .is_some() + { + info!( + "Client {:?} was already registered before!", + req.destination_address.to_base58_string() + ) + } else if let Err(e) = self + .client_storage + .create_storage_dir(req.destination_address.clone()) + .await + { + error!("We failed to create inbox directory for the client -{:?}\nReverting issued token...", e); + // we must revert our changes if this operation failed + self.client_ledger + .remove_token(&req.destination_address) + .await; + } + + Ok(ClientProcessingResult::RegisterResponse(auth_token)) + } + + fn generate_new_auth_token(&self, client_address: DestinationAddressBytes) -> AuthToken { + type HmacSha256 = Hmac; + + // note that `new_varkey` doesn't even have an execution branch returning an error + // (true as of hmac 0.7.1) + let mut auth_token_raw = HmacSha256::new_varkey(&self.secret_key.to_bytes()).unwrap(); + auth_token_raw.input(client_address.as_bytes()); + let mut auth_token = [0u8; 32]; + auth_token.copy_from_slice(auth_token_raw.result().code().as_slice()); + AuthToken::from_bytes(auth_token) + } + + pub(crate) async fn process_pull_request( + &self, + req: PullRequest, + ) -> Result { + if self + .client_ledger + .verify_token(&req.auth_token, &req.destination_address) + .await + { + let retrieved_messages = self + .client_storage + .retrieve_client_files(req.destination_address) + .await?; + return Ok(ClientProcessingResult::PullResponse(retrieved_messages)); + } + + Err(ClientProcessingError::InvalidToken) + } + + pub(crate) async fn delete_sent_messages(&self, file_paths: Vec) -> io::Result<()> { + self.client_storage.delete_files(file_paths).await + } +} + +#[cfg(test)] +mod generating_new_auth_token { + use super::*; + + #[test] + fn for_the_same_input_generates_the_same_auth_token() { + let client_address1 = DestinationAddressBytes::from_bytes([1; 32]); + let client_address2 = DestinationAddressBytes::from_bytes([1; 32]); + let key = encryption::PrivateKey::from_bytes(&[2u8; 32]); + + let request_processor = RequestProcessor { + secret_key: Arc::new(key), + client_storage: ClientStorage::new(3, 16, Default::default()), + client_ledger: ClientLedger::new(), + }; + + let token1 = request_processor.generate_new_auth_token(client_address1); + let token2 = request_processor.generate_new_auth_token(client_address2); + assert_eq!(token1, token2); + } + + #[test] + fn for_different_inputs_generates_different_auth_tokens() { + let client_address1 = DestinationAddressBytes::from_bytes([1; 32]); + let client_address2 = DestinationAddressBytes::from_bytes([2; 32]); + let key1 = encryption::PrivateKey::from_bytes(&[3u8; 32]); + let key2 = encryption::PrivateKey::from_bytes(&[4u8; 32]); + + let request_processor1 = RequestProcessor { + secret_key: Arc::new(key1), + client_storage: ClientStorage::new(3, 16, Default::default()), + client_ledger: ClientLedger::new(), + }; + + let request_processor2 = RequestProcessor { + secret_key: Arc::new(key2), + client_storage: ClientStorage::new(3, 16, Default::default()), + client_ledger: ClientLedger::new(), + }; + + let token1 = request_processor1.generate_new_auth_token(client_address1.clone()); + let token2 = request_processor1.generate_new_auth_token(client_address2.clone()); + + let token3 = request_processor2.generate_new_auth_token(client_address1); + let token4 = request_processor2.generate_new_auth_token(client_address2); + + assert_ne!(token1, token2); + assert_ne!(token1, token3); + assert_ne!(token1, token4); + assert_ne!(token2, token3); + assert_ne!(token2, token4); + assert_ne!(token3, token4); + } +} diff --git a/sfw-provider/src/provider/mix_handling/listener.rs b/sfw-provider/src/provider/mix_handling/listener.rs new file mode 100644 index 0000000000..70a37c7841 --- /dev/null +++ b/sfw-provider/src/provider/mix_handling/listener.rs @@ -0,0 +1,75 @@ +use crate::provider::mix_handling::packet_processing::{MixProcessingResult, PacketProcessor}; +use log::*; +use std::io; +use std::net::SocketAddr; +use tokio::prelude::*; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +async fn process_received_packet( + packet_data: [u8; sphinx::PACKET_SIZE], + packet_processor: PacketProcessor, +) { + match packet_processor.process_sphinx_packet(packet_data).await { + Err(e) => debug!("We failed to process received sphinx packet - {:?}", e), + Ok(res) => match res { + MixProcessingResult::ForwardHop => { + error!("Somehow processed a forward hop message - those are not implemented for providers!") + } + MixProcessingResult::FinalHop => { + trace!("successfully processed [and stored] a final hop packet") + } + }, + } +} + +async fn process_socket_connection( + mut socket: tokio::net::TcpStream, + packet_processor: PacketProcessor, +) { + let mut buf = [0u8; sphinx::PACKET_SIZE]; + loop { + match socket.read(&mut buf).await { + // socket closed + Ok(n) if n == 0 => { + trace!("Remote connection closed."); + return; + } + Ok(n) => { + if n != sphinx::PACKET_SIZE { + warn!("read data of different length than expected sphinx packet size - {} (expected {})", n, sphinx::PACKET_SIZE); + continue; + } + + // we must be able to handle multiple packets from same connection independently + tokio::spawn(process_received_packet(buf, packet_processor.clone())) + } + Err(e) => { + warn!( + "failed to read from socket. Closing the connection; err = {:?}", + e + ); + return; + } + }; + } +} + +pub(crate) fn run_mix_socket_listener( + handle: &Handle, + addr: SocketAddr, + packet_processor: PacketProcessor, +) -> JoinHandle> { + let handle_clone = handle.clone(); + handle.spawn(async move { + let mut listener = tokio::net::TcpListener::bind(addr).await?; + loop { + let (socket, _) = listener.accept().await?; + + let thread_packet_processor = packet_processor.clone(); + handle_clone.spawn(async move { + process_socket_connection(socket, thread_packet_processor).await; + }); + } + }) +} diff --git a/sfw-provider/src/provider/mix_handling/mod.rs b/sfw-provider/src/provider/mix_handling/mod.rs index 338943efd1..236fd73481 100644 --- a/sfw-provider/src/provider/mix_handling/mod.rs +++ b/sfw-provider/src/provider/mix_handling/mod.rs @@ -1,95 +1,2 @@ -use crate::provider::storage::StoreData; -use crypto::identity::DummyMixIdentityPrivateKey; -use log::{error, warn}; -use sphinx::{ProcessedPacket, SphinxPacket}; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; - -// TODO: this will probably need to be moved elsewhere I imagine -// DUPLICATE WITH MIXNODE CODE!!! -#[derive(Debug)] -pub enum MixProcessingError { - FileIOFailure, - InvalidPayload, - NonMatchingRecipient, - ReceivedForwardHopError, - SphinxRecoveryError, - SphinxProcessingError, -} - -impl From for MixProcessingError { - // for time being just have a single error instance for all possible results of sphinx::ProcessingError - fn from(_: sphinx::ProcessingError) -> Self { - use MixProcessingError::*; - - SphinxRecoveryError - } -} - -impl From for MixProcessingError { - fn from(_: std::io::Error) -> Self { - use MixProcessingError::*; - - FileIOFailure - } -} - -// ProcessingData defines all data required to correctly unwrap sphinx packets -#[derive(Debug, Clone)] -pub(crate) struct MixProcessingData { - secret_key: DummyMixIdentityPrivateKey, - pub(crate) store_dir: PathBuf, -} - -impl MixProcessingData { - pub(crate) fn new(secret_key: DummyMixIdentityPrivateKey, store_dir: PathBuf) -> Self { - MixProcessingData { - secret_key, - store_dir, - } - } - - pub(crate) fn add_arc_rwlock(self) -> Arc> { - Arc::new(RwLock::new(self)) - } -} - -pub(crate) struct MixPacketProcessor(()); - -impl MixPacketProcessor { - pub fn process_sphinx_data_packet( - packet_data: &[u8], - processing_data: &RwLock, - ) -> Result { - let packet = SphinxPacket::from_bytes(packet_data.to_vec())?; - let read_processing_data = match processing_data.read() { - Ok(guard) => guard, - Err(e) => { - error!("processing data lock was poisoned! - {:?}", e); - std::process::exit(1) - } - }; - let (client_address, client_surb_id, payload) = - match packet.process(read_processing_data.secret_key.as_scalar()) { - Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, surb_id, payload)) => { - (client_address, surb_id, payload) - } - Ok(_) => return Err(MixProcessingError::ReceivedForwardHopError), - Err(e) => { - warn!("Error unwrapping Sphinx packet: {:?}", e); - return Err(MixProcessingError::SphinxProcessingError); - } - }; - - // TODO: should provider try to be recovering plaintext? this would potentially make client retrieve messages of non-constant length, - // perhaps provider should be re-padding them on retrieval or storing full data? - let (payload_destination, message) = payload - .try_recover_destination_and_plaintext() - .ok_or_else(|| MixProcessingError::InvalidPayload)?; - if client_address != payload_destination { - return Err(MixProcessingError::NonMatchingRecipient); - } - - Ok(StoreData::new(client_address, client_surb_id, message)) - } -} +pub(crate) mod listener; +pub(crate) mod packet_processing; diff --git a/sfw-provider/src/provider/mix_handling/packet_processing.rs b/sfw-provider/src/provider/mix_handling/packet_processing.rs new file mode 100644 index 0000000000..12d50c322c --- /dev/null +++ b/sfw-provider/src/provider/mix_handling/packet_processing.rs @@ -0,0 +1,100 @@ +use crate::provider::storage::{ClientStorage, StoreData}; +use crypto::encryption; +use log::*; +use sphinx::payload::Payload; +use sphinx::route::{DestinationAddressBytes, SURBIdentifier}; +use sphinx::{ProcessedPacket, SphinxPacket}; +use std::io; +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Debug)] +pub enum MixProcessingError { + ReceivedForwardHopError, + NonMatchingRecipient, + InvalidPayload, + SphinxProcessingError, + IOError(String), +} + +pub enum MixProcessingResult { + #[allow(dead_code)] + ForwardHop, + FinalHop, +} + +impl From for MixProcessingError { + // for time being just have a single error instance for all possible results of sphinx::ProcessingError + fn from(_: sphinx::ProcessingError) -> Self { + use MixProcessingError::*; + + SphinxProcessingError + } +} + +impl From for MixProcessingError { + fn from(e: io::Error) -> Self { + use MixProcessingError::*; + + IOError(e.to_string()) + } +} + +// PacketProcessor contains all data required to correctly unwrap and store sphinx packets +#[derive(Clone)] +pub struct PacketProcessor { + secret_key: Arc, + client_store: ClientStorage, +} + +impl PacketProcessor { + pub(crate) fn new(secret_key: encryption::PrivateKey, client_store: ClientStorage) -> Self { + PacketProcessor { + secret_key: Arc::new(secret_key), + client_store, + } + } + + async fn process_final_hop( + &self, + client_address: DestinationAddressBytes, + surb_id: SURBIdentifier, + payload: Payload, + ) -> Result { + // TODO: should provider try to be recovering plaintext? this would potentially make client retrieve messages of non-constant length, + // perhaps provider should be re-padding them on retrieval or storing full data? + let (payload_destination, message) = payload + .try_recover_destination_and_plaintext() + .ok_or_else(|| MixProcessingError::InvalidPayload)?; + if client_address != payload_destination { + return Err(MixProcessingError::NonMatchingRecipient); + } + + let store_data = StoreData::new(client_address, surb_id, message); + self.client_store.store_processed_data(store_data).await?; + + Ok(MixProcessingResult::FinalHop) + } + + pub(crate) async fn process_sphinx_packet( + &self, + raw_packet_data: [u8; sphinx::PACKET_SIZE], + ) -> Result { + let packet = SphinxPacket::from_bytes(&raw_packet_data)?; + + match packet.process(self.secret_key.deref().inner()) { + Ok(ProcessedPacket::ProcessedPacketForwardHop(_, _, _)) => { + warn!("Received a forward hop message - those are not implemented for providers"); + Err(MixProcessingError::ReceivedForwardHopError) + } + Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, surb_id, payload)) => { + self.process_final_hop(client_address, surb_id, payload) + .await + } + Err(e) => { + warn!("Failed to unwrap Sphinx packet: {:?}", e); + Err(MixProcessingError::SphinxProcessingError) + } + } + } +} diff --git a/sfw-provider/src/provider/mod.rs b/sfw-provider/src/provider/mod.rs index 4e16e5a2a2..e107150908 100644 --- a/sfw-provider/src/provider/mod.rs +++ b/sfw-provider/src/provider/mod.rs @@ -1,19 +1,10 @@ -use crate::provider::client_handling::{ClientProcessingData, ClientRequestProcessor}; -use crate::provider::mix_handling::{MixPacketProcessor, MixProcessingData}; +use crate::config::persistence::pathfinder::ProviderPathfinder; +use crate::config::Config; +use crate::provider::client_handling::ledger::ClientLedger; use crate::provider::storage::ClientStorage; -use crypto::identity::{DummyMixIdentityPrivateKey, DummyMixIdentityPublicKey}; -use directory_client::presence::providers::MixProviderClient; -use futures::io::Error; -use futures::lock::Mutex as FMutex; +use crypto::encryption; use log::*; -use sfw_provider_requests::AuthToken; -use sphinx::route::DestinationAddressBytes; -use std::collections::HashMap; -use std::net::{Shutdown, SocketAddr}; -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::RwLock; -use tokio::prelude::*; +use pemstore::pemstore::PemStore; use tokio::runtime::Runtime; mod client_handling; @@ -21,300 +12,103 @@ mod mix_handling; pub mod presence; mod storage; -// TODO: if we ever create config file, this should go there -const STORED_MESSAGE_FILENAME_LENGTH: usize = 16; -const MESSAGE_RETRIEVAL_LIMIT: usize = 5; - -pub struct Config { - pub client_socket_address: SocketAddr, - pub directory_server: String, - pub mix_socket_address: SocketAddr, - pub public_key: DummyMixIdentityPublicKey, - pub secret_key: DummyMixIdentityPrivateKey, - pub store_dir: PathBuf, -} - -#[derive(Debug)] -pub enum ProviderError { - TcpListenerBindingError, - TcpListenerConnectionError, - TcpListenerUnexpectedEof, - - TcpListenerUnknownError, -} - -impl From for ProviderError { - fn from(err: Error) -> Self { - use ProviderError::*; - match err.kind() { - io::ErrorKind::ConnectionRefused => TcpListenerConnectionError, - io::ErrorKind::ConnectionReset => TcpListenerConnectionError, - io::ErrorKind::ConnectionAborted => TcpListenerConnectionError, - io::ErrorKind::NotConnected => TcpListenerConnectionError, - - io::ErrorKind::AddrInUse => TcpListenerBindingError, - io::ErrorKind::AddrNotAvailable => TcpListenerBindingError, - io::ErrorKind::UnexpectedEof => TcpListenerUnexpectedEof, - _ => TcpListenerUnknownError, - } - } -} - -#[derive(Debug)] -pub struct ClientLedger(HashMap); - -impl ClientLedger { - fn new() -> Self { - ClientLedger(HashMap::new()) - } - - fn add_arc_futures_mutex(self) -> Arc> { - Arc::new(FMutex::new(self)) - } - - fn has_token(&self, auth_token: AuthToken) -> bool { - return self.0.contains_key(&auth_token); - } - - fn insert_token( - &mut self, - auth_token: AuthToken, - client_address: DestinationAddressBytes, - ) -> Option { - self.0.insert(auth_token, client_address) - } - - fn current_clients(&self) -> Vec { - self.0 - .iter() - .map(|(_, v)| bs58::encode(v).into_string()) - .map(|pub_key| MixProviderClient { pub_key }) - .collect() - } - - #[allow(dead_code)] - fn load(_file: PathBuf) -> Self { - unimplemented!() - } -} - pub struct ServiceProvider { - directory_server: String, - mix_network_address: SocketAddr, - client_network_address: SocketAddr, - public_key: DummyMixIdentityPublicKey, - secret_key: DummyMixIdentityPrivateKey, - store_dir: PathBuf, + runtime: Runtime, + config: Config, + sphinx_keypair: encryption::KeyPair, registered_clients_ledger: ClientLedger, } impl ServiceProvider { + fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { + let sphinx_keypair = PemStore::new(ProviderPathfinder::new_from_config(&config_file)) + .read_encryption() + .expect("Failed to read stored sphinx key files"); + println!( + "Public encryption key: {}\nFor time being, it is identical to identity keys", + sphinx_keypair.public_key().to_base58_string() + ); + sphinx_keypair + } + pub fn new(config: Config) -> Self { + let sphinx_keypair = Self::load_sphinx_keys(&config); + let registered_clients_ledger = ClientLedger::load(config.get_clients_ledger_path()); ServiceProvider { - mix_network_address: config.mix_socket_address, - client_network_address: config.client_socket_address, - secret_key: config.secret_key, - public_key: config.public_key, - store_dir: PathBuf::from(config.store_dir.clone()), - // TODO: load initial ledger from file - registered_clients_ledger: ClientLedger::new(), - directory_server: config.directory_server.clone(), + runtime: Runtime::new().unwrap(), + config, + sphinx_keypair, + registered_clients_ledger, } } - async fn process_mixnet_socket_connection( - mut socket: tokio::net::TcpStream, - processing_data: Arc>, - ) { - let mut buf = [0u8; sphinx::PACKET_SIZE]; - - // In a loop, read data from the socket and write the data back. - loop { - match socket.read(&mut buf).await { - // socket closed - Ok(n) if n == 0 => { - trace!("Remote connection closed."); - return; - } - Ok(_) => { - let store_data = match MixPacketProcessor::process_sphinx_data_packet( - buf.as_ref(), - processing_data.as_ref(), - ) { - Ok(sd) => sd, - Err(e) => { - warn!("failed to process sphinx packet; err = {:?}", e); - return; - } - }; - let processing_data_lock = match processing_data.read() { - Ok(guard) => guard, - Err(e) => { - error!("processing data lock was poisoned! - {:?}", e); - std::process::exit(1) - } - }; - ClientStorage::store_processed_data( - store_data, - processing_data_lock.store_dir.as_path(), - ) - .unwrap_or_else(|e| { - error!("failed to store processed sphinx message; err = {:?}", e); - return; - }); - } - Err(e) => { - warn!("failed to read from socket; err = {:?}", e); - return; - } - }; - - // Write the some data back - if let Err(e) = socket.write_all(b"foomp").await { - warn!("failed to write reply to socket; err = {:?}", e); - return; - } - } + fn start_presence_notifier(&self) { + info!("Starting presence notifier..."); + let notifier_config = presence::NotifierConfig::new( + self.config.get_presence_directory_server(), + self.config.get_mix_announce_address(), + self.config.get_clients_announce_address(), + self.sphinx_keypair.public_key().to_base58_string(), + self.config.get_presence_sending_delay(), + ); + presence::Notifier::new(notifier_config, self.registered_clients_ledger.clone()) + .start(self.runtime.handle()); } - async fn send_response(mut socket: tokio::net::TcpStream, data: &[u8]) { - if let Err(e) = socket.write_all(data).await { - warn!("failed to write reply to socket; err = {:?}", e) - } - if let Err(e) = socket.shutdown(Shutdown::Write) { - warn!("failed to close write part of the socket; err = {:?}", e) - } - } - - // TODO: FIGURE OUT HOW TO SET READ_DEADLINES IN TOKIO - async fn process_client_socket_connection( - mut socket: tokio::net::TcpStream, - processing_data: Arc, - ) { - let mut buf = [0; 1024]; - - // TODO: restore the for loop once we go back to persistent tcp socket connection - let response = match socket.read(&mut buf).await { - // socket closed - Ok(n) if n == 0 => { - trace!("Remote connection closed."); - Err(()) - } - Ok(n) => { - match ClientRequestProcessor::process_client_request( - buf[..n].as_ref(), - processing_data, - ) - .await - { - Err(e) => { - warn!("failed to process client request; err = {:?}", e); - Err(()) - } - Ok(res) => Ok(res), - } - } - Err(e) => { - warn!("failed to read from socket; err = {:?}", e); - Err(()) - } - }; - - if let Err(e) = socket.shutdown(Shutdown::Read) { - warn!("failed to close read part of the socket; err = {:?}", e) - } - - match response { - Ok(res) => { - ServiceProvider::send_response(socket, &res).await; - } - _ => { - ServiceProvider::send_response(socket, b"bad foomp").await; - } - } - } - - async fn start_mixnet_listening( - address: SocketAddr, - secret_key: DummyMixIdentityPrivateKey, - store_dir: PathBuf, - ) -> Result<(), ProviderError> { - let mut listener = tokio::net::TcpListener::bind(address).await?; - let processing_data = MixProcessingData::new(secret_key, store_dir).add_arc_rwlock(); - - loop { - let (socket, _) = listener.accept().await?; - // do note that the underlying data is NOT copied here; arc is incremented and lock is shared - // (if I understand it all correctly) - let thread_processing_data = processing_data.clone(); - tokio::spawn(async move { - ServiceProvider::process_mixnet_socket_connection(socket, thread_processing_data) - .await - }); - } - } - - async fn start_client_listening( - address: SocketAddr, - store_dir: PathBuf, - client_ledger: Arc>, - secret_key: DummyMixIdentityPrivateKey, - ) -> Result<(), ProviderError> { - let mut listener = tokio::net::TcpListener::bind(address).await?; - let processing_data = - ClientProcessingData::new(store_dir, client_ledger, secret_key).add_arc(); - - loop { - let (socket, _) = listener.accept().await?; - // do note that the underlying data is NOT copied here; arc is incremented and lock is shared - // (if I understand it all correctly) - let thread_processing_data = processing_data.clone(); - tokio::spawn(async move { - ServiceProvider::process_client_socket_connection(socket, thread_processing_data) - .await - }); - } - } - - // Note: this now consumes the provider - pub fn start(self) -> Result<(), Box> { - // Create the runtime, probably later move it to Provider struct itself? - // TODO: figure out the difference between Runtime and Handle - let mut rt = Runtime::new()?; - // let mut h = rt.handle(); - - let initial_client_ledger = self.registered_clients_ledger; - let thread_shareable_ledger = initial_client_ledger.add_arc_futures_mutex(); - - let presence_notifier = presence::Notifier::new( - self.directory_server, - self.client_network_address.clone(), - self.mix_network_address.clone(), - self.public_key, - thread_shareable_ledger.clone(), + fn start_mix_socket_listener(&self, client_storage: ClientStorage) { + info!("Starting mix socket listener..."); + let packet_processor = mix_handling::packet_processing::PacketProcessor::new( + self.sphinx_keypair.private_key().clone(), + client_storage, ); - let presence_future = rt.spawn(presence_notifier.run()); - let mix_future = rt.spawn(ServiceProvider::start_mixnet_listening( - self.mix_network_address, - self.secret_key.clone(), - self.store_dir.clone(), - )); - let client_future = rt.spawn(ServiceProvider::start_client_listening( - self.client_network_address, - self.store_dir.clone(), - thread_shareable_ledger, - self.secret_key, - )); - // Spawn the root task - rt.block_on(async { - let future_results = - futures::future::join3(mix_future, client_future, presence_future).await; - assert!(future_results.0.is_ok() && future_results.1.is_ok()); - }); + mix_handling::listener::run_mix_socket_listener( + self.runtime.handle(), + self.config.get_mix_listening_address(), + packet_processor, + ); + } - // this line in theory should never be reached as the runtime should be permanently blocked on listeners - error!("The server went kaput..."); - Ok(()) + fn start_client_socket_listener(&self, client_storage: ClientStorage) { + info!("Starting client socket listener..."); + let packet_processor = client_handling::request_processing::RequestProcessor::new( + self.sphinx_keypair.private_key().clone(), + client_storage, + self.registered_clients_ledger.clone(), + ); + + client_handling::listener::run_client_socket_listener( + self.runtime.handle(), + self.config.get_clients_listening_address(), + packet_processor, + ); + } + + pub fn run(&mut self) { + // A possible future optimisation, depending on bottlenecks and resource usage: + // considering, presumably, there will be more mix packets received than client requests: + // create 2 separate runtimes - one with bigger threadpool dedicated solely for + // the mix handling and the other one for the rest of tasks + + let client_storage = ClientStorage::new( + self.config.get_message_retrieval_limit() as usize, + self.config.get_stored_messages_filename_length(), + self.config.get_clients_inboxes_dir(), + ); + + self.start_presence_notifier(); + self.start_mix_socket_listener(client_storage.clone()); + self.start_client_socket_listener(client_storage); + + if let Err(e) = self.runtime.block_on(tokio::signal::ctrl_c()) { + error!( + "There was an error while capturing SIGINT - {:?}. We will terminate regardless", + e + ); + } + + println!( + "Received SIGINT - the provider will terminate now (threads are not YET nicely stopped)" + ); } } diff --git a/sfw-provider/src/provider/presence.rs b/sfw-provider/src/provider/presence.rs index adfcebaddc..babdeacca6 100644 --- a/sfw-provider/src/provider/presence.rs +++ b/sfw-provider/src/provider/presence.rs @@ -1,54 +1,73 @@ +use crate::built_info; use crate::provider::ClientLedger; -use crypto::identity::DummyMixIdentityPublicKey; use directory_client::presence::providers::MixProviderPresence; use directory_client::requests::presence_providers_post::PresenceMixProviderPoster; use directory_client::DirectoryClient; -use futures::lock::Mutex as FMutex; use log::{debug, error}; -use std::net::SocketAddr; -use std::sync::Arc; use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +pub struct NotifierConfig { + directory_server: String, + mix_announce_host: String, + clients_announce_host: String, + pub_key_string: String, + sending_delay: Duration, +} + +impl NotifierConfig { + pub fn new( + directory_server: String, + mix_announce_host: String, + clients_announce_host: String, + pub_key_string: String, + sending_delay: Duration, + ) -> Self { + NotifierConfig { + directory_server, + mix_announce_host, + clients_announce_host, + pub_key_string, + sending_delay, + } + } +} pub struct Notifier { - pub net_client: directory_client::Client, - client_ledger: Arc>, + net_client: directory_client::Client, + client_ledger: ClientLedger, + sending_delay: Duration, client_listener: String, mixnet_listener: String, - pub_key: String, + pub_key_string: String, } impl Notifier { - pub fn new( - directory_server_address: String, - client_listener: SocketAddr, - mixnet_listener: SocketAddr, - pub_key: DummyMixIdentityPublicKey, - client_ledger: Arc>, - ) -> Notifier { - let directory_config = directory_client::Config { - base_url: directory_server_address, + pub fn new(config: NotifierConfig, client_ledger: ClientLedger) -> Notifier { + let directory_client_cfg = directory_client::Config { + base_url: config.directory_server, }; - let net_client = directory_client::Client::new(directory_config); + let net_client = directory_client::Client::new(directory_client_cfg); Notifier { - net_client, - client_listener: client_listener.to_string(), - mixnet_listener: mixnet_listener.to_string(), - pub_key: pub_key.to_base58_string(), client_ledger, + net_client, + client_listener: config.clients_announce_host, + mixnet_listener: config.mix_announce_host, + pub_key_string: config.pub_key_string, + sending_delay: config.sending_delay, } } async fn make_presence(&self) -> MixProviderPresence { - let unlocked_ledger = self.client_ledger.lock().await; - MixProviderPresence { client_listener: self.client_listener.clone(), mixnet_listener: self.mixnet_listener.clone(), - pub_key: self.pub_key.clone(), - registered_clients: unlocked_ledger.current_clients(), + pub_key: self.pub_key_string.clone(), + registered_clients: self.client_ledger.current_clients().await, last_seen: 0, - version: env!("CARGO_PKG_VERSION").to_string(), + version: built_info::PKG_VERSION.to_string(), } } @@ -59,12 +78,13 @@ impl Notifier { } } - pub async fn run(self) { - loop { - let presence = self.make_presence().await; - self.notify(presence); - let delay_duration = Duration::from_secs(5); - tokio::time::delay_for(delay_duration).await; - } + pub fn start(self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + loop { + let presence = self.make_presence().await; + self.notify(presence); + tokio::time::delay_for(self.sending_delay).await; + } + }) } } diff --git a/sfw-provider/src/provider/storage/mod.rs b/sfw-provider/src/provider/storage/mod.rs index 73c32a84a3..9df6ff440a 100644 --- a/sfw-provider/src/provider/storage/mod.rs +++ b/sfw-provider/src/provider/storage/mod.rs @@ -1,23 +1,36 @@ -use crate::provider::{MESSAGE_RETRIEVAL_LIMIT, STORED_MESSAGE_FILENAME_LENGTH}; +use futures::lock::Mutex; +use futures::StreamExt; use log::*; use rand::Rng; use sfw_provider_requests::DUMMY_MESSAGE_CONTENT; use sphinx::route::{DestinationAddressBytes, SURBIdentifier}; -use std::fs::File; use std::io; -use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::fs; +use tokio::fs::File; +use tokio::prelude::*; -pub enum StoreError { - ClientDoesntExistError, - FileIOFailure, +fn dummy_message() -> ClientFile { + ClientFile { + content: DUMMY_MESSAGE_CONTENT.to_vec(), + path: Default::default(), + } } -impl From for StoreError { - fn from(_: std::io::Error) -> Self { - use StoreError::*; +#[derive(Clone, Debug)] +pub struct ClientFile { + content: Vec, + path: PathBuf, +} - FileIOFailure +impl ClientFile { + fn new(content: Vec, path: PathBuf) -> Self { + ClientFile { content, path } + } + + pub(crate) fn into_tuple(self) -> (Vec, PathBuf) { + (self.content, self.path) } } @@ -42,27 +55,64 @@ impl StoreData { } } -// TODO: replace with database -pub struct ClientStorage(()); +// TODO: replace with proper database... +// Note: you should NEVER create more than a single instance of this using 'new()'. +// You should always use .clone() to create additional instances +#[derive(Clone, Debug)] +pub struct ClientStorage { + inner: Arc>, +} -// TODO: change it to some generic implementation to inject fs +// even though the data inside is extremely cheap to copy, we have to have a single mutex, +// so might as well store the data behind it +pub struct ClientStorageInner { + message_retrieval_limit: usize, + filename_length: u16, + main_store_path_dir: PathBuf, +} + +// TODO: change it to some generic implementation to inject fs (or even better - proper database) impl ClientStorage { - pub(crate) fn generate_random_file_name() -> String { + pub(crate) fn new(message_limit: usize, filename_len: u16, main_store_dir: PathBuf) -> Self { + ClientStorage { + inner: Arc::new(Mutex::new(ClientStorageInner { + message_retrieval_limit: message_limit, + filename_length: filename_len, + main_store_path_dir: main_store_dir, + })), + } + } + + // TODO: does this method really require locking? + // The worst that can happen is client sending 2 requests: to pull messages and register + // if register does not lock, then under specific timing pull messages will fail, + // but can simply be retried with no issues + pub(crate) async fn create_storage_dir( + &self, + client_address: DestinationAddressBytes, + ) -> io::Result<()> { + let inner_data = self.inner.lock().await; + + let client_dir_name = client_address.to_base58_string(); + let full_store_dir = inner_data.main_store_path_dir.join(client_dir_name); + fs::create_dir_all(full_store_dir).await + } + + pub(crate) fn generate_random_file_name(length: usize) -> String { rand::thread_rng() .sample_iter(&rand::distributions::Alphanumeric) - .take(STORED_MESSAGE_FILENAME_LENGTH) + .take(length) .collect::() } - fn dummy_message() -> Vec { - // TODO: should it be padded to constant length? - DUMMY_MESSAGE_CONTENT.to_vec() - } + pub(crate) async fn store_processed_data(&self, store_data: StoreData) -> io::Result<()> { + let inner_data = self.inner.lock().await; - pub fn store_processed_data(store_data: StoreData, store_dir: &Path) -> io::Result<()> { - let client_dir_name = bs58::encode(store_data.client_address).into_string(); - let full_store_dir = store_dir.join(client_dir_name); - let full_store_path = full_store_dir.join(ClientStorage::generate_random_file_name()); + let client_dir_name = store_data.client_address.to_base58_string(); + let full_store_dir = inner_data.main_store_path_dir.join(client_dir_name); + let full_store_path = full_store_dir.join(Self::generate_random_file_name( + inner_data.filename_length as usize, + )); debug!( "going to store: {:?} in file: {:?}", store_data.message, full_store_path @@ -70,43 +120,62 @@ impl ClientStorage { // TODO: what to do with surbIDs?? - // we can use normal io here, no need for tokio as it's all happening in one thread per connection - let mut file = File::create(full_store_path)?; - file.write_all(store_data.message.as_ref())?; - - Ok(()) + let mut file = File::create(full_store_path).await?; + file.write_all(store_data.message.as_ref()).await } - pub fn retrieve_client_files( + pub(crate) async fn retrieve_client_files( + &self, client_address: DestinationAddressBytes, - store_dir: &Path, - ) -> Result>, StoreError> { - let client_dir_name = bs58::encode(client_address).into_string(); - let full_store_dir = store_dir.join(client_dir_name); + ) -> io::Result> { + let inner_data = self.inner.lock().await; + + let client_dir_name = client_address.to_base58_string(); + let full_store_dir = inner_data.main_store_path_dir.join(client_dir_name); trace!("going to lookup: {:?}!", full_store_dir); if !full_store_dir.exists() { - return Err(StoreError::ClientDoesntExistError); + return Err(io::Error::new( + io::ErrorKind::NotFound, + "Target client does not exist", + )); } - let msgs: Vec<_> = std::fs::read_dir(full_store_dir)? - .filter_map(|entry| entry.ok()) - .filter(|entry| ClientStorage::is_valid_file(entry)) - .map(|entry| { - // Not yet sure how to exactly get rid of those unwraps - let content = std::fs::read(entry.path()).unwrap(); - ClientStorage::delete_file(entry.path()).unwrap(); - content - }) // TODO: THIS MAP IS UNSAFE (BOTH FOR READING AND DELETING)!! - in the case where there are multiple requests from the same client being processed in parallel - .chain(std::iter::repeat(ClientStorage::dummy_message())) - .take(MESSAGE_RETRIEVAL_LIMIT) - .collect(); + let mut msgs = Vec::new(); + let mut read_dir = fs::read_dir(full_store_dir).await?; + + while let Some(dir_entry) = read_dir.next().await { + if let Ok(dir_entry) = dir_entry { + if !Self::is_valid_file(&dir_entry).await { + continue; + } + // Do not delete the file itself here! + // Only do it after client has received it + let client_file = + ClientFile::new(fs::read(dir_entry.path()).await?, dir_entry.path()); + msgs.push(client_file) + } + if msgs.len() == inner_data.message_retrieval_limit { + break; + } + } + + let dummy_message = dummy_message(); + + // make sure we always return as many messages as we need + if msgs.len() != inner_data.message_retrieval_limit as usize { + msgs = msgs + .into_iter() + .chain(std::iter::repeat(dummy_message)) + .take(inner_data.message_retrieval_limit) + .collect(); + } Ok(msgs) } - fn is_valid_file(entry: &std::fs::DirEntry) -> bool { - let metadata = match entry.metadata() { + async fn is_valid_file(entry: &fs::DirEntry) -> bool { + let metadata = match entry.metadata().await { Ok(meta) => meta, Err(e) => { error!( @@ -129,11 +198,18 @@ impl ClientStorage { is_file } - // TODO: THIS NEEDS A LOCKING MECHANISM!!! (or a db layer on top - basically 'ClientStorage' on steroids) - // TODO 2: This should only be called AFTER we sent the reply. Because if client's connection failed after sending request - // the messages would be deleted but he wouldn't have received them - fn delete_file(path: PathBuf) -> io::Result<()> { - trace!("Here {:?} will be deleted!", path); - std::fs::remove_file(path) // another argument for db layer -> remove_file is NOT guaranteed to immediately get rid of the file + pub(crate) async fn delete_files(&self, file_paths: Vec) -> io::Result<()> { + let dummy_message = dummy_message(); + let _guard = self.inner.lock().await; + + for file_path in file_paths { + if file_path == dummy_message.path { + continue; + } + if let Err(e) = fs::remove_file(file_path).await { + error!("Failed to delete client message! - {:?}", e) + } + } + Ok(()) } } diff --git a/validator/Cargo.toml b/validator/Cargo.toml index c64de656cd..5398c6a719 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -1,26 +1,28 @@ [package] build = "build.rs" name = "nym-validator" -version = "0.4.1" +version = "0.5.0-rc.1" authors = ["Jedrzej Stuczynski "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +abci = "0.6.4" +byteorder = "1.3.2" clap = "2.33.0" +dirs = "2.0.2" # Read notes https://crates.io/crates/dotenv - tl;dr: don't use in production, set environmental variables properly. dotenv = "0.15.0" futures = "0.3.1" log = "0.4" pretty_env_logger = "0.3" serde = "1.0.104" -serde_derive = "1.0.104" tokio = { version = "0.2", features = ["full"] } -toml = "0.5.5" ## internal crypto = {path = "../common/crypto"} +config = {path = "../common/config"} directory-client = { path = "../common/clients/directory-client" } healthcheck = {path = "../common/healthcheck" } topology = {path = "../common/topology"} @@ -29,3 +31,8 @@ topology = {path = "../common/topology"} built = "0.3.2" [dev-dependencies] +tempfile = "3.1.0" + +[features] +qa = [] +local = [] \ No newline at end of file diff --git a/validator/README.md b/validator/README.md new file mode 100644 index 0000000000..c8bbad5042 --- /dev/null +++ b/validator/README.md @@ -0,0 +1,21 @@ +Nym Validator +============= + +The Nym Validator has several jobs: + +* use Tendermint (v0.33.0) to maintain a total global ordering of incoming transactions +* track quality of service for mixnet nodes (mixmining) +* generate Coconut credentials and ensure they're not double spent +* maintain a decentralized directory of all Nym nodes that have staked into the system + +Some of these functions may be moved away to their own node types in the future, for example to increase scalability or performance. At the moment, we'd like to keep deployments simple, so they're all in the validator node. + +Running the validator on your local machine +------------------------------------------- + +1. Download and install [Tendermint 0.32.7](https://github.com/tendermint/tendermint/releases/tag/v0.32.7) +2. `tendermint init` sets up Tendermint for use +3. `tendermint node` runs Tendermint. You'll get errors until you run the Nym validator, this is normal :). +4. `cp sample-configs/validator-config.toml.sample sample-configs/validator-config.toml` +5. `cargo run -- run --config ../sample-configs/validator-config.toml` builds the Nym Validator and runs it + diff --git a/validator/src/built_info.rs b/validator/src/built_info.rs new file mode 100644 index 0000000000..d11fb6389f --- /dev/null +++ b/validator/src/built_info.rs @@ -0,0 +1,2 @@ +// The file has been placed there by the build script. +include!(concat!(env!("OUT_DIR"), "/built.rs")); diff --git a/validator/src/commands/init.rs b/validator/src/commands/init.rs new file mode 100644 index 0000000000..c741dca5ee --- /dev/null +++ b/validator/src/commands/init.rs @@ -0,0 +1,38 @@ +use crate::commands::override_config; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("init") + .about("Initialise the validator") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-validator we want to create config for.") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the validator is sending presence to and uses for mix mining") + .takes_value(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + println!("Initialising validator {}...", id); + + let mut config = crate::config::Config::new(id); + + config = override_config(config, matches); + + let config_save_location = config.get_config_file_save_location(); + config + .save_to_file(None) + .expect("Failed to save the config file"); + println!("Saved configuration file to {:?}", config_save_location); + + println!("Validator configuration completed.\n\n\n") +} diff --git a/validator/src/commands/mod.rs b/validator/src/commands/mod.rs new file mode 100644 index 0000000000..60ec03984d --- /dev/null +++ b/validator/src/commands/mod.rs @@ -0,0 +1,13 @@ +use crate::config::Config; +use clap::ArgMatches; + +pub mod init; +pub mod run; + +pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { + if let Some(directory) = matches.value_of("directory") { + config = config.with_custom_directory(directory); + } + + config +} diff --git a/validator/src/commands/run.rs b/validator/src/commands/run.rs new file mode 100644 index 0000000000..8f06e0e2d9 --- /dev/null +++ b/validator/src/commands/run.rs @@ -0,0 +1,45 @@ +use crate::commands::override_config; +use crate::config::Config; +use crate::validator::Validator; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; + +pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { + App::new("run") + .about("Starts the validator") + .arg( + Arg::with_name("id") + .long("id") + .help("Id of the nym-validator we want to run") + .takes_value(true) + .required(true), + ) + // the rest of arguments are optional, they are used to override settings in config file + .arg( + Arg::with_name("config") + .long("config") + .help("Custom path to the nym-validator configuration file") + .takes_value(true), + ) + .arg( + Arg::with_name("directory") + .long("directory") + .help("Address of the directory server the validator is sending presence to and uses for mix mining") + .takes_value(true), + ) +} + +pub fn execute(matches: &ArgMatches) { + let id = matches.value_of("id").unwrap(); + + println!("Starting validator {}...", id); + + let mut config = + Config::load_from_file(matches.value_of("config").map(|path| path.into()), Some(id)) + .expect("Failed to load config file"); + + config = override_config(config, matches); + + let validator = Validator::new(config); + validator.start() +} diff --git a/validator/src/config/mod.rs b/validator/src/config/mod.rs new file mode 100644 index 0000000000..e23c6f86c0 --- /dev/null +++ b/validator/src/config/mod.rs @@ -0,0 +1,246 @@ +use crate::config::template::config_template; +use config::NymConfig; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time; + +mod template; + +// where applicable, the below are defined in milliseconds + +// 'MIXMINING' +const DEFAULT_MIX_MINING_DELAY: u64 = 10_000; +const DEFAULT_MIX_MINING_RESOLUTION_TIMEOUT: u64 = 5_000; + +const DEFAULT_NUMBER_OF_MIX_MINING_TEST_PACKETS: u64 = 2; + +// 'DEBUG' +const DEFAULT_PRESENCE_SENDING_DELAY: u64 = 3000; + +#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + validator: Validator, + + mix_mining: MixMining, + + tendermint: Tendermint, + + #[serde(default)] + logging: Logging, + #[serde(default)] + debug: Debug, +} + +impl NymConfig for Config { + fn template() -> &'static str { + config_template() + } + + fn config_file_name() -> String { + "config.toml".to_string() + } + + fn default_root_directory() -> PathBuf { + dirs::home_dir() + .expect("Failed to evaluate $HOME value") + .join(".nym") + .join("validators") + } + + fn root_directory(&self) -> PathBuf { + self.validator.nym_root_directory.clone() + } + + fn config_directory(&self) -> PathBuf { + self.validator + .nym_root_directory + .join(&self.validator.id) + .join("config") + } + + fn data_directory(&self) -> PathBuf { + self.validator + .nym_root_directory + .join(&self.validator.id) + .join("data") + } +} + +impl Config { + fn default_directory_server() -> String { + #[cfg(feature = "qa")] + return "https://qa-directory.nymtech.net".to_string(); + #[cfg(feature = "local")] + return "http://localhost:8080".to_string(); + + "https://directory.nymtech.net".to_string() + } + + pub fn new>(id: S) -> Self { + Config::default().with_id(id) + } + + // builder methods + pub fn with_id>(mut self, id: S) -> Self { + let id = id.into(); + + // calls to any defaults requiring id (see: client, mixnode, provider): + + self.validator.id = id; + self + } + + pub fn with_custom_directory>(mut self, directory_server: S) -> Self { + let directory_server_string = directory_server.into(); + self.debug.presence_directory_server = directory_server_string.clone(); + self.mix_mining.directory_server = directory_server_string; + self + } + + // getters + pub fn get_config_file_save_location(&self) -> PathBuf { + self.config_directory().join(Self::config_file_name()) + } + + pub fn get_mix_mining_directory_server(&self) -> String { + self.mix_mining.directory_server.clone() + } + + // dead_code until validator actually sends the presence data + #[allow(dead_code)] + pub fn get_presence_directory_server(&self) -> String { + self.debug.presence_directory_server.clone() + } + + #[allow(dead_code)] + pub fn get_presence_sending_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.presence_sending_delay) + } + + pub fn get_mix_mining_run_delay(&self) -> time::Duration { + time::Duration::from_millis(self.mix_mining.run_delay) + } + + pub fn get_mix_mining_resolution_timeout(&self) -> time::Duration { + time::Duration::from_millis(self.mix_mining.resolution_timeout) + } + + pub fn get_mix_mining_number_of_test_packets(&self) -> u64 { + self.mix_mining.number_of_test_packets + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Validator { + /// ID specifies the human readable ID of this particular validator. + id: String, + + /// nym_home_directory specifies absolute path to the home nym MixNodes directory. + /// It is expected to use default value and hence .toml file should not redefine this field. + nym_root_directory: PathBuf, +} + +impl Validator {} + +impl Default for Validator { + fn default() -> Self { + Validator { + id: "".to_string(), + nym_root_directory: Config::default_root_directory(), + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixMining { + /// Directory server from which the validator will obtain initial topology. + directory_server: String, + + /// The uniform delay every which validator are running their mix-mining procedure. + /// The provided value is interpreted as milliseconds. + run_delay: u64, + + /// During the mix-mining process, test packets are sent through various network + /// paths. This timeout determines waiting period until it is decided that the packet + /// did not reach its destination. + /// The provided value is interpreted as milliseconds. + resolution_timeout: u64, + + /// How many packets should be sent through each path during the mix-mining procedure. + number_of_test_packets: u64, +} + +impl Default for MixMining { + fn default() -> Self { + MixMining { + directory_server: Config::default_directory_server(), + run_delay: DEFAULT_MIX_MINING_DELAY, + resolution_timeout: DEFAULT_MIX_MINING_RESOLUTION_TIMEOUT, + number_of_test_packets: DEFAULT_NUMBER_OF_MIX_MINING_TEST_PACKETS, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Tendermint {} + +impl Default for Tendermint { + fn default() -> Self { + Tendermint {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Logging {} + +impl Default for Logging { + fn default() -> Self { + Logging {} + } +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Directory server to which the server will be reporting their presence data. + presence_directory_server: String, + + /// Delay between each subsequent presence data being sent. + presence_sending_delay: u64, +} + +impl Debug {} + +impl Default for Debug { + fn default() -> Self { + Debug { + presence_directory_server: Config::default_directory_server(), + presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, + } + } +} + +#[cfg(test)] +mod validator_config { + use super::*; + + #[test] + fn after_saving_default_config_the_loaded_one_is_identical() { + // need to figure out how to do something similar but without touching the disk + // or the file system at all... + let temp_location = tempfile::tempdir().unwrap().path().join("config.toml"); + let default_config = Config::default().with_id("foomp".to_string()); + default_config + .save_to_file(Some(temp_location.clone())) + .unwrap(); + + let loaded_config = Config::load_from_file(Some(temp_location), None).unwrap(); + + assert_eq!(default_config, loaded_config); + } +} diff --git a/validator/src/config/template.rs b/validator/src/config/template.rs new file mode 100644 index 0000000000..3e8b0c1f82 --- /dev/null +++ b/validator/src/config/template.rs @@ -0,0 +1,70 @@ +pub(crate) fn config_template() -> &'static str { + // While using normal toml marshalling would have been way simpler with less overhead, + // I think it's useful to have comments attached to the saved config file to explain behaviour of + // particular fields. + // Note: any changes to the template must be reflected in the appropriate structs in mod.rs. + r#" +# This is a TOML config file. +# For more information, see https://github.com/toml-lang/toml + +##### main base mixnode config options ##### + +[validator] +# Human readable ID of this particular validator. +id = "{{ validator.id }}" + +##### advanced configuration options ##### + +# nym_home_directory specifies absolute path to the home nym validators directory. +# It is expected to use default value and hence .toml file should not redefine this field. +nym_root_directory = "{{ validator.nym_root_directory }}" + + +##### mix mining config options ##### + +[mix_mining] + +# Directory server from which the validator will obtain initial topology. +directory_server = "{{ mix_mining.directory_server }}" + +# The uniform delay every which validator are running their mix-mining procedure. +# The provided value is interpreted as milliseconds. +run_delay = {{ mix_mining.run_delay }} + +# During the mix-mining process, test packets are sent through various network +# paths. This timeout determines waiting period until it is decided that the packet +# did not reach its destination. +# The provided value is interpreted as milliseconds. +resolution_timeout = {{ mix_mining.resolution_timeout }} + +# How many packets should be sent through each path during the mix-mining procedure. +number_of_test_packets = {{ mix_mining.number_of_test_packets }} + + +##### tendermint config options ##### + +[tendermint] + + + +##### logging configuration options ##### + +[logging] + +# TODO + + +##### debug configuration options ##### +# The following options should not be modified unless you know EXACTLY what you are doing +# as if set incorrectly, they may impact your anonymity. + +[debug] + +# Directory server to which the server will be reporting their presence data. +presence_directory_server = "{{ debug.presence_directory_server }}" + +# Delay between each subsequent presence data being sent. +presence_sending_delay = {{ debug.presence_sending_delay }} + +"# +} diff --git a/validator/src/main.rs b/validator/src/main.rs index 39d0d375f3..fc21b153c3 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1,68 +1,39 @@ -use crate::validator::config::Config; -use crate::validator::Validator; -use clap::{App, Arg, ArgMatches, SubCommand}; -use log::{error, trace}; -use std::process; -use toml; +use clap::{App, ArgMatches}; +pub mod built_info; +mod commands; +mod config; +mod network; +mod services; mod validator; fn main() { dotenv::dotenv().ok(); pretty_env_logger::init(); + println!("{}", banner()); + let arg_matches = App::new("Nym Validator") .version(built_info::PKG_VERSION) .author("Nymtech") .about("Implementation of Nym Validator") - .subcommand( - SubCommand::with_name("run") - .about("Starts the validator") - .arg( - Arg::with_name("config") - .long("config") - .help("Location of the validator configuration file") - .takes_value(true) - .required(true), - ), - ) + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) .get_matches(); - if let Err(e) = execute(arg_matches) { - error!("{:?}", e); - process::exit(1); - } + execute(arg_matches); } -fn run(matches: &ArgMatches) { - let config = parse_config(matches); - trace!("read config: {:?}", config); - - let validator = Validator::new(config); - validator.start() -} - -fn parse_config(matches: &ArgMatches) -> Config { - let config_file_path = matches.value_of("config").unwrap(); - // since this is happening at the very startup, it's fine to panic if file doesn't exist - let config_content = std::fs::read_to_string(config_file_path).unwrap(); - toml::from_str(&config_content).unwrap() -} - -pub mod built_info { - // The file has been placed there by the build script. - include!(concat!(env!("OUT_DIR"), "/built.rs")); -} - -fn execute(matches: ArgMatches) -> Result<(), String> { +fn execute(matches: ArgMatches) { match matches.subcommand() { - ("run", Some(m)) => Ok(run(m)), - _ => Err(usage()), + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), } } -fn usage() -> String { - banner() + "usage: --help to see available options.\n\n" +fn usage() -> &'static str { + "usage: --help to see available options.\n\n" } fn banner() -> String { diff --git a/validator/src/network/ethereum.rs b/validator/src/network/ethereum.rs new file mode 100644 index 0000000000..9aa85267fe --- /dev/null +++ b/validator/src/network/ethereum.rs @@ -0,0 +1 @@ +// placeholder for Ethereum / ERC20 bridge integration diff --git a/validator/src/network/mod.rs b/validator/src/network/mod.rs new file mode 100644 index 0000000000..6ad3fa6cb5 --- /dev/null +++ b/validator/src/network/mod.rs @@ -0,0 +1,4 @@ +//! The `network` module provides interfaces to external systems via network +//! connectivity. +//! +pub mod tendermint; diff --git a/validator/src/network/tendermint.rs b/validator/src/network/tendermint.rs new file mode 100644 index 0000000000..4a581774e6 --- /dev/null +++ b/validator/src/network/tendermint.rs @@ -0,0 +1,67 @@ +use abci::*; +use byteorder::{BigEndian, ByteOrder}; + +// Convert incoming tx network data to the proper BigEndian size. txs.len() > 8 will return 0 +fn convert_tx(tx: &[u8]) -> u64 { + if tx.len() < 8 { + let pad = 8 - tx.len(); + let mut x = vec![0; pad]; + x.extend_from_slice(tx); + return BigEndian::read_u64(x.as_slice()); + } + BigEndian::read_u64(tx) +} + +pub struct Abci { + count: u64, +} + +impl Abci { + pub fn new() -> Abci { + Abci { count: 0 } + } + + pub async fn run(self) { + abci::run_local(self); + } +} + +impl abci::Application for Abci { + // Validate transactions. Rule: Transactions must be incremental: 1,2,3,4... + fn check_tx(&mut self, req: &RequestCheckTx) -> ResponseCheckTx { + // Get the Tx [u8] and convert to u64 + let c = convert_tx(req.get_tx()); + let mut resp = ResponseCheckTx::new(); + + // Validation logic + if c != self.count + 1 { + resp.set_code(1); + resp.set_log(String::from("Count must be incremental!")); + return resp; + } + + // Update state to keep state correct for next check_tx call + self.count = c; + resp + } + + fn deliver_tx(&mut self, req: &RequestDeliverTx) -> ResponseDeliverTx { + // Get the Tx [u8] + let c = convert_tx(req.get_tx()); + // Update state + self.count = c; + // Return default code 0 == bueno + ResponseDeliverTx::new() + } + + fn commit(&mut self, _req: &RequestCommit) -> ResponseCommit { + // Create the response + let mut resp = ResponseCommit::new(); + // Convert count to bits + let mut buf = [0; 8]; + BigEndian::write_u64(&mut buf, self.count); + // Set data so last state is included in the block + resp.set_data(buf.to_vec()); + resp + } +} diff --git a/validator/src/services/mixmining/health_check_runner.rs b/validator/src/services/mixmining/health_check_runner.rs new file mode 100644 index 0000000000..a8f4983958 --- /dev/null +++ b/validator/src/services/mixmining/health_check_runner.rs @@ -0,0 +1,46 @@ +use healthcheck::HealthChecker; +use log::*; +use std::time::Duration; +use topology::NymTopology; + +pub struct HealthCheckRunner { + directory_server: String, + health_checker: HealthChecker, + interval: Duration, +} + +impl HealthCheckRunner { + pub fn new( + directory_server: String, + interval: Duration, + health_checker: HealthChecker, + ) -> HealthCheckRunner { + HealthCheckRunner { + directory_server, + health_checker, + interval, + } + } + + pub async fn run(self) { + debug!("healthcheck will run every {:?}", self.interval); + loop { + let full_topology = + directory_client::presence::Topology::new(self.directory_server.clone()); + let version_filtered_topology = full_topology.filter_node_versions( + crate::built_info::PKG_VERSION, + crate::built_info::PKG_VERSION, + crate::built_info::PKG_VERSION, + ); + match self + .health_checker + .do_check(&version_filtered_topology) + .await + { + Ok(health) => info!("current network health: \n{}", health), + Err(err) => error!("failed to perform healthcheck - {:?}", err), + }; + tokio::time::delay_for(self.interval).await; + } + } +} diff --git a/validator/src/services/mixmining/mod.rs b/validator/src/services/mixmining/mod.rs new file mode 100644 index 0000000000..b80e115b6d --- /dev/null +++ b/validator/src/services/mixmining/mod.rs @@ -0,0 +1 @@ +pub mod health_check_runner; diff --git a/validator/src/services/mod.rs b/validator/src/services/mod.rs new file mode 100644 index 0000000000..ce0375046b --- /dev/null +++ b/validator/src/services/mod.rs @@ -0,0 +1 @@ +pub mod mixmining; diff --git a/validator/src/validator.rs b/validator/src/validator.rs new file mode 100644 index 0000000000..1d06347f94 --- /dev/null +++ b/validator/src/validator.rs @@ -0,0 +1,47 @@ +use crate::config::Config; +use crate::network::tendermint; +use crate::services::mixmining::health_check_runner; +use crypto::identity::MixIdentityKeyPair; +use healthcheck::HealthChecker; +use tokio::runtime::Runtime; + +// allow for a generic validator +pub struct Validator { + // when you re-introduce keys, check which ones you want: + // MixIdentityKeyPair (like 'nym-client' ) <- probably that one (after maybe renaming to just identity::KeyPair) + // encryption::KeyPair (like 'nym-mixnode' or 'sfw-provider') + health_check_runner: health_check_runner::HealthCheckRunner, + tendermint_abci: tendermint::Abci, +} + +// but for time being, since it's a dummy one, have it use dummy keys +impl Validator { + pub fn new(config: Config) -> Self { + let dummy_healthcheck_keypair = MixIdentityKeyPair::new(); + let hc = HealthChecker::new( + config.get_mix_mining_resolution_timeout(), + config.get_mix_mining_number_of_test_packets() as usize, + dummy_healthcheck_keypair, + ); + + let health_check_runner = health_check_runner::HealthCheckRunner::new( + config.get_mix_mining_directory_server(), + config.get_mix_mining_run_delay(), + hc, + ); + + Validator { + health_check_runner, + + // perhaps you might want to pass &config to the constructor + // there to get the config.tendermint (assuming you create appropriate fields + getters) + tendermint_abci: tendermint::Abci::new(), + } + } + + pub fn start(self) { + let mut rt = Runtime::new().unwrap(); + rt.spawn(self.health_check_runner.run()); + rt.block_on(self.tendermint_abci.run()); + } +} diff --git a/validator/src/validator/config/mod.rs b/validator/src/validator/config/mod.rs deleted file mode 100644 index 4ef70b9c7e..0000000000 --- a/validator/src/validator/config/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde_derive::Deserialize; - -#[derive(Deserialize, Debug)] -pub struct Config { - #[serde(rename(deserialize = "healthcheck"))] - pub health_check: healthcheck::config::HealthCheck, -} diff --git a/validator/src/validator/mod.rs b/validator/src/validator/mod.rs deleted file mode 100644 index 26f031fab5..0000000000 --- a/validator/src/validator/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::validator::config::Config; -use crypto::identity::{ - DummyMixIdentityKeyPair, DummyMixIdentityPrivateKey, DummyMixIdentityPublicKey, - MixnetIdentityKeyPair, MixnetIdentityPrivateKey, MixnetIdentityPublicKey, -}; -use directory_client::presence::Topology; -use healthcheck::HealthChecker; -use log::{debug, error, info}; -use std::time::Duration; -use tokio::runtime::Runtime; -use topology::NymTopology; - -pub mod config; - -// allow for a generic validator -pub struct Validator -where - IDPair: MixnetIdentityKeyPair, - Priv: MixnetIdentityPrivateKey, - Pub: MixnetIdentityPublicKey, -{ - config: Config, - #[allow(dead_code)] - identity_keypair: IDPair, - heath_check: HealthChecker, -} - -// but for time being, since it's a dummy one, have it use dummy keys -impl Validator { - pub fn new(config: Config) -> Self { - debug!("validator new"); - - let dummy_keypair = DummyMixIdentityKeyPair::new(); - - Validator { - identity_keypair: dummy_keypair.clone(), - heath_check: HealthChecker::new( - config.health_check.resolution_timeout, - config.health_check.num_test_packets, - dummy_keypair, - ), - config, - } - } - - async fn healthcheck_runner(&self) { - let healthcheck_interval = Duration::from_secs_f64(self.config.health_check.interval); - debug!("healthcheck will run every {:?}", healthcheck_interval); - - loop { - let full_topology = T::new(self.config.health_check.directory_server.clone()); - let version_filtered_topology = full_topology.filter_node_versions( - crate::built_info::PKG_VERSION, - crate::built_info::PKG_VERSION, - crate::built_info::PKG_VERSION, - ); - - match self.heath_check.do_check(&version_filtered_topology).await { - Ok(health) => info!("current network health: \n{}", health), - Err(err) => error!("failed to perform healthcheck - {:?}", err), - }; - - tokio::time::delay_for(healthcheck_interval).await; - } - } - - pub fn start(self) { - debug!("validator run"); - - let mut rt = Runtime::new().unwrap(); - - let health_check_future = self.healthcheck_runner::(); - rt.block_on(health_check_future); - } -}