diff --git a/.gitignore b/.gitignore index b30f994948..4675987263 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ scripts/deploy_qa.sh scripts/run_gate.sh scripts/run_mix.sh scripts/start_local_tmux_network.sh +/.floo +/.flooignore diff --git a/Cargo.lock b/Cargo.lock index 4d9f659988..a217020fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,7 +669,7 @@ checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -741,6 +741,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenv" version = "0.15.0" @@ -824,7 +830,7 @@ dependencies = [ "proc-macro-error", "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -845,7 +851,7 @@ checksum = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "synstructure", ] @@ -993,7 +999,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -2246,7 +2252,7 @@ dependencies = [ "pest_meta", "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -2316,7 +2322,7 @@ checksum = "385322a45f2ecf3410c68d2a549a4a2685e8051d0f278e39743ff4e451cb9b3f" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -2372,7 +2378,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "version_check 0.9.1", ] @@ -2384,7 +2390,7 @@ checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "syn-mid", "version_check 0.9.1", ] @@ -2893,7 +2899,7 @@ checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -2965,6 +2971,26 @@ dependencies = [ "libc", ] +[[package]] +name = "simple-socks5" +version = "0.1.0" +dependencies = [ + "bs58", + "clap", + "futures 0.3.4", + "futures-util", + "log 0.4.8", + "nymsphinx", + "pretty_env_logger", + "simple-socks5-requests", + "tokio 0.2.16", + "tokio-tungstenite", +] + +[[package]] +name = "simple-socks5-requests" +version = "0.1.0" + [[package]] name = "siphasher" version = "0.2.3" @@ -3031,6 +3057,28 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3757cb9d89161a2f24e1cf78efa0c1fcff485d18e3f55e0aa3480824ddaa0f3f" +[[package]] +name = "snafu" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b028158eb06caa8345bee10cccfb25fa632beccf0ef5308832b4fd4b78a7db48" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf50aaef500c248a590e2696e8bf8c7620ca2235b9bb90a70363d82dd1abec6a" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "socket2" version = "0.3.12" @@ -3043,6 +3091,42 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "socks5" +version = "0.8.0-dev" +dependencies = [ + "bs58", + "built", + "clap", + "config", + "crypto", + "curve25519-dalek", + "directory-client", + "dirs", + "dotenv", + "futures 0.3.4", + "gateway-client", + "gateway-requests", + "log 0.4.8", + "mixnet-client", + "nymsphinx", + "pem", + "pemstore", + "pretty_env_logger", + "rand 0.7.3", + "reqwest 0.9.24", + "serde", + "serde_json", + "simple-socks5-requests", + "sled 0.33.0", + "snafu", + "tempfile", + "tokio 0.2.16", + "tokio-tungstenite", + "topology", + "url 2.1.1", +] + [[package]] name = "sphinx" version = "0.1.0" @@ -3110,6 +3194,17 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + [[package]] name = "syn" version = "1.0.17" @@ -3129,7 +3224,7 @@ checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -3140,7 +3235,7 @@ checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "unicode-xid 0.2.0", ] @@ -3301,7 +3396,7 @@ checksum = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", ] [[package]] @@ -3699,7 +3794,7 @@ dependencies = [ "log 0.4.8", "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "wasm-bindgen-shared", ] @@ -3746,7 +3841,7 @@ checksum = "d68a5b36eef1be7868f668632863292e37739656a80fc4b9acec7b0bd35a4931" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3894,6 +3989,6 @@ checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" dependencies = [ "proc-macro2 1.0.10", "quote 1.0.3", - "syn", + "syn 1.0.17", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 207047a8b2..0b7b12a7e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ opt-level = "s" members = [ "clients/native", + "clients/socks5", "clients/webassembly", "common/client-libs/directory-client", "common/client-libs/directory-client/models", @@ -27,6 +28,7 @@ members = [ "common/topology", "gateway", "gateway/gateway-requests", + "service-providers/simple-socks5", "mixnode", "validator", ] diff --git a/clients/native/examples/go-examples/websocket/go.mod b/clients/native/examples/go-examples/websocket/go.mod new file mode 100644 index 0000000000..e43873ebd6 --- /dev/null +++ b/clients/native/examples/go-examples/websocket/go.mod @@ -0,0 +1,8 @@ +module github.com/nymtech/nym/clients/native/examples/go + +go 1.14 + +require ( + github.com/btcsuite/btcutil v1.0.2 // indirect + github.com/gorilla/websocket v1.4.2 // indirect +) diff --git a/clients/native/examples/go-examples/websocket/go.sum b/clients/native/examples/go-examples/websocket/go.sum new file mode 100644 index 0000000000..5f43e25d55 --- /dev/null +++ b/clients/native/examples/go-examples/websocket/go.sum @@ -0,0 +1,37 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 6c3f3b4586..5f53829dd4 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -207,19 +207,13 @@ impl NymClient { self.config.get_gateway_response_timeout(), ); - let shared_key = self.runtime.block_on(async { + self.runtime.block_on(async { gateway_client .authenticate_and_start() .await .expect("could not authenticate and start up the gateway connection") }); - // TODO: if we didn't have a shared_key initially, save it to config or something? - info!( - "Performed initial authentication. Auth token is {:?}", - shared_key.to_base58_string() - ); - gateway_client } @@ -413,5 +407,17 @@ impl NymClient { } info!("Client startup finished!"); + info!( + "Client identity public key is: {}", + self.as_mix_recipient().identity().to_base58_string() + ); + info!( + "Client encryption public key is: {}", + self.as_mix_recipient().encryption_key().to_base58_string() + ); + info!( + "Gateway identity public key is: {:?}", + self.config.get_gateway_id() + ); } } diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index 306a7b3c76..366c1844f2 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -123,7 +123,7 @@ impl TopologyAccessor { } // only used by the client at startup to get a slightly more reasonable error message - // (currently displays as unused because healthchecker is disabled due to required changes) + // (currently displays as unused because health checker is disabled due to required changes) pub(crate) async fn is_routable(&self) -> bool { match &self.inner.read().await.0 { None => false, diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml new file mode 100644 index 0000000000..773a765d61 --- /dev/null +++ b/clients/socks5/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "socks5" +version = "0.8.0-dev" +authors = ["Dave Hrycyszyn "] +edition = "2018" + +[lib] +name = "nym_socks5" +path = "src/lib.rs" + +[dependencies] +bs58 = "0.3.0" +clap = "2.33.0" +curve25519-dalek = "2.0.0" +dirs = "2.0.2" +dotenv = "0.15.0" +futures = "0.3.1" +log = "0.4" +pem = "0.7.0" +pretty_env_logger = "0.3" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} +reqwest = "0.9.22" +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.44" +sled = "0.33" +snafu = "0.4.1" +tokio = { version = "0.2", features = ["full"] } +tokio-tungstenite = "0.10.1" +url = "2.1" + +## internal +config = {path = "../../common/config"} +crypto = {path = "../../common/crypto"} +directory-client = { path = "../../common/client-libs/directory-client" } +gateway-client = { path = "../../common/client-libs/gateway-client" } +gateway-requests = { path = "../../gateway/gateway-requests" } +mixnet-client = { path = "../../common/client-libs/mixnet-client" } +nymsphinx = { path = "../../common/nymsphinx" } +pemstore = {path = "../../common/pemstore"} +simple-socks5-requests = { path = "../../service-providers/simple-socks5/simple-socks5-requests"} +topology = {path = "../../common/topology" } + +[build-dependencies] +built = "0.3.2" + +[dev-dependencies] +tempfile = "3.1.0" \ No newline at end of file diff --git a/clients/socks5/build.rs b/clients/socks5/build.rs new file mode 100644 index 0000000000..56d753472c --- /dev/null +++ b/clients/socks5/build.rs @@ -0,0 +1,19 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use built; + +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); +} diff --git a/clients/socks5/src/built_info.rs b/clients/socks5/src/built_info.rs new file mode 100644 index 0000000000..38b46d59e6 --- /dev/null +++ b/clients/socks5/src/built_info.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// The file has been placed there by the build script. +include!(concat!(env!("OUT_DIR"), "/built.rs")); diff --git a/clients/socks5/src/client/cover_traffic_stream.rs b/clients/socks5/src/client/cover_traffic_stream.rs new file mode 100644 index 0000000000..53adb4a78a --- /dev/null +++ b/clients/socks5/src/client/cover_traffic_stream.rs @@ -0,0 +1,186 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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 nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::cover::generate_loop_cover_packet; +use nymsphinx::utils::sample_poisson_duration; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use std::pin::Pin; +use std::sync::Arc; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; +use tokio::time; + +pub(crate) struct LoopCoverTrafficStream +where + R: CryptoRng + Rng, +{ + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: time::Duration, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: time::Duration, + + /// Average delay between sending subsequent cover packets. + average_cover_message_sending_delay: time::Duration, + + /// Internal state, determined by `average_message_sending_delay`, + /// used to keep track of when a next packet should be sent out. + next_delay: time::Delay, + + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them + /// out to the network without any further delays. + mix_tx: MixMessageSender, + + /// Represents full address of this client. + our_full_destination: Recipient, + + /// Instance of a cryptographically secure random number generator. + rng: R, + + /// Accessor to the common instance of network topology. + topology_access: TopologyAccessor, +} + +impl Stream for LoopCoverTrafficStream +where + R: CryptoRng + Rng + Unpin, +{ + // 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; + }; + + // 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 avg_delay = self.average_cover_message_sending_delay; + let now = self.next_delay.deadline(); + let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_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(())) + } +} + +// obviously when we finally make shared rng that is on 'higher' level, this should become +// generic `R` +impl LoopCoverTrafficStream { + pub(crate) fn new( + ack_key: Arc, + average_ack_delay: time::Duration, + average_packet_delay: time::Duration, + average_cover_message_sending_delay: time::Duration, + mix_tx: MixMessageSender, + our_full_destination: Recipient, + topology_access: TopologyAccessor, + ) -> Self { + let rng = OsRng; + + LoopCoverTrafficStream { + ack_key, + average_ack_delay, + average_packet_delay, + average_cover_message_sending_delay, + next_delay: time::delay_for(Default::default()), + mix_tx, + our_full_destination, + rng, + topology_access, + } + } + + async fn on_new_message(&mut self) { + trace!("next cover message!"); + + // TODO for way down the line: in very rare cases (during topology update) we might have + // to wait a really tiny bit before actually obtaining the permit hence messing with our + // poisson delay, but is it really a problem? + let topology_permit = self.topology_access.get_read_permit().await; + // the ack is sent back to ourselves (and then ignored) + let topology_ref_option = topology_permit.try_get_valid_topology_ref( + &self.our_full_destination, + Some(&self.our_full_destination), + ); + if topology_ref_option.is_none() { + warn!("No valid topology detected - won't send any loop cover message this time"); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let cover_message = generate_loop_cover_packet( + &mut self.rng, + topology_ref, + &*self.ack_key, + &self.our_full_destination, + self.average_ack_delay, + self.average_packet_delay, + ) + .expect("Somehow failed to generate a loop cover message with a valid topology"); + + // 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(cover_message.0, cover_message.1)) + .unwrap(); + + // TODO: I'm not entirely sure whether this is really required, because I'm not 100% + // sure how `yield_now()` works - whether it just notifies the scheduler or whether it + // properly blocks. So to play it on the safe side, just explicitly drop the read permit + drop(topology_permit); + + // JS: due to identical logical structure to OutQueueControl::on_message(), this is also + // presumably required to prevent bugs in the future. Exact reason is still unknown to me. + tokio::task::yield_now().await; + } + + async fn run(&mut self) { + // we should set initial delay only when we actually start the stream + self.next_delay = time::delay_for(sample_poisson_duration( + &mut self.rng, + 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/clients/socks5/src/client/inbound_messages.rs b/clients/socks5/src/client/inbound_messages.rs new file mode 100644 index 0000000000..ab4abe4445 --- /dev/null +++ b/clients/socks5/src/client/inbound_messages.rs @@ -0,0 +1,33 @@ +use futures::channel::mpsc; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::ReplySURB; + +pub(crate) type InputMessageSender = mpsc::UnboundedSender; +pub(crate) type InputMessageReceiver = mpsc::UnboundedReceiver; + +#[derive(Debug)] +pub(crate) enum InputMessage { + Fresh { + recipient: Recipient, + data: Vec, + with_reply_surb: bool, + }, + Reply { + reply_surb: ReplySURB, + data: Vec, + }, +} + +impl InputMessage { + pub(crate) fn new_fresh(recipient: Recipient, data: Vec, with_reply_surb: bool) -> Self { + InputMessage::Fresh { + recipient, + data, + with_reply_surb, + } + } + + pub(crate) fn new_reply(reply_surb: ReplySURB, data: Vec) -> Self { + InputMessage::Reply { reply_surb, data } + } +} diff --git a/clients/socks5/src/client/key_manager.rs b/clients/socks5/src/client/key_manager.rs new file mode 100644 index 0000000000..89cc2829d6 --- /dev/null +++ b/clients/socks5/src/client/key_manager.rs @@ -0,0 +1,168 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::persistence::key_pathfinder::ClientKeyPathfinder; +use crypto::asymmetric::{encryption, identity}; +use gateway_requests::registration::handshake::SharedKeys; +use log::*; +use nymsphinx::acknowledgements::AckKey; +use rand::{CryptoRng, RngCore}; +use std::io; +use std::sync::Arc; + +// Note: to support key rotation in the future, all keys will require adding an extra smart pointer, +// most likely an AtomicCell, or if it doesn't work as I think it does, a Mutex. Although I think +// AtomicCell includes a Mutex implicitly if the underlying type does not work atomically. +// And I guess there will need to be some mechanism for a grace period when you can still +// use the old key after new one was issued. + +// Remember that Arc has Deref implementation for T +pub(crate) struct KeyManager { + /// identity key associated with the client instance. + identity_keypair: Arc, + + /// encryption key associated with the client instance. + encryption_keypair: Arc, + + /// shared key derived with the gateway during "registration handshake" + gateway_shared_key: Option>, + + /// key used for producing and processing acknowledgement packets. + ack_key: Arc, +} + +// The expected flow of a KeyManager "lifetime" is as follows: +/* + 1. ::new() is called during client-init + 2. after gateway registration is completed [in init] ::insert_gateway_shared_key() is called + 3. ::store_keys() is called before init finishes execution. + 4. ::load_keys() is called at the beginning of each subsequent client-run + 5. [not implemented] ::rotate_keys() is called periodically during client-run I presume? +*/ + +impl KeyManager { + // this is actually **NOT** dead code + // I have absolutely no idea why the compiler insists it's unused. The call happens during client::init::execute + #[allow(dead_code)] + /// Creates new instance of a [`KeyManager`] + pub(crate) fn new(rng: &mut R) -> Self + where + R: RngCore + CryptoRng, + { + KeyManager { + identity_keypair: Arc::new(identity::KeyPair::new_with_rng(rng)), + encryption_keypair: Arc::new(encryption::KeyPair::new_with_rng(rng)), + gateway_shared_key: None, + ack_key: Arc::new(AckKey::new(rng)), + } + } + + // this is actually **NOT** dead code + // I have absolutely no idea why the compiler insists it's unused. The call happens during client::init::execute + #[allow(dead_code)] + /// After shared key with the gateway is derived, puts its ownership to this instance of a [`KeyManager`]. + pub(crate) fn insert_gateway_shared_key(&mut self, gateway_shared_key: SharedKeys) { + self.gateway_shared_key = Some(Arc::new(gateway_shared_key)) + } + + /// Loads previously stored keys from the disk. + pub(crate) fn load_keys(client_pathfinder: &ClientKeyPathfinder) -> io::Result { + let identity_keypair: identity::KeyPair = + pemstore::load_keypair(&pemstore::KeyPairPath::new( + client_pathfinder.private_identity_key().to_owned(), + client_pathfinder.public_identity_key().to_owned(), + ))?; + let encryption_keypair: encryption::KeyPair = + pemstore::load_keypair(&pemstore::KeyPairPath::new( + client_pathfinder.private_encryption_key().to_owned(), + client_pathfinder.public_encryption_key().to_owned(), + ))?; + + let gateway_shared_key: SharedKeys = + pemstore::load_key(&client_pathfinder.gateway_shared_key().to_owned())?; + + let ack_key: AckKey = pemstore::load_key(&client_pathfinder.ack_key().to_owned())?; + + // TODO: ack key is never stored so it is generated now. But perhaps it should be stored + // after all for consistency sake? + Ok(KeyManager { + identity_keypair: Arc::new(identity_keypair), + encryption_keypair: Arc::new(encryption_keypair), + gateway_shared_key: Some(Arc::new(gateway_shared_key)), + ack_key: Arc::new(ack_key), + }) + } + + // this is actually **NOT** dead code + // I have absolutely no idea why the compiler insists it's unused. The call happens during client::init::execute + #[allow(dead_code)] + /// Stores all available keys on the disk. + // While perhaps there is no much point in storing the `AckKey` on the disk, + // it is done so for the consistency sake so that you wouldn't require an rng instance + // during `load_keys` to generate the said key. + pub(crate) fn store_keys(&self, client_pathfinder: &ClientKeyPathfinder) -> io::Result<()> { + pemstore::store_keypair( + self.identity_keypair.as_ref(), + &pemstore::KeyPairPath::new( + client_pathfinder.private_identity_key().to_owned(), + client_pathfinder.public_identity_key().to_owned(), + ), + )?; + pemstore::store_keypair( + self.encryption_keypair.as_ref(), + &pemstore::KeyPairPath::new( + client_pathfinder.private_encryption_key().to_owned(), + client_pathfinder.public_encryption_key().to_owned(), + ), + )?; + + pemstore::store_key(self.ack_key.as_ref(), &client_pathfinder.ack_key())?; + + match self.gateway_shared_key.as_ref() { + None => warn!("No gateway shared key available to store!"), + Some(gate_key) => { + pemstore::store_key(gate_key.as_ref(), &client_pathfinder.gateway_shared_key())? + } + } + + Ok(()) + } + + /// Gets an atomically reference counted pointer to [`identity::KeyPair`]. + pub(crate) fn identity_keypair(&self) -> Arc { + Arc::clone(&self.identity_keypair) + } + + /// Gets an atomically reference counted pointer to [`encryption::KeyPair`]. + pub(crate) fn encryption_keypair(&self) -> Arc { + Arc::clone(&self.encryption_keypair) + } + + /// Gets an atomically reference counted pointer to [`SharedKey`]. + // since this function is not fully public, it is not expected to be used externally and + // hence it's up to us to ensure it's called in correct context + pub(crate) fn gateway_shared_key(&self) -> Arc { + Arc::clone( + &self + .gateway_shared_key + .as_ref() + .expect("tried to unwrap empty gateway key!"), + ) + } + + /// Gets an atomically reference counted pointer to [`AckKey`]. + pub(crate) fn ack_key(&self) -> Arc { + Arc::clone(&self.ack_key) + } +} diff --git a/clients/socks5/src/client/mix_traffic.rs b/clients/socks5/src/client/mix_traffic.rs new file mode 100644 index 0000000000..cd5b78fa6c --- /dev/null +++ b/clients/socks5/src/client/mix_traffic.rs @@ -0,0 +1,91 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use futures::channel::mpsc; +use futures::StreamExt; +use gateway_client::GatewayClient; +use log::*; +use nymsphinx::{addressing::nodes::NymNodeRoutingAddress, SphinxPacket}; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +pub(crate) struct MixMessage(NymNodeRoutingAddress, SphinxPacket); +pub(crate) type MixMessageSender = mpsc::UnboundedSender; +pub(crate) type MixMessageReceiver = mpsc::UnboundedReceiver; + +impl MixMessage { + pub(crate) fn new(address: NymNodeRoutingAddress, packet: SphinxPacket) -> Self { + MixMessage(address, packet) + } +} + +const MAX_FAILURE_COUNT: usize = 100; + +pub(crate) struct MixTrafficController<'a> { + // TODO: most likely to be replaced by some higher level construct as + // later on gateway_client will need to be accessible by other entities + gateway_client: GatewayClient<'a, url::Url>, + mix_rx: MixMessageReceiver, + + // TODO: this is temporary work-around. + // in long run `gateway_client` will be moved away from `MixTrafficController` anyway. + consecutive_gateway_failure_count: usize, +} + +impl<'a> MixTrafficController<'static> { + pub(crate) fn new( + mix_rx: MixMessageReceiver, + gateway_client: GatewayClient<'a, url::Url>, + ) -> MixTrafficController<'a> { + MixTrafficController { + gateway_client, + mix_rx, + consecutive_gateway_failure_count: 0, + } + } + + async fn on_message(&mut self, mix_message: MixMessage) { + debug!("Got a mix_message for {:?}", mix_message.0); + match self + .gateway_client + .send_sphinx_packet(mix_message.0, mix_message.1) + .await + { + Err(e) => { + error!("Failed to send sphinx packet to the gateway! - {:?}", e); + self.consecutive_gateway_failure_count += 1; + if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { + // todo: in the future this should initiate a 'graceful' shutdown + panic!("failed to send sphinx packet to the gateway {} times in a row - assuming the gateway is dead. Can't do anything about it yet :(", MAX_FAILURE_COUNT) + } + } + Ok(_) => { + trace!("We *might* have managed to forward sphinx packet to the gateway!"); + self.consecutive_gateway_failure_count = 0; + } + } + } + + 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/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs new file mode 100644 index 0000000000..0693c569e2 --- /dev/null +++ b/clients/socks5/src/client/mod.rs @@ -0,0 +1,443 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::cover_traffic_stream::LoopCoverTrafficStream; +use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; +use crate::client::key_manager::KeyManager; +use crate::client::mix_traffic::{MixMessageReceiver, MixMessageSender, MixTrafficController}; +use crate::client::real_messages_control::RealMessagesController; +use crate::client::received_buffer::{ + ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, +}; +use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::topology_control::{ + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, +}; +use crate::config::persistence::key_pathfinder::ClientKeyPathfinder; +use crate::config::{Config, SocketType}; +use crate::socks::{ + authentication::{AuthenticationMethods, Authenticator, User}, + server::SphinxSocksServer, +}; +use crypto::asymmetric::identity; +use futures::channel::mpsc; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; +use log::*; +use nymsphinx::addressing::clients::{ClientEncryptionKey, ClientIdentity, Recipient}; +use nymsphinx::addressing::nodes::NodeIdentity; +use nymsphinx::anonymous_replies::ReplySURB; +use nymsphinx::receiver::ReconstructedMessage; +use received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; +use tokio::runtime::Runtime; + +mod cover_traffic_stream; +pub(crate) mod inbound_messages; +pub(crate) mod key_manager; +mod mix_traffic; +pub(crate) mod real_messages_control; +pub(crate) mod received_buffer; +mod reply_key_storage; +pub(crate) mod topology_control; + +pub struct NymClient { + /// Client configuration options, including, among other things, packet sending rates, + /// key filepaths, etc. + config: Config, + + /// Tokio runtime used for futures execution. + // TODO: JS: Personally I think I prefer the implicit way of using it that we've done with the + // gateway. + runtime: Runtime, + + /// KeyManager object containing smart pointers to all relevant keys used by the client. + key_manager: KeyManager, + + /// Channel used for transforming 'raw' messages into sphinx packets and sending them + /// through the mix network. + /// It is only available if the client started with the websocket listener disabled. + input_tx: Option, + + /// Channel used for obtaining reconstructed messages received from the mix network. + /// It is only available if the client started with the websocket listener disabled. + receive_tx: Option, +} + +impl NymClient { + pub fn new(config: Config) -> Self { + let pathfinder = ClientKeyPathfinder::new_from_config(&config); + let key_manager = KeyManager::load_keys(&pathfinder).expect("failed to load stored keys"); + + NymClient { + runtime: Runtime::new().unwrap(), + config, + key_manager, + input_tx: None, + receive_tx: None, + } + } + + pub fn as_mix_recipient(&self) -> Recipient { + Recipient::new( + *self.key_manager.identity_keypair().public_key(), + self.key_manager.encryption_keypair().public_key().clone(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(self.config.get_gateway_id()).unwrap(), + ) + } + + // 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( + self.key_manager.ack_key(), + self.config.get_average_ack_delay(), + self.config.get_average_packet_delay(), + self.config.get_loop_cover_traffic_average_delay(), + mix_tx, + self.as_mix_recipient(), + topology_accessor, + ) + }) + .start(self.runtime.handle()); + } + + fn start_real_traffic_controller( + &self, + topology_accessor: TopologyAccessor, + reply_key_storage: ReplyKeyStorage, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: MixMessageSender, + ) { + let controller_config = real_messages_control::Config::new( + self.key_manager.ack_key(), + self.config.get_ack_wait_multiplier(), + self.config.get_ack_wait_addition(), + self.config.get_average_ack_delay(), + self.config.get_message_sending_average_delay(), + self.config.get_average_packet_delay(), + self.as_mix_recipient(), + ); + + info!("Starting real traffic stream..."); + // we need to explicitly enter runtime due to "next_delay: time::delay_for(Default::default())" + // set in the constructor [of OutQueueControl] which HAS TO be called within context of a tokio runtime + // When refactoring this restriction should definitely be removed. + let real_messages_controller = self.runtime.enter(|| { + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + reply_key_storage, + ) + }); + real_messages_controller.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, + mixnet_receiver: MixnetMessageReceiver, + reply_key_storage: ReplyKeyStorage, + ) { + info!("Starting received messages buffer controller..."); + ReceivedMessagesBufferController::new( + self.key_manager.encryption_keypair(), + query_receiver, + mixnet_receiver, + reply_key_storage, + ) + .start(self.runtime.handle()) + } + + fn start_gateway_client( + &mut self, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, + ) -> GatewayClient<'static, url::Url> { + let gateway_id = self.config.get_gateway_id(); + if gateway_id.is_empty() { + panic!("The identity of the gateway is unknown - did you run `nym-client` init?") + } + let gateway_address_str = self.config.get_gateway_listener(); + if gateway_address_str.is_empty() { + panic!("The address of the gateway is unknown - did you run `nym-client` init?") + } + + let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) + .expect("provided gateway id is invalid!"); + + // TODO: since we presumably now get something valid-ish from the `init`, can we just + // ditch url::Url and operate on `String`? + let gateway_address = + url::Url::parse(&gateway_address_str).expect("provided gateway address is invalid!"); + + let mut gateway_client = GatewayClient::new( + gateway_address, + self.key_manager.identity_keypair(), + gateway_identity, + Some(self.key_manager.gateway_shared_key()), + mixnet_message_sender, + ack_sender, + self.config.get_gateway_response_timeout(), + ); + + self.runtime.block_on(async { + gateway_client + .authenticate_and_start() + .await + .expect("could not authenticate and start up the gateway connection") + }); + + gateway_client + } + + // 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 topology_refresher_config = TopologyRefresherConfig::new( + self.config.get_directory_server(), + self.config.get_topology_refresh_rate(), + ); + let mut topology_refresher = + TopologyRefresher::new_directory_client(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 from {}", + self.config.get_directory_server() + ); + self.runtime.block_on(topology_refresher.refresh()); + + // TODO: a slightly more graceful termination here + if !self + .runtime + .block_on(topology_refresher.is_topology_routable()) + { + panic!( + "The current network topology seem to be insufficient to route any packets through\ + - check if enough nodes and a gateway are online" + ); + } + + info!("Starting topology refresher..."); + topology_refresher.start(self.runtime.handle()); + } + + // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) + // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership + // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for + // requests? + fn start_mix_traffic_controller( + &mut self, + mix_rx: MixMessageReceiver, + gateway_client: GatewayClient<'static, url::Url>, + ) { + info!("Starting mix traffic controller..."); + MixTrafficController::new(mix_rx, gateway_client).start(self.runtime.handle()); + } + + fn start_socks5_listener( + &self, + buffer_requester: ReceivedBufferRequestSender, + msg_input: InputMessageSender, + ) { + info!("Starting socks5 listener..."); + let mut auth_methods: Vec = Vec::new(); + auth_methods.push(AuthenticationMethods::NoAuth as u8); + let allowed_users: Vec = Vec::new(); + + let authenticator = Authenticator::new(auth_methods, allowed_users); + let recipient = self.load_socks5_service_provider(); + let mut sphinx_socks = SphinxSocksServer::new(1080, "127.0.0.1", authenticator, recipient); + self.runtime + .spawn(async move { sphinx_socks.serve(msg_input, buffer_requester).await }); + } + // TODO: make this configurable in the client config file + // TODO: Talk to JS about where I can easily find these. + fn load_socks5_service_provider(&self) -> Recipient { + // load from file here, or better yet, inject it + let identity = "7qBXQor8nHXUXDAUM4aLvqDJ2MECFKn3AJ3brQsu5qz8"; + let encryption_key = "4uzn7m3vPEy5MhPHLqzFCJHk2BCnvbCsGdrYzng7jnor"; + let gateway_key = "e3vUAo6YhB7zq3GH8B4k3iiGT4H2USjdd5ZMZoUsHdF"; + + let client_identity = ClientIdentity::from_base58_string(identity).unwrap(); + let client_encryption_key = + ClientEncryptionKey::from_base58_string(encryption_key).unwrap(); + let gateway = NodeIdentity::from_base58_string(gateway_key).unwrap(); + Recipient::new(client_identity, client_encryption_key, gateway) + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's untested and there are absolutely no guarantees about it (but seems to have worked + /// well enough in local tests) + pub fn send_message(&mut self, recipient: Recipient, message: Vec, with_reply_surb: bool) { + let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb); + + self.input_tx + .as_ref() + .expect("start method was not called before!") + .unbounded_send(input_msg) + .unwrap(); + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's untested and there are absolutely no guarantees about it (but seems to have worked + /// well enough in local tests) + pub fn send_reply(&mut self, reply_surb: ReplySURB, message: Vec) { + let input_msg = InputMessage::new_reply(reply_surb, message); + + self.input_tx + .as_ref() + .expect("start method was not called before!") + .unbounded_send(input_msg) + .unwrap(); + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's untested and there are absolutely no guarantees about it (but seems to have worked + /// well enough in local tests) + /// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple + /// messages, you might have to call this function repeatedly. + // TODO: I guess this should really return something that `impl Stream` + pub async fn wait_for_messages(&mut self) -> Vec { + use futures::StreamExt; + + self.receive_tx + .as_mut() + .expect("start method was not called before!") + .next() + .await + .expect("buffer controller seems to have somehow died!") + } + + /// 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 client will terminate now (threads are not YET nicely stopped)" + ); + } + + pub fn start(&mut self) { + info!("Starting nym client"); + // channels for inter-component communication + // TODO: make the channels be internally created by the relevant components + // rather than creating them here, so say for example the buffer controller would create the request channels + // and would allow anyone to clone the sender channel + + // sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet + // they are used by cover traffic stream and real traffic stream + // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic + let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); + + // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway + // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer + let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); + + // used for announcing connection or disconnection of a channel for pushing re-assembled messages to + let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); + + // channels responsible for controlling real messages + let (input_sender, input_receiver) = mpsc::unbounded::(); + + // channels responsible for controlling ack messages + let (ack_sender, ack_receiver) = mpsc::unbounded(); + let shared_topology_accessor = TopologyAccessor::new(); + + let reply_key_storage = + ReplyKeyStorage::load(self.config.get_reply_encryption_key_store_path()) + .expect("Failed to load reply key storage!"); + + // 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_buffer_request_receiver, + mixnet_messages_receiver, + reply_key_storage.clone(), + ); + + let gateway_client = self.start_gateway_client(mixnet_messages_sender, ack_sender); + + self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); + self.start_real_traffic_controller( + shared_topology_accessor.clone(), + reply_key_storage, + ack_receiver, + input_receiver, + sphinx_message_sender.clone(), + ); + self.start_cover_traffic_stream(shared_topology_accessor, sphinx_message_sender); + + match self.config.get_socket_type() { + SocketType::WebSocket => { + self.start_socks5_listener(received_buffer_request_sender, input_sender) + } + SocketType::None => { + // if we did not start the socket, it means we're running (supposedly) in the native mode + // and hence we should announce 'ourselves' to the buffer + let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); + + // tell the buffer to start sending stuff to us + received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .expect("the buffer request failed!"); + + self.receive_tx = Some(reconstructed_receiver); + self.input_tx = Some(input_sender); + } + } + + info!("Client startup finished!"); + info!( + "Client identity public key is: {}", + self.as_mix_recipient().identity().to_base58_string() + ); + info!( + "Client encryption public key is: {}", + self.as_mix_recipient().encryption_key().to_base58_string() + ); + info!( + "Gateway identity public key is: {:?}", + self.config.get_gateway_id() + ); + } +} diff --git a/clients/socks5/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs b/clients/socks5/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs new file mode 100644 index 0000000000..9c6bb539ff --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs @@ -0,0 +1,92 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::PendingAcksMap; +use futures::StreamExt; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::{ + acknowledgements::{identifier::recover_identifier, AckKey}, + chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}, +}; +use std::sync::Arc; + +// responsible for cancelling retransmission timers and removed entries from the map +pub(super) struct AcknowledgementListener { + ack_key: Arc, + ack_receiver: AcknowledgementReceiver, + pending_acks: PendingAcksMap, +} + +impl AcknowledgementListener { + pub(super) fn new( + ack_key: Arc, + ack_receiver: AcknowledgementReceiver, + pending_acks: PendingAcksMap, + ) -> Self { + AcknowledgementListener { + ack_key, + ack_receiver, + pending_acks, + } + } + + async fn on_ack(&mut self, ack_content: Vec) { + debug!("Received an ack"); + let frag_id = match recover_identifier(&self.ack_key, &ack_content) { + None => { + warn!("Received invalid ACK!"); // should we do anything else about that? + return; + } + Some(frag_id_bytes) => match FragmentIdentifier::try_from_bytes(frag_id_bytes) { + Ok(frag_id) => frag_id, + Err(err) => { + warn!("Received invalid ACK! - {:?}", err); // should we do anything else about that? + return; + } + }, + }; + + if frag_id == COVER_FRAG_ID { + trace!("Received an ack for a cover message - no need to do anything"); + return; + } else if frag_id.is_reply() { + debug!("Received an ack for a reply message - no need to do anything!"); + // TODO: probably there will need to be some extra procedure here, something to notify + // user that his reply reached the recipient (since we got an ack) + info!("We received an ack for one of the replies we sent!"); + return; + } + + if let Some(pending_ack) = self.pending_acks.write().await.remove(&frag_id) { + // cancel the retransmission future + pending_ack.retransmission_cancel.notify(); + } else { + warn!("received ACK for packet we haven't stored! - {:?}", frag_id); + } + } + + pub(super) async fn run(&mut self) { + debug!("Started AcknowledgementListener"); + while let Some(acks) = self.ack_receiver.next().await { + // realistically we would only be getting one ack at the time, but if we managed to + // introduce batching in gateway client, this call should be improved to not re-acquire + // write permit on the map every loop iteration + for ack in acks { + self.on_ack(ack).await; + } + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/socks5/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs b/clients/socks5/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs new file mode 100644 index 0000000000..46bc94b991 --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs @@ -0,0 +1,205 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{PendingAcknowledgement, PendingAcksMap}; +use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::{ + inbound_messages::{InputMessage, InputMessageReceiver}, + real_messages_control::real_traffic_stream::{RealMessage, RealMessageSender}, + topology_control::TopologyAccessor, +}; +use futures::StreamExt; +use log::*; +use nymsphinx::anonymous_replies::ReplySURB; +use nymsphinx::preparer::MessagePreparer; +use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; + +// responsible for splitting received message and initial sending attempt +pub(super) struct InputMessageListener +where + R: CryptoRng + Rng, +{ + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_preparer: MessagePreparer, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: ReplyKeyStorage, +} + +impl InputMessageListener +where + R: CryptoRng + Rng, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_preparer: MessagePreparer, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: ReplyKeyStorage, + ) -> Self { + InputMessageListener { + ack_key, + ack_recipient, + input_receiver, + message_preparer, + pending_acks, + real_message_sender, + topology_access, + reply_key_storage, + } + } + + async fn handle_reply(&mut self, reply_surb: ReplySURB, data: Vec) -> Option { + let topology_permit = self.topology_access.get_read_permit().await; + let topology_ref_option = + topology_permit.try_get_valid_topology_ref(&self.ack_recipient, None); + if topology_ref_option.is_none() { + warn!("Could not process the message - the network topology is invalid"); + return None; + } + let topology = topology_ref_option.unwrap(); + + match self + .message_preparer + .prepare_reply_for_use(data, reply_surb, topology, &self.ack_key) + { + Ok((reply_id, sphinx_packet, first_hop)) => { + // TODO: later probably write pending ack here + // and deal with them.... + // ... somehow + + Some(RealMessage::new(first_hop, sphinx_packet, reply_id)) + } + Err(err) => { + // TODO: should we have some mechanism to indicate to the user that the `reply_surb` + // could be reused since technically it wasn't used up here? + warn!("failed to deal with received reply surb - {:?}", err); + None + } + } + } + + async fn handle_fresh_message( + &mut self, + recipient: Recipient, + content: Vec, + with_reply_surb: bool, + ) -> Vec { + let topology_permit = self.topology_access.get_read_permit().await; + let topology_ref_option = + topology_permit.try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient)); + if topology_ref_option.is_none() { + warn!("Could not process the message - the network topology is invalid"); + return Vec::new(); + } + let topology = topology_ref_option.unwrap(); + + // split the message, attach optional reply surb + let (split_message, reply_key) = self + .message_preparer + .prepare_and_split_message(content, with_reply_surb, topology) + .expect("somehow the topology was invalid after all!"); + + if let Some(reply_key) = reply_key { + self.reply_key_storage + .insert_encryption_key(reply_key) + .expect("Failed to insert surb reply key to the store!") + } + + // encrypt chunks, put them inside sphinx packets and generate acks + let mut pending_acks = Vec::with_capacity(split_message.len()); + let mut real_messages = Vec::with_capacity(split_message.len()); + for message_chunk in split_message { + // since the paths can be constructed, this CAN'T fail, if it does, there's a bug somewhere + let frag_id = message_chunk.fragment_identifier(); + // we need to clone it because we need to keep it in memory in case we had to retransmit + // it. And then we'd need to recreate entire ACK again. + let chunk_clone = message_chunk.clone(); + let prepared_fragment = self + .message_preparer + .prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient) + .unwrap(); + + real_messages.push(RealMessage::new( + prepared_fragment.first_hop_address, + prepared_fragment.sphinx_packet, + frag_id, + )); + + let pending_ack = PendingAcknowledgement::new( + message_chunk, + prepared_fragment.total_delay, + recipient.clone(), + ); + + pending_acks.push((frag_id, pending_ack)); + } + + // first insert pending_acks only then request fragments to be sent, otherwise you might get + // some very nasty (and time-consuming to figure out...) race condition. + let mut pending_acks_map_write_guard = self.pending_acks.write().await; + for (frag_id, pending_ack) in pending_acks.into_iter() { + if pending_acks_map_write_guard + .insert(frag_id, pending_ack) + .is_some() + { + panic!("Tried to insert duplicate pending ack") + } + } + + real_messages + } + + async fn on_input_message(&mut self, msg: InputMessage) { + let real_messages = match msg { + InputMessage::Fresh { + recipient, + data, + with_reply_surb, + } => { + self.handle_fresh_message(recipient, data, with_reply_surb) + .await + } + InputMessage::Reply { reply_surb, data } => { + if let Some(real_message) = self.handle_reply(reply_surb, data).await { + vec![real_message] + } else { + return; + } + } + }; + + for real_message in real_messages { + self.real_message_sender + .unbounded_send(real_message) + .unwrap(); + } + } + + pub(super) async fn run(&mut self) { + debug!("Started InputMessageListener"); + while let Some(input_msg) = self.input_receiver.next().await { + self.on_input_message(input_msg).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/socks5/src/client/real_messages_control/acknowlegement_control/mod.rs b/clients/socks5/src/client/real_messages_control/acknowlegement_control/mod.rs new file mode 100644 index 0000000000..6ed812f2bd --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/acknowlegement_control/mod.rs @@ -0,0 +1,230 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use self::{ + acknowledgement_listener::AcknowledgementListener, + input_message_listener::InputMessageListener, + retransmission_request_listener::RetransmissionRequestListener, + sent_notification_listener::SentNotificationListener, +}; +use super::real_traffic_stream::RealMessageSender; +use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::preparer::MessagePreparer; +use nymsphinx::{ + acknowledgements::AckKey, + addressing::clients::Recipient, + chunking::fragment::{Fragment, FragmentIdentifier}, + Delay, +}; +use rand::{CryptoRng, Rng}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::{ + sync::{Notify, RwLock}, + task::JoinHandle, +}; + +mod acknowledgement_listener; +mod input_message_listener; +mod retransmission_request_listener; +mod sent_notification_listener; + +type RetransmissionRequestSender = mpsc::UnboundedSender; +type RetransmissionRequestReceiver = mpsc::UnboundedReceiver; + +pub(super) type SentPacketNotificationSender = mpsc::UnboundedSender; +type SentPacketNotificationReceiver = mpsc::UnboundedReceiver; + +type PendingAcksMap = Arc>>; + +struct PendingAcknowledgement { + message_chunk: Fragment, + delay: Delay, + recipient: Recipient, + retransmission_cancel: Arc, +} + +impl PendingAcknowledgement { + fn new(message_chunk: Fragment, delay: Delay, recipient: Recipient) -> Self { + PendingAcknowledgement { + message_chunk, + delay, + retransmission_cancel: Arc::new(Notify::new()), + recipient, + } + } + + fn update_delay(&mut self, new_delay: Delay) { + self.delay = new_delay; + } +} + +pub(super) struct AcknowledgementControllerConnectors { + real_message_sender: RealMessageSender, + input_receiver: InputMessageReceiver, + sent_notifier: SentPacketNotificationReceiver, + ack_receiver: AcknowledgementReceiver, +} + +impl AcknowledgementControllerConnectors { + pub(super) fn new( + real_message_sender: RealMessageSender, + input_receiver: InputMessageReceiver, + sent_notifier: SentPacketNotificationReceiver, + ack_receiver: AcknowledgementReceiver, + ) -> Self { + AcknowledgementControllerConnectors { + real_message_sender, + input_receiver, + sent_notifier, + ack_receiver, + } + } +} + +pub(super) struct AcknowledgementController +where + R: CryptoRng + Rng, +{ + acknowledgement_listener: Option, + input_message_listener: Option>, + retransmission_request_listener: Option>, + sent_notification_listener: Option, +} + +impl AcknowledgementController +where + R: 'static + CryptoRng + Rng + Clone + Send, +{ + pub(super) fn new( + rng: R, + topology_access: TopologyAccessor, + ack_key: Arc, + ack_recipient: Recipient, + reply_key_storage: ReplyKeyStorage, + average_packet_delay: Duration, + average_ack_delay: Duration, + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + connectors: AcknowledgementControllerConnectors, + ) -> Self { + let pending_acks = Arc::new(RwLock::new(HashMap::new())); + let message_preparer = MessagePreparer::new( + rng, + ack_recipient.clone(), + average_packet_delay, + average_ack_delay, + ); + + let acknowledgement_listener = AcknowledgementListener::new( + Arc::clone(&ack_key), + connectors.ack_receiver, + Arc::clone(&pending_acks), + ); + + let input_message_listener = InputMessageListener::new( + Arc::clone(&ack_key), + ack_recipient.clone(), + connectors.input_receiver, + message_preparer.clone(), + Arc::clone(&pending_acks), + connectors.real_message_sender.clone(), + topology_access.clone(), + reply_key_storage, + ); + + let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); + + let retransmission_request_listener = RetransmissionRequestListener::new( + Arc::clone(&ack_key), + ack_recipient, + message_preparer, + Arc::clone(&pending_acks), + connectors.real_message_sender, + retransmission_rx, + topology_access, + ); + + let sent_notification_listener = SentNotificationListener::new( + ack_wait_multiplier, + ack_wait_addition, + connectors.sent_notifier, + pending_acks, + retransmission_tx, + ); + + AcknowledgementController { + acknowledgement_listener: Some(acknowledgement_listener), + input_message_listener: Some(input_message_listener), + retransmission_request_listener: Some(retransmission_request_listener), + sent_notification_listener: Some(sent_notification_listener), + } + } + + pub(super) async fn run(&mut self) { + let mut acknowledgement_listener = self.acknowledgement_listener.take().unwrap(); + let mut input_message_listener = self.input_message_listener.take().unwrap(); + let mut retransmission_request_listener = + self.retransmission_request_listener.take().unwrap(); + let mut sent_notification_listener = self.sent_notification_listener.take().unwrap(); + + // TODO: perhaps an extra 'DEBUG' task that would periodically check for stale entries in + // pending acks map? + // It would only be 'DEBUG' as I don't expect any stale entries to exist there to begin with, + // but when can bugs be expected to begin with? + + // the below are log messages are errors as at the current stage we do not expect any of + // the task to ever finish. This will of course change once we introduce + // graceful shutdowns. + let ack_listener_fut = tokio::spawn(async move { + acknowledgement_listener.run().await; + error!("The acknowledgement listener has finished execution!"); + acknowledgement_listener + }); + let input_listener_fut = tokio::spawn(async move { + input_message_listener.run().await; + error!("The input listener has finished execution!"); + input_message_listener + }); + let retransmission_req_fut = tokio::spawn(async move { + retransmission_request_listener.run().await; + error!("The retransmission request listener has finished execution!"); + retransmission_request_listener + }); + let sent_notification_fut = tokio::spawn(async move { + sent_notification_listener.run().await; + error!("The sent notification listener has finished execution!"); + sent_notification_listener + }); + + // technically we don't have to bring `AcknowledgementController` back to a valid state + // but we can do it, so why not? Perhaps it might be useful if we wanted to allow + // for restarts of certain modules without killing the entire process. + self.acknowledgement_listener = Some(ack_listener_fut.await.unwrap()); + self.input_message_listener = Some(input_listener_fut.await.unwrap()); + self.retransmission_request_listener = Some(retransmission_req_fut.await.unwrap()); + self.sent_notification_listener = Some(sent_notification_fut.await.unwrap()); + } + + #[allow(dead_code)] + pub(super) fn start(mut self) -> JoinHandle { + tokio::spawn(async move { + self.run().await; + self + }) + } +} diff --git a/clients/socks5/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs b/clients/socks5/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs new file mode 100644 index 0000000000..ed25032438 --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs @@ -0,0 +1,129 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{PendingAcksMap, RetransmissionRequestReceiver}; +use crate::client::{ + real_messages_control::real_traffic_stream::{RealMessage, RealMessageSender}, + topology_control::TopologyAccessor, +}; +use futures::StreamExt; +use log::*; +use nymsphinx::preparer::MessagePreparer; +use nymsphinx::{ + acknowledgements::AckKey, addressing::clients::Recipient, + chunking::fragment::FragmentIdentifier, +}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; + +// responsible for packet retransmission upon fired timer +pub(super) struct RetransmissionRequestListener +where + R: CryptoRng + Rng, +{ + ack_key: Arc, + ack_recipient: Recipient, + message_preparer: MessagePreparer, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, +} + +impl RetransmissionRequestListener +where + R: CryptoRng + Rng, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + message_preparer: MessagePreparer, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, + ) -> Self { + RetransmissionRequestListener { + ack_key, + ack_recipient, + message_preparer, + pending_acks, + real_message_sender, + request_receiver, + topology_access, + } + } + + async fn on_retransmission_request(&mut self, frag_id: FragmentIdentifier) { + let pending_acks_map_read_guard = self.pending_acks.read().await; + // if the unwrap failed here, we have some weird bug somewhere - honestly, I'm not sure + // if it's even possible for it to happen + let unreceived_ack_fragment = pending_acks_map_read_guard + .get(&frag_id) + .expect("wanted to retransmit ack'd fragment"); + + let packet_recipient = unreceived_ack_fragment.recipient.clone(); + let chunk_clone = unreceived_ack_fragment.message_chunk.clone(); + let frag_id = unreceived_ack_fragment.message_chunk.fragment_identifier(); + + // TODO: we need some proper benchmarking here to determine whether it could + // be more efficient to just get write lock and keep it while doing sphinx computation, + // but my gut feeling tells me we should re-acquire it. + drop(pending_acks_map_read_guard); + + let topology_permit = self.topology_access.get_read_permit().await; + let topology_ref_option = topology_permit + .try_get_valid_topology_ref(&self.ack_recipient, Some(&packet_recipient)); + if topology_ref_option.is_none() { + warn!("Could not retransmit the packet - the network topology is invalid"); + // TODO: perhaps put back into pending acks and reset the timer? + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let prepared_fragment = self + .message_preparer + .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, &packet_recipient) + .unwrap(); + + // minor optimization to not hold the permit while we no longer need it and might have to block + // waiting for the write lock on `pending_acks` + drop(topology_permit); + + self.pending_acks + .write() + .await + .get_mut(&frag_id) + .expect( + "on_retransmission_request: somehow we already received an ack for this packet?", + ) + .update_delay(prepared_fragment.total_delay); + + self.real_message_sender + .unbounded_send(RealMessage::new( + prepared_fragment.first_hop_address, + prepared_fragment.sphinx_packet, + frag_id, + )) + .unwrap(); + } + + pub(super) async fn run(&mut self) { + debug!("Started RetransmissionRequestListener"); + while let Some(frag_id) = self.request_receiver.next().await { + self.on_retransmission_request(frag_id).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/socks5/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs b/clients/socks5/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs new file mode 100644 index 0000000000..30efb7cc81 --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs @@ -0,0 +1,109 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::{PendingAcksMap, RetransmissionRequestSender, SentPacketNotificationReceiver}; +use futures::StreamExt; +use log::*; +use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; +use std::sync::Arc; +use std::time::Duration; + +// responsible for starting and controlling retransmission timers +// it is required because when we send our packet to the `real traffic stream` controlled +// with poisson timer, there's no guarantee the message will be sent immediately, so we might +// accidentally fire retransmission way quicker than we would have wanted. +pub(super) struct SentNotificationListener { + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + sent_notifier: SentPacketNotificationReceiver, + pending_acks: PendingAcksMap, + retransmission_sender: RetransmissionRequestSender, +} + +impl SentNotificationListener { + pub(super) fn new( + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + sent_notifier: SentPacketNotificationReceiver, + pending_acks: PendingAcksMap, + retransmission_sender: RetransmissionRequestSender, + ) -> Self { + SentNotificationListener { + ack_wait_multiplier, + ack_wait_addition, + sent_notifier, + pending_acks, + retransmission_sender, + } + } + + async fn on_sent_message(&mut self, frag_id: FragmentIdentifier) { + if frag_id == COVER_FRAG_ID { + trace!("sent off a cover message - no need to start retransmission timer!"); + return; + } else if frag_id.is_reply() { + debug!("sent off a reply message - no need to start retransmission timer!"); + // TODO: probably there will need to be some extra procedure here, like it would + // be nice to know that our reply actually reached the recipient (i.e. we got the ack) + return; + } + + let pending_acks_map_read_guard = self.pending_acks.read().await; + // if the unwrap failed here, we have some weird bug somewhere + // although when I think about it, it *theoretically* could happen under extremely heavy client + // load that `on_sent_message()` is not called (and we do not receive the read permit) + // until we already received and processed an ack for the packet + // but this seems extremely unrealistic, but perhaps we should guard against that? + let pending_ack_data = pending_acks_map_read_guard + .get(&frag_id) + .expect("on_sent_message: somehow we already received an ack for this packet?"); + + // if this assertion ever fails, we have some bug due to some unintended leak. + // the only reason I see it could happen if the `tokio::select` in the spawned + // task below somehow did not drop it + debug_assert_eq!( + Arc::strong_count(&pending_ack_data.retransmission_cancel), + 1 + ); + + // TODO: read more about Arc::downgrade. it could be useful here + let retransmission_cancel = Arc::clone(&pending_ack_data.retransmission_cancel); + + let retransmission_timeout = tokio::time::delay_for( + (pending_ack_data.delay.clone() * self.ack_wait_multiplier).to_duration() + + self.ack_wait_addition, + ); + + let retransmission_sender = self.retransmission_sender.clone(); + tokio::spawn(async move { + tokio::select! { + _ = retransmission_cancel.notified() => { + trace!("received ack for the fragment. Cancelling retransmission future"); + } + _ = retransmission_timeout => { + trace!("did not receive an ack - will retransmit the packet"); + retransmission_sender.unbounded_send(frag_id).unwrap(); + } + } + }); + } + + pub(super) async fn run(&mut self) { + debug!("Started SentNotificationListener"); + while let Some(frag_id) = self.sent_notifier.next().await { + self.on_sent_message(frag_id).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/socks5/src/client/real_messages_control/mod.rs b/clients/socks5/src/client/real_messages_control/mod.rs new file mode 100644 index 0000000000..3988475b41 --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/mod.rs @@ -0,0 +1,170 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// INPUT: InputMessage from user +// INPUT2: Acks from mix +// OUTPUT: MixMessage to mix traffic + +use self::{ + acknowlegement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, +}; +use crate::client::real_messages_control::acknowlegement_control::AcknowledgementControllerConnectors; +use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::{ + inbound_messages::InputMessageReceiver, mix_traffic::MixMessageSender, + topology_control::TopologyAccessor, +}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +mod acknowlegement_control; +mod real_traffic_stream; + +pub(crate) struct Config { + ack_key: Arc, + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + self_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + average_message_sending_delay: Duration, +} + +impl Config { + pub(crate) fn new( + ack_key: Arc, + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + average_ack_delay_duration: Duration, + average_message_sending_delay: Duration, + average_packet_delay_duration: Duration, + self_recipient: Recipient, + ) -> Self { + Config { + ack_key, + self_recipient, + average_packet_delay_duration, + average_ack_delay_duration, + average_message_sending_delay, + ack_wait_multiplier, + ack_wait_addition, + } + } +} + +pub(crate) struct RealMessagesController +where + R: CryptoRng + Rng, +{ + out_queue_control: Option>, + ack_control: Option>, +} + +// obviously when we finally make shared rng that is on 'higher' level, this should become +// generic `R` +impl RealMessagesController { + pub(crate) fn new( + config: Config, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: MixMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: ReplyKeyStorage, + ) -> Self { + let rng = OsRng; + + let (real_message_sender, real_message_receiver) = mpsc::unbounded(); + let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); + + let ack_controller_connectors = AcknowledgementControllerConnectors::new( + real_message_sender, + input_receiver, + sent_notifier_rx, + ack_receiver, + ); + + let ack_control = AcknowledgementController::new( + rng, + topology_access.clone(), + Arc::clone(&config.ack_key), + config.self_recipient.clone(), + reply_key_storage, + config.average_packet_delay_duration, + config.average_ack_delay_duration, + config.ack_wait_multiplier, + config.ack_wait_addition, + ack_controller_connectors, + ); + + let out_queue_control = OutQueueControl::new( + Arc::clone(&config.ack_key), + config.average_ack_delay_duration, + config.average_packet_delay_duration, + config.average_message_sending_delay, + sent_notifier_tx, + mix_sender, + real_message_receiver, + rng, + config.self_recipient, + topology_access, + ); + + RealMessagesController { + out_queue_control: Some(out_queue_control), + ack_control: Some(ack_control), + } + } + + pub(super) async fn run(&mut self) { + let mut out_queue_control = self.out_queue_control.take().unwrap(); + let mut ack_control = self.ack_control.take().unwrap(); + + // the below are log messages are errors as at the current stage we do not expect any of + // the task to ever finish. This will of course change once we introduce + // graceful shutdowns. + let out_queue_control_fut = tokio::spawn(async move { + out_queue_control.run_out_queue_control().await; + error!("The out queue controller has finished execution!"); + out_queue_control + }); + let ack_control_fut = tokio::spawn(async move { + ack_control.run().await; + error!("The acknowledgement controller has finished execution!"); + ack_control + }); + + // technically we don't have to bring `RealMessagesController` back to a valid state + // but we can do it, so why not? Perhaps it might be useful if we wanted to allow + // for restarts of certain modules without killing the entire process. + self.out_queue_control = Some(out_queue_control_fut.await.unwrap()); + self.ack_control = Some(ack_control_fut.await.unwrap()); + } + + // &Handle is only passed for consistency sake with other client modules, but I think + // when we get to refactoring, we should apply gateway approach and make it implicit + pub(super) fn start(mut self, handle: &Handle) -> JoinHandle { + handle.spawn(async move { + self.run().await; + self + }) + } +} diff --git a/clients/socks5/src/client/real_messages_control/real_traffic_stream.rs b/clients/socks5/src/client/real_messages_control/real_traffic_stream.rs new file mode 100644 index 0000000000..539370a4bb --- /dev/null +++ b/clients/socks5/src/client/real_messages_control/real_traffic_stream.rs @@ -0,0 +1,245 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::mix_traffic::{MixMessage, MixMessageSender}; +use crate::client::real_messages_control::acknowlegement_control::SentPacketNotificationSender; +use crate::client::topology_control::TopologyAccessor; +use futures::channel::mpsc; +use futures::task::{Context, Poll}; +use futures::{Future, Stream, StreamExt}; +use log::*; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::{clients::Recipient, nodes::NymNodeRoutingAddress}; +use nymsphinx::chunking::fragment::FragmentIdentifier; +use nymsphinx::cover::generate_loop_cover_packet; +use nymsphinx::utils::sample_poisson_duration; +use nymsphinx::SphinxPacket; +use rand::{CryptoRng, Rng}; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::time; + +pub(crate) struct OutQueueControl +where + R: CryptoRng + Rng, +{ + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: Duration, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: Duration, + + /// Average delay between sending subsequent packets. + average_message_sending_delay: Duration, + + /// Channel used for notifying of a real packet being sent out. Used to start up retransmission timer. + sent_notifier: SentPacketNotificationSender, + + /// Internal state, determined by `average_message_sending_delay`, + /// used to keep track of when a next packet should be sent out. + next_delay: time::Delay, + + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them + /// out to the network without any further delays. + mix_tx: MixMessageSender, + + /// Channel used for receiving real, prepared, messages that must be first sufficiently delayed + /// before being sent out into the network. + real_receiver: RealMessageReceiver, + + /// Represents full address of this client. + our_full_destination: Recipient, + + /// Instance of a cryptographically secure random number generator. + rng: R, + + /// Accessor to the common instance of network topology. + topology_access: TopologyAccessor, +} + +pub(crate) struct RealMessage { + first_hop_address: NymNodeRoutingAddress, + packet: SphinxPacket, + fragment_id: FragmentIdentifier, +} + +impl RealMessage { + pub(crate) fn new( + first_hop_address: NymNodeRoutingAddress, + packet: SphinxPacket, + fragment_id: FragmentIdentifier, + ) -> Self { + RealMessage { + first_hop_address, + packet, + fragment_id, + } + } +} + +// messages are already prepared, etc. the real point of it is to forward it to mix_traffic +// after sufficient delay +pub(crate) type RealMessageSender = mpsc::UnboundedSender; +type RealMessageReceiver = mpsc::UnboundedReceiver; + +pub(crate) enum StreamMessage { + Cover, + Real(RealMessage), +} + +impl Stream for OutQueueControl +where + R: CryptoRng + Rng + Unpin, +{ + type Item = StreamMessage; + + 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; + }; + + // 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 avg_delay = self.average_message_sending_delay; + let now = self.next_delay.deadline(); + let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_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); + + // decide what kind of message to send + match Pin::new(&mut self.real_receiver).poll_next(cx) { + // in the case our real message channel stream was closed, we should also indicate we are closed + // (and whoever is using the stream should panic) + Poll::Ready(None) => Poll::Ready(None), + + // if there's an actual message - return it + Poll::Ready(Some(real_message)) => Poll::Ready(Some(StreamMessage::Real(real_message))), + + // otherwise construct a dummy one + Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)), + } + } +} + +impl OutQueueControl +where + R: CryptoRng + Rng + Unpin, +{ + pub(crate) fn new( + ack_key: Arc, + average_ack_delay: Duration, + average_packet_delay: Duration, + average_message_sending_delay: Duration, + sent_notifier: SentPacketNotificationSender, + mix_tx: MixMessageSender, + real_receiver: RealMessageReceiver, + rng: R, + our_full_destination: Recipient, + topology_access: TopologyAccessor, + ) -> Self { + OutQueueControl { + ack_key, + average_ack_delay, + average_packet_delay, + average_message_sending_delay, + sent_notifier, + next_delay: time::delay_for(Default::default()), + mix_tx, + real_receiver, + our_full_destination, + rng, + topology_access, + } + } + + async fn on_message(&mut self, next_message: StreamMessage) { + trace!("created new message"); + + let next_message = match next_message { + StreamMessage::Cover => { + // TODO for way down the line: in very rare cases (during topology update) we might have + // to wait a really tiny bit before actually obtaining the permit hence messing with our + // poisson delay, but is it really a problem? + let topology_permit = self.topology_access.get_read_permit().await; + // the ack is sent back to ourselves (and then ignored) + let topology_ref_option = topology_permit.try_get_valid_topology_ref( + &self.our_full_destination, + Some(&self.our_full_destination), + ); + if topology_ref_option.is_none() { + warn!( + "No valid topology detected - won't send any loop cover message this time" + ); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let cover_message = generate_loop_cover_packet( + &mut self.rng, + topology_ref, + &*self.ack_key, + &self.our_full_destination, + self.average_ack_delay, + self.average_packet_delay, + ) + .expect("Somehow failed to generate a loop cover message with a valid topology"); + + MixMessage::new(cover_message.0, cover_message.1) + } + StreamMessage::Real(real_message) => { + // well technically the message was not sent just yet, but now it's up to internal + // queues and client load rather than the required delay. So realistically we can treat + // whatever is about to happen as negligible additional delay. + self.sent_notifier + .unbounded_send(real_message.fragment_id) + .unwrap(); + MixMessage::new(real_message.first_hop_address, real_message.packet) + } + }; + + // 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(next_message).unwrap(); + + // JS: Not entirely sure why or how it fixes stuff, but without the yield call, + // the UnboundedReceiver [of mix_rx] will not get a chance to read anything + // JS2: Basically it was the case that with high enough rate, the stream had already a next value + // ready and hence was immediately re-scheduled causing other tasks to be starved; + // yield makes it go back the scheduling queue regardless of its value availability + tokio::task::yield_now().await; + } + + 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(sample_poisson_duration( + &mut self.rng, + self.average_message_sending_delay, + )); + + info!("Starting out queue controller..."); + while let Some(next_message) = self.next().await { + self.on_message(next_message).await; + } + } +} diff --git a/clients/socks5/src/client/received_buffer.rs b/clients/socks5/src/client/received_buffer.rs new file mode 100644 index 0000000000..c3bf2d2bb2 --- /dev/null +++ b/clients/socks5/src/client/received_buffer.rs @@ -0,0 +1,374 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::client::reply_key_storage::ReplyKeyStorage; +use crypto::asymmetric::encryption; +use crypto::symmetric::stream_cipher; +use crypto::Digest; +use futures::channel::mpsc; +use futures::lock::Mutex; +use futures::StreamExt; +use gateway_client::MixnetMessageReceiver; +use log::*; +use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SURBEncryptionKey}; +use nymsphinx::params::{ReplySURBEncryptionAlgorithm, ReplySURBKeyDigestAlgorithm}; +use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; +use std::collections::HashSet; +use std::sync::Arc; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +// Buffer Requests to say "hey, send any reconstructed messages to this channel" +// or to say "hey, I'm going offline, don't send anything more to me. Just buffer them instead" +pub(crate) type ReceivedBufferRequestSender = mpsc::UnboundedSender; +pub(crate) type ReceivedBufferRequestReceiver = mpsc::UnboundedReceiver; + +// The channel set for the above +pub(crate) type ReconstructedMessagesSender = mpsc::UnboundedSender>; +pub(crate) type ReconstructedMessagesReceiver = mpsc::UnboundedReceiver>; + +struct ReceivedMessagesBufferInner { + messages: Vec, + local_encryption_keypair: Arc, + + // TODO: looking how it 'looks' here, perhaps `MessageReceiver` should be renamed to something + // else instead. + message_receiver: MessageReceiver, + message_sender: Option, + + // TODO: this will get cleared upon re-running the client + // but perhaps it should be changed to include timestamps of when the message was reconstructed + // and every now and then remove ids older than X + recently_reconstructed: HashSet, +} + +impl ReceivedMessagesBufferInner { + fn process_received_fragment(&mut self, raw_fragment: Vec) -> Option { + let fragment_data = match self + .message_receiver + .recover_plaintext(self.local_encryption_keypair.private_key(), raw_fragment) + { + Err(e) => { + warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + return None; + } + Ok(frag_data) => frag_data, + }; + + if nymsphinx::cover::is_cover(&fragment_data) { + trace!("The message was a loop cover message! Skipping it"); + return None; + } + + let fragment = match self.message_receiver.recover_fragment(&fragment_data) { + Err(e) => { + warn!("failed to recover fragment from raw data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + return None; + } + Ok(frag) => frag, + }; + + if self.recently_reconstructed.contains(&fragment.id()) { + debug!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id()); + return None; + } + + // if we returned an error the underlying message is malformed in some way + match self.message_receiver.insert_new_fragment(fragment) { + Err(err) => match err { + MessageRecoveryError::MalformedReconstructedMessage(message_sets) => { + // TODO: should we really insert reconstructed sets? could this be abused for some attack? + for set_id in message_sets { + if !self.recently_reconstructed.insert(set_id) { + // or perhaps we should even panic at this point? + error!("Reconstructed another message containing already used set id!") + } + } + None + } + _ => unreachable!( + "no other error kind should have been returned here! If so, it's a bug!" + ), + }, + Ok(reconstruction_result) => match reconstruction_result { + Some((reconstructed_message, used_sets)) => { + for set_id in used_sets { + if !self.recently_reconstructed.insert(set_id) { + // or perhaps we should even panic at this point? + error!("Reconstructed another message containing already used set id!") + } + } + Some(reconstructed_message) + } + None => None, + }, + } + } +} + +#[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>, + + /// Storage containing keys to all [`ReplySURB`]s ever sent out that we did not receive back. + // There's no need to put it behind a Mutex since it's already properly concurrent + reply_key_storage: ReplyKeyStorage, +} + +impl ReceivedMessagesBuffer { + fn new( + local_encryption_keypair: Arc, + reply_key_storage: ReplyKeyStorage, + ) -> Self { + ReceivedMessagesBuffer { + inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { + messages: Vec::new(), + local_encryption_keypair, + message_receiver: MessageReceiver::new(), + message_sender: None, + recently_reconstructed: HashSet::new(), + })), + reply_key_storage, + } + } + + async fn disconnect_sender(&mut self) { + let mut guard = self.inner.lock().await; + if guard.message_sender.is_none() { + // in theory we could just ignore it, but that situation should have never happened + // in the first place, so this way we at least know we have an important bug to fix + panic!("trying to disconnect non-existent sender!") + } + guard.message_sender = None; + } + + async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) { + let mut guard = self.inner.lock().await; + if guard.message_sender.is_some() { + // in theory we could just ignore it, but that situation should have never happened + // in the first place, so this way we at least know we have an important bug to fix + panic!("trying overwrite an existing sender!") + } + + // while we're at it, also empty the buffer if we happened to receive anything while + // no sender was connected + let stored_messages = std::mem::replace(&mut guard.messages, Vec::new()); + if !stored_messages.is_empty() { + if let Err(err) = sender.unbounded_send(stored_messages) { + error!( + "The sender channel we just received is already invalidated - {:?}", + err + ); + // put the values back to the buffer + // the returned error has two fields: err: SendError and val: T, + // where val is the value that was failed to get sent; + // it's returned by the `into_inner` call + guard.messages = err.into_inner(); + return; + } + } + guard.message_sender = Some(sender); + } + + async fn add_reconstructed_messages(&mut self, msgs: Vec) { + debug!("Adding {:?} new messages to the buffer!", msgs.len()); + trace!("Adding new messages to the buffer! {:?}", msgs); + self.inner.lock().await.messages.extend(msgs) + } + + fn process_received_reply( + reply_ciphertext: &[u8], + reply_key: SURBEncryptionKey, + ) -> Option { + let zero_iv = stream_cipher::zero_iv::(); + + let mut reply_msg = stream_cipher::decrypt::( + &reply_key.inner(), + &zero_iv, + reply_ciphertext, + ); + if let Err(err) = MessageReceiver::remove_padding(&mut reply_msg) { + warn!("Received reply had malformed padding! - {:?}", err); + None + } else { + // TODO: perhaps having to say it doesn't have a surb an indication the type should be changed? + Some(ReconstructedMessage { + message: reply_msg, + reply_SURB: None, + }) + } + } + + async fn handle_new_received(&mut self, msgs: Vec>) { + debug!( + "Processing {:?} new message that might get added to the buffer!", + msgs.len() + ); + + let mut completed_messages = Vec::new(); + let mut inner_guard = self.inner.lock().await; + + let reply_surb_digest_size = ReplySURBKeyDigestAlgorithm::output_size(); + + // first check if this is a reply or a chunked message + // TODO: verify with @AP if this way of doing it is safe or whether it could + // cause some attacks due to, I don't know, stupid edge case collisions? + // Update: this DOES introduce a possible leakage: https://github.com/nymtech/nym/issues/296 + for msg in msgs { + let possible_key_digest = + EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]); + + // check first `HasherOutputSize` bytes if they correspond to known encryption key + // if yes - this is a reply message + + // TODO: this might be a bottleneck - since the keys are stored on disk we, presumably, + // are doing a disk operation every single received fragment + if let Some(reply_encryption_key) = self + .reply_key_storage + .get_and_remove_encryption_key(possible_key_digest) + .expect("storage operation failed!") + { + if let Some(completed_message) = Self::process_received_reply( + &msg[reply_surb_digest_size..], + reply_encryption_key, + ) { + completed_messages.push(completed_message) + } + } else { + // otherwise - it's a 'normal' message + if let Some(completed_message) = inner_guard.process_received_fragment(msg) { + completed_messages.push(completed_message) + } + } + } + + if !completed_messages.is_empty() { + if let Some(sender) = &inner_guard.message_sender { + trace!("Sending reconstructed messages to announced sender"); + if let Err(err) = sender.unbounded_send(completed_messages) { + warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {:?})", err); + // make sure to drop the lock to not deadlock + // (it is required by `add_reconstructed_messages`) + inner_guard.message_sender = None; + drop(inner_guard); + self.add_reconstructed_messages(err.into_inner()).await; + } + } else { + // make sure to drop the lock to not deadlock + // (it is required by `add_reconstructed_messages`) + drop(inner_guard); + trace!("No sender available - buffering reconstructed messages"); + self.add_reconstructed_messages(completed_messages).await; + } + } + } +} + +pub(crate) enum ReceivedBufferMessage { + // Signals a websocket connection (or a native implementation) was established and we should stop buffering messages, + // and instead send them directly to the received channel + ReceiverAnnounce(ReconstructedMessagesSender), + + // Explicit signal that Receiver connection will no longer accept messages + ReceiverDisconnect, +} + +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 { + match request { + ReceivedBufferMessage::ReceiverAnnounce(sender) => { + self.received_buffer.connect_sender(sender).await; + } + ReceivedBufferMessage::ReceiverDisconnect => { + self.received_buffer.disconnect_sender().await + } + } + } + }) + } +} + +struct FragmentedMessageReceiver { + received_buffer: ReceivedMessagesBuffer, + mixnet_packet_receiver: MixnetMessageReceiver, +} + +impl FragmentedMessageReceiver { + fn new( + received_buffer: ReceivedMessagesBuffer, + mixnet_packet_receiver: MixnetMessageReceiver, + ) -> Self { + FragmentedMessageReceiver { + received_buffer, + mixnet_packet_receiver, + } + } + fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + while let Some(new_messages) = self.mixnet_packet_receiver.next().await { + self.received_buffer.handle_new_received(new_messages).await; + } + }) + } +} + +pub(crate) struct ReceivedMessagesBufferController { + fragmented_message_receiver: FragmentedMessageReceiver, + request_receiver: RequestReceiver, +} + +impl ReceivedMessagesBufferController { + pub(crate) fn new( + local_encryption_keypair: Arc, + query_receiver: ReceivedBufferRequestReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, + reply_key_storage: ReplyKeyStorage, + ) -> Self { + let received_buffer = + ReceivedMessagesBuffer::new(local_encryption_keypair, reply_key_storage); + + ReceivedMessagesBufferController { + fragmented_message_receiver: FragmentedMessageReceiver::new( + received_buffer.clone(), + mixnet_packet_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.fragmented_message_receiver.start(handle); + self.request_receiver.start(handle); + } +} diff --git a/clients/socks5/src/client/reply_key_storage.rs b/clients/socks5/src/client/reply_key_storage.rs new file mode 100644 index 0000000000..e1dc4a6b9a --- /dev/null +++ b/clients/socks5/src/client/reply_key_storage.rs @@ -0,0 +1,105 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use log::*; +use nymsphinx::anonymous_replies::{ + encryption_key::EncryptionKeyDigest, encryption_key::Unsigned, SURBEncryptionKey, + SURBEncryptionKeySize, +}; +use std::path::Path; + +#[derive(Debug)] +pub(crate) enum ReplyKeyStorageError { + DbReadError(sled::Error), + DbWriteError(sled::Error), + DbOpenError(sled::Error), +} + +/// Permanent storage for keys in all sent [`ReplySURB`] +/// +/// Each sent out [`ReplySURB`] has a new key associated with it that is going to be used for +/// payload encryption. In order to decrypt whatever reply we receive, we need to know which +/// key to use for that purpose. We do it based on received `H(t)` which has to be included +/// with each reply. +/// Moreover, there is no restriction when the [`ReplySURB`] might get used so we need to +/// have a permanent storage for all the keys that we might ever see in the future. +#[derive(Debug, Clone)] +pub struct ReplyKeyStorage { + db: sled::Db, +} + +impl ReplyKeyStorage { + pub(crate) fn load>(path: P) -> Result { + let db = match sled::open(path) { + Err(e) => return Err(ReplyKeyStorageError::DbOpenError(e)), + Ok(db) => db, + }; + + Ok(ReplyKeyStorage { db }) + } + + fn read_encryption_key(&self, raw_key: sled::IVec) -> SURBEncryptionKey { + let key_bytes_ref = raw_key.as_ref(); + // if this fails it means we have some database corruption and we + // absolutely can't continue + + if key_bytes_ref.len() != SURBEncryptionKeySize::to_usize() { + error!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); + panic!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); + } + + // this can only fail if the bytes have invalid length but we already asserted it + SURBEncryptionKey::try_from_bytes(key_bytes_ref).unwrap() + } + + // TOOD: perhaps we could also store some part of original message here too? + pub(crate) fn insert_encryption_key( + &mut self, + encryption_key: SURBEncryptionKey, + ) -> Result<(), ReplyKeyStorageError> { + let digest = encryption_key.compute_digest(); + + let insertion_result = match self.db.insert(digest.to_vec(), encryption_key.to_bytes()) { + Err(e) => Err(ReplyKeyStorageError::DbWriteError(e)), + Ok(existing_key) => { + if existing_key.is_some() { + panic!("HASH COLLISION DETECTED") + }; + Ok(()) + } + }; + + // TODO: perhaps we could implement some batching mechanism to avoid frequent flushes? + self.db.flush().unwrap(); + insertion_result + } + + // Once we use key once, we do not expect to use it again + pub(crate) fn get_and_remove_encryption_key( + &self, + key_digest: EncryptionKeyDigest, + ) -> Result, ReplyKeyStorageError> { + let removal_result = match self.db.remove(&key_digest.to_vec()) { + Err(e) => Err(ReplyKeyStorageError::DbReadError(e)), + Ok(existing_key) => { + Ok(existing_key.map(|existing_key| self.read_encryption_key(existing_key))) + } + }; + + // TODO: not sure how to feel about flushing it every single time here... + // same with insertion + self.db.flush().unwrap(); + removal_result + } +} diff --git a/clients/socks5/src/client/server/auth.rs b/clients/socks5/src/client/server/auth.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/clients/socks5/src/client/server/handler.rs b/clients/socks5/src/client/server/handler.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/clients/socks5/src/client/server/listener.rs b/clients/socks5/src/client/server/listener.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/clients/socks5/src/client/topology_control.rs b/clients/socks5/src/client/topology_control.rs new file mode 100644 index 0000000000..366c1844f2 --- /dev/null +++ b/clients/socks5/src/client/topology_control.rs @@ -0,0 +1,204 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::built_info; +use directory_client::DirectoryClient; +use log::*; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; +use std::convert::TryInto; +use std::ops::Deref; +use std::sync::Arc; +use std::time; +use std::time::Duration; +use tokio::runtime::Handle; +use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::task::JoinHandle; +use topology::NymTopology; + +// I'm extremely curious why compiler NEVER complained about lack of Debug here before +#[derive(Debug)] +pub(super) struct TopologyAccessorInner(Option); + +impl AsRef> for TopologyAccessorInner { + fn as_ref(&self) -> &Option { + &self.0 + } +} + +impl TopologyAccessorInner { + fn new() -> Self { + TopologyAccessorInner(None) + } + + fn update(&mut self, new: Option) { + self.0 = new; + } +} + +pub(super) struct TopologyReadPermit<'a> { + permit: RwLockReadGuard<'a, TopologyAccessorInner>, +} + +impl<'a> Deref for TopologyReadPermit<'a> { + type Target = TopologyAccessorInner; + + fn deref(&self) -> &Self::Target { + &self.permit + } +} + +impl<'a> TopologyReadPermit<'a> { + /// Using provided topology read permit, tries to get an immutable reference to the underlying + /// topology. For obvious reasons the lifetime of the topology reference is bound to the permit. + pub(super) fn try_get_valid_topology_ref( + &'a self, + ack_recipient: &Recipient, + packet_recipient: Option<&Recipient>, + ) -> Option<&'a NymTopology> { + // Note: implicit deref with Deref for TopologyReadPermit is happening here + let topology_ref_option = self.permit.as_ref(); + match topology_ref_option { + None => None, + Some(topology_ref) => { + // see if it's possible to route the packet to both gateways + if !topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS) + || !topology_ref.gateway_exists(&ack_recipient.gateway()) + || if let Some(packet_recipient) = packet_recipient { + !topology_ref.gateway_exists(&packet_recipient.gateway()) + } else { + false + } + { + None + } else { + Some(topology_ref) + } + } + } + } +} + +impl<'a> From> for TopologyReadPermit<'a> { + fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self { + TopologyReadPermit { + permit: read_permit, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct TopologyAccessor { + // `RwLock` *seems to* be the better approach for this as write access is only requested every + // few seconds, while reads are needed every single packet generated. + // However, proper benchmarks will be needed to determine if `RwLock` is indeed a better + // approach than a `Mutex` + inner: Arc>, +} + +impl TopologyAccessor { + pub(crate) fn new() -> Self { + TopologyAccessor { + inner: Arc::new(RwLock::new(TopologyAccessorInner::new())), + } + } + + pub(super) async fn get_read_permit(&self) -> TopologyReadPermit<'_> { + self.inner.read().await.into() + } + + async fn update_global_topology(&mut self, new_topology: Option) { + self.inner.write().await.update(new_topology); + } + + // only used by the client at startup to get a slightly more reasonable error message + // (currently displays as unused because health checker is disabled due to required changes) + pub(crate) async fn is_routable(&self) -> bool { + match &self.inner.read().await.0 { + None => false, + Some(ref topology) => topology.can_construct_path_through(DEFAULT_NUM_MIX_HOPS), + } + } +} + +pub(crate) struct TopologyRefresherConfig { + directory_server: String, + refresh_rate: time::Duration, +} + +impl TopologyRefresherConfig { + pub(crate) fn new(directory_server: String, refresh_rate: time::Duration) -> Self { + TopologyRefresherConfig { + directory_server, + refresh_rate, + } + } +} + +pub(crate) struct TopologyRefresher { + directory_client: directory_client::Client, + topology_accessor: TopologyAccessor, + refresh_rate: Duration, +} + +impl TopologyRefresher { + pub(crate) fn new_directory_client( + cfg: TopologyRefresherConfig, + topology_accessor: TopologyAccessor, + ) -> Self { + let directory_client_config = directory_client::Config::new(cfg.directory_server); + let directory_client = directory_client::Client::new(directory_client_config); + + TopologyRefresher { + directory_client, + topology_accessor, + refresh_rate: cfg.refresh_rate, + } + } + + async fn get_current_compatible_topology(&self) -> Option { + match self.directory_client.get_topology().await { + Err(err) => { + error!("failed to get network topology! - {:?}", err); + None + } + Ok(topology) => { + let nym_topology: NymTopology = topology.try_into().ok()?; + Some(nym_topology.filter_system_version(built_info::PKG_VERSION)) + } + } + } + + pub(crate) async fn refresh(&mut self) { + trace!("Refreshing the topology"); + let new_topology = self.get_current_compatible_topology().await; + + self.topology_accessor + .update_global_topology(new_topology) + .await; + } + + pub(crate) async fn is_topology_routable(&self) -> bool { + self.topology_accessor.is_routable().await + } + + pub(crate) fn start(mut self, handle: &Handle) -> JoinHandle<()> { + handle.spawn(async move { + loop { + tokio::time::delay_for(self.refresh_rate).await; + self.refresh().await; + } + }) + } +} diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs new file mode 100644 index 0000000000..f6f7da0c9b --- /dev/null +++ b/clients/socks5/src/commands/init.rs @@ -0,0 +1,211 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::built_info; +use crate::client::key_manager::KeyManager; +use crate::commands::override_config; +use crate::config::persistence::key_pathfinder::ClientKeyPathfinder; +use clap::{App, Arg, ArgMatches}; +use config::NymConfig; +use crypto::asymmetric::identity; +use directory_client::DirectoryClient; +use gateway_client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use rand::rngs::OsRng; +use std::convert::TryInto; +use std::sync::Arc; +use std::time::Duration; +use topology::{gateway, 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("gateway") + .long("gateway") + .help("Id of the gateway we have preference to connect to. If left empty, a random gateway 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("disable-socket") + .long("disable-socket") + .help("Whether to not start the websocket") + ) + .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) + ) + .arg(Arg::with_name("fastmode") + .long("fastmode") + .hidden(true) // this will prevent this flag from being displayed in `--help` + .help("Mostly debug-related option to increase default traffic rate so that you would not need to modify config post init") + ) +} + +async fn try_gateway_registration( + gateways: &[gateway::Node], + our_identity: Arc, +) -> Option<(String, String, SharedKeys)> { + let timeout = Duration::from_millis(1500); + for gateway in gateways { + let mut gateway_client = GatewayClient::new_init( + url::Url::parse(&gateway.client_listener).unwrap(), + gateway.identity_key, + our_identity.clone(), + timeout, + ); + if gateway_client.establish_connection().await.is_ok() { + if let Ok(shared_key) = gateway_client.register().await { + if let Err(err) = gateway_client.close_connection().await { + eprintln!("Error while closing connection to the gateway! - {:?}", err); + continue; + } else { + return Some(( + gateway.identity_key.to_base58_string(), + gateway.client_listener.clone(), + shared_key, + )); + } + } + } + } + None +} + +async fn choose_gateway( + directory_server: String, + our_identity: Arc, +) -> (String, String, SharedKeys) { + let directory_client_config = directory_client::Config::new(directory_server.clone()); + let directory_client = directory_client::Client::new(directory_client_config); + let topology = directory_client.get_topology().await.unwrap(); + let nym_topology: NymTopology = topology.try_into().expect("Invalid topology data!"); + + let version_filtered_topology = nym_topology.filter_system_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 gateways + let gateways = version_filtered_topology.gateways(); + + // 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 gateway + let registration_result = try_gateway_registration(gateways, our_identity).await; + match registration_result { + None => { + // while technically there's no issue client-side, it will be impossible to execute + // `nym-client run` as no gateway 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 gateways available on the network ({}). \ + Please try to run `init` again at later time or change your directory server", + directory_server + ) + } + Some((gateway_id, gateway_listener, shared_key)) => { + (gateway_id, gateway_listener, shared_key) + } + } +} + +async fn get_gateway_listener(directory_server: String, gateway_identity: &str) -> Option { + let directory_client_config = directory_client::Config::new(directory_server); + let directory_client = directory_client::Client::new(directory_client_config); + let topology = directory_client.get_topology().await.unwrap(); + + // technically we don't need to do conversion here, but let's be consistent + let nym_topology: NymTopology = topology.try_into().ok()?; + let gateways = nym_topology.gateways(); + + for gateway in gateways { + if gateway.identity_key.to_base58_string() == gateway_identity { + return Some(gateway.client_listener.clone()); + } + } + None +} + +pub fn execute(matches: &ArgMatches) { + println!("Initialising client..."); + + let id = matches.value_of("id").unwrap(); // required for now + let mut config = crate::config::Config::new(id); + let mut rng = OsRng; + + config = override_config(config, matches); + if matches.is_present("fastmode") { + config = config.set_high_default_traffic_volume(); + } + + // create identity, encryption and ack keys. + let mut key_manager = KeyManager::new(&mut rng); + + // if there is no gateway chosen, get a random-ish one from the topology + if config.get_gateway_id().is_empty() { + // 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 (gateway_id, gateway_listener, shared_key) = rt.block_on(choose_gateway( + config.get_directory_server(), + key_manager.identity_keypair(), + )); + + config = config + .with_gateway_id(gateway_id) + .with_gateway_listener(gateway_listener); + + key_manager.insert_gateway_shared_key(shared_key) + } + + // we specified our gateway but don't know its physical address + if config.get_gateway_listener().is_empty() { + // 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 gateway_listener = rt + .block_on(get_gateway_listener( + config.get_directory_server(), + &config.get_gateway_id(), + )) + .expect("No gateway with provided id exists!"); + + config = config.with_gateway_listener(gateway_listener); + } + + let pathfinder = ClientKeyPathfinder::new_from_config(&config); + key_manager + .store_keys(&pathfinder) + .expect("Failed to generated keys"); + println!("Saved all generated keys"); + + 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 gateway: {}...", + config.get_gateway_id(), + ); + + println!("Client configuration completed.\n\n\n") +} diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs new file mode 100644 index 0000000000..f9bb333966 --- /dev/null +++ b/clients/socks5/src/commands/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::{Config, SocketType}; +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); + } + + if let Some(gateway_id) = matches.value_of("gateway") { + config = config.with_gateway_id(gateway_id); + } + + if matches.is_present("disable-socket") { + config = config.with_socket(SocketType::None); + } + + 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/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs new file mode 100644 index 0000000000..83af38944e --- /dev/null +++ b/clients/socks5/src/commands/run.rs @@ -0,0 +1,68 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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("gateway") + .long("gateway") + .help("Id of the gateway we want to connect to. If overridden, it is user's responsibility to ensure prior registration happened") + .takes_value(true) + ) + .arg(Arg::with_name("disable-socket") + .long("disable-socket") + .help("Whether to not start the websocket") + ) + .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/clients/socks5/src/config/mod.rs b/clients/socks5/src/config/mod.rs new file mode 100644 index 0000000000..4ccb1606f5 --- /dev/null +++ b/clients/socks5/src/config/mod.rs @@ -0,0 +1,503 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::template::config_template; +use config::NymConfig; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time; + +pub mod persistence; +mod template; + +// 'CLIENT' +const DEFAULT_LISTENING_PORT: u16 = 1977; +const DEFAULT_DIRECTORY_SERVER: &str = "https://directory.nymtech.net"; +// 'DEBUG' +// where applicable, the below are defined in milliseconds +const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; +const DEFAULT_ACK_WAIT_ADDITION: u64 = 800; +const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: u64 = 1000; // 1s +const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: u64 = 500; // 0.5s +const DEFAULT_AVERAGE_PACKET_DELAY: u64 = 200; // 0.2s +const DEFAULT_TOPOLOGY_REFRESH_RATE: u64 = 30_000; // 30s +const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: u64 = 5_000; // 5s + +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: u64 = 1_500; // 1.5s + +#[derive(Debug, Deserialize, PartialEq, Serialize, Clone, Copy)] +#[serde(deny_unknown_fields)] +pub enum SocketType { + WebSocket, + None, +} + +impl SocketType { + pub fn from_string>(val: S) -> Self { + let mut upper = val.into(); + upper.make_ascii_uppercase(); + match upper.as_ref() { + "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(); + + // identity key setting + 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); + } + + // encryption key setting + if self + .client + .private_encryption_key_file + .as_os_str() + .is_empty() + { + self.client.private_encryption_key_file = + self::Client::default_private_encryption_key_file(&id); + } + if self + .client + .public_encryption_key_file + .as_os_str() + .is_empty() + { + self.client.public_encryption_key_file = + self::Client::default_public_encryption_key_file(&id); + } + + // shared gateway key setting + if self.client.gateway_shared_key_file.as_os_str().is_empty() { + self.client.gateway_shared_key_file = + self::Client::default_gateway_shared_key_file(&id); + } + + // ack key setting + if self.client.ack_key_file.as_os_str().is_empty() { + self.client.ack_key_file = self::Client::default_ack_key_file(&id); + } + + if self + .client + .reply_encryption_key_store_path + .as_os_str() + .is_empty() + { + self.client.reply_encryption_key_store_path = + self::Client::default_reply_encryption_key_store_path(&id); + } + + self.client.id = id; + self + } + + pub fn with_gateway_id>(mut self, id: S) -> Self { + self.client.gateway_id = id.into(); + self + } + + pub fn with_gateway_listener>(mut self, gateway_listener: S) -> Self { + self.client.gateway_listener = gateway_listener.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 + } + + pub fn set_high_default_traffic_volume(mut self) -> Self { + self.debug.average_packet_delay = 10; + self.debug.loop_cover_traffic_average_delay = 20; // 50 cover messages / s + self.debug.message_sending_average_delay = 5; // 200 "real" messages / s + 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_private_encryption_key_file(&self) -> PathBuf { + self.client.private_encryption_key_file.clone() + } + + pub fn get_public_encryption_key_file(&self) -> PathBuf { + self.client.public_encryption_key_file.clone() + } + + pub fn get_gateway_shared_key_file(&self) -> PathBuf { + self.client.gateway_shared_key_file.clone() + } + + pub fn get_reply_encryption_key_store_path(&self) -> PathBuf { + self.client.reply_encryption_key_store_path.clone() + } + + pub fn get_ack_key_file(&self) -> PathBuf { + self.client.ack_key_file.clone() + } + + pub fn get_directory_server(&self) -> String { + self.client.directory_server.clone() + } + + pub fn get_gateway_id(&self) -> String { + self.client.gateway_id.clone() + } + + pub fn get_gateway_listener(&self) -> String { + self.client.gateway_listener.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_average_ack_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.average_ack_delay) + } + + pub fn get_ack_wait_multiplier(&self) -> f64 { + self.debug.ack_wait_multiplier + } + + pub fn get_ack_wait_addition(&self) -> time::Duration { + time::Duration::from_millis(self.debug.ack_wait_addition) + } + + 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_message_sending_average_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.message_sending_average_delay) + } + + pub fn get_gateway_response_timeout(&self) -> time::Duration { + time::Duration::from_millis(self.debug.gateway_response_timeout) + } + + 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) + } +} + +#[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, + + /// Path to file containing private encryption key. + private_encryption_key_file: PathBuf, + + /// Path to file containing public encryption key. + public_encryption_key_file: PathBuf, + + /// Path to file containing shared key derived with the specified gateway that is used + /// for all communication with it. + gateway_shared_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + ack_key_file: PathBuf, + + /// Full path to file containing reply encryption keys of all reply-SURBs we have ever + /// sent but not received back. + reply_encryption_key_store_path: PathBuf, + + /// gateway_id specifies ID of the gateway to which the client should send messages. + /// If initially omitted, a random gateway will be chosen from the available topology. + gateway_id: String, + + /// Address of the gateway listener to which all client requests should be sent. + gateway_listener: String, + + /// 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: DEFAULT_DIRECTORY_SERVER.to_string(), + private_identity_key_file: Default::default(), + public_identity_key_file: Default::default(), + private_encryption_key_file: Default::default(), + public_encryption_key_file: Default::default(), + gateway_shared_key_file: Default::default(), + ack_key_file: Default::default(), + reply_encryption_key_store_path: Default::default(), + gateway_id: "".to_string(), + gateway_listener: "".to_string(), + nym_root_directory: Config::default_root_directory(), + } + } +} + +impl Client { + 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") + } + + fn default_private_encryption_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_encryption.pem") + } + + fn default_public_encryption_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_encryption.pem") + } + + fn default_gateway_shared_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("gateway_shared.pem") + } + + fn default_ack_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("ack_key.pem") + } + + fn default_reply_encryption_key_store_path(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("reply_key_store") + } +} + +#[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::WebSocket, + 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(default, 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, + /// sent acknowledgement is going to be delayed at any given mix node. + /// So for an ack 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_ack_delay: u64, + + /// Value multiplied with the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 1. + ack_wait_multiplier: f64, + + /// Value added to the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 0. + /// The provided value is interpreted as milliseconds. + ack_wait_addition: 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 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, + + /// How long we're willing to wait for a response to a message sent to the gateway, + /// before giving up on it. + /// The provided value is interpreted as milliseconds. + gateway_response_timeout: u64, + + /// 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, +} + +impl Default for Debug { + fn default() -> Self { + Debug { + average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY, + ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER, + ack_wait_addition: DEFAULT_ACK_WAIT_ADDITION, + loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, + message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, + gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, + topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, + topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, + } + } +} + +#[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/clients/socks5/src/config/persistence/key_pathfinder.rs b/clients/socks5/src/config/persistence/key_pathfinder.rs new file mode 100644 index 0000000000..3804c38fea --- /dev/null +++ b/clients/socks5/src/config/persistence/key_pathfinder.rs @@ -0,0 +1,76 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::config::Config; +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct ClientKeyPathfinder { + identity_private_key: PathBuf, + identity_public_key: PathBuf, + encryption_private_key: PathBuf, + encryption_public_key: PathBuf, + gateway_shared_key: PathBuf, + ack_key: PathBuf, +} + +impl ClientKeyPathfinder { + pub fn new(id: String) -> Self { + let os_config_dir = dirs::config_dir().expect("no config directory known for this OS"); // grabs the OS default config dir + let config_dir = os_config_dir.join("nym").join("clients").join(id); + ClientKeyPathfinder { + identity_private_key: config_dir.join("private_identity.pem"), + identity_public_key: config_dir.join("public_identity.pem"), + encryption_private_key: config_dir.join("public_encryption.pem"), + encryption_public_key: config_dir.join("private_encryption.pem"), + gateway_shared_key: config_dir.join("gateway_shared.pem"), + ack_key: config_dir.join("ack_key.pem"), + } + } + + pub fn new_from_config(config: &Config) -> Self { + ClientKeyPathfinder { + identity_private_key: config.get_private_identity_key_file(), + identity_public_key: config.get_public_identity_key_file(), + encryption_private_key: config.get_private_encryption_key_file(), + encryption_public_key: config.get_public_encryption_key_file(), + gateway_shared_key: config.get_gateway_shared_key_file(), + ack_key: config.get_ack_key_file(), + } + } + + pub fn private_identity_key(&self) -> &Path { + &self.identity_private_key + } + + pub fn public_identity_key(&self) -> &Path { + &self.identity_public_key + } + + pub fn private_encryption_key(&self) -> &Path { + &self.encryption_private_key + } + + pub fn public_encryption_key(&self) -> &Path { + &self.encryption_public_key + } + + pub fn gateway_shared_key(&self) -> &Path { + &self.gateway_shared_key + } + + pub fn ack_key(&self) -> &Path { + &self.ack_key + } +} diff --git a/clients/socks5/src/config/persistence/mod.rs b/clients/socks5/src/config/persistence/mod.rs new file mode 100644 index 0000000000..b87c0be15d --- /dev/null +++ b/clients/socks5/src/config/persistence/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod key_pathfinder; diff --git a/clients/socks5/src/config/template.rs b/clients/socks5/src/config/template.rs new file mode 100644 index 0000000000..fbe0f404c2 --- /dev/null +++ b/clients/socks5/src/config/template.rs @@ -0,0 +1,102 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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 }}' + +# Path to file containing private encryption key. +private_encryption_key_file = '{{ client.private_encryption_key_file }}' + +# Path to file containing public encryption key. +public_encryption_key_file = '{{ client.public_encryption_key_file }}' + +# Full path to file containing reply encryption keys of all reply-SURBs we have ever +# sent but not received back. +reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' + +##### additional client config options ##### + +# ID of the gateway from which the client should be fetching messages. +gateway_id = '{{ client.gateway_id }}' + +# Address of the gateway listener to which all client requests should be sent. +gateway_listener = '{{ client.gateway_listener }}' + +# A gateway specific, optional, base58 stringified shared key used for +# communication with particular gateway. +gateway_shared_key_file = '{{ client.gateway_shared_key_file }}' + +# Path to file containing key used for encrypting and decrypting the content of an +# acknowledgement so that nobody besides the client knows which packet it refers to. +ack_key_file = '{{ client.ack_key_file }}' + +##### 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 'WebSocket' or 'None' +socket_type = '{{ socket.socket_type }}' + +# if applicable (for the case of '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] + +average_packet_delay = {{ debug.average_packet_delay }} +average_ack_delay = {{ debug.average_ack_delay }} +loop_cover_traffic_average_delay = {{ debug.loop_cover_traffic_average_delay }} +message_sending_average_delay = {{ debug.message_sending_average_delay }} + +"# +} diff --git a/clients/socks5/src/lib.rs b/clients/socks5/src/lib.rs new file mode 100644 index 0000000000..8efb3dd47a --- /dev/null +++ b/clients/socks5/src/lib.rs @@ -0,0 +1,17 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +pub mod built_info; +pub mod client; +pub mod config; +pub mod socks; diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs new file mode 100644 index 0000000000..3e2aac76ec --- /dev/null +++ b/clients/socks5/src/main.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::{App, ArgMatches}; + +pub mod built_info; +pub mod client; +mod commands; +pub mod config; +pub mod socks; + +fn main() { + dotenv::dotenv().ok(); + setup_logging(); + println!("{}", banner()); + + let arg_matches = App::new("Nym Socks5 Proxy") + .version(built_info::PKG_VERSION) + .author("Nymtech") + .about("A Socks5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address") + .subcommand(commands::init::command_args()) + .subcommand(commands::run::command_args()) + .get_matches(); + + execute(arg_matches); +} + +fn execute(matches: ArgMatches) { + match matches.subcommand() { + ("init", Some(m)) => commands::init::execute(m), + ("run", Some(m)) => commands::run::execute(m), + _ => println!("{}", usage()), + } +} + +fn usage() -> &'static str { + "usage: --help to see available options.\n\n" +} + +fn banner() -> String { + format!( + r#" + + _ __ _ _ _ __ ___ + | '_ \| | | | '_ \ _ \ + | | | | |_| | | | | | | + |_| |_|\__, |_| |_| |_| + |___/ + + (socks5 proxy - version {:}) + + "#, + built_info::PKG_VERSION + ) +} + +fn setup_logging() { + let mut log_builder = pretty_env_logger::formatted_timed_builder(); + if let Ok(s) = ::std::env::var("RUST_LOG") { + log_builder.parse_filters(&s); + } else { + // default to 'Info' + log_builder.filter(None, log::LevelFilter::Info); + } + + log_builder + .filter_module("hyper", log::LevelFilter::Warn) + .filter_module("tokio_reactor", log::LevelFilter::Warn) + .filter_module("reqwest", log::LevelFilter::Warn) + .filter_module("mio", log::LevelFilter::Warn) + .filter_module("want", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) + .init(); +} diff --git a/clients/socks5/src/socks/authentication.rs b/clients/socks5/src/socks/authentication.rs new file mode 100644 index 0000000000..e000ebf3c6 --- /dev/null +++ b/clients/socks5/src/socks/authentication.rs @@ -0,0 +1,133 @@ +use serde::Deserialize; + +/// Client Authentication Methods +pub(crate) enum AuthenticationMethods { + /// No Authentication + NoAuth = 0x00, + // GssApi = 0x01, + /// Authenticate with a username / password + UserPass = 0x02, + /// Cannot authenticate + NoMethods = 0xFF, +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct User { + pub username: String, + pub password: String, +} + +#[derive(Clone, Debug)] + +pub(crate) struct Authenticator { + allowed_users: Vec, + pub auth_methods: Vec, // DH TODO: this should not be public +} + +impl Authenticator { + pub(crate) fn new(auth_methods: Vec, allowed_users: Vec) -> Authenticator { + Authenticator { + allowed_users, + auth_methods, + } + } + + /// Check if username + password pair are valid + pub fn is_allowed(&self, user: &User) -> bool { + if self + .auth_methods + .contains(&(AuthenticationMethods::UserPass as u8)) + { + self.allowed_users.contains(user) + } else { + false + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constructor_works() { + let mut auth_methods: Vec = Vec::new(); + auth_methods.push(AuthenticationMethods::NoAuth as u8); + auth_methods.push(AuthenticationMethods::UserPass as u8); + + let admin = User { + username: "foo".to_string(), + password: "bar".to_string(), + }; + let allowed_users = vec![admin.clone()]; + + let authenticator = Authenticator::new(auth_methods, allowed_users); + + assert!(authenticator.allowed_users.contains(&admin)); + assert!(authenticator + .auth_methods + .contains(&(AuthenticationMethods::NoAuth as u8))); + assert!(authenticator + .auth_methods + .contains(&(AuthenticationMethods::UserPass as u8))); + } + + mod without_user_and_password_auth_enabled { + use super::*; + + #[test] + fn user_pass_authentication_fails() { + let auth_methods: Vec = Vec::new(); // it's empty + + let admin = User { + username: "foo".to_string(), + password: "bar".to_string(), + }; + + let allowed_users = vec![admin.clone()]; + + let authenticator = Authenticator::new(auth_methods, allowed_users); + + assert!(!authenticator.is_allowed(&admin)); + } + } + + #[cfg(test)] + mod with_user_and_password_auth_enabled { + use super::*; + + #[test] + fn allowed_user_passes_authentication_check() { + let mut auth_methods: Vec = Vec::new(); + auth_methods.push(AuthenticationMethods::UserPass as u8); + + let admin = User { + username: "foo".to_string(), + password: "bar".to_string(), + }; + + let allowed_users = vec![admin.clone()]; + + let authenticator = Authenticator::new(auth_methods, allowed_users); + + assert!(authenticator.is_allowed(&admin)); + } + + #[test] + fn disallowed_user_fails_authentication_check() { + let mut auth_methods: Vec = Vec::new(); + auth_methods.push(AuthenticationMethods::UserPass as u8); + + let bad_user = User { + username: "ashy".to_string(), + password: "slashy".to_string(), + }; + + let allowed_users = Vec::new(); + + let authenticator = Authenticator::new(auth_methods, allowed_users); + + assert!(!authenticator.is_allowed(&bad_user)); + } + } +} diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs new file mode 100644 index 0000000000..242bb0ac09 --- /dev/null +++ b/clients/socks5/src/socks/client.rs @@ -0,0 +1,334 @@ +#![forbid(unsafe_code)] + +use rand::RngCore; +use std::{collections::HashMap, net::Shutdown, sync::Arc}; + +use log::*; +use tokio::prelude::*; +use tokio::{self, net::TcpStream}; + +use nymsphinx::addressing::clients::Recipient; + +use crate::client::inbound_messages::InputMessage; +use crate::client::inbound_messages::InputMessageSender; +use futures::{channel::oneshot, lock::Mutex}; + +use super::authentication::{AuthenticationMethods, Authenticator, User}; +use super::request::{SocksCommand, SocksRequest}; +use super::types::{ResponseCode, SocksProxyError}; +use super::{RESERVED, SOCKS_VERSION}; +use simple_socks5_requests::{ConnectionId, Request}; + +/// A client connecting to the Socks proxy server, because +/// it wants to make a Nym-protected outbound request. Typically, this is +/// something like e.g. a wallet app running on your laptop connecting to +/// SphinxSocksServer. +pub(crate) struct SocksClient { + active_streams: ActiveStreams, + stream: TcpStream, + auth_nmethods: u8, + authenticator: Authenticator, + socks_version: u8, + input_sender: InputMessageSender, + connection_id: ConnectionId, + service_provider: Recipient, +} + +type StreamResponseSender = oneshot::Sender>; + +pub(crate) type ActiveStreams = Arc>>; + +impl Drop for SocksClient { + fn drop(&mut self) { + println!("socksclient is going out of scope - the stream is getting dropped!") + } +} + +impl SocksClient { + /// Create a new SOCKClient + pub fn new( + stream: TcpStream, + authenticator: Authenticator, + input_sender: InputMessageSender, + service_provider: Recipient, + active_streams: ActiveStreams, + ) -> Self { + let connection_id = Self::generate_random(); + SocksClient { + active_streams, + connection_id, + stream, + auth_nmethods: 0, + socks_version: 0, + authenticator, + input_sender, + service_provider, + } + } + + fn generate_random() -> ConnectionId { + let mut rng = rand::rngs::OsRng; + rng.next_u64() + } + + // Send an error back to the client + pub async fn error(&mut self, r: ResponseCode) -> Result<(), SocksProxyError> { + self.stream.write_all(&[5, r as u8]).await?; + Ok(()) + } + + /// Shutdown the TcpStream to the client and end the session + pub fn shutdown(&mut self) -> Result<(), SocksProxyError> { + println!("client is shutting down its TCP stream"); + self.stream.shutdown(Shutdown::Both)?; + Ok(()) + } + + /// Initializes the new client, checking that the correct Socks version (5) + /// is in use and that the client is authenticated, then runs the request. + pub async fn run(&mut self) -> Result<(), SocksProxyError> { + debug!("New connection from: {}", self.stream.peer_addr()?.ip()); + let mut header = [0u8; 2]; + // Read a byte from the stream and determine the version being requested + self.stream.read_exact(&mut header).await?; + + self.socks_version = header[0]; + self.auth_nmethods = header[1]; + + // Handle SOCKS4 requests + if header[0] != SOCKS_VERSION { + warn!("Init: Unsupported version: SOCKS{}", self.socks_version); + self.shutdown()?; + } + // Valid SOCKS5 + else { + // Authenticate w/ client + self.authenticate().await?; + // Handle requests + self.handle_request().await?; + } + + Ok(()) + } + + async fn send_request_to_mixnet(&mut self, request: Request) { + self.send_to_mixnet(request.into_bytes()).await; + } + + async fn send_request_to_mixnet_and_get_response(&mut self, request: Request) -> Vec { + self.send_to_mixnet(request.into_bytes()).await; + + // refactor idea: crossbeam oneshot channels are faster + let (sender, receiver) = oneshot::channel(); + let mut active_streams_guard = self.active_streams.lock().await; + if active_streams_guard + .insert(self.connection_id, sender) + .is_some() + { + panic!("there is already an active request with the same id present - it's probably a bug!") + }; + drop(active_streams_guard); + receiver.await.unwrap() + } + + /// Handles a client request. + async fn handle_request(&mut self) -> Result<(), SocksProxyError> { + debug!("Handling CONNECT Command"); + + let request = SocksRequest::from_stream(&mut self.stream).await?; + let remote_address = request.to_string(); + + match request.command { + // Use the Proxy to connect to the specified addr/port + SocksCommand::Connect => { + trace!("Connecting to: {:?}", request.to_socket()); + self.acknowledge_socks5().await; + + let request_data_bytes = + SocksRequest::try_read_request_data(&mut self.stream).await?; + let socks_provider_request = Request::new_connect( + self.connection_id, + remote_address.clone(), + request_data_bytes, + ); + let response_data = self + .send_request_to_mixnet_and_get_response(socks_provider_request) + .await; + self.stream.write_all(&response_data).await.unwrap(); + + loop { + if let Ok(request_data_bytes) = + SocksRequest::try_read_request_data(&mut self.stream).await + { + if request_data_bytes.is_empty() { + break; + } + let socks_provider_request = + Request::new_send(self.connection_id, request_data_bytes); + let response_data = self + .send_request_to_mixnet_and_get_response(socks_provider_request) + .await; + self.stream.write_all(&response_data).await.unwrap(); + } else { + break; + } + } + let socks_provider_request = Request::new_close(self.connection_id); + self.send_request_to_mixnet(socks_provider_request).await; + // TODO: where is connection removed from active connection?? + } + + SocksCommand::Bind => unimplemented!(), // not handled + SocksCommand::UdpAssociate => unimplemented!(), // not handled + }; + Ok(()) + } + + /// Send serialized Socks5 request bytes to the mixnet. The request stream + /// will be chunked up into a series of one or more Sphinx packets and + /// reassembled at the destination service provider at the other end, then + /// sent onwards anonymously. + async fn send_to_mixnet(&self, request_bytes: Vec) { + let input_message = + InputMessage::new_fresh(self.service_provider.clone(), request_bytes, false); + self.input_sender.unbounded_send(input_message).unwrap(); + } + + /// Writes a Socks5 header back to the requesting client's TCP stream, + /// basically saying "I acknowledge your request and am dealing with it". + async fn acknowledge_socks5(&mut self) { + self.stream + .write_all(&[ + SOCKS_VERSION, + ResponseCode::Success as u8, + RESERVED, + 1, + 127, + 0, + 0, + 1, + 0, + 0, + ]) + .await + .unwrap(); + } + + /// Authenticate the incoming request. Each request is checked for its + /// authentication method. A user/password request will extract the + /// username and password from the stream, then check with the Authenticator + /// to see if the resulting user is allowed. + /// + /// A lot of this could probably be put into the the `SocksRequest::from_stream()` + /// constructor, and/or cleaned up with tokio::codec. It's mostly just + /// read-a-byte-or-two. The bytes being extracted look like this: + /// + /// +----+------+----------+------+------------+ + // |ver | ulen | uname | plen | password | + /// +----+------+----------+------+------------+ + /// | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + /// +----+------+----------+------+------------+ + /// + /// Pulling out the stream code into its own home, and moving the if/else logic + /// into the Authenticator (where it'll be more easily testable) + /// would be a good next step. + async fn authenticate(&mut self) -> Result<(), SocksProxyError> { + debug!("Authenticating w/ {}", self.stream.peer_addr()?.ip()); + // Get valid auth methods + let methods = self.get_available_methods().await?; + trace!("methods: {:?}", methods); + + let mut response = [0u8; 2]; + + // Set the version in the response + response[0] = SOCKS_VERSION; + if methods.contains(&(AuthenticationMethods::UserPass as u8)) { + // Set the default auth method (NO AUTH) + response[1] = AuthenticationMethods::UserPass as u8; + + debug!("Sending USER/PASS packet"); + self.stream.write_all(&response).await?; + + let mut header = [0u8; 2]; + + // Read a byte from the stream and determine the version being requested + self.stream.read_exact(&mut header).await?; + + // debug!("Auth Header: [{}, {}]", header[0], header[1]); + + // Username parsing + let ulen = header[1]; + + let mut username = Vec::with_capacity(ulen as usize); + + // For some reason the vector needs to actually be full + for _ in 0..ulen { + username.push(0); + } + + self.stream.read_exact(&mut username).await?; + + // Password Parsing + let mut plen = [0u8; 1]; + self.stream.read_exact(&mut plen).await?; + + let mut password = Vec::with_capacity(plen[0] as usize); + + // For some reason the vector needs to actually be full + for _ in 0..plen[0] { + password.push(0); + } + + self.stream.read_exact(&mut password).await?; + + let username_str = String::from_utf8(username)?; + let password_str = String::from_utf8(password)?; + + let user = User { + username: username_str, + password: password_str, + }; + + // Authenticate passwords + if self.authenticator.is_allowed(&user) { + debug!("Access Granted. User: {}", user.username); + let response = [1, ResponseCode::Success as u8]; + self.stream.write_all(&response).await?; + } else { + debug!("Access Denied. User: {}", user.username); + let response = [1, ResponseCode::Failure as u8]; + self.stream.write_all(&response).await?; + + // Shutdown + self.shutdown()?; + } + + Ok(()) + } else if methods.contains(&(AuthenticationMethods::NoAuth as u8)) { + // set the default auth method (no auth) + response[1] = AuthenticationMethods::NoAuth as u8; + debug!("Sending NOAUTH packet"); + self.stream.write_all(&response).await?; + Ok(()) + } else { + warn!("Client has no suitable authentication methods!"); + response[1] = AuthenticationMethods::NoMethods as u8; + self.stream.write_all(&response).await?; + self.shutdown()?; + Err(ResponseCode::Failure.into()) + } + } + + /// Return the available methods based on `self.auth_nmethods` + async fn get_available_methods(&mut self) -> Result, SocksProxyError> { + let mut methods: Vec = Vec::with_capacity(self.auth_nmethods as usize); + for _ in 0..self.auth_nmethods { + let mut method = [0u8; 1]; + self.stream.read_exact(&mut method).await?; + if self.authenticator.auth_methods.contains(&method[0]) { + methods.append(&mut method.to_vec()); + } + } + Ok(methods) + } +} diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs new file mode 100644 index 0000000000..3b785ec8be --- /dev/null +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -0,0 +1,79 @@ +use super::client::ActiveStreams; +use crate::client::received_buffer::ReconstructedMessagesReceiver; +use crate::client::received_buffer::{ReceivedBufferMessage, ReceivedBufferRequestSender}; +use futures::channel::mpsc; +use futures::StreamExt; +use log::*; +use nymsphinx::receiver::ReconstructedMessage; +use simple_socks5_requests::Response; + +pub(crate) struct MixnetResponseListener { + buffer_requester: ReceivedBufferRequestSender, + mix_response_receiver: ReconstructedMessagesReceiver, + active_streams: ActiveStreams, +} + +impl Drop for MixnetResponseListener { + fn drop(&mut self) { + self.buffer_requester + .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) + .expect("the buffer request failed!") + } +} + +impl MixnetResponseListener { + pub(crate) fn new( + buffer_requester: ReceivedBufferRequestSender, + active_streams: ActiveStreams, + ) -> Self { + let (mix_response_sender, mix_response_receiver) = mpsc::unbounded(); + buffer_requester + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(mix_response_sender)) + .unwrap(); + + MixnetResponseListener { + active_streams, + buffer_requester, + mix_response_receiver, + } + } + + async fn on_message(&self, bytes: Vec) { + let reconstructed_message = + ReconstructedMessage::try_from_bytes(&bytes).expect("todo: error handling"); + let raw_message = reconstructed_message.message; + if reconstructed_message.reply_SURB.is_some() { + println!("this message had a surb - we didn't do anything with it"); + } + + let response = match Response::try_from_bytes(&raw_message) { + Err(err) => { + warn!("failed to parse received response - {:?}", err); + return; + } + Ok(data) => data, + }; + + let mut active_streams_guard = self.active_streams.lock().await; + + // `remove` gives back the entry (assuming it exists). There's no reason + // for it to persist after we send data back + if let Some(stream_receiver) = active_streams_guard.remove(&response.connection_id) { + stream_receiver.send(response.data).unwrap() + } else { + warn!( + "no connection_id exists with id: {:?}", + &response.connection_id + ) + } + } + + pub(crate) async fn run(&mut self) { + while let Some(received_responses) = self.mix_response_receiver.next().await { + for received_response in received_responses { + self.on_message(received_response.into_bytes()).await; + } + } + println!("We should never see this message"); + } +} diff --git a/clients/socks5/src/socks/mod.rs b/clients/socks5/src/socks/mod.rs new file mode 100644 index 0000000000..20dd99f125 --- /dev/null +++ b/clients/socks5/src/socks/mod.rs @@ -0,0 +1,14 @@ +#![forbid(unsafe_code)] + +pub mod authentication; +mod client; +pub(crate) mod mixnet_responses; +mod request; +pub mod server; +pub mod types; +pub mod utils; + +/// Version of socks +const SOCKS_VERSION: u8 = 0x05; + +const RESERVED: u8 = 0x00; diff --git a/clients/socks5/src/socks/request.rs b/clients/socks5/src/socks/request.rs new file mode 100644 index 0000000000..7ff1dd11fe --- /dev/null +++ b/clients/socks5/src/socks/request.rs @@ -0,0 +1,162 @@ +use super::types::{AddrType, ResponseCode, SocksProxyError}; +use super::{utils, SOCKS_VERSION}; +use log::*; +use std::net::SocketAddr; +use tokio::net::TcpStream; +use tokio::prelude::*; + +/// A Socks5 request hitting the proxy. +pub(crate) struct SocksRequest { + pub version: u8, + pub command: SocksCommand, + pub addr_type: AddrType, + pub addr: Vec, + pub port: u16, +} + +impl SocksRequest { + /// Parse a SOCKS5 request from a TcpStream + pub async fn from_stream(stream: &mut TcpStream) -> Result { + let mut packet = [0u8; 4]; + // Read a byte from the stream and determine the version being requested + stream.read_exact(&mut packet).await?; + + if packet[0] != SOCKS_VERSION { + warn!("from_stream Unsupported version: SOCKS{}", packet[0]); + stream.shutdown(); + } + + // Get command + let mut command: SocksCommand = SocksCommand::Connect; + match SocksCommand::from(packet[1] as usize) { + Some(com) => { + command = com; + Ok(()) + } + None => { + warn!("Invalid Command"); + stream.shutdown(); + Err(ResponseCode::CommandNotSupported) + } + }?; + + // DST.address + + let mut addr_type: AddrType = AddrType::V6; + match AddrType::from(packet[3] as usize) { + Some(addr) => { + addr_type = addr; + Ok(()) + } + None => { + error!("No Addr"); + stream.shutdown(); + Err(ResponseCode::AddrTypeNotSupported) + } + }?; + + trace!("Getting Addr"); + // Get Addr from addr_type and stream + let addr: Result, SocksProxyError> = match addr_type { + AddrType::Domain => { + let mut domain_length = [0u8; 1]; + stream.read_exact(&mut domain_length).await?; + + let mut domain = vec![0u8; domain_length[0] as usize]; + stream.read_exact(&mut domain).await?; + + Ok(domain) + } + AddrType::V4 => { + let mut addr = [0u8; 4]; + stream.read_exact(&mut addr).await?; + Ok(addr.to_vec()) + } + AddrType::V6 => { + let mut addr = [0u8; 16]; + stream.read_exact(&mut addr).await?; + Ok(addr.to_vec()) + } + }; + + let addr = addr?; + + // read DST.port + let mut port = [0u8; 2]; + stream.read_exact(&mut port).await?; + // Merge two u8s into u16 + let port = (u16::from(port[0]) << 8) | u16::from(port[1]); + + // Return parsed request + Ok(SocksRequest { + version: packet[0], + command, + addr_type, + addr, + port, + }) + } + + /// Print out the address and port to a String. + /// This might return domain:port, ipv6:port, or ipv4:port. + pub(crate) fn to_string(&self) -> String { + let address = utils::pretty_print_addr(&self.addr_type, &self.addr); + format!("{}:{}", address, self.port) + } + + /// Convert the request object to a SocketAddr + pub(crate) fn to_socket(&self) -> Result, SocksProxyError> { + utils::addr_to_socket(&self.addr_type, &self.addr, self.port) + } + + /// Attempts to read data from the Socks5 request stream. Times out and + /// returns what it's got if no data is read for the timeout_duration + pub(crate) async fn try_read_request_data( + reader: &mut R, + ) -> io::Result> { + let timeout_duration = std::time::Duration::from_millis(500); + let mut data = Vec::new(); + let mut timeout = tokio::time::delay_for(timeout_duration); + loop { + let mut buf = [0u8; 1024]; + tokio::select! { + _ = &mut timeout => { + println!("we timed out!"); + return Ok(data) + } + read_data = reader.read(&mut buf) => { + match read_data { + Err(err) => return Err(err), + Ok(0) => return Ok(data), + Ok(n) => { + let now = timeout.deadline(); + let next = now + timeout_duration; + timeout.reset(next); + data.extend_from_slice(&buf[..n]) + } + } + } + } + } + } +} + +/// SOCK5 CMD type +#[derive(Debug)] +pub(crate) enum SocksCommand { + Connect = 0x01, + Bind = 0x02, + UdpAssociate = 0x3, +} + +impl SocksCommand { + /// Parse bytes to SocksCommand + fn from(n: usize) -> Option { + match n { + 1 => Some(SocksCommand::Connect), + 2 => Some(SocksCommand::Bind), + 3 => Some(SocksCommand::UdpAssociate), + _ => None, + } + } +} diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs new file mode 100644 index 0000000000..1c36e429e4 --- /dev/null +++ b/clients/socks5/src/socks/server.rs @@ -0,0 +1,104 @@ +use super::authentication::Authenticator; +use super::client::SocksClient; +use super::{ + mixnet_responses::MixnetResponseListener, + types::{ResponseCode, SocksProxyError}, +}; +use crate::client::{ + inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, +}; +use futures::lock::Mutex; +use log::*; +use nymsphinx::addressing::clients::Recipient; +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; + +/// A Socks5 server that listens for connections. +pub struct SphinxSocksServer { + authenticator: Authenticator, + listening_address: SocketAddr, + service_provider: Recipient, +} + +impl SphinxSocksServer { + /// Create a new SphinxSocks instance + pub(crate) fn new( + port: u16, + ip: &str, + authenticator: Authenticator, + service_provider: Recipient, + ) -> Self { + info!("Listening on {}:{}", ip, port); + SphinxSocksServer { + authenticator, + listening_address: format!("{}:{}", ip, port).parse().unwrap(), + service_provider, + } + } + + /// Set up the listener and initiate connection handling when something + /// connects to the server. + pub(crate) async fn serve( + &mut self, + input_sender: InputMessageSender, + buffer_requester: ReceivedBufferRequestSender, + ) -> Result<(), SocksProxyError> { + info!("Serving Connections..."); + let mut listener = TcpListener::bind(self.listening_address).await.unwrap(); + + // todo: probably just create a wrapper type for this guy + let active_streams = Arc::new(Mutex::new(HashMap::new())); + + let mut mixnet_response_listener = + MixnetResponseListener::new(buffer_requester, Arc::clone(&active_streams)); + + tokio::spawn(async move { + mixnet_response_listener.run().await; + }); + + loop { + if let Ok((stream, _remote)) = listener.accept().await { + // TODO Optimize this + let mut client = SocksClient::new( + stream, + self.authenticator.clone(), + input_sender.clone(), + self.service_provider.clone(), + Arc::clone(&active_streams), + ); + + tokio::spawn(async move { + { + match client.run().await { + Ok(_) => {} + Err(error) => { + error!("Error! {}", error); + let error_text = format!("{}", error); + + let response: ResponseCode; + + if error_text.contains("Host") { + response = ResponseCode::HostUnreachable; + } else if error_text.contains("Network") { + response = ResponseCode::NetworkUnreachable; + } else if error_text.contains("ttl") { + response = ResponseCode::TtlExpired + } else { + response = ResponseCode::Failure + } + + if client.error(response).await.is_err() { + warn!("Failed to send error code"); + }; + if client.shutdown().is_err() { + warn!("Failed to shutdown TcpStream"); + }; + } + }; + // client gets dropped here + } + }); + } + } + } +} diff --git a/clients/socks5/src/socks/types.rs b/clients/socks5/src/socks/types.rs new file mode 100644 index 0000000000..cb3c9af5ef --- /dev/null +++ b/clients/socks5/src/socks/types.rs @@ -0,0 +1,62 @@ +use snafu::Snafu; +#[derive(Debug, Snafu)] +/// Possible SOCKS5 Response Codes +pub(crate) enum ResponseCode { + Success = 0x00, + #[snafu(display("SOCKS5 Server Failure"))] + Failure = 0x01, + #[snafu(display("SOCKS5 Rule failure"))] + RuleFailure = 0x02, + #[snafu(display("network unreachable"))] + NetworkUnreachable = 0x03, + #[snafu(display("host unreachable"))] + HostUnreachable = 0x04, + #[snafu(display("connection refused"))] + ConnectionRefused = 0x05, + #[snafu(display("TTL expired"))] + TtlExpired = 0x06, + #[snafu(display("Command not supported"))] + CommandNotSupported = 0x07, + #[snafu(display("Addr Type not supported"))] + AddrTypeNotSupported = 0x08, +} + +#[derive(Debug)] +pub enum SocksProxyError { + GenericError(String), +} + +impl std::fmt::Display for SocksProxyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "foomp") + } +} + +impl From for SocksProxyError +where + E: std::error::Error, +{ + fn from(err: E) -> Self { + SocksProxyError::GenericError(err.to_string()) + } +} + +/// DST.addr variant types +#[derive(PartialEq)] +pub(crate) enum AddrType { + V4 = 0x01, + Domain = 0x03, + V6 = 0x04, +} + +impl AddrType { + /// Parse Byte to Command + pub(crate) fn from(n: usize) -> Option { + match n { + 1 => Some(AddrType::V4), + 3 => Some(AddrType::Domain), + 4 => Some(AddrType::V6), + _ => None, + } + } +} diff --git a/clients/socks5/src/socks/utils.rs b/clients/socks5/src/socks/utils.rs new file mode 100644 index 0000000000..2d0aa837d5 --- /dev/null +++ b/clients/socks5/src/socks/utils.rs @@ -0,0 +1,71 @@ +use super::types::{AddrType, SocksProxyError}; +use log::*; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}; + +/// Convert an address and AddrType to a SocketAddr +pub(crate) fn addr_to_socket( + addr_type: &AddrType, + addr: &[u8], + port: u16, +) -> Result, SocksProxyError> { + match addr_type { + AddrType::V6 => { + let new_addr = (0..8) + .map(|x| { + trace!("{} and {}", x * 2, (x * 2) + 1); + (u16::from(addr[(x * 2)]) << 8) | u16::from(addr[(x * 2) + 1]) + }) + .collect::>(); + + Ok(vec![SocketAddr::from(SocketAddrV6::new( + Ipv6Addr::new( + new_addr[0], + new_addr[1], + new_addr[2], + new_addr[3], + new_addr[4], + new_addr[5], + new_addr[6], + new_addr[7], + ), + port, + 0, + 0, + ))]) + } + AddrType::V4 => Ok(vec![SocketAddr::from(SocketAddrV4::new( + Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]), + port, + ))]), + AddrType::Domain => { + let mut domain = String::from_utf8_lossy(&addr[..]).to_string(); + domain.push_str(&":"); + domain.push_str(&port.to_string()); + + Ok(domain.to_socket_addrs()?.collect()) + } + } +} + +/// Convert an AddrType and address to String +pub(crate) fn pretty_print_addr(addr_type: &AddrType, addr: &[u8]) -> String { + match addr_type { + AddrType::Domain => String::from_utf8_lossy(addr).to_string(), + AddrType::V4 => addr + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join("."), + AddrType::V6 => { + let addr_16 = (0..8) + .map(|x| (u16::from(addr[(x * 2)]) << 8) | u16::from(addr[(x * 2) + 1])) + .collect::>(); + + addr_16 + .iter() + .map(|x| format!("{:x}", x)) + .collect::>() + .join(":") + } + } +} diff --git a/common/client-libs/directory-client/models/src/presence/topology.rs b/common/client-libs/directory-client/models/src/presence/topology.rs index e79e76af77..98d8219d18 100644 --- a/common/client-libs/directory-client/models/src/presence/topology.rs +++ b/common/client-libs/directory-client/models/src/presence/topology.rs @@ -99,9 +99,7 @@ mod converting_mixnode_presence_into_topology_mixnode { }; let result: Result = mix_presence.try_into(); - assert!(result.is_err()) // This fails only for me. Why? - // ¯\_(ツ)_/¯ - works on my machine (and travis) - // Is it still broken? + assert!(result.is_err()); } #[test] diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 588db8978e..73f2ce00c8 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -508,7 +508,7 @@ impl<'a, R> GatewayClient<'static, R> { } let shared_key = self.perform_initial_authentication().await?; - // that call is NON-blocking + // this call is NON-blocking self.start_listening_for_mixnet_messages()?; Ok(shared_key) diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 54de22108b..ed0aeb5613 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -625,7 +625,7 @@ mod fragment { 10, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); assert!(Fragment::try_new( @@ -635,7 +635,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); @@ -646,7 +646,7 @@ mod fragment { 10, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); assert!(Fragment::try_new( @@ -656,7 +656,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); } @@ -678,7 +678,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -688,7 +688,7 @@ mod fragment { 5, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); @@ -699,7 +699,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -709,7 +709,7 @@ mod fragment { 5, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -719,7 +719,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); @@ -730,7 +730,7 @@ mod fragment { 1, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -740,7 +740,7 @@ mod fragment { 5, None, None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); } @@ -761,7 +761,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); assert!(Fragment::try_new( @@ -771,7 +771,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); assert!(Fragment::try_new( @@ -781,7 +781,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); assert!(Fragment::try_new( @@ -791,7 +791,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); @@ -802,7 +802,7 @@ mod fragment { u8::max_value(), None, Some(link_id), - max_plaintext_size() + max_plaintext_size(), ) .is_ok()); } @@ -823,7 +823,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -833,7 +833,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -843,7 +843,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -853,7 +853,7 @@ mod fragment { 1, Some(link_id), None, - max_plaintext_size() + max_plaintext_size(), ) .is_err()); @@ -864,7 +864,7 @@ mod fragment { u8::max_value(), None, Some(link_id), - max_plaintext_size() + max_plaintext_size(), ) .is_err()); assert!(Fragment::try_new( @@ -874,7 +874,7 @@ mod fragment { u8::max_value(), None, Some(link_id), - max_plaintext_size() + max_plaintext_size(), ) .is_err()); @@ -885,7 +885,7 @@ mod fragment { u8::max_value(), None, Some(link_id), - max_plaintext_size() + max_plaintext_size(), ) .is_err()); } @@ -989,7 +989,7 @@ mod fragment_header { u8::max_value(), u8::max_value(), None, - Some(0) + Some(0), ) .is_err()); } diff --git a/common/nymsphinx/src/preparer.rs b/common/nymsphinx/src/preparer.rs index 40c58d95f9..23e4905f9a 100644 --- a/common/nymsphinx/src/preparer.rs +++ b/common/nymsphinx/src/preparer.rs @@ -239,6 +239,7 @@ where // serialize fragment and encrypt its content let mut chunk_data = fragment.into_bytes(); + let zero_iv = stream_cipher::zero_iv::(); stream_cipher::encrypt_in_place::( &shared_key, diff --git a/scripts/start_local_network.sh b/scripts/start_local_network.sh index 0c4ec1c118..3e4929af0c 100755 --- a/scripts/start_local_network.sh +++ b/scripts/start_local_network.sh @@ -37,7 +37,7 @@ kill_old export RUST_LOG=warning # NOTE: If we wanted to suppress stdout and stderr, replace `&` with `> /dev/null 2>&1 &` in the `run` -cargo run --bin nym-gateway -- init --id gateway-local --mix-host 127.0.0.1:10000 --clients-host 127.0.0.1:10001 --directory $DIR +# cargo run --bin nym-gateway -- init --id gateway-local --mix-host 127.0.0.1:10000 --clients-host 127.0.0.1:10001 --directory $DIR cargo run --bin nym-gateway -- run --id gateway-local & sleep 1 diff --git a/service-providers/simple-socks5/Cargo.toml b/service-providers/simple-socks5/Cargo.toml new file mode 100644 index 0000000000..d59def383d --- /dev/null +++ b/service-providers/simple-socks5/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "simple-socks5" +version = "0.1.0" +authors = ["Dave Hrycyszyn "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bs58 = "0.3.0" +clap = "2.33.0" +futures = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["async-await", "sink", "std"] } +log = "0.4" +pretty_env_logger = "0.3" +tokio = { version = "0.2", features = ["full"] } +tokio-tungstenite = "0.10.1" + +nymsphinx = { path = "../../common/nymsphinx" } +simple-socks5-requests = { path = "simple-socks5-requests" } \ No newline at end of file diff --git a/service-providers/simple-socks5/simple-socks5-requests/Cargo.toml b/service-providers/simple-socks5/simple-socks5-requests/Cargo.toml new file mode 100644 index 0000000000..bb416f5d42 --- /dev/null +++ b/service-providers/simple-socks5/simple-socks5-requests/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "simple-socks5-requests" +version = "0.1.0" +authors = ["Dave Hrycyszyn "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/service-providers/simple-socks5/simple-socks5-requests/src/lib.rs b/service-providers/simple-socks5/simple-socks5-requests/src/lib.rs new file mode 100644 index 0000000000..6415885625 --- /dev/null +++ b/service-providers/simple-socks5/simple-socks5-requests/src/lib.rs @@ -0,0 +1,5 @@ +pub mod request; +pub mod response; + +pub use request::*; +pub use response::*; diff --git a/service-providers/simple-socks5/simple-socks5-requests/src/request.rs b/service-providers/simple-socks5/simple-socks5-requests/src/request.rs new file mode 100644 index 0000000000..007ccd4839 --- /dev/null +++ b/service-providers/simple-socks5/simple-socks5-requests/src/request.rs @@ -0,0 +1,348 @@ +use std::convert::TryFrom; + +pub type ConnectionId = u64; +pub type RemoteAddress = String; + +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum RequestFlag { + Connect = 0, + Send = 1, + Close = 2, +} + +#[derive(Debug, PartialEq)] +pub enum RequestError { + AddressLengthTooShort, + AddressTooShort, + ConnectionIdTooShort, + NoData, + UnknownRequestFlag, +} + +impl TryFrom for RequestFlag { + type Error = RequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), + _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), + _ if value == (RequestFlag::Close as u8) => Ok(Self::Close), + _ => Err(RequestError::UnknownRequestFlag), + } + } +} + +/// A request from a SOCKS5 client that a Nym Socks5 service provider should +/// take an action for an application using a (probably local) Nym Socks5 proxy. +#[derive(Debug)] +pub enum Request { + /// Start a new TCP connection to the specified `RemoteAddress` and send + /// the request data up the connection. + Connect(ConnectionId, RemoteAddress, Vec), + + /// Re-use an existing TCP connection, sending more request data up it. + Send(ConnectionId, Vec), + + /// Close an existing TCP connection. + Close(ConnectionId), +} + +impl Request { + /// Construct a new Request::Connect instance + pub fn new_connect( + conn_id: ConnectionId, + remote_addr: RemoteAddress, + data: Vec, + ) -> Request { + Request::Connect(conn_id, remote_addr, data) + } + + /// Construct a new Request::Send instance + pub fn new_send(conn_id: ConnectionId, data: Vec) -> Request { + Request::Send(conn_id, data) + } + + /// Construct a new Request::Close instance + pub fn new_close(conn_id: ConnectionId) -> Request { + Request::Close(conn_id) + } + + /// Deserialize the request type, connection id, destination address and port, + /// and the request body from bytes. + /// + /// Serialized bytes looks like this: + /// + /// -------------------------------------------------------------------------------------- + /// request_flag | connection_id | address_length | remote_address_bytes | request_data | + /// 1 | 8 | 2 | address_length | ... | + /// -------------------------------------------------------------------------------------- + /// + /// The request_flag tells us whether this is a new connection request (`new_connect`), + /// an already-established connection we should send up (`new_send`), or + /// a request to close an established connection (`new_close`). + pub fn try_from_bytes(b: &[u8]) -> Result { + // each request needs to at least contain flag and ConnectionId + if b.is_empty() { + return Err(RequestError::NoData); + } + + if b.len() < 9 { + return Err(RequestError::ConnectionIdTooShort); + } + let connection_id = u64::from_be_bytes([b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8]]); + match RequestFlag::try_from(b[0])? { + RequestFlag::Connect => { + let connect_request_bytes = &b[9..]; + + // we need to be able to read at least 2 bytes that specify address length + if connect_request_bytes.len() < 2 { + return Err(RequestError::AddressLengthTooShort); + } + + let address_length = + u16::from_be_bytes([connect_request_bytes[0], connect_request_bytes[1]]) + as usize; + + if connect_request_bytes.len() < 2 + address_length { + return Err(RequestError::AddressTooShort); + } + + let address_start = 2; + let address_end = address_start + address_length; + let address_bytes = &connect_request_bytes[address_start..address_end]; + let remote_address = String::from_utf8_lossy(&address_bytes).to_string(); + + let request_data = &connect_request_bytes[address_end..]; + Ok(Request::Connect( + connection_id, + remote_address, + request_data.to_vec(), + )) + } + RequestFlag::Send => Ok(Request::Send(connection_id, b[9..].as_ref().to_vec())), + RequestFlag::Close => Ok(Request::Close(connection_id)), + } + } + + /// Serialize a Socks5 request into bytes, so that it can be packed inside + /// a Sphinx packet, sent through the mixnet, and deserialized by the Socks5 + /// service provider which will make the request. + pub fn into_bytes(self) -> Vec { + match self { + Request::Connect(conn_id, remote_address, data) => { + let remote_address_bytes = remote_address.into_bytes(); + let remote_address_bytes_len = remote_address_bytes.len() as u16; + std::iter::once(RequestFlag::Connect as u8) + .chain(conn_id.to_be_bytes().iter().cloned()) + .chain(remote_address_bytes_len.to_be_bytes().iter().cloned()) + .chain(remote_address_bytes.into_iter()) + .chain(data.into_iter()) + .collect() + } + Request::Send(conn_id, data) => std::iter::once(RequestFlag::Send as u8) + .chain(conn_id.to_be_bytes().iter().cloned()) + .chain(data.into_iter()) + .collect(), + Request::Close(conn_id) => std::iter::once(RequestFlag::Close as u8) + .chain(conn_id.to_be_bytes().iter().cloned()) + .collect(), + } + } +} + +#[cfg(test)] +mod request_deserialization_tests { + use super::*; + + mod all_request_types { + use super::*; + + #[test] + fn returns_error_when_zero_bytes() { + let request_bytes = Vec::new(); + assert_eq!( + RequestError::NoData, + Request::try_from_bytes(&request_bytes).unwrap_err() + ); + } + + #[test] + fn returns_error_when_connection_id_too_short() { + let request_bytes = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7].to_vec(); // 7 bytes connection id + assert_eq!( + RequestError::ConnectionIdTooShort, + Request::try_from_bytes(&request_bytes).unwrap_err() + ); + } + } + + #[cfg(test)] + mod sending_data_over_a_new_connection { + use super::*; + + #[test] + fn returns_error_when_address_length_is_too_short() { + let request_bytes1 = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8].to_vec(); // 8 bytes connection id, 0 bytes address length (2 were expected) + let request_bytes2 = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0].to_vec(); // 8 bytes connection id, 1 bytes address length (2 were expected) + + assert_eq!( + RequestError::AddressLengthTooShort, + Request::try_from_bytes(&request_bytes1).unwrap_err() + ); + + assert_eq!( + RequestError::AddressLengthTooShort, + Request::try_from_bytes(&request_bytes2).unwrap_err() + ); + } + + #[test] + fn returns_error_when_address_too_short_for_given_address_length() { + let request_bytes = [RequestFlag::Connect as u8, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1].to_vec(); // 8 bytes connection id, 2 bytes address length, missing address + assert_eq!( + RequestError::AddressTooShort, + Request::try_from_bytes(&request_bytes).unwrap_err() + ); + } + + #[test] + fn works_when_request_is_sized_properly_even_without_data() { + // this one has "foo.com" remote address, correct 8 bytes of connection_id, and 0 bytes request data + let request_bytes = [ + RequestFlag::Connect as u8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 0, + 7, + 102, + 111, + 111, + 46, + 99, + 111, + 109, + ] + .to_vec(); + let request = Request::try_from_bytes(&request_bytes).unwrap(); + match request { + Request::Connect(conn_id, remote_address, data) => { + assert_eq!("foo.com".to_string(), remote_address); + assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), conn_id); + assert_eq!(Vec::::new(), data); + } + _ => unreachable!(), + } + } + + #[test] + fn works_when_request_is_sized_properly_and_has_data() { + // this one has a 1-byte remote address, correct 16 bytes of connection_id, and 3 bytes request data + let request_bytes = [ + RequestFlag::Connect as u8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 0, + 7, + 102, + 111, + 111, + 46, + 99, + 111, + 109, + 255, + 255, + 255, + ] + .to_vec(); + + let request = Request::try_from_bytes(&request_bytes).unwrap(); + match request { + Request::Connect(conn_id, remote_address, data) => { + assert_eq!("foo.com".to_string(), remote_address); + assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), conn_id); + assert_eq!(vec![255, 255, 255], data); + } + _ => unreachable!(), + } + } + } + + #[cfg(test)] + mod sending_additional_data_over_an_existing_connection { + use super::*; + + #[test] + fn works_when_request_is_sized_properly_even_without_data() { + // correct 8 bytes of connection_id, and 0 bytes request data + let request_bytes = [RequestFlag::Send as u8, 1, 2, 3, 4, 5, 6, 7, 8].to_vec(); + let request = Request::try_from_bytes(&request_bytes).unwrap(); + match request { + Request::Send(conn_id, data) => { + assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), conn_id); + assert_eq!(Vec::::new(), data); + } + _ => unreachable!(), + } + } + + #[test] + fn works_when_request_is_sized_properly_and_has_data() { + // correct 8 bytes of connection_id, and 3 bytes request data (all 255) + let request_bytes = [ + RequestFlag::Send as u8, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 255, + 255, + 255, + ] + .to_vec(); + + let request = Request::try_from_bytes(&request_bytes).unwrap(); + match request { + Request::Send(conn_id, data) => { + assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), conn_id); + assert_eq!(vec![255, 255, 255], data); + } + _ => unreachable!(), + } + } + } + + #[cfg(test)] + mod connection_close_requests { + use super::*; + + #[test] + fn can_be_constructed() { + let request_bytes = [RequestFlag::Close as u8, 1, 2, 3, 4, 5, 6, 7, 8].to_vec(); + let request = Request::try_from_bytes(&request_bytes).unwrap(); + match request { + Request::Close(conn_id) => { + assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), conn_id); + } + _ => unreachable!(), + } + } + } +} diff --git a/service-providers/simple-socks5/simple-socks5-requests/src/response.rs b/service-providers/simple-socks5/simple-socks5-requests/src/response.rs new file mode 100644 index 0000000000..3f0fa23909 --- /dev/null +++ b/service-providers/simple-socks5/simple-socks5-requests/src/response.rs @@ -0,0 +1,108 @@ +use crate::ConnectionId; + +#[derive(Debug, PartialEq)] +pub enum ResponseError { + ConnectionIdTooShort, + NoData, +} +/// A remote network response retrieved by the Socks5 service provider. This +/// can be serialized and sent back through the mixnet to the requesting +/// application. +#[derive(Debug)] +pub struct Response { + pub data: Vec, + pub connection_id: ConnectionId, +} + +impl Response { + /// Constructor for responses + pub fn new(connection_id: ConnectionId, data: Vec) -> Self { + Response { + connection_id, + data, + } + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.is_empty() { + return Err(ResponseError::NoData); + } + + if b.len() < 8 { + return Err(ResponseError::ConnectionIdTooShort); + } + + let mut connection_id_bytes = b.to_vec(); + let data = connection_id_bytes.split_off(8); + + let connection_id = u64::from_be_bytes([ + connection_id_bytes[0], + connection_id_bytes[1], + connection_id_bytes[2], + connection_id_bytes[3], + connection_id_bytes[4], + connection_id_bytes[5], + connection_id_bytes[6], + connection_id_bytes[7], + ]); + + let response = Response::new(connection_id, data); + Ok(response) + } + + /// Serializes the response into bytes so that it can be sent back through + /// the mixnet to the requesting application. + pub fn into_bytes(self) -> Vec { + self.connection_id + .to_be_bytes() + .iter() + .cloned() + .chain(self.data.into_iter()) + .collect() + } +} + +#[cfg(test)] +mod constructing_socks5_responses_from_bytes { + use super::*; + + #[test] + fn fails_when_zero_bytes_are_supplied() { + let response_bytes = Vec::new(); + + assert_eq!( + ResponseError::NoData, + Response::try_from_bytes(&response_bytes).unwrap_err() + ); + } + + #[test] + fn fails_when_connection_id_bytes_are_too_short() { + let response_bytes = vec![0, 1, 2, 3, 4, 5, 6]; + assert_eq!( + ResponseError::ConnectionIdTooShort, + Response::try_from_bytes(&response_bytes).unwrap_err() + ); + } + + #[test] + fn works_when_there_is_no_data() { + let response_bytes = vec![0, 1, 2, 3, 4, 5, 6, 7]; + let expected = Response::new(u64::from_be_bytes([0, 1, 2, 3, 4, 5, 6, 7]), Vec::new()); + let actual = Response::try_from_bytes(&response_bytes).unwrap(); + assert_eq!(expected.connection_id, actual.connection_id); + assert_eq!(expected.data, actual.data); + } + + #[test] + fn works_when_there_is_data() { + let response_bytes = vec![0, 1, 2, 3, 4, 5, 6, 7, 255, 255, 255]; + let expected = Response::new( + u64::from_be_bytes([0, 1, 2, 3, 4, 5, 6, 7]), + vec![255, 255, 255], + ); + let actual = Response::try_from_bytes(&response_bytes).unwrap(); + assert_eq!(expected.connection_id, actual.connection_id); + assert_eq!(expected.data, actual.data); + } +} diff --git a/service-providers/simple-socks5/src/connection.rs b/service-providers/simple-socks5/src/connection.rs new file mode 100644 index 0000000000..6a0bf31e01 --- /dev/null +++ b/service-providers/simple-socks5/src/connection.rs @@ -0,0 +1,58 @@ +use simple_socks5_requests::{ConnectionId, RemoteAddress}; +use tokio::net::TcpStream; +use tokio::prelude::*; + +/// A TCP connection between the Socks5 service provider, which makes +/// outbound requests on behalf of users and returns the responses through +/// the mixnet. +#[derive(Debug)] +pub(crate) struct Connection { + id: ConnectionId, + address: RemoteAddress, + conn: TcpStream, +} + +impl Connection { + pub(crate) async fn new( + id: ConnectionId, + address: RemoteAddress, + initial_data: &[u8], + ) -> io::Result { + let conn = TcpStream::connect(&address).await?; + let mut connection = Connection { id, address, conn }; + connection.send_data(&initial_data).await?; + Ok(connection) + } + + pub(crate) async fn send_data(&mut self, data: &[u8]) -> io::Result<()> { + self.conn.write_all(&data).await + } + + /// Read response data by looping, waiting for anything we get back from the + /// remote server. Returns once it times out or the connection closes. + pub(crate) async fn try_read_response_data(&mut self) -> io::Result> { + let timeout_duration = std::time::Duration::from_millis(500); + let mut data = Vec::new(); + let mut timeout = tokio::time::delay_for(timeout_duration); + loop { + let mut buf = [0u8; 1024]; + tokio::select! { + _ = &mut timeout => { + return Ok(data) // we return all response data on timeout + } + read_data = self.conn.read(&mut buf) => { + match read_data { + Err(err) => return Err(err), + Ok(0) => return Ok(data), + Ok(n) => { + let now = timeout.deadline(); + let next = now + timeout_duration; + timeout.reset(next); + data.extend_from_slice(&buf[..n]) + } + } + } + } + } + } +} diff --git a/service-providers/simple-socks5/src/controller.rs b/service-providers/simple-socks5/src/controller.rs new file mode 100644 index 0000000000..2f98aa5e97 --- /dev/null +++ b/service-providers/simple-socks5/src/controller.rs @@ -0,0 +1,88 @@ +use crate::connection::Connection; +use simple_socks5_requests::{ConnectionId, RemoteAddress, Request, Response}; +use std::collections::HashMap; +use std::io; + +#[derive(Debug)] +pub enum ConnectionError { + ConnectionFailed(io::Error), + MissingConnection, +} + +impl From for ConnectionError { + fn from(e: io::Error) -> Self { + ConnectionError::ConnectionFailed(e) + } +} + +pub(crate) struct Controller { + open_connections: HashMap, +} + +impl Controller { + pub(crate) fn new() -> Self { + Controller { + open_connections: HashMap::new(), + } + } + + pub(crate) async fn process_request( + &mut self, + request: Request, + ) -> Result, ConnectionError> { + match request { + Request::Connect(conn_id, remote_addr, data) => { + let response = self + .create_new_connection(conn_id, remote_addr, data) + .await?; + Ok(Some(response)) + } + Request::Send(conn_id, data) => { + let response = self.send_to_connection(conn_id, data).await?; + Ok(Some(response)) + } + Request::Close(conn_id) => { + self.close_connection(conn_id)?; + Ok(None) + } + } + } + + async fn create_new_connection( + &mut self, + conn_id: ConnectionId, + remote_addr: RemoteAddress, + init_data: Vec, + ) -> Result { + let mut connection = Connection::new(conn_id, remote_addr, &init_data).await?; + + let response_data = connection.try_read_response_data().await?; + self.open_connections.insert(conn_id, connection); + Ok(Response::new(conn_id, response_data)) + } + + async fn send_to_connection( + &mut self, + conn_id: ConnectionId, + data: Vec, + ) -> Result { + let connection = self + .open_connections + .get_mut(&conn_id) + .ok_or_else(|| ConnectionError::MissingConnection)?; + connection.send_data(&data).await?; + + let response_data = connection.try_read_response_data().await?; + Ok(Response::new(conn_id, response_data)) + } + + fn close_connection(&mut self, conn_id: ConnectionId) -> Result<(), ConnectionError> { + match self.open_connections.remove(&conn_id) { + // I *think* connection is closed implicitly on drop, but I'm not 100% sure! + Some(_conn) => (), + None => log::error!("tried to close non-existent connection - {}", conn_id), + } + + Ok(()) + } +} diff --git a/service-providers/simple-socks5/src/core.rs b/service-providers/simple-socks5/src/core.rs new file mode 100644 index 0000000000..1a36f0181a --- /dev/null +++ b/service-providers/simple-socks5/src/core.rs @@ -0,0 +1,80 @@ +use crate::{controller::Controller, websocket}; +use futures::SinkExt; +use futures_util::StreamExt; +use nymsphinx::params::MessageType; +use nymsphinx::receiver::ReconstructedMessage; +use simple_socks5_requests::Request; +use tokio::net::TcpStream; +use tokio::runtime::Runtime; +use tokio_tungstenite::tungstenite::protocol::Message; +use tokio_tungstenite::WebSocketStream; +use websocket::WebsocketConnectionError; + +pub struct ServiceProvider { + runtime: Runtime, +} + +impl ServiceProvider { + pub fn new() -> ServiceProvider { + let runtime = Runtime::new().unwrap(); + ServiceProvider { runtime } + } + + /// Start all subsystems + pub fn start(&mut self) { + let websocket_stream = self.connect_websocket("ws://localhost:1977"); + let (mut websocket_writer, mut websocket_reader) = websocket_stream.split(); + let mut controller = Controller::new(); + + self.runtime.block_on(async { + println!("\nAll systems go. Press CTRL-C to stop the server."); + while let Some(msg) = websocket_reader.next().await { + let data = msg.unwrap().into_data(); + + let reconstructed_message = ReconstructedMessage::try_from_bytes(&data).expect("todo: error handling"); + let raw_message = reconstructed_message.message; + let request = Request::try_from_bytes(&raw_message).unwrap(); + let response = controller.process_request(request).await.unwrap(); + if response.is_none() { // restart the loop if we got nothing back + continue; + } + + // TODO: wire SURBs in here once they're available + let return_address = "7tVXwePpo6SM99sqM1xEp6S4T1TSpxYx97fTpEdvmF7i.GgrN8998SmwvQghNEvqtPPZCgMQqJovWBrzspMnBESsE@e3vUAo6YhB7zq3GH8B4k3iiGT4H2USjdd5ZMZoUsHdF"; + let recipient = nymsphinx::addressing::clients::Recipient::try_from_string(return_address).unwrap(); + + let response_message = std::iter::once(MessageType::WithoutReplySURB as u8) + .chain(recipient.into_bytes().iter().cloned()) + .chain(response.unwrap().into_bytes().into_iter()) + .collect(); + + let message = Message::Binary(response_message); + websocket_writer.send(message).await.unwrap(); + } + }); + } + + /// Keep running until the user hits CTRL-C. + pub fn run_forever(&mut self) { + if let Err(e) = self.runtime.block_on(tokio::signal::ctrl_c()) { + println!("Stopping with error: {:?}", e); + } + println!("\nStopping..."); + } + + // Make the websocket connection so we can receive incoming Mixnet messages. + fn connect_websocket(&mut self, uri: &str) -> WebSocketStream { + self.runtime.block_on(async { + let ws_stream = match websocket::Connection::new(uri).connect().await { + Ok(ws_stream) => { + println!("* connected to local websocket server at {}", uri); + ws_stream + } + Err(WebsocketConnectionError::ConnectionNotEstablished) => { + panic!("Error: websocket connection attempt failed, is the Nym client running?") + } + }; + return ws_stream; + }) + } +} diff --git a/service-providers/simple-socks5/src/main.rs b/service-providers/simple-socks5/src/main.rs new file mode 100644 index 0000000000..b11257b2ec --- /dev/null +++ b/service-providers/simple-socks5/src/main.rs @@ -0,0 +1,11 @@ +mod connection; +mod controller; +mod core; +mod websocket; + +fn main() { + println!("Starting socks5 service provider:"); + let mut server = core::ServiceProvider::new(); + server.start(); + server.run_forever(); +} diff --git a/service-providers/simple-socks5/src/websocket.rs b/service-providers/simple-socks5/src/websocket.rs new file mode 100644 index 0000000000..e38152e4cc --- /dev/null +++ b/service-providers/simple-socks5/src/websocket.rs @@ -0,0 +1,27 @@ +use tokio::net::TcpStream; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::WebSocketStream; + +pub struct Connection { + uri: String, +} + +impl Connection { + pub fn new(uri: &str) -> Connection { + Connection { + uri: String::from(uri), + } + } + + pub async fn connect(&self) -> Result, WebsocketConnectionError> { + match connect_async(&self.uri).await { + Ok((ws_stream, _)) => return Ok(ws_stream), + Err(_e) => return Err(WebsocketConnectionError::ConnectionNotEstablished), + }; + } +} + +#[derive(Debug)] +pub enum WebsocketConnectionError { + ConnectionNotEstablished, +}