From b57a54835007b57de0bd88ebc6dd78e24f0fecf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 16 Jun 2020 11:22:02 +0100 Subject: [PATCH] Feature/packet retransmission (#263) * Ability to send sphinx packets of different sizes + more efficient decoding * Closing connection on connection corruption * Missing semicolons * Missing license notices * Default for packetsize * Split nymsphinx * Replaced Mutex with RwLock for TopologyAccessor + impl Deref * Sphinx update + import cleanup * Moved packet_sizes file * Updated NymTopology API * sphinx version bump * Missing license notice * nymsphinx-params crate * Changes due to ibid. * Chunking rework to allow variable size fragments * Initial ack crate * Version bump to new dev build * Cargo lock changes * random_route_to_gateway by node address * exposing getting read permit * Very initial draft on ack control * Correctly dereferencing out of topology read permit * All pending changes + compilation todo!s * Restricted scope of deref on TopologyAccessorInner * Type path alias for generate_key * Derived traits for MessageChunker * Ack control starting to take shape! * Awaiting callbacks * Most of work done on acks. Now to wire it all together * Import cleanup * rng generalization * Connected real traffic together; only acks from gateway left * Removed redundant things from nymsphinx::utils * nymsphinx-cover crate * Ack-related fields in client config * Decreased packet store log level * Restored forward sphinx request * Slight adjustements to surb acks * Changed TopologyReadPermit from type alias into a struct * Changes due to ibid. * Sphinx version upgrade * Gateway being able to understand and handle acks * Special Cover FragmentIdentifier + removal of dead code * Initial packet router for gateway client * Kill client if it fails to send to gateway too many times * Cover messages with acks * Moved out gateway client errors * Ignoring cover traffic acks * Changes in ack control * Another sphinx version upgrade * websocket handler delegating message chunking * Using config defined ack wait additions * Other minor changes I should have been more dilligent with splitting * Import path fix * sphinx_receiver => mixnet_receiver * Missing renamed variable instance * Updated aes-ctr to 0.4.0 * Removed concept of 'unfragmented' single fragment * Replay fragments detection * Long method split * typo * Cleaner client init * Fixed race condition * Fixed similar issue for retransmission * Cargo fmt * Minor clenaup --- Cargo.lock | 187 ++- Cargo.toml | 7 + clients/native/Cargo.toml | 3 +- .../native/src/client/cover_traffic_stream.rs | 120 +- clients/native/src/client/mix_traffic.rs | 21 +- clients/native/src/client/mod.rs | 119 +- .../acknowledgement_listener.rs | 86 ++ .../input_message_listener.rs | 127 ++ .../acknowlegement_control/mod.rs | 240 ++++ .../retransmission_request_listener.rs | 128 ++ .../sent_notification_listener.rs | 99 ++ .../src/client/real_messages_control/mod.rs | 170 +++ .../real_traffic_stream.rs | 250 ++++ .../native/src/client/real_traffic_stream.rs | 189 --- clients/native/src/client/received_buffer.rs | 87 +- clients/native/src/client/topology_control.rs | 117 +- clients/native/src/commands/init.rs | 15 +- clients/native/src/config/mod.rs | 35 + clients/native/src/config/template.rs | 1 + clients/native/src/websocket/handler.rs | 19 +- clients/webassembly/src/lib.rs | 5 +- .../client-libs/gateway-client/src/error.rs | 75 ++ common/client-libs/gateway-client/src/lib.rs | 166 +-- .../gateway-client/src/packet_router.rs | 83 ++ common/healthcheck/src/path_check.rs | 1 - common/nymsphinx/Cargo.toml | 12 +- common/nymsphinx/acknowledgements/Cargo.toml | 19 + .../acknowledgements/src/identifier.rs | 103 ++ .../src/lib.rs} | 12 +- .../acknowledgements/src/surb_ack.rs | 119 ++ common/nymsphinx/addressing/Cargo.toml | 10 + .../addressing => addressing/src}/clients.rs | 6 +- .../mod.rs => addressing/src/lib.rs} | 0 .../addressing => addressing/src}/nodes.rs | 30 +- common/nymsphinx/chunking/Cargo.toml | 17 + common/nymsphinx/chunking/src/fragment.rs | 1100 ++++++++++++++++ common/nymsphinx/chunking/src/lib.rs | 240 ++++ .../src}/reconstruction.rs | 1047 +++++++++++---- .../{src/chunking => chunking/src}/set.rs | 539 ++++---- common/nymsphinx/cover/Cargo.toml | 17 + common/nymsphinx/cover/src/lib.rs | 116 ++ common/nymsphinx/framing/Cargo.toml | 14 + .../framing/mod.rs => framing/src/lib.rs} | 4 +- common/nymsphinx/params/Cargo.toml | 10 + common/nymsphinx/params/src/lib.rs | 15 + .../packets.rs => params/src/packet_sizes.rs} | 23 +- common/nymsphinx/src/chunking/fragment.rs | 1157 ----------------- common/nymsphinx/src/chunking/mod.rs | 116 -- common/nymsphinx/src/lib.rs | 34 +- common/nymsphinx/src/utils/encapsulation.rs | 76 -- common/nymsphinx/src/utils/mod.rs | 35 +- common/nymsphinx/types/Cargo.toml | 11 + common/nymsphinx/types/src/lib.rs | 27 + common/topology/Cargo.toml | 3 +- common/topology/src/gateway.rs | 6 +- common/topology/src/lib.rs | 27 +- common/topology/src/mix.rs | 6 +- common/topology/src/provider.rs | 6 +- gateway/Cargo.toml | 2 +- gateway/gateway-requests/src/types.rs | 23 +- .../receiver/connection_handler.rs | 1 + .../receiver/packet_processing.rs | 139 +- gateway/src/node/mod.rs | 9 +- gateway/src/node/storage/inboxes.rs | 5 +- mixnode/Cargo.toml | 2 +- validator/Cargo.toml | 2 +- 66 files changed, 5097 insertions(+), 2393 deletions(-) create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs create mode 100644 clients/native/src/client/real_messages_control/mod.rs create mode 100644 clients/native/src/client/real_messages_control/real_traffic_stream.rs delete mode 100644 clients/native/src/client/real_traffic_stream.rs create mode 100644 common/client-libs/gateway-client/src/error.rs create mode 100644 common/client-libs/gateway-client/src/packet_router.rs create mode 100644 common/nymsphinx/acknowledgements/Cargo.toml create mode 100644 common/nymsphinx/acknowledgements/src/identifier.rs rename common/nymsphinx/{src/utils/poisson.rs => acknowledgements/src/lib.rs} (55%) create mode 100644 common/nymsphinx/acknowledgements/src/surb_ack.rs create mode 100644 common/nymsphinx/addressing/Cargo.toml rename common/nymsphinx/{src/addressing => addressing/src}/clients.rs (95%) rename common/nymsphinx/{src/addressing/mod.rs => addressing/src/lib.rs} (100%) rename common/nymsphinx/{src/addressing => addressing/src}/nodes.rs (89%) create mode 100644 common/nymsphinx/chunking/Cargo.toml create mode 100644 common/nymsphinx/chunking/src/fragment.rs create mode 100644 common/nymsphinx/chunking/src/lib.rs rename common/nymsphinx/{src/chunking => chunking/src}/reconstruction.rs (55%) rename common/nymsphinx/{src/chunking => chunking/src}/set.rs (55%) create mode 100644 common/nymsphinx/cover/Cargo.toml create mode 100644 common/nymsphinx/cover/src/lib.rs create mode 100644 common/nymsphinx/framing/Cargo.toml rename common/nymsphinx/{src/framing/mod.rs => framing/src/lib.rs} (97%) create mode 100644 common/nymsphinx/params/Cargo.toml create mode 100644 common/nymsphinx/params/src/lib.rs rename common/nymsphinx/{src/packets.rs => params/src/packet_sizes.rs} (80%) delete mode 100644 common/nymsphinx/src/chunking/fragment.rs delete mode 100644 common/nymsphinx/src/chunking/mod.rs delete mode 100644 common/nymsphinx/src/utils/encapsulation.rs create mode 100644 common/nymsphinx/types/Cargo.toml create mode 100644 common/nymsphinx/types/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 669d9d7883..c004f43b3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,10 +29,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee" dependencies = [ - "aes-soft", - "aesni", - "ctr", - "stream-cipher", + "aes-soft 0.3.3", + "aesni 0.6.0", + "ctr 0.3.2", + "stream-cipher 0.3.2", +] + +[[package]] +name = "aes-ctr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e60aeefd2a0243bd53a42e92444e039f67c3d7f0382c9813577696e7c10bf3" +dependencies = [ + "aes-soft 0.4.0", + "aesni 0.7.0", + "ctr 0.4.0", + "stream-cipher 0.4.1", ] [[package]] @@ -46,6 +58,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes-soft" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7" +dependencies = [ + "block-cipher", + "byteorder", + "opaque-debug", +] + [[package]] name = "aesni" version = "0.6.0" @@ -54,7 +77,18 @@ checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" dependencies = [ "block-cipher-trait", "opaque-debug", - "stream-cipher", + "stream-cipher 0.3.2", +] + +[[package]] +name = "aesni" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" +dependencies = [ + "block-cipher", + "opaque-debug", + "stream-cipher 0.4.1", ] [[package]] @@ -212,7 +246,16 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "block-cipher" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" +dependencies = [ + "generic-array 0.14.1", ] [[package]] @@ -221,7 +264,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] [[package]] @@ -514,7 +557,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" dependencies = [ - "generic-array", + "generic-array 0.12.3", "subtle 1.0.0", ] @@ -525,7 +568,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736" dependencies = [ "block-cipher-trait", - "stream-cipher", + "stream-cipher 0.3.2", +] + +[[package]] +name = "ctr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3592740fd55aaf61dd72df96756bd0d11e6037b89dcf30ae2e1895b267692be" +dependencies = [ + "stream-cipher 0.4.1", ] [[package]] @@ -575,7 +627,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] [[package]] @@ -929,6 +981,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2664c2cf08049036f31015b04c6ac3671379a1d86f52ed2416893f16022deb" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1667,7 +1728,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "bs58", "built", @@ -1687,6 +1748,7 @@ dependencies = [ "pem", "pemstore", "pretty_env_logger", + "rand 0.7.3", "reqwest 0.9.24", "serde", "serde_json", @@ -1717,7 +1779,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "built", "clap", @@ -1747,7 +1809,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "bs58", "built", @@ -1773,7 +1835,7 @@ dependencies = [ [[package]] name = "nym-validator" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "abci", "bodyparser", @@ -1803,14 +1865,86 @@ dependencies = [ name = "nymsphinx" version = "0.1.0" dependencies = [ - "bytes 0.5.4", - "log 0.4.8", + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-chunking", + "nymsphinx-cover", + "nymsphinx-framing", + "nymsphinx-params", + "nymsphinx-types", "rand 0.7.3", "rand_distr", - "sphinx", +] + +[[package]] +name = "nymsphinx-acknowledgements" +version = "0.1.0" +dependencies = [ + "aes-ctr 0.4.0", + "nymsphinx-addressing", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-addressing" +version = "0.1.0" +dependencies = [ + "nymsphinx-types", +] + +[[package]] +name = "nymsphinx-chunking" +version = "0.1.0" +dependencies = [ + "log 0.4.8", + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-cover" +version = "0.1.0" +dependencies = [ + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-chunking", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-framing" +version = "0.1.0" +dependencies = [ + "bytes 0.5.4", + "nymsphinx-params", + "nymsphinx-types", "tokio-util", ] +[[package]] +name = "nymsphinx-params" +version = "0.1.0" +dependencies = [ + "nymsphinx-types", +] + +[[package]] +name = "nymsphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx", +] + [[package]] name = "opaque-debug" version = "0.2.3" @@ -2755,9 +2889,9 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f#fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f" +source = "git+https://github.com/nymtech/sphinx?rev=cd9f09b85de33c60030ba6d7fae613c42cbe1faf#cd9f09b85de33c60030ba6d7fae613c42cbe1faf" dependencies = [ - "aes-ctr", + "aes-ctr 0.3.0", "arrayref", "blake2", "bs58", @@ -2780,7 +2914,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" dependencies = [ - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "stream-cipher" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88" +dependencies = [ + "block-cipher", + "generic-array 0.14.1", ] [[package]] @@ -3130,7 +3274,8 @@ dependencies = [ "futures 0.3.4", "itertools", "log 0.4.8", - "nymsphinx", + "nymsphinx-addressing", + "nymsphinx-types", "pretty_env_logger", "rand 0.7.3", "serde", diff --git a/Cargo.toml b/Cargo.toml index cecc3be58f..3bc05a60bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,13 @@ members = [ "common/crypto", "common/healthcheck", "common/nymsphinx", + "common/nymsphinx/acknowledgements", + "common/nymsphinx/addressing", + "common/nymsphinx/chunking", + "common/nymsphinx/cover", + "common/nymsphinx/framing", + "common/nymsphinx/params", + "common/nymsphinx/types", "common/pemstore", "common/topology", "gateway", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 6176f343c2..ed7662676a 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-client" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" @@ -21,6 +21,7 @@ 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" diff --git a/clients/native/src/client/cover_traffic_stream.rs b/clients/native/src/client/cover_traffic_stream.rs index 5354e0c9aa..4c3fe3b9fb 100644 --- a/clients/native/src/client/cover_traffic_stream.rs +++ b/clients/native/src/client/cover_traffic_stream.rs @@ -17,25 +17,58 @@ use crate::client::topology_control::TopologyAccessor; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::utils::{encapsulation, poisson}; +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::time::Duration; +use std::sync::Arc; use tokio::runtime::Handle; use tokio::task::JoinHandle; use tokio::time; use topology::NymTopology; -pub(crate) struct LoopCoverTrafficStream { - average_packet_delay: Duration, - average_cover_message_sending_delay: Duration, +pub(crate) struct LoopCoverTrafficStream +where + R: CryptoRng + Rng, + T: NymTopology, +{ + /// 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 { +impl Stream for LoopCoverTrafficStream +where + R: CryptoRng + Rng + Unpin, + T: NymTopology, // this really confuses me, why T doesn't need to be 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. @@ -50,8 +83,9 @@ impl Stream for LoopCoverTrafficStream { // 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 = poisson::sample(self.average_cover_message_sending_delay); + 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. @@ -62,52 +96,58 @@ impl Stream for LoopCoverTrafficStream { } } -impl LoopCoverTrafficStream { +// 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, - average_cover_message_sending_delay: time::Duration, - average_packet_delay: time::Duration, ) -> 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!"); - let route = match self - .topology_access - .random_route_to_gateway(&self.our_full_destination.gateway()) - .await - { - None => { - warn!("No valid topology detected - won't send any loop cover message this time"); - return; - } - Some(route) => route, - }; - let cover_message = match encapsulation::loop_cover_message_route( - self.our_full_destination.destination().clone(), - route, + // 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, &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, - ) { - Ok(message) => message, - Err(err) => { - error!( - "Somehow we managed to create an invalid cover message - {:?}", - err - ); - return; - } - }; + ) + .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 @@ -116,6 +156,12 @@ impl LoopCoverTrafficStream { 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; @@ -123,8 +169,10 @@ impl LoopCoverTrafficStream { async fn run(&mut self) { // we should set initial delay only when we actually start the stream - self.next_delay = - time::delay_for(poisson::sample(self.average_cover_message_sending_delay)); + 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; diff --git a/clients/native/src/client/mix_traffic.rs b/clients/native/src/client/mix_traffic.rs index 3ccded4532..1e277a8371 100644 --- a/clients/native/src/client/mix_traffic.rs +++ b/clients/native/src/client/mix_traffic.rs @@ -31,11 +31,17 @@ impl MixMessage { } } +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> { @@ -46,6 +52,7 @@ impl<'a> MixTrafficController<'static> { MixTrafficController { gateway_client, mix_rx, + consecutive_gateway_failure_count: 0, } } @@ -56,8 +63,18 @@ impl<'a> MixTrafficController<'static> { .send_sphinx_packet(mix_message.0, mix_message.1) .await { - Err(e) => error!("Failed to send sphinx packet to the gateway! - {:?}", e), - Ok(_) => trace!("We *might* have managed to forward sphinx packet to the gateway!"), + 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; + } } } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 6ad9efb433..42d400887a 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -15,6 +15,7 @@ use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; use crate::client::mix_traffic::{MixMessageReceiver, MixMessageSender, MixTrafficController}; +use crate::client::real_messages_control::RealMessagesController; use crate::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, }; @@ -26,20 +27,24 @@ use crate::websocket; use crypto::identity::MixIdentityKeyPair; use directory_client::presence; use futures::channel::mpsc; -use gateway_client::{GatewayClient, SphinxPacketReceiver, SphinxPacketSender}; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; use gateway_requests::auth_token::AuthToken; use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::chunking::split_and_prepare_payloads; use nymsphinx::NodeAddressBytes; use received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; +use std::sync::Arc; use tokio::runtime::Runtime; use topology::NymTopology; mod cover_traffic_stream; pub(crate) mod inbound_messages; mod mix_traffic; -mod real_traffic_stream; +pub(crate) mod real_messages_control; pub(crate) mod received_buffer; pub(crate) mod topology_control; @@ -79,6 +84,7 @@ impl NymClient { // the pumped traffic goes to the MixTrafficController fn start_cover_traffic_stream( &self, + ack_key: Arc, topology_accessor: TopologyAccessor, mix_tx: MixMessageSender, ) { @@ -88,37 +94,54 @@ impl NymClient { self.runtime .enter(|| { LoopCoverTrafficStream::new( + 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, - self.config.get_loop_cover_traffic_average_delay(), - self.config.get_average_packet_delay(), ) }) .start(self.runtime.handle()); } - fn start_real_traffic_stream( + // TODO: I'm not a fan of this function signature, i.e. that it returns the ACK key, but then again + // I think the ACK key should be generated by 'acknowledgement_control' rather than by + // the client itself. However, it all might change once we have to implement key rotation. + fn start_real_traffic_controller( &self, topology_accessor: TopologyAccessor, - mix_tx: MixMessageSender, - input_rx: InputMessageReceiver, - ) { + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: MixMessageSender, + ) -> Arc { + let controller_config = real_messages_control::Config::new( + 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 which HAS TO be called within context of a tokio runtime - self.runtime - .enter(|| { - real_traffic_stream::OutQueueControl::new( - mix_tx, - input_rx, - self.as_mix_recipient(), - topology_accessor, - self.config.get_average_packet_delay(), - self.config.get_message_sending_average_delay(), - ) - }) - .start(self.runtime.handle()); + // 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, + ) + }); + let ack_key = real_messages_controller.ack_key(); + real_messages_controller.start(self.runtime.handle()); + + ack_key } // buffer controlling all messages fetched from provider @@ -126,16 +149,17 @@ impl NymClient { fn start_received_messages_buffer_controller( &self, query_receiver: ReceivedBufferRequestReceiver, - sphinx_receiver: SphinxPacketReceiver, + mixnet_receiver: MixnetMessageReceiver, ) { info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new(query_receiver, sphinx_receiver) + ReceivedMessagesBufferController::new(query_receiver, mixnet_receiver) .start(self.runtime.handle()) } fn start_gateway_client( &mut self, - sphinx_packet_sender: SphinxPacketSender, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, gateway_address: url::Url, ) -> GatewayClient<'static, url::Url> { let auth_token = self @@ -148,7 +172,8 @@ impl NymClient { gateway_address, self.as_mix_recipient().destination(), auth_token, - sphinx_packet_sender, + mixnet_message_sender, + ack_sender, self.config.get_gateway_response_timeout(), ); @@ -265,19 +290,13 @@ impl NymClient { /// 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) { - let split_message = split_and_prepare_payloads(&message); - debug!( - "Splitting message into {:?} fragments!", - split_message.len() - ); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.input_tx - .as_ref() - .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap() - } + let input_msg = InputMessage::new(recipient, message); + + self.input_tx + .as_ref() + .expect("start method was not called before!") + .unbounded_send(input_msg) + .unwrap(); } /// EXPERIMENTAL DIRECT RUST API @@ -323,9 +342,9 @@ impl NymClient { // 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 [unwrapped] sphinx messages received from the gateway + // 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 (unwrapped_sphinx_sender, unwrapped_sphinx_receiver) = mpsc::unbounded(); + 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(); @@ -333,6 +352,9 @@ impl NymClient { // 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(); + // TODO: when we switch to our graph topology, we need to remember to change 'presence::Topology' type let shared_topology_accessor = TopologyAccessor::::new(); // the components are started in very specific order. Unless you know what you are doing, @@ -340,24 +362,29 @@ impl NymClient { self.start_topology_refresher(shared_topology_accessor.clone()); self.start_received_messages_buffer_controller( received_buffer_request_receiver, - unwrapped_sphinx_receiver, + mixnet_messages_receiver, ); let gateway_url = self.runtime.block_on(Self::get_gateway_address( self.config.get_gateway_id(), shared_topology_accessor.clone(), )); - let gateway_client = self.start_gateway_client(unwrapped_sphinx_sender, gateway_url); + let gateway_client = + self.start_gateway_client(mixnet_messages_sender, ack_sender, gateway_url); self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); - self.start_cover_traffic_stream( + + let ack_key = self.start_real_traffic_controller( shared_topology_accessor.clone(), + ack_receiver, + input_receiver, sphinx_message_sender.clone(), ); - self.start_real_traffic_stream( + + self.start_cover_traffic_stream( + ack_key, shared_topology_accessor.clone(), sphinx_message_sender, - input_receiver, ); match self.config.get_socket_type() { diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs new file mode 100644 index 0000000000..7b81fbe31a --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.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 super::PendingAcksMap; +use futures::StreamExt; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::{ + acknowledgements::{identifier::recover_identifier, AckAes128Key}, + 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; + } + + 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/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs new file mode 100644 index 0000000000..f0b93eba82 --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs @@ -0,0 +1,127 @@ +// 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::{ + inbound_messages::{InputMessage, InputMessageReceiver}, + real_messages_control::real_traffic_stream::{RealMessage, RealMessageSender}, + topology_control::TopologyAccessor, +}; +use futures::StreamExt; +use log::*; +use nymsphinx::{ + acknowledgements::AckAes128Key, addressing::clients::Recipient, chunking::MessageChunker, +}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use topology::NymTopology; + +// responsible for splitting received message and initial sending attempt +pub(super) struct InputMessageListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, +} + +impl InputMessageListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, + ) -> Self { + InputMessageListener { + ack_key, + ack_recipient, + input_receiver, + message_chunker, + pending_acks, + real_message_sender, + topology_access, + } + } + + async fn on_input_message(&mut self, msg: InputMessage) { + let (recipient, content) = msg.destruct(); + let split_message = self.message_chunker.split_message(&content); + let topology_permit = self.topology_access.get_read_permit().await; + + let topology_ref_option = + topology_permit.try_get_valid_topology_ref(&self.ack_recipient, &recipient); + if topology_ref_option.is_none() { + warn!("Could not process the message - the network topology is invalid"); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + 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 (total_delay, (first_hop, packet)) = self + .message_chunker + .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, &recipient) + .unwrap(); + + real_messages.push(RealMessage::new(first_hop, packet, frag_id)); + + let pending_ack = + PendingAcknowledgement::new(message_chunk, 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 let Some(_) = pending_acks_map_write_guard.insert(frag_id, pending_ack) { + panic!("Tried to insert duplicate pending ack") + } + } + + 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/native/src/client/real_messages_control/acknowlegement_control/mod.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs new file mode 100644 index 0000000000..52d3e5f86f --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs @@ -0,0 +1,240 @@ +// 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::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::{ + acknowledgements::{self, identifier::AckAes128Key}, + addressing::clients::Recipient, + chunking::{ + fragment::{Fragment, FragmentIdentifier}, + MessageChunker, + }, + Delay, +}; +use rand::{CryptoRng, Rng}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::{ + sync::{Notify, RwLock}, + task::JoinHandle, +}; +use topology::NymTopology; + +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, + T: NymTopology, +{ + ack_key: Arc, + acknowledgement_listener: Option, + input_message_listener: Option>, + retransmission_request_listener: Option>, + sent_notification_listener: Option, +} + +impl AcknowledgementController +where + R: 'static + CryptoRng + Rng + Clone + Send, + T: 'static + NymTopology, +{ + pub(super) fn new( + mut rng: R, + topology_access: TopologyAccessor, + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + connectors: AcknowledgementControllerConnectors, + ) -> Self { + // note for future-self: perhaps for key rotation we could replace it with Arc> ? + // actually same could be true for any keys we use + let ack_key = Arc::new(acknowledgements::generate_key(&mut rng)); + let pending_acks = Arc::new(RwLock::new(HashMap::new())); + let message_chunker = MessageChunker::new_with_rng( + rng, + ack_recipient.clone(), + average_packet_delay_duration, + average_ack_delay_duration, + ); + + 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_chunker.clone(), + Arc::clone(&pending_acks), + connectors.real_message_sender.clone(), + topology_access.clone(), + ); + + let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); + + let retransmission_request_listener = RetransmissionRequestListener::new( + Arc::clone(&ack_key), + ack_recipient, + message_chunker, + 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 { + ack_key, + 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) fn ack_key(&self) -> Arc { + Arc::clone(&self.ack_key) + } + + 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/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs new file mode 100644 index 0000000000..940467f1ba --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs @@ -0,0 +1,128 @@ +// 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::{ + acknowledgements::AckAes128Key, + addressing::clients::Recipient, + chunking::{fragment::FragmentIdentifier, MessageChunker}, +}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use topology::NymTopology; + +// responsible for packet retransmission upon fired timer +pub(super) struct RetransmissionRequestListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + ack_key: Arc, + ack_recipient: Recipient, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, +} + +impl RetransmissionRequestListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, + ) -> Self { + RetransmissionRequestListener { + ack_key, + ack_recipient, + message_chunker, + 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, &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 (total_delay, (first_hop, packet)) = self + .message_chunker + .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(total_delay); + + self.real_message_sender + .unbounded_send(RealMessage::new(first_hop, 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/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs new file mode 100644 index 0000000000..77c6279b72 --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs @@ -0,0 +1,99 @@ +// 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; +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) { + 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/native/src/client/real_messages_control/mod.rs b/clients/native/src/client/real_messages_control/mod.rs new file mode 100644 index 0000000000..83be7007a3 --- /dev/null +++ b/clients/native/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::{ + inbound_messages::InputMessageReceiver, mix_traffic::MixMessageSender, + topology_control::TopologyAccessor, +}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; +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; +use topology::NymTopology; + +mod acknowlegement_control; +mod real_traffic_stream; + +pub(crate) struct Config { + 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_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 { + 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, + T: NymTopology, +{ + 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, + ) -> 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(), + config.self_recipient.clone(), + 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( + ack_control.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), + } + } + + // Note: this method can only be called before `run` is called + pub(super) fn ack_key(&self) -> Arc { + self.ack_control.as_ref().unwrap().ack_key() + } + + 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/native/src/client/real_messages_control/real_traffic_stream.rs b/clients/native/src/client/real_messages_control/real_traffic_stream.rs new file mode 100644 index 0000000000..90b9c05b47 --- /dev/null +++ b/clients/native/src/client/real_messages_control/real_traffic_stream.rs @@ -0,0 +1,250 @@ +// 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::identifier::AckAes128Key; +use nymsphinx::addressing::clients::Recipient; +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::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::time; +use topology::NymTopology; + +pub(crate) struct OutQueueControl +where + R: CryptoRng + Rng, + T: NymTopology, +{ + /// 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: SocketAddr, + packet: SphinxPacket, + fragment_id: FragmentIdentifier, +} + +impl RealMessage { + pub(crate) fn new( + first_hop_address: SocketAddr, + 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, + T: NymTopology, // this really confuses me, why T doesn't need to be 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, + T: NymTopology, +{ + 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, + &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/native/src/client/real_traffic_stream.rs b/clients/native/src/client/real_traffic_stream.rs deleted file mode 100644 index 9d03d7f817..0000000000 --- a/clients/native/src/client/real_traffic_stream.rs +++ /dev/null @@ -1,189 +0,0 @@ -// 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::inbound_messages::InputMessage; -use crate::client::mix_traffic::MixMessage; -use crate::client::topology_control::TopologyAccessor; -use futures::channel::mpsc; -use futures::task::{Context, Poll}; -use futures::{Future, Stream, StreamExt}; -use log::{error, info, trace, warn}; -use nymsphinx::addressing::clients::Recipient; -use nymsphinx::utils::{encapsulation, poisson}; -use std::pin::Pin; -use std::time::Duration; -use tokio::runtime::Handle; -use tokio::task::JoinHandle; -use tokio::time; -use topology::NymTopology; - -pub(crate) struct OutQueueControl { - average_packet_delay: Duration, - average_message_sending_delay: Duration, - next_delay: time::Delay, - mix_tx: mpsc::UnboundedSender, - input_rx: mpsc::UnboundedReceiver, - our_full_destination: Recipient, - topology_access: TopologyAccessor, -} - -pub(crate) enum StreamMessage { - Cover, - Real(InputMessage), -} - -impl Stream for OutQueueControl { - 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 now = self.next_delay.deadline(); - let next_poisson_delay = poisson::sample(self.average_message_sending_delay); - - // The next interval value is `next_poisson_delay` after the one that just - // yielded. - let next = now + next_poisson_delay; - self.next_delay.reset(next); - - // decide what kind of message to send - match Pin::new(&mut self.input_rx).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 { - pub(crate) fn new( - mix_tx: mpsc::UnboundedSender, - input_rx: mpsc::UnboundedReceiver, - our_full_destination: Recipient, - topology_access: TopologyAccessor, - average_packet_delay: Duration, - average_message_sending_delay: Duration, - ) -> Self { - OutQueueControl { - average_packet_delay, - average_message_sending_delay, - next_delay: time::delay_for(Default::default()), - mix_tx, - input_rx, - our_full_destination, - topology_access, - } - } - - async fn get_route(&self, other_recipient: Option<&Recipient>) -> Option> { - match other_recipient { - // we are sending to ourselves - None => { - self.topology_access - .random_route_to_gateway(&self.our_full_destination.gateway()) - .await - } - Some(other_recipient) => { - self.topology_access - .random_route_to_gateway(&other_recipient.gateway()) - .await - } - } - } - - async fn on_message(&mut self, next_message: StreamMessage) { - trace!("created new message"); - - let next_packet = match next_message { - StreamMessage::Cover => { - let route = self.get_route(None).await; - if route.is_none() { - warn!("No valid topology detected - won't send any real or loop message this time"); - return; - } - let route = route.unwrap(); - encapsulation::loop_cover_message_route( - self.our_full_destination.destination().clone(), - route, - self.average_packet_delay, - ) - } - StreamMessage::Real(real_message) => { - let (recipient, data) = real_message.destruct(); - let route = self.get_route(Some(&recipient)).await; - if route.is_none() { - warn!("No valid topology detected - won't send any real or loop message this time"); - return; - } - let route = route.unwrap(); - encapsulation::encapsulate_message_route( - recipient.destination(), - data, - route, - self.average_packet_delay, - ) - } - }; - - let next_packet = match next_packet { - Ok(message) => message, - Err(err) => { - error!( - "Somehow we managed to create an invalid traffic message - {:?}", - err - ); - return; - } - }; - - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx - .unbounded_send(MixMessage::new(next_packet.0, next_packet.1)) - .unwrap(); - // 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(poisson::sample(self.average_message_sending_delay)); - - info!("Starting out queue controller..."); - while let Some(next_message) = self.next().await { - self.on_message(next_message).await; - } - } - - pub(crate) fn start(self, handle: &Handle) -> JoinHandle<()> { - handle.spawn(async move { self.run_out_queue_control().await }) - } -} diff --git a/clients/native/src/client/received_buffer.rs b/clients/native/src/client/received_buffer.rs index 029d36d8d0..408df39640 100644 --- a/clients/native/src/client/received_buffer.rs +++ b/clients/native/src/client/received_buffer.rs @@ -13,14 +13,13 @@ // limitations under the License. use futures::channel::mpsc; -use futures::lock::Mutex; +use futures::lock::{Mutex, MutexGuard}; use futures::StreamExt; -use gateway_client::SphinxPacketReceiver; +use gateway_client::MixnetMessageReceiver; use log::*; -use nymsphinx::{ - chunking::reconstruction::MessageReconstructor, - utils::encapsulation::LOOP_COVER_MESSAGE_PAYLOAD, -}; +use nymsphinx::chunking::reconstruction::MessageReconstructor; +use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; +use std::collections::HashSet; use std::sync::Arc; use tokio::runtime::Handle; use tokio::task::JoinHandle; @@ -38,6 +37,11 @@ struct ReceivedMessagesBufferInner { messages: Vec>, message_reconstructor: MessageReconstructor, 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, } #[derive(Debug, Clone)] @@ -54,6 +58,7 @@ impl ReceivedMessagesBuffer { messages: Vec::new(), message_reconstructor: MessageReconstructor::new(), message_sender: None, + recently_reconstructed: HashSet::new(), })), } } @@ -102,6 +107,47 @@ impl ReceivedMessagesBuffer { self.inner.lock().await.messages.extend(msgs) } + fn process_received_fragment( + mutex_guard: &mut MutexGuard, + raw_fragment: Vec, + ) -> Option> { + if raw_fragment == LOOP_COVER_MESSAGE_PAYLOAD { + trace!("The message was a loop cover message! Skipping it"); + return None; + } + + let fragment = match mutex_guard + .message_reconstructor + .recover_fragment(raw_fragment) + { + Err(e) => { + warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + return None; + } + Ok(frag) => frag, + }; + + if mutex_guard.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 let Some(reconstructed_message) = mutex_guard + .message_reconstructor + .insert_new_fragment(fragment) + { + let (completed_message, message_sets) = reconstructed_message; + for set_id in message_sets { + if !mutex_guard.recently_reconstructed.insert(set_id) { + // or perhaps we should even panic at this point? + error!("Reconstructed another message containing already used set id!") + } + } + return Some(completed_message); + } + None + } + async fn add_new_message_fragments(&mut self, msgs: Vec>) { debug!( "Adding {:?} new message fragments to the buffer!", @@ -112,15 +158,10 @@ impl ReceivedMessagesBuffer { let mut completed_messages = Vec::new(); let mut inner_guard = self.inner.lock().await; for msg_fragment in msgs { - if msg_fragment == LOOP_COVER_MESSAGE_PAYLOAD { - trace!("The message was a loop cover message! Skipping it"); - continue; - } - - if let Some(reconstructed_message) = - inner_guard.message_reconstructor.new_fragment(msg_fragment) + if let Some(completed_message) = + Self::process_received_fragment(&mut inner_guard, msg_fragment) { - completed_messages.push(reconstructed_message); + completed_messages.push(completed_message) } } @@ -189,22 +230,22 @@ impl RequestReceiver { struct FragmentedMessageReceiver { received_buffer: ReceivedMessagesBuffer, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, } impl FragmentedMessageReceiver { fn new( received_buffer: ReceivedMessagesBuffer, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, ) -> Self { FragmentedMessageReceiver { received_buffer, - sphinx_packet_receiver, + mixnet_packet_receiver, } } fn start(mut self, handle: &Handle) -> JoinHandle<()> { handle.spawn(async move { - while let Some(new_messages) = self.sphinx_packet_receiver.next().await { + while let Some(new_messages) = self.mixnet_packet_receiver.next().await { self.received_buffer .add_new_message_fragments(new_messages) .await; @@ -214,21 +255,21 @@ impl FragmentedMessageReceiver { } pub(crate) struct ReceivedMessagesBufferController { - fragmented_messsage_receiver: FragmentedMessageReceiver, + fragmented_message_receiver: FragmentedMessageReceiver, request_receiver: RequestReceiver, } impl ReceivedMessagesBufferController { pub(crate) fn new( query_receiver: ReceivedBufferRequestReceiver, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new(); ReceivedMessagesBufferController { - fragmented_messsage_receiver: FragmentedMessageReceiver::new( + fragmented_message_receiver: FragmentedMessageReceiver::new( received_buffer.clone(), - sphinx_packet_receiver, + mixnet_packet_receiver, ), request_receiver: RequestReceiver::new(received_buffer, query_receiver), } @@ -236,7 +277,7 @@ impl ReceivedMessagesBufferController { pub(crate) fn start(self, handle: &Handle) { // TODO: should we do anything with JoinHandle(s) returned by start methods? - self.fragmented_messsage_receiver.start(handle); + self.fragmented_message_receiver.start(handle); self.request_receiver.start(handle); } } diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index fb949f4e88..740ffe34ec 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -13,18 +13,29 @@ // limitations under the License. use crate::built_info; -use futures::lock::Mutex; use log::*; -use nymsphinx::NodeAddressBytes; +use nymsphinx::addressing::clients::Recipient; +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::{provider, NymTopology}; +// I'm extremely curious why compiler NEVER complained about lack of Debug here before +#[derive(Debug)] struct TopologyAccessorInner(Option); +impl Deref for TopologyAccessorInner { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl TopologyAccessorInner { fn new() -> Self { TopologyAccessorInner(None) @@ -35,26 +46,78 @@ impl TopologyAccessorInner { } } +pub(super) struct TopologyReadPermit<'a, T: NymTopology> { + permit: RwLockReadGuard<'a, TopologyAccessorInner>, +} + +impl<'a, T: NymTopology> TopologyReadPermit<'a, T> { + /// 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: &Recipient, + ) -> Option<&'a T> { + // first we need to deref out of RwLockReadGuard + // then we need to deref out of TopologyAccessorInner + // then we must take ref of option, i.e. Option<&T> + // and finally try to unwrap it to obtain &T + let topology_ref_option = (*self.permit.deref()).as_ref(); + + if topology_ref_option.is_none() { + return None; + } + + let topology_ref = topology_ref_option.unwrap(); + + // see if it's possible to route the packet to both gateways + if !topology_ref.can_construct_path_through() + || !topology_ref.gateway_exists(&packet_recipient.gateway()) + || !topology_ref.gateway_exists(&ack_recipient.gateway()) + { + None + } else { + Some(topology_ref) + } + } +} + +impl<'a, T: NymTopology> From>> + for TopologyReadPermit<'a, T> +{ + fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self { + TopologyReadPermit { + permit: read_permit, + } + } +} + #[derive(Clone, Debug)] pub(crate) struct TopologyAccessor { - // TODO: this requires some actual benchmarking to determine if obtaining mutex is not going - // to cause some bottlenecking and whether perhaps RwLock would be better - inner: Arc>>, + // `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(Mutex::new(TopologyAccessorInner::new())), + inner: Arc::new(RwLock::new(TopologyAccessorInner::new())), } } + pub(super) async fn get_read_permit(&self) -> TopologyReadPermit<'_, T> { + self.inner.read().await.into() + } + async fn update_global_topology(&mut self, new_topology: Option) { - self.inner.lock().await.update(new_topology); + self.inner.write().await.update(new_topology); } pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => None, Some(ref topology) => topology .gateways() @@ -67,7 +130,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) pub(crate) async fn is_routable(&self) -> bool { - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => false, Some(ref topology) => topology.can_construct_path_through(), } @@ -75,7 +138,7 @@ impl TopologyAccessor { pub(crate) async fn get_all_clients(&self) -> Option> { // TODO: this will need to be modified to instead return pairs (provider, client) - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => None, Some(ref topology) => Some( topology @@ -87,40 +150,6 @@ impl TopologyAccessor { ), } } - - pub(crate) async fn random_route_to_gateway( - &self, - gateway: &NodeAddressBytes, - ) -> Option> { - let b58_address = gateway.to_base58_string(); - let guard = self.inner.lock().await; - let topology = guard.0.as_ref()?; - - let gateway = topology - .gateways() - .iter() - .cloned() - .find(|gateway| gateway.pub_key == b58_address.clone())?; - - topology.random_route_to_gateway(gateway.into()).ok() - } - - // // this is a rather temporary solution as each client will have an associated provider - // // currently that is not implemented yet and there only exists one provider in the network - // pub(crate) async fn random_route(&self) -> Option> { - // match &self.inner.lock().await.0 { - // None => None, - // Some(ref topology) => { - // let mut gateways = topology.gateways(); - // if gateways.is_empty() { - // return None; - // } - // // unwrap is fine here as we asserted there is at least single provider - // let provider = gateways.pop().unwrap().into(); - // topology.random_route_to_gateway(provider).ok() - // } - // } - // } } pub(crate) struct TopologyRefresherConfig { diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 740479bf7c..28ecdd5255 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -19,7 +19,6 @@ use clap::{App, Arg, ArgMatches}; use config::NymConfig; use crypto::identity::MixIdentityKeyPair; use directory_client::presence::Topology; -use futures::channel::mpsc; use gateway_client::GatewayClient; use gateway_requests::AuthToken; use nymsphinx::DestinationAddressBytes; @@ -68,20 +67,18 @@ async fn try_gateway_registration( gateways: Vec, our_address: DestinationAddressBytes, ) -> Option<(String, AuthToken)> { - // TODO: having to do something like this suggests that perhaps GatewayClient's constructor - // could be improved - let (sphinx_tx, _) = mpsc::unbounded(); let timeout = Duration::from_millis(1500); for gateway in gateways { - let mut gateway_client = GatewayClient::new( + let mut gateway_client = GatewayClient::new_init( url::Url::parse(&gateway.client_listener).unwrap(), our_address.clone(), - None, - sphinx_tx.clone(), timeout, ); - if gateway_client.establish_connection().await.is_ok() { - if let Ok(token) = gateway_client.register().await { + if let Ok(token) = gateway_client.register_without_listening().await { + if let Err(err) = gateway_client.close_connection().await { + eprintln!("Error while closing connection to the gateway! - {:?}", err); + continue; + } else { return Some((gateway.pub_key, token)); } } diff --git a/clients/native/src/config/mod.rs b/clients/native/src/config/mod.rs index 8ab8826fd5..7711129b9c 100644 --- a/clients/native/src/config/mod.rs +++ b/clients/native/src/config/mod.rs @@ -26,6 +26,8 @@ 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 @@ -189,6 +191,18 @@ impl Config { 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) } @@ -312,6 +326,24 @@ pub struct Debug { /// 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. @@ -345,6 +377,9 @@ 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, diff --git a/clients/native/src/config/template.rs b/clients/native/src/config/template.rs index 0afa6c2b33..590e567584 100644 --- a/clients/native/src/config/template.rs +++ b/clients/native/src/config/template.rs @@ -77,6 +77,7 @@ listening_port = {{ socket.listening_port }} [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/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 8ca9d9b867..7ca5554c33 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -24,7 +24,6 @@ use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::chunking::split_and_prepare_payloads; use std::convert::TryFrom; use tokio::net::TcpStream; use tokio_tungstenite::{ @@ -104,12 +103,9 @@ impl Handler { } }; - // in case the message is too long and needs to be split into multiple packets: - let split_message = split_and_prepare_payloads(&message_bytes); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.msg_input.unbounded_send(input_msg).unwrap(); - } + // the ack control is now responsible for chunking, etc. + let input_msg = InputMessage::new(recipient, message_bytes); + self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Text; @@ -155,12 +151,9 @@ impl Handler { } async fn handle_binary_send(&mut self, recipient: Recipient, data: Vec) -> ServerResponse { - // in case the message is too long and needs to be split into multiple packets: - let split_message = split_and_prepare_payloads(&data); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.msg_input.unbounded_send(input_msg).unwrap(); - } + // the ack control is now responsible for chunking, etc. + let input_msg = InputMessage::new(recipient, data); + self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Binary; ServerResponse::Send diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index c69045983e..f20973d2cc 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -55,6 +55,7 @@ pub struct NodeData { /// /// Message chunking is currently not implemented. If the message exceeds the /// capacity of a single Sphinx packet, the extra information will be discarded. +/// #[wasm_bindgen] pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> Vec { utils::set_panic_hook(); // nicer js errors. @@ -73,7 +74,7 @@ pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> let message = msg.as_bytes().to_vec(); let destination = Destination::new(recipient.destination(), Default::default()); - let sphinx_packet = SphinxPacket::new(message, &route, &destination, &delays, None).unwrap(); + let sphinx_packet = SphinxPacket::new(message, &route, &destination, &delays).unwrap(); payload(sphinx_packet, route) } @@ -123,7 +124,7 @@ impl TryFrom for SphinxNode { let src = MixIdentityPublicKey::from_base58_string(node_data.public_key).to_bytes(); let mut dest: [u8; 32] = [0; 32]; dest.copy_from_slice(&src); - nymsphinx::key::new(dest) + nymsphinx::public_key_from_bytes(dest) }; Ok(SphinxNode { address, pub_key }) diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs new file mode 100644 index 0000000000..82f4c1d8c1 --- /dev/null +++ b/common/client-libs/gateway-client/src/error.rs @@ -0,0 +1,75 @@ +// 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 gateway_requests::auth_token::AuthTokenConversionError; +use std::fmt::{self, Error, Formatter}; +use tokio_tungstenite::tungstenite::Error as WsError; + +#[derive(Debug)] +pub enum GatewayClientError { + ConnectionNotEstablished, + GatewayError(String), + NetworkError(WsError), + NoAuthTokenAvailable, + ConnectionAbruptlyClosed, + MalformedResponse, + NotAuthenticated, + ConnectionInInvalidState, + AuthenticationFailure, + Timeout, +} + +impl From for GatewayClientError { + fn from(err: WsError) -> Self { + GatewayClientError::NetworkError(err) + } +} + +impl From for GatewayClientError { + fn from(_err: AuthTokenConversionError) -> Self { + GatewayClientError::MalformedResponse + } +} + +// better human readable representation of the error, mostly so that GatewayClientError +// would implement std::error::Error +impl fmt::Display for GatewayClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + match self { + GatewayClientError::ConnectionNotEstablished => { + write!(f, "connection to the gateway is not established") + } + GatewayClientError::NoAuthTokenAvailable => { + write!(f, "no AuthToken was provided or obtained") + } + GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), + GatewayClientError::NetworkError(err) => { + write!(f, "there was a network error - {}", err) + } + GatewayClientError::ConnectionAbruptlyClosed => { + write!(f, "connection was abruptly closed") + } + GatewayClientError::Timeout => write!(f, "timed out"), + GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), + GatewayClientError::ConnectionInInvalidState => write!( + f, + "connection is in an invalid state - please send a bug report" + ), + GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), + GatewayClientError::GatewayError(err) => { + write!(f, "gateway returned an error response - {}", err) + } + } + } +} diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index a8923ebc77..2d806d24b4 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -12,14 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::error::GatewayClientError; +use crate::packet_router::PacketRouter; +pub use crate::packet_router::{ + AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, +}; use futures::stream::{SplitSink, SplitStream}; -use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; -use gateway_requests::auth_token::{AuthToken, AuthTokenConversionError}; +use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; +use gateway_requests::auth_token::AuthToken; use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; use log::*; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; use std::convert::TryFrom; -use std::fmt::{self, Error, Formatter}; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -27,10 +31,13 @@ use tokio::net::TcpStream; use tokio::sync::Notify; use tokio_tungstenite::{ connect_async, - tungstenite::{client::IntoClientRequest, protocol::Message, Error as WsError}, + tungstenite::{client::IntoClientRequest, protocol::Message}, WebSocketStream, }; +pub mod error; +pub mod packet_router; + // TODO: combine the duplicate reading procedure, i.e. /* msg read from conn.next().await: @@ -51,70 +58,10 @@ msg read from conn.next().await: */ // TODO: some batching mechanism to allow reading and sending more than a single packet through -pub type SphinxPacketSender = mpsc::UnboundedSender>>; -pub type SphinxPacketReceiver = mpsc::UnboundedReceiver>>; // type alias for not having to type the whole thing every single time type WsConn = WebSocketStream; -#[derive(Debug)] -pub enum GatewayClientError { - ConnectionNotEstablished, - GatewayError(String), - NetworkError(WsError), - NoAuthTokenAvailable, - ConnectionAbruptlyClosed, - MalformedResponse, - NotAuthenticated, - ConnectionInInvalidState, - AuthenticationFailure, - Timeout, -} - -impl From for GatewayClientError { - fn from(err: WsError) -> Self { - GatewayClientError::NetworkError(err) - } -} - -impl From for GatewayClientError { - fn from(_err: AuthTokenConversionError) -> Self { - GatewayClientError::MalformedResponse - } -} - -// better human readable representation of the error, mostly so that GatewayClientError -// would implement std::error::Error -impl fmt::Display for GatewayClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - match self { - GatewayClientError::ConnectionNotEstablished => { - write!(f, "connection to the gateway is not established") - } - GatewayClientError::NoAuthTokenAvailable => { - write!(f, "no AuthToken was provided or obtained") - } - GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), - GatewayClientError::NetworkError(err) => { - write!(f, "there was a network error - {}", err) - } - GatewayClientError::ConnectionAbruptlyClosed => { - write!(f, "connection was abruptly closed") - } - GatewayClientError::Timeout => write!(f, "timed out"), - GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), - GatewayClientError::ConnectionInInvalidState => write!( - f, - "connection is in an invalid state - please send a bug report" - ), - GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), - GatewayClientError::GatewayError(err) => { - write!(f, "gateway returned an error response - {}", err) - } - } - } -} - // We have ownership over sink half of the connection, but the stream is owned // by some other task, however, we can notify it to get the stream back. struct PartiallyDelegated<'a> { @@ -129,9 +76,9 @@ impl<'a> PartiallyDelegated<'a> { // TODO: this can be potentially bad as we have no direct restrictions of ensuring it's called // within tokio runtime. Perhaps we should use the "old" way of passing explicit // runtime handle to the constructor and using that instead? - fn split_and_listen_for_sphinx_packets( + fn split_and_listen_for_mixnet_messages( conn: WsConn, - sphinx_packet_sender: SphinxPacketSender, + packet_router: PacketRouter, ) -> Result { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -140,7 +87,7 @@ impl<'a> PartiallyDelegated<'a> { let (sink, mut stream) = conn.split(); - let sphinx_receiver_future = async move { + let mixnet_receiver_future = async move { let mut should_return = false; while !should_return { tokio::select! { @@ -161,7 +108,7 @@ impl<'a> PartiallyDelegated<'a> { Message::Binary(bin_msg) => { // TODO: some batching mechanism to allow reading and sending more than // one packet at the time, because the receiver can easily handle it - sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + packet_router.route_received(vec![bin_msg]) }, // I think that in the future we should perhaps have some sequence number system, i.e. // so each request/reponse pair can be easily identified, so that if messages are @@ -176,7 +123,7 @@ impl<'a> PartiallyDelegated<'a> { Ok(stream) }; - let spawned_boxed_task = tokio::spawn(sphinx_receiver_future) + let spawned_boxed_task = tokio::spawn(mixnet_receiver_future) .map(|join_handle| { join_handle.expect("task must have not failed to finish its execution!") }) @@ -244,7 +191,7 @@ pub struct GatewayClient<'a, R: IntoClientRequest + Unpin + Clone> { our_address: DestinationAddressBytes, auth_token: Option, connection: SocketState<'a>, - sphinx_packet_sender: SphinxPacketSender, + packet_router: PacketRouter, response_timeout_duration: Duration, } @@ -263,7 +210,8 @@ where gateway_address: R, our_address: DestinationAddressBytes, auth_token: Option, - sphinx_packet_sender: SphinxPacketSender, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, response_timeout_duration: Duration, ) -> Self { GatewayClient { @@ -272,11 +220,65 @@ where our_address, auth_token, connection: SocketState::NotConnected, - sphinx_packet_sender, + packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), response_timeout_duration, } } + pub fn new_init( + gateway_address: R, + our_address: DestinationAddressBytes, + response_timeout_duration: Duration, + ) -> Self { + use futures::channel::mpsc; + + // note: this packet_router is completely invalid in normal circumstances, but "works" + // perfectly fine here, because it's not meant to be used + let (ack_tx, _) = mpsc::unbounded(); + let (mix_tx, _) = mpsc::unbounded(); + let packet_router = PacketRouter::new(ack_tx, mix_tx); + + GatewayClient { + authenticated: false, + gateway_address, + our_address, + auth_token: None, + connection: SocketState::NotConnected, + packet_router, + response_timeout_duration, + } + } + + pub async fn register_without_listening(&mut self) -> Result { + if !self.connection.is_established() { + self.establish_connection().await?; + } + let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); + let token = match self.send_websocket_message(msg).await? { + ServerResponse::Register { token } => { + self.authenticated = true; + Ok(AuthToken::try_from_base58_string(token)?) + } + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => unreachable!(), + }?; + Ok(token) + } + + pub async fn close_connection(&mut self) -> Result<(), GatewayClientError> { + if self.connection.is_partially_delegated() { + self.recover_socket_connection().await?; + } + + match std::mem::replace(&mut self.connection, SocketState::NotConnected) { + SocketState::Available(mut socket) => Ok(socket.close(None).await?), + SocketState::PartiallyDelegated(_) => { + unreachable!("this branch should have never been reached!") + } + _ => Ok(()), // no need to do anything in those cases + } + } + pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { let ws_stream = match connect_async(self.gateway_address.clone()).await { Ok((ws_stream, _)) => ws_stream, @@ -320,7 +322,7 @@ where }; match msg { Message::Binary(bin_msg) => { - self.sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + self.packet_router.route_received(vec![bin_msg]); } Message::Text(txt_msg) => { res = Some(ServerResponse::try_from(txt_msg).map_err(|_| GatewayClientError::MalformedResponse)); @@ -340,10 +342,10 @@ where &mut self, msg: Message, ) -> Result { - let mut should_restart_sphinx_listener = false; + let mut should_restart_mixnet_listener = false; if self.connection.is_partially_delegated() { self.recover_socket_connection().await?; - should_restart_sphinx_listener = true; + should_restart_mixnet_listener = true; } let conn = match self.connection { @@ -354,8 +356,8 @@ where conn.send(msg).await?; let response = self.read_control_response().await; - if should_restart_sphinx_listener { - self.start_listening_for_sphinx_packets()?; + if should_restart_mixnet_listener { + self.start_listening_for_mixnet_messages()?; } response } @@ -387,7 +389,7 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_sphinx_packets()?; + self.start_listening_for_mixnet_messages()?; Ok(token) } @@ -414,7 +416,7 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_sphinx_packets()?; + self.start_listening_for_mixnet_messages()?; Ok(authenticated) } @@ -468,7 +470,7 @@ where Ok(()) } - fn start_listening_for_sphinx_packets(&mut self) -> Result<(), GatewayClientError> { + fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_partially_delegated() { return Ok(()); } @@ -479,9 +481,9 @@ where let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { - PartiallyDelegated::split_and_listen_for_sphinx_packets( + PartiallyDelegated::split_and_listen_for_mixnet_messages( conn, - self.sphinx_packet_sender.clone(), + self.packet_router.clone(), )? } _ => unreachable!(), diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs new file mode 100644 index 0000000000..fcd4f6224c --- /dev/null +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -0,0 +1,83 @@ +// 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. + +// JS: I personally don't like this name very much, but could not think of anything better. +// I will gladly take any suggestions on how to rename this. + +use futures::channel::mpsc; +use log::*; +use nymsphinx::params::packet_sizes::PacketSize; + +pub type MixnetMessageSender = mpsc::UnboundedSender>>; +pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; + +pub type AcknowledgementSender = mpsc::UnboundedSender>>; +pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; + +#[derive(Clone, Debug)] +pub(super) struct PacketRouter { + ack_sender: AcknowledgementSender, + mixnet_message_sender: MixnetMessageSender, +} + +impl PacketRouter { + pub(super) fn new( + ack_sender: AcknowledgementSender, + mixnet_message_sender: MixnetMessageSender, + ) -> Self { + PacketRouter { + ack_sender, + mixnet_message_sender, + } + } + + pub(super) fn route_received(&self, unwrapped_packets: Vec>) { + let mut received_messages = Vec::new(); + let mut received_acks = Vec::new(); + + for received_packet in unwrapped_packets { + // TODO: currently this is not true because gateways are removing padding from the packets + // but will be fixed soon enough by all other changes in the pipeline + // the question is, however, what exactly will gateways be returning instead. payloads? + // 'plaintext'?. To be determined later on. + // if received_packet.len() == PacketSize::ACKPacket.payload_size() { + + // this is an extremely ugly if statement, but will be improved once things are actually + // constant length everywhere + if received_packet.len() == 21 { + received_acks.push(received_packet); + } else { + // well, technically all 21 bytes packets will be considered acks which is not + // entirely true, but for time being let's stick with it until other changes are + // introduced + received_messages.push(received_packet); + } + } + + // due to how we are currently using it, those unwraps can't fail, but if we ever + // wanted to make `gateway-client` into some more generic library, we would probably need + // to catch that error or something. + if !received_messages.is_empty() { + trace!("routing 'real'"); + self.mixnet_message_sender + .unbounded_send(received_messages) + .unwrap(); + } + + if !received_acks.is_empty() { + trace!("routing acks"); + self.ack_sender.unbounded_send(received_acks).unwrap(); + } + } +} diff --git a/common/healthcheck/src/path_check.rs b/common/healthcheck/src/path_check.rs index b61f921df8..f7fc2a0e6d 100644 --- a/common/healthcheck/src/path_check.rs +++ b/common/healthcheck/src/path_check.rs @@ -285,7 +285,6 @@ impl PathChecker { &path[..], &self.our_destination, &delays, - None, ) .unwrap(); diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index df40720199..b300b5fad0 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -7,12 +7,14 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bytes = "0.5" -log = "0.4.8" rand = {version = "0.7.3", features = ["wasm-bindgen"]} rand_distr = "0.2.2" -tokio-util = { version = "0.3.1", features = ["codec"] } -## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f" } +nymsphinx-acknowledgements = { path = "acknowledgements" } +nymsphinx-addressing = { path = "addressing" } +nymsphinx-chunking = { path = "chunking" } +nymsphinx-cover = { path = "cover" } +nymsphinx-framing = { path = "framing" } +nymsphinx-params = { path = "params" } +nymsphinx-types = { path = "types" } diff --git a/common/nymsphinx/acknowledgements/Cargo.toml b/common/nymsphinx/acknowledgements/Cargo.toml new file mode 100644 index 0000000000..c697fccd87 --- /dev/null +++ b/common/nymsphinx/acknowledgements/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nymsphinx-acknowledgements" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aes-ctr = "0.4.0" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} + + + +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } + +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs new file mode 100644 index 0000000000..6ff2406ce3 --- /dev/null +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -0,0 +1,103 @@ +// 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 aes_ctr::{ + stream_cipher::{ + generic_array::{ + typenum::{marker_traits::Unsigned, U16}, + GenericArray, + }, + NewStreamCipher, SyncStreamCipher, + }, + Aes128Ctr, +}; +use nymsphinx_params::packet_sizes::PacketSize; +use rand::{CryptoRng, RngCore}; + +// the 'U16' type is taken directly from the `Ctr128` for consistency sake +pub type Aes128KeySize = U16; +pub type Aes128NonceSize = U16; + +pub type AckAes128Key = GenericArray; +type AckAes128IV = GenericArray; + +pub fn generate_key(rng: &mut R) -> AckAes128Key { + let mut ack_key = GenericArray::default(); + rng.fill_bytes(&mut ack_key); + ack_key +} + +fn random_iv(rng: &mut R) -> AckAes128IV { + let mut iv = GenericArray::default(); + rng.fill_bytes(&mut iv); + iv +} + +pub fn prepare_identifier( + rng: &mut R, + key: &AckAes128Key, + marshaled_id: &[u8], +) -> Vec { + // TODO: should we have some length checks on the id? + + let iv = random_iv(rng); + let mut cipher = Aes128Ctr::new(key, &iv); + let mut output = marshaled_id.to_vec(); + + cipher.apply_keystream(&mut output); + + iv.into_iter().chain(output.into_iter()).collect() +} + +pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option> { + // first few bytes are expected to be the concatenated IV. It must be followed by at least 1 more + // byte that we wish to recover, but it can be no longer from what we can physically store inside + // an ack + if iv_ciphertext.len() <= Aes128NonceSize::to_usize() + || iv_ciphertext.len() > PacketSize::ACKPacket.plaintext_size() + { + return None; + } + + let iv = GenericArray::from_slice(&iv_ciphertext[..Aes128NonceSize::to_usize()]); + let mut cipher = Aes128Ctr::new(key, &iv); + let mut output = iv_ciphertext[Aes128NonceSize::to_usize()..].to_vec(); + cipher.apply_keystream(&mut output); + + Some(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::OsRng; + + #[test] + fn id_is_recoverable() { + let mut rng = OsRng; + let key = generate_key(&mut rng); + + let id1 = vec![42]; // single byte case + let id2 = vec![1, 2, 3, 4, 5]; // 5byte we expect to use + let id3 = vec![42; 8]; // some reasonable upper bound id size we could use later on + + let iv_ciphertext1 = prepare_identifier(&mut rng, &key, &id1); + let iv_ciphertext2 = prepare_identifier(&mut rng, &key, &id2); + let iv_ciphertext3 = prepare_identifier(&mut rng, &key, &id3); + + assert_eq!(id1, recover_identifier(&key, &iv_ciphertext1).unwrap()); + assert_eq!(id2, recover_identifier(&key, &iv_ciphertext2).unwrap()); + assert_eq!(id3, recover_identifier(&key, &iv_ciphertext3).unwrap()); + } +} diff --git a/common/nymsphinx/src/utils/poisson.rs b/common/nymsphinx/acknowledgements/src/lib.rs similarity index 55% rename from common/nymsphinx/src/utils/poisson.rs rename to common/nymsphinx/acknowledgements/src/lib.rs index 22414cb402..b091185843 100644 --- a/common/nymsphinx/src/utils/poisson.rs +++ b/common/nymsphinx/acknowledgements/src/lib.rs @@ -12,13 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rand_distr::{Distribution, Exp}; -use std::time; +pub mod identifier; +pub mod surb_ack; -pub fn sample(average_duration: time::Duration) -> time::Duration { - // this is our internal code used by our traffic streams - // the error is only thrown if average delay is less than 0, which will never happen - // so call to unwrap is perfectly safe here - let exp = Exp::new(1.0 / average_duration.as_nanos() as f64).unwrap(); - time::Duration::from_nanos(exp.sample(&mut rand::thread_rng()).round() as u64) -} +pub use identifier::{generate_key, AckAes128Key}; diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs new file mode 100644 index 0000000000..953ab9da1e --- /dev/null +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -0,0 +1,119 @@ +// 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::identifier::{prepare_identifier, AckAes128Key}; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{ + delays::{self, Delay}, + Destination, SphinxPacket, +}; +use rand::{CryptoRng, RngCore}; +use std::convert::TryFrom; +use std::time; +use topology::{NymTopology, NymTopologyError}; + +#[allow(non_snake_case)] +pub struct SURBAck { + surb_ack_packet: SphinxPacket, + first_hop_address: NymNodeRoutingAddress, + expected_total_delay: Delay, +} + +#[derive(Debug)] +pub enum SURBAckRecoveryError { + InvalidPacketSize, + InvalidAddress, + InvalidSphinxPacket, +} + +impl SURBAck { + pub fn construct( + rng: &mut R, + recipient: &Recipient, + ack_key: &AckAes128Key, + marshaled_fragment_id: &[u8], + average_delay: time::Duration, + topology: &T, + ) -> Result + where + R: RngCore + CryptoRng, + T: NymTopology, + { + let route = topology.random_route_to_gateway(&recipient.gateway())?; + let delays = delays::generate_from_average_duration(route.len(), average_delay); + let destination = Destination::new(recipient.destination(), Default::default()); + + let surb_ack_payload = prepare_identifier(rng, ack_key, marshaled_fragment_id); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let surb_ack_packet = SphinxPacketBuilder::new() + .with_payload_size(PacketSize::ACKPacket.payload_size()) + .build_packet(surb_ack_payload, &route, &destination, &delays) + .unwrap(); + + let expected_total_delay = delays.iter().sum(); + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok(SURBAck { + surb_ack_packet, + first_hop_address, + expected_total_delay, + }) + } + + pub fn len() -> usize { + // TODO: this will be variable once/if we decide to introduce optimization described + // in common/nymsphinx/chunking/src/lib.rs:available_plaintext_size() + PacketSize::ACKPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN + } + + pub fn prepare_for_sending(self) -> (Delay, Vec) { + // SURB_FIRST_HOP || SURB_ACK + let surb_bytes: Vec<_> = self + .first_hop_address + .as_zero_padded_bytes(MAX_NODE_ADDRESS_UNPADDED_LEN) + .into_iter() + .chain(self.surb_ack_packet.to_bytes().into_iter()) + .collect(); + (self.expected_total_delay, surb_bytes) + } + + // partial reciprocal of `prepare_for_sending` performed by the gateway + pub fn try_recover_first_hop_packet( + b: &[u8], + ) -> Result<(NymNodeRoutingAddress, SphinxPacket), SURBAckRecoveryError> { + if b.len() != Self::len() { + Err(SURBAckRecoveryError::InvalidPacketSize) + } else { + let address = match NymNodeRoutingAddress::try_from_bytes(&b) { + Ok(address) => address, + Err(_) => return Err(SURBAckRecoveryError::InvalidAddress), + }; + + // TODO: this will be variable once/if we decide to introduce optimization described + // in common/nymsphinx/chunking/src/lib.rs:available_plaintext_size() + let address_offset = MAX_NODE_ADDRESS_UNPADDED_LEN; + let packet = match SphinxPacket::from_bytes(&b[address_offset..]) { + Ok(packet) => packet, + Err(_) => return Err(SURBAckRecoveryError::InvalidSphinxPacket), + }; + + Ok((address, packet)) + } + } +} diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml new file mode 100644 index 0000000000..f4cee942bd --- /dev/null +++ b/common/nymsphinx/addressing/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nymsphinx-addressing" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nymsphinx-types = { path = "../types" } \ No newline at end of file diff --git a/common/nymsphinx/src/addressing/clients.rs b/common/nymsphinx/addressing/src/clients.rs similarity index 95% rename from common/nymsphinx/src/addressing/clients.rs rename to common/nymsphinx/addressing/src/clients.rs index b64e4a3a47..676368c13b 100644 --- a/common/nymsphinx/src/addressing/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -4,15 +4,15 @@ // of a helper/utils structure, because before it reaches the gateway // it's already destructed). -use crate::{ +use nymsphinx_types::{ DestinationAddressBytes, NodeAddressBytes, DESTINATION_ADDRESS_LENGTH, NODE_ADDRESS_LENGTH, }; #[derive(Debug)] pub struct RecipientFormattingError; -impl From for RecipientFormattingError { - fn from(_: crate::Error) -> Self { +impl From for RecipientFormattingError { + fn from(_: nymsphinx_types::Error) -> Self { Self } } diff --git a/common/nymsphinx/src/addressing/mod.rs b/common/nymsphinx/addressing/src/lib.rs similarity index 100% rename from common/nymsphinx/src/addressing/mod.rs rename to common/nymsphinx/addressing/src/lib.rs diff --git a/common/nymsphinx/src/addressing/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs similarity index 89% rename from common/nymsphinx/src/addressing/nodes.rs rename to common/nymsphinx/addressing/src/nodes.rs index 8acd798f68..ebcbd0b789 100644 --- a/common/nymsphinx/src/addressing/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; +use nymsphinx_types::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; use std::convert::{TryFrom, TryInto}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -21,6 +21,10 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// Currently, that routing information is an IP address, but in principle it can be anything /// for as long as it's going to fit in the field. +/// MAX_UNPADDED_LEN represents maximum length an unpadded address could have. +/// In this case it's an ipv6 socket address (with version prefix) +pub const MAX_NODE_ADDRESS_UNPADDED_LEN: usize = 19; + #[derive(Debug)] pub enum NymNodeRoutingAddressError { InsufficientNumberOfBytesAvailableError, @@ -59,6 +63,23 @@ impl NymNodeRoutingAddress { .collect() } + /// Converts self into a vector of bytes optionally padded with zeroes to the `expected_len`. + /// Note this does not necessarily represent a NodeAddressBytes, unless + /// `expected_len` == NODE_ADDRESS_LENGTH + pub fn as_zero_padded_bytes(&self, expected_len: usize) -> Vec { + let self_bytes = self.as_bytes(); + if self_bytes.len() >= expected_len { + // can't add padding + self_bytes + } else { + self_bytes + .into_iter() + .chain(std::iter::repeat(0)) + .take(expected_len) + .collect() + } + } + /// Tries to recover `Self` from a bytes slice. /// Does not care if it's zero-padded or not. pub fn try_from_bytes(b: &[u8]) -> Result { @@ -129,12 +150,7 @@ impl TryInto for NymNodeRoutingAddress { return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); } - let unpadded_address = self.as_bytes(); - let padded_address: Vec<_> = unpadded_address - .into_iter() - .chain(std::iter::repeat(0)) - .take(NODE_ADDRESS_LENGTH) - .collect(); + let padded_address = self.as_zero_padded_bytes(NODE_ADDRESS_LENGTH); let mut node_address_bytes = [0u8; 32]; node_address_bytes.copy_from_slice(&padded_address); diff --git a/common/nymsphinx/chunking/Cargo.toml b/common/nymsphinx/chunking/Cargo.toml new file mode 100644 index 0000000000..88f53ce322 --- /dev/null +++ b/common/nymsphinx/chunking/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nymsphinx-chunking" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.8" +rand = { version = "0.7.3", features = ["wasm-bindgen"] } + +nymsphinx-acknowledgements = { path = "../acknowledgements" } +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs new file mode 100644 index 0000000000..76d9c432f0 --- /dev/null +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -0,0 +1,1100 @@ +// 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::ChunkingError; +use std::convert::TryInto; + +// Personal reflection: In hindsight I've spent too much time on relatively too little +// gain here, as even though I might have saved couple of bytes per packet, the gain +// is negligible in the context of having to include SURB-ACKs and reply-SURBs in the packets. +// +// However, if we really cared about those tiny optimisations, `UNLINKED_FRAGMENTED_HEADER` +// could be further compressed: if current_fragment != 1 && current_fragment != 255, you don't +// need to use the tail byte to indicate lack of linking as it can be implied from the fragment +// position. + +// TODO for later: with the removal of 'unfragmented' fragments, the first bit of each header +// is completely useless, we should then think how to make the set_id become u32 instead of i32. +// (the current limitation for making the seemingly trivial change is "linked id" which +// has to have same amount of space available and right now it only has 31 bits available) + +/// When the underlying message has to be split into multiple Fragments, but still manages to fit +/// into a single `FragmentSet`, each `FragmentHeader` needs to hold additional information to allow +/// for correct message reconstruction: 4 bytes for set id, 1 byte to represent total number +/// of fragments in the set, 1 byte to represent position of the current fragment in the set +/// and finally an extra byte to indicate the fragment has no links to other sets. +pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7; + +/// Logically almost identical to `UNLINKED_FRAGMENTED_HEADER_LEN`, however, the extra three +/// bytes are due to changing the final byte that used to indicate the `Fragment` is not linked +/// into 4 byte id of either previous or the next set. +/// Note that the linked headers can potentially be used only for very first and very last +/// `Fragment` in a `FragmentSet`. +pub const LINKED_FRAGMENTED_HEADER_LEN: usize = 10; + +/// Maximum size of payload of each fragment is always the maximum amount of plaintext data +/// we can put into a sphinx packet minus length of respective fragment header. +pub const fn unlinked_fragment_payload_max_len(max_plaintext_size: usize) -> usize { + max_plaintext_size - UNLINKED_FRAGMENTED_HEADER_LEN +} + +/// Maximum size of payload of each fragment is always the maximum amount of plaintext data +/// we can put into a sphinx packet minus length of respective fragment header. +pub const fn linked_fragment_payload_max_len(max_plaintext_size: usize) -> usize { + max_plaintext_size - LINKED_FRAGMENTED_HEADER_LEN +} + +// TODO: should this be defined in this module or in `cover`? I can see arguments for both options... +/// A special `FragmentIdentifier` that is not valid in all cases unless if it's used in a loop +/// cover message. +pub const COVER_FRAG_ID: FragmentIdentifier = FragmentIdentifier { + set_id: 0, + fragment_position: 0, +}; + +/// Identifier to uniquely identify a fragment. It represents 31bit ID of given `FragmentSet` +/// and u8 position of the `Fragment` in the set. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct FragmentIdentifier { + set_id: i32, + fragment_position: u8, +} + +impl FragmentIdentifier { + pub fn to_bytes(&self) -> Vec { + self.set_id + .to_be_bytes() + .iter() + .cloned() + .chain(std::iter::once(self.fragment_position)) + .collect() + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.len() != 5 { + return Err(ChunkingError::MalformedFragmentIdentifier); + } + + let set_id = i32::from_be_bytes([b[0], b[1], b[2], b[3]]); + // set_id == 0 is valid for, and only for, COVER_FRAG_ID + if set_id < 0 { + return Err(ChunkingError::MalformedFragmentIdentifier); + } + + Ok(FragmentIdentifier { + set_id, + fragment_position: b[4], + }) + } +} + +/// The basic unit of division of underlying bytes message sent through the mix network. +/// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. +/// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional +/// header used to reconstruct the message after being received. +#[derive(PartialEq, Clone, Debug)] +pub struct Fragment { + header: FragmentHeader, + payload: Vec, +} + +impl Fragment { + /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. + /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata + /// is malformed or self-contradictory, for example if current_fragment > total_fragments. + pub(crate) fn try_new( + payload: &[u8], + id: i32, + total_fragments: u8, + current_fragment: u8, + previous_fragments_set_id: Option, + next_fragments_set_id: Option, + max_plaintext_size: usize, + ) -> Result { + let header = FragmentHeader::try_new( + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + )?; + + // check for whether payload has expected length, which depend on whether fragment is linked + // and if it's the only one or the last one in the set (then lower bound is removed) + if previous_fragments_set_id.is_some() { + if total_fragments > 1 { + if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if payload.len() > linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } + } else if next_fragments_set_id.is_some() { + if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if total_fragments != current_fragment { + if payload.len() != unlinked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if payload.len() > unlinked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } + } + + Ok(Fragment { + header, + payload: payload.to_vec(), + }) + } + + /// Convert this `Fragment` into vector of bytes which can be put into a sphinx packet. + pub(crate) fn into_bytes(self) -> Vec { + self.header + .to_bytes() + .into_iter() + .chain(self.payload.into_iter()) + .collect() + } + + /// Derive identifier unique for this particular fragment + pub fn fragment_identifier(&self) -> FragmentIdentifier { + FragmentIdentifier { + set_id: self.header.id, + fragment_position: self.header.current_fragment, + } + } + + /// Extracts id of this `Fragment`. + pub fn id(&self) -> i32 { + self.header.id + } + + /// Extracts total number of fragments associated with this particular `Fragment` (belonging to + /// the same `FragmentSet`). + pub fn total_fragments(&self) -> u8 { + self.header.total_fragments + } + + /// Extracts position of this `Fragment` in a `FragmentSet`. + pub fn current_fragment(&self) -> u8 { + self.header.current_fragment + } + + /// Extracts information regarding id of pre-linked `FragmentSet` + pub fn previous_fragments_set_id(&self) -> Option { + self.header.previous_fragments_set_id + } + + /// Extracts information regarding id of post-linked `FragmentSet` + pub fn next_fragments_set_id(&self) -> Option { + self.header.next_fragments_set_id + } + + /// Consumes `self` to obtain payload (i.e. part of original message) associated with this + /// `Fragment`. + pub(crate) fn extract_payload(self) -> Vec { + self.payload + } + + /// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet. + /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata + /// is malformed or self-contradictory, for example if current_fragment > total_fragments. + pub(crate) fn try_from_bytes(b: &[u8]) -> Result { + let (header, n) = FragmentHeader::try_from_bytes(b)?; + + // there's no sane way to decide if payload has correct range anymore as + // it's no longer fixed + + Ok(Fragment { + header, + payload: b[n..].to_vec(), + }) + } +} + +/// In order to be able to re-assemble fragmented message sent through a mix-network, some +/// metadata is attached alongside the actual message payload. The idea is to include as little +/// of that data as possible due to computationally very costly process of sphinx encapsulation. +/// +/// The generic `FragmentHeader` is represented as follows: +/// IF flag || 31 bit ID || TotalFragments || CurrentFragment || LID flag || 31 bit Linked ID +/// note that LID is a valid flag only for first and +/// last fragment (if TotalFragments == CurrentFragment == 255) in given set. +/// +/// further note if LID is not set, +/// then the Linked ID bytes in the header are used as payload. +/// +/// Hence after marshaling `FragmentHeader` into bytes, +/// the following three alternatives are possible: +/// +/// 7 byte long sequence representing that this `Fragment` is one of multiple ones in the set. +/// However, the set is not linked to any other sets: +/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '0'byte +/// +/// 10 byte sequence representing first (or last) fragment in the set, +/// where the set is linked to either preceding data (TF == 1) or proceeding data (TF == CF == 255) +/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '1'bit || 31-bit LID +/// +/// And hence for messages larger than `max_plaintext_size` but small enough +/// to avoid set division (which happens if message has to be fragmented into more than 255 fragments) +/// there is 7 bytes of overhead inside each sphinx packet sent +/// and for the longest messages, without upper bound, there is usually also only 7 bytes +/// of overhead apart from first and last fragments in each set that instead have 10 bytes of overhead. +#[derive(PartialEq, Clone, Debug)] +pub(crate) struct FragmentHeader { + /// ID associated with `FragmentSet` to which this particular `Fragment` belongs. + /// Its value is restricted to (0, i32::max_value()]. + /// Note that it *excludes* 0, but *includes* i32::max_value(). + /// This allows the field to be represented using 31 bits. + id: i32, + + /// Total number of `Fragment`s in `FragmentSet` used to be able to determine if entire + /// set was fully received as well as to perform bound checks. + total_fragments: u8, + + /// Since message is always fragmented into payloads of constant lengths + /// (apart from possibly the last one), there's no need to use offsets like ipv4/ipv6 + /// and we can just simply enumerate the fragments to later reconstruct the message. + current_fragment: u8, + + /// Optional ID of previous `FragmentSet` into which the original message was split. + /// Note, this option is only valid of `current_fragment == 1` + previous_fragments_set_id: Option, + + /// Optional ID of next `FragmentSet` into which the original message was split. + /// Note, this option is only valid of `current_fragment == total_fragments == u8::max_value()` + next_fragments_set_id: Option, +} + +impl FragmentHeader { + /// Tries to create a new `FragmentHeader` using provided metadata. Bunch of logical + /// checks are performed to see if the data is not self-contradictory, + /// for example if current_fragment > total_fragments. + fn try_new( + id: i32, + total_fragments: u8, + current_fragment: u8, + previous_fragments_set_id: Option, + next_fragments_set_id: Option, + ) -> Result { + if id <= 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if total_fragments < current_fragment { + return Err(ChunkingError::MalformedHeaderError); + } + if total_fragments == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if current_fragment == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if let Some(pfid) = previous_fragments_set_id { + if pfid <= 0 || current_fragment != 1 || pfid == id { + return Err(ChunkingError::MalformedHeaderError); + } + } + if let Some(nfid) = next_fragments_set_id { + if nfid <= 0 || current_fragment != total_fragments || nfid == id { + return Err(ChunkingError::MalformedHeaderError); + } + } + + Ok(FragmentHeader { + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + }) + } + + /// Tries to recover `FragmentHeader` from slice of bytes extracted from received sphinx packet. + /// If successful, returns `Self` and number of bytes used, as those can differ based on the + /// type of header (unlinked or linked). + fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { + // header needs to be at least 7 bytes long + if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { + return Err(ChunkingError::TooShortFragmentData); + } + let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); + // sanity check for the fragmentation flag + if ((frag_id >> 31) & 1) == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + + let id = frag_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly + let total_fragments = b[4]; + let current_fragment = b[5]; + + if total_fragments == 0 || current_fragment == 0 || current_fragment > total_fragments { + return Err(ChunkingError::MalformedHeaderError); + } + + let mut previous_fragments_set_id = None; + let mut next_fragments_set_id = None; + + // check if the linking id flag might be set + let read_bytes = if b[6] != 0 { + // there's linking ID supposedly attached, make sure we have enough bytes to parse + if b.len() < LINKED_FRAGMENTED_HEADER_LEN { + return Err(ChunkingError::TooShortFragmentData); + } + let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); + + // sanity check for the linked flag + if ((flagged_linked_id >> 31) & 1) == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + + let linked_id = flagged_linked_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly + + if current_fragment == 1 { + previous_fragments_set_id = Some(linked_id); + } else if total_fragments == current_fragment && current_fragment == u8::max_value() { + next_fragments_set_id = Some(linked_id); + } else { + return Err(ChunkingError::MalformedHeaderError); + } + + 10 + } else { + 7 + }; + + Ok(( + Self::try_new( + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + )?, + read_bytes, + )) + } + + /// Marshal this `FragmentHeader` into vector of bytes which can be put into a sphinx packet. + fn to_bytes(&self) -> Vec { + let frag_id = self.id | (1 << 31); + let frag_id_bytes = frag_id.to_be_bytes(); + let bytes_prefix_iter = frag_id_bytes + .iter() + .cloned() + .chain(std::iter::once(self.total_fragments)) + .chain(std::iter::once(self.current_fragment)); + + let is_linked = + self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some(); + if is_linked { + let linked_id = self + .previous_fragments_set_id + .unwrap_or_else(|| self.next_fragments_set_id.unwrap()); + let linked_id_entry = linked_id | (1 << 31); + let linked_id_bytes = linked_id_entry.to_be_bytes(); + bytes_prefix_iter + .chain(linked_id_bytes.iter().cloned()) + .collect() + } else { + bytes_prefix_iter.chain(std::iter::once(0)).collect() + } + } +} + +// everything below are tests + +#[cfg(test)] +mod fragment { + use super::*; + use nymsphinx_params::packet_sizes::PacketSize; + use rand::{thread_rng, RngCore}; + + fn max_plaintext_size() -> usize { + PacketSize::default().plaintext_size() - PacketSize::ACKPacket.size() + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_unfragmented_payload() { + let mut rng = thread_rng(); + + let mlen = 40; + let mut valid_message = vec![0u8; mlen]; + rng.fill_bytes(&mut valid_message); + + let valid_unfragmented_packet = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, None, None).unwrap(), + payload: valid_message, + }; + let packet_bytes = valid_unfragmented_packet.clone().into_bytes(); + assert_eq!( + valid_unfragmented_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let empty_unfragmented_packet = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, None, None).unwrap(), + payload: Vec::new(), + }; + let packet_bytes = empty_unfragmented_packet.clone().into_bytes(); + assert_eq!( + empty_unfragmented_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_unlinked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let non_last_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = non_last_packet.clone().into_bytes(); + assert_eq!( + non_last_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let last_full_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 10, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = last_full_packet.clone().into_bytes(); + assert_eq!( + last_full_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let last_non_full_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 10, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = last_non_full_packet.clone().into_bytes(); + + assert_eq!( + last_non_full_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_pre_linked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, Some(1234), None).unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + // TODO: + // TODO: + // packet_bytes len assertion + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_post_linked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + // TODO: + // TODO: + // packet_bytes len assertion + + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + } + + #[test] + fn unlinked_fragment_can_be_created_with_payload_of_valid_length() { + let id = 12345; + let full_payload = vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + let non_full_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 60]; + + assert!( + Fragment::try_new(&full_payload, id, 10, 1, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 10, 5, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 10, 10, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 1, 1, None, None, max_plaintext_size()).is_ok() + ); + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 10, + None, + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_ok()); + + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 10, + None, + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_ok()); + } + + #[test] + fn unlinked_fragment_returns_error_when_created_with_payload_of_invalid_length() { + let id = 12345; + let non_full_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 20]; + let too_much_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) + 1]; + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + } + + #[test] + fn linked_fragment_can_be_created_with_payload_of_valid_length() { + let id = 12345; + let link_id = 1234; + let full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size())]; + let non_full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + + assert!(Fragment::try_new( + &full_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &full_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + + assert!(Fragment::try_new( + &full_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_ok()); + } + + #[test] + fn linked_fragment_returns_error_when_created_with_payload_of_invalid_length() { + let id = 12345; + let link_id = 1234; + let non_full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + let too_much_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) + 1]; + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &non_full_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &too_much_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + } +} + +#[cfg(test)] +mod fragment_header { + use super::*; + + #[cfg(test)] + mod unlinked_fragmented_payload { + use super::*; + + #[test] + fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn retrieval_from_bytes_fail_for_insufficient_number_of_bytes_provided() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let header_bytes = &header_bytes[..header_bytes.len() - 1]; + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()) + } + + #[test] + fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { + let fragmented_header = FragmentHeader::try_new(10, 10, 5, None, None).unwrap(); + + let mut header_bytes_low = fragmented_header.to_bytes(); + + // clear the fragmentation flag + header_bytes_low[0] &= !(1 << 7); + + let mut header_bytes_high = header_bytes_low.clone(); + // make sure first byte of id is non-empty (apart from the fragmentation flag) + // note for anyone reading this test in the future: choice of '3' here is arbitrary. + header_bytes_high[0] |= 1 << 3; + + // This will have cause an error as there will be a value in the first byte + assert!(FragmentHeader::try_from_bytes(&header_bytes_high).is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_for_invalid_link_flag() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + // set linked flag + header_bytes[6] |= 1 << 7; + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + + #[test] + fn creation_of_header_fails_if_current_fragment_is_higher_than_total() { + assert!(FragmentHeader::try_new(12345, 10, 11, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_current_fragment_is_zero() { + assert!(FragmentHeader::try_new(12345, 10, 0, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_total_fragments_is_zero() { + assert!(FragmentHeader::try_new(12345, 0, 0, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_id_is_negative() { + assert!(FragmentHeader::try_new(-10, 10, 5, None, None).is_err()); + } + + #[test] + fn fragmented_header_cannot_be_created_with_zero_id() { + assert!(FragmentHeader::try_new(0, 10, 5, None, None).is_err()); + assert!(FragmentHeader::try_new(12345, 10, 5, Some(0), None).is_err()); + assert!(FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(0) + ) + .is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_if_current_fragment_is_higher_than_total() { + // manually create header to overwrite any constructor checks + let header = FragmentHeader { + id: 1234, + total_fragments: 10, + current_fragment: 11, + previous_fragments_set_id: None, + next_fragments_set_id: None, + }; + let header_bytes = header.to_bytes(); + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_if_current_or_total_fragment_is_zero() { + // manually create header to overwrite any constructor checks + let header = FragmentHeader { + id: 1234, + total_fragments: 0, + current_fragment: 0, + previous_fragments_set_id: None, + next_fragments_set_id: None, + }; + let header_bytes = header.to_bytes(); + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + } + + #[cfg(test)] + mod linked_fragmented_payload { + use super::*; + + #[test] + fn cannot_be_linked_to_itself() { + assert!(FragmentHeader::try_new(12345, 10, 1, Some(12345), None).is_err()); + assert!(FragmentHeader::try_new(12345, 10, 10, None, Some(12345)).is_err()); + } + + #[test] + fn can_only_be_pre_linked_for_first_fragment() { + assert!(FragmentHeader::try_new(12345, 10, 1, Some(1234), None).is_ok()); + assert!(FragmentHeader::try_new(12345, 10, 2, Some(1234), None).is_err()); + } + + #[test] + fn can_only_be_post_linked_for_last_fragment() { + assert!(FragmentHeader::try_new(12345, 10, 10, None, Some(1234)).is_ok()); + assert!(FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .is_ok()); + assert!(FragmentHeader::try_new(12345, 10, 2, Some(1234), None).is_err()); + } + + #[test] + fn pre_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = + FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn pre_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = + FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn pre_linked_is_successfully_recovered_if_its_both_first_and_final_fragment() { + let fragmented_header = FragmentHeader::try_new(12345, 1, 1, Some(1234), None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn post_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = + FragmentHeader::try_new(12345, u8::max_value(), u8::max_value(), None, Some(1234)) + .unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn post_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = + FragmentHeader::try_new(12345, u8::max_value(), u8::max_value(), None, Some(1234)) + .unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + } +} diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs new file mode 100644 index 0000000000..df9e968b04 --- /dev/null +++ b/common/nymsphinx/chunking/src/lib.rs @@ -0,0 +1,240 @@ +// 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::fragment::{Fragment, FragmentIdentifier}; +use crate::set::split_into_sets; +use nymsphinx_acknowledgements::identifier::AckAes128Key; +use nymsphinx_acknowledgements::surb_ack::SURBAck; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{delays, Delay, Destination, SphinxPacket}; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::time::Duration; +use topology::{NymTopology, NymTopologyError}; + +// Future consideration: currently in a lot of places, the payloads have randomised content +// which is not a perfect testing strategy as it might not detect some edge cases I never would +// have assumed could be possible. A better approach would be to research some Fuzz testing +// library like: https://github.com/rust-fuzz/afl.rs and use that instead for the inputs. + +// perhaps it might be useful down the line for interaction testing between client,mixes,etc? + +pub mod fragment; +pub mod reconstruction; +pub mod set; + +type DefaultRng = OsRng; +const DEFAULT_RNG: DefaultRng = OsRng; + +/// The idea behind the process of chunking is to incur as little data overhead as possible due +/// to very computationally costly sphinx encapsulation procedure. +/// +/// To achieve this, the underlying message is split into so-called "sets", which are further +/// subdivided into the base unit of "fragment" that is directly encapsulated by a Sphinx packet. +/// This allows to encapsulate messages of arbitrary length. +/// +/// Each message, regardless of its size, consists of at least a single `Set` that has at least +/// a single `Fragment`. +/// +/// Each `Fragment` can have variable, yet fully deterministic, length, +/// that depends on its position in the set as well as total number of sets. This is further +/// explained in `fragment.rs` file. +/// +/// Similarly, each `Set` can have a variable number of `Fragment`s inside. However, that +/// value is more restrictive: if it's the last set into which the message was split +/// (or implicitly the only one), it has no lower bound on the number of `Fragment`s. +/// (Apart from the restriction of containing at least a single one). If the set is located +/// somewhere in the middle, *it must be* full. Finally, regardless of its position, it must also be +/// true that it contains no more than `u8::max_value()`, i.e. 255 `Fragment`s. +/// Again, the reasoning for this is further explained in `set.rs` file. However, you might +/// also want to look at `fragment.rs` to understand the full context behind that design choice. +/// +/// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` +/// are further explained in the respective files. + +#[derive(PartialEq, Debug)] +pub enum ChunkingError { + InvalidPayloadLengthError, + TooBigMessageToSplit, + MalformedHeaderError, + NoValidProvidersError, + NoValidRoutesAvailableError, + InvalidTopologyError, + TooShortFragmentData, + MalformedFragmentData, + UnexpectedFragmentCount, + MalformedFragmentIdentifier, +} + +// Note: `Rng` implies `RngCore` +#[derive(Debug, Clone)] +pub struct MessageChunker { + rng: R, + ack_recipient: Recipient, + packet_size: PacketSize, + reply_surbs: bool, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, +} + +impl MessageChunker { + pub fn new( + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ) -> Self { + Self::new_with_rng( + DEFAULT_RNG, + ack_recipient, + average_packet_delay_duration, + average_ack_delay_duration, + ) + } + + #[cfg(test)] + pub(crate) fn test_fixture() -> Self { + use nymsphinx_types::{DestinationAddressBytes, NodeAddressBytes}; + + let empty_address = [0u8; 32]; + let empty_recipient = Recipient::new( + DestinationAddressBytes::from_bytes(empty_address), + NodeAddressBytes::from_bytes(empty_address), + ); + Self::new(empty_recipient, Default::default(), Default::default()) + } +} + +impl MessageChunker { + pub fn new_with_rng( + rng: R, + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ) -> Self { + MessageChunker { + rng, + ack_recipient, + packet_size: Default::default(), + reply_surbs: false, + average_packet_delay_duration, + average_ack_delay_duration, + } + } + + pub fn available_plaintext_size(&self) -> usize { + // we need to put first hop's destination alongside the actual ack + // TODO: a possible optimization way down the line: currently we're always assuming that + // the addresses will have `MAX_NODE_ADDRESS_UNPADDED_LEN`, i.e. be ipv6. In most cases + // they're actually going to be ipv4 hence wasting few bytes every packet. + // To fully utilise all available space, I guess first we'd need to generate routes for ACKs + // and only then perform the chunking with `available_plaintext_size` being called per chunk. + // However this will probably introduce bunch of complexity + // for relatively not a lot of gain, so it shouldn't be done just yet. + let available_size = self.packet_size.plaintext_size() + - PacketSize::ACKPacket.size() + - MAX_NODE_ADDRESS_UNPADDED_LEN; + if self.reply_surbs { + // TODO + unimplemented!(); + } + available_size + } + + pub fn with_reply_surbs(mut self, reply_surbs: bool) -> Self { + self.reply_surbs = reply_surbs; + self + } + + pub fn with_packet_size(mut self, packet_size: PacketSize) -> Self { + self.packet_size = packet_size; + self + } + + /// Tries to convert this `Fragment` into a `SphinxPacket` that can be sent through the Nym mix-network, + /// such that it contains required SURB-ACK. + /// This method can fail if the provided network topology is invalid. + /// It returns total expected delay as well as the `SphinxPacket` to be sent through the network. + pub fn prepare_chunk_for_sending( + &mut self, + fragment: Fragment, + topology: &T, + ack_key: &AckAes128Key, + packet_recipient: &Recipient, + ) -> Result<(Delay, (SocketAddr, SphinxPacket)), NymTopologyError> { + let (ack_delay, surb_bytes) = self + .generate_surb_ack(&fragment.fragment_identifier(), topology, ack_key)? + .prepare_for_sending(); + + // SURB_FIRST_HOP || SURB_ACK || CHUNK_DATA + let packet_payload: Vec<_> = surb_bytes + .into_iter() + .chain(fragment.into_bytes().into_iter()) + .collect(); + + let route = topology.random_route_to_gateway(&packet_recipient.gateway())?; + let delays = + delays::generate_from_average_duration(route.len(), self.average_packet_delay_duration); + let destination = Destination::new(packet_recipient.destination(), Default::default()); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let packet = SphinxPacketBuilder::new() + .with_payload_size(self.packet_size.payload_size()) + .build_packet(packet_payload, &route, &destination, &delays) + .unwrap(); + + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok(( + delays.iter().sum::() + ack_delay, + (first_hop_address.into(), packet), + )) + } + + fn generate_surb_ack( + &mut self, + fragment_id: &FragmentIdentifier, + topology: &T, + ack_key: &AckAes128Key, + ) -> Result + where + T: NymTopology, + { + SURBAck::construct( + &mut self.rng, + &self.ack_recipient, + ack_key, + &fragment_id.to_bytes(), + self.average_ack_delay_duration, + topology, + ) + } + + /// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets + /// after attaching SURB-ACK. + /// After receiving they can be combined using `reconstruction::MessageReconstructor` + /// to obtain the original message back. + pub fn split_message(&mut self, message: &[u8]) -> Vec { + let available_plaintext_per_fragment = self.available_plaintext_size(); + + split_into_sets(&mut self.rng, message, available_plaintext_per_fragment) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } +} diff --git a/common/nymsphinx/src/chunking/reconstruction.rs b/common/nymsphinx/chunking/src/reconstruction.rs similarity index 55% rename from common/nymsphinx/src/chunking/reconstruction.rs rename to common/nymsphinx/chunking/src/reconstruction.rs index bcfac94389..e25ac3df03 100644 --- a/common/nymsphinx/src/chunking/reconstruction.rs +++ b/common/nymsphinx/chunking/src/reconstruction.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::chunking::fragment::Fragment; +use crate::fragment::Fragment; +use crate::ChunkingError; use log::*; use std::collections::HashMap; @@ -44,6 +45,10 @@ struct ReconstructionBuffer { fragments: Vec>, } +/// Type alias representing fully reconstructed message - its original data and list of all +/// set ids used for the reconstructions processed so that they could be used for replay prevention. +pub type ReconstructedMessage = (Vec, Vec); + impl ReconstructionBuffer { /// Initialises new instance of a `ReconstructionBuffer` with given size, i.e. /// number of expected `Fragment`s in the set. @@ -114,6 +119,8 @@ impl ReconstructionBuffer { let fragment_index = fragment.current_fragment() as usize - 1; if self.fragments[fragment_index].is_some() { // TODO: what to do in that case? give up on the message? overwrite it? panic? + // it *might* be due to lock ack-packet, but let's keep the `warn` level in case + // it could be somehow exploited warn!( "duplicate fragment received! - frag - {} (set id: {})", fragment.current_fragment(), @@ -253,31 +260,26 @@ impl MessageReconstructor { /// Given id of *any* one of the sets into which message was divided, /// reconstruct the entire original message. /// Note, before you call this method, you *must* ensure all sets were fully received - fn reconstruct_message(&mut self, set_id: i32) -> Vec { + fn reconstruct_message(&mut self, set_id: i32) -> ReconstructedMessage { debug_assert!(self.is_message_fully_received(set_id)); let starting_id = self.find_starting_set_id(set_id).unwrap(); let set_id_sequence: Vec<_> = std::iter::successors(Some(starting_id), |&id| self.next_linked_set_id(id)).collect(); - set_id_sequence + let message_content = set_id_sequence .iter() .map(|&id| self.extract_set_payload(id)) .flat_map(|payload| payload.into_iter()) - .collect() + .collect(); + + (message_content, set_id_sequence) } - /// Given raw `Fragment` data, tries to decode it and add into an appropriate `ReconstructionBuffer`. + /// Given recovered `Fragment`, tries to insert it into an appropriate `ReconstructionBuffer`. /// If a buffer does not exist, a new instance is created. /// If it was last remaining `Fragment` for the original message, the message is reconstructed - /// and returned. - pub fn new_fragment(&mut self, fragment_data: Vec) -> Option> { - let fragment_res = Fragment::try_from_bytes(&fragment_data); - if let Err(e) = fragment_res { - warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - let fragment = fragment_res.unwrap(); - + /// and returned alongside all (if applicable) set ids used in the message. + pub fn insert_new_fragment(&mut self, fragment: Fragment) -> Option { let set_id = fragment.id(); let set_len = fragment.total_fragments(); @@ -293,14 +295,19 @@ impl MessageReconstructor { None } } + + /// Given raw `Fragment` data, tries to decode and return it. + pub fn recover_fragment(&self, fragment_data: Vec) -> Result { + Fragment::try_from_bytes(&fragment_data) + } } #[cfg(test)] mod reconstruction_buffer { use super::*; - use crate::chunking::fragment::UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; - use crate::chunking::set::MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; - use crate::chunking::split_and_prepare_payloads; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::max_one_way_linked_set_payload_length; + use crate::MessageChunker; #[test] fn creating_new_instance_correctly_initialised_fragments_buffer() { @@ -331,35 +338,72 @@ mod reconstruction_buffer { #[test] fn reconstructing_set_data_works_for_buffers_of_different_sizes() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(1); - let message = [42u8; 42]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![42u8; 42]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + + // acks are ignored as they will be stripped by gateways before getting to the reconstruction + buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); assert_eq!(message.to_vec(), buf.reconstruct_set_data()); let mut buf = ReconstructionBuffer::new(3); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + assert_eq!(raw_fragments.len(), 3); + buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); assert_eq!(message.to_vec(), buf.reconstruct_set_data()); - let mut buf = ReconstructionBuffer::new(u8::max_value()); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * u8::max_value() as usize]; - let raw_fragments = split_and_prepare_payloads(&message); - for raw_fragment in raw_fragments { - buf.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap()) - } - assert_eq!(message.to_vec(), buf.reconstruct_set_data()); + // let mut buf = ReconstructionBuffer::new(u8::max_value()); + // let message = vec![ + // 42u8; + // unlinked_fragmented_payload_max_len(message_chunker.available_plaintext_size()) + // * u8::max_value() as usize + // ]; + // let raw_fragments: Vec<_> = message_chunker + // .split_message(&message) + // .into_iter() + // .map(|x| x.into_bytes()) + // .collect(); + // for raw_fragment in raw_fragments { + // buf.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap()) + // } + // assert_eq!(message.to_vec(), buf.reconstruct_set_data()); } #[test] #[should_panic] fn reconstructing_set_data_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let raw_fragments = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); @@ -368,9 +412,19 @@ mod reconstruction_buffer { #[test] fn inserting_new_fragment_puts_it_in_correct_location_based_on_its_ordering() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let raw_fragments = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); assert!(buf.fragments[0].is_none()); @@ -380,9 +434,19 @@ mod reconstruction_buffer { #[test] fn inserting_final_fragment_correctly_sets_auxiliary_flags() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -395,8 +459,17 @@ mod reconstruction_buffer { assert!(buf.next_fragments_set_id.is_none()); let mut buf = ReconstructionBuffer::new(255); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..254 { buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[i]).unwrap()); } @@ -423,13 +496,31 @@ mod reconstruction_buffer { #[test] #[should_panic] fn does_not_allow_for_inserting_new_fragments_with_different_ids() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); // they will have different IDs - let raw_fragments1 = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); - let raw_fragments2 = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments1: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + let raw_fragments2: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments1[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments2[0]).unwrap()); @@ -439,32 +530,46 @@ mod reconstruction_buffer { #[cfg(test)] mod message_reconstructor { use super::*; - use crate::chunking::fragment::{ - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - }; - use crate::chunking::set::{ - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH, TWO_WAY_LINKED_SET_PAYLOAD_LENGTH, - }; - use crate::chunking::split_and_prepare_payloads; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::{max_one_way_linked_set_payload_length, two_way_linked_set_payload_length}; + use crate::MessageChunker; use rand::{thread_rng, RngCore}; #[test] #[should_panic] fn checking_front_chain_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // first set is fully inserted for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) - .is_none()); + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) + .is_none()) } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -474,21 +579,43 @@ mod message_reconstructor { #[test] #[should_panic] fn checking_back_chain_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // finish next set for good measure assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[256].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[256].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -497,24 +624,46 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_false_for_complete_set_but_incomplete_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[256].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[256].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -523,20 +672,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_false_for_complete_set_but_incomplete_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -547,20 +714,38 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_true_for_if_there_are_no_more_front_sets() { // case of 2 sets: [id1 -- id2], where id1 is completed and being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -571,20 +756,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_true_for_if_there_are_no_more_back_sets() { // case of 2 sets: [id1 -- id2], where id2 is completed and being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -594,23 +797,39 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_true_for_complete_front_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id1 and id2 are completed and id2 is being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + two_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..(u8::max_value() as usize) * 2 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of third set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[300]).unwrap().id(); @@ -621,20 +840,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_true_for_complete_back_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id2 and id3 are completed and id2 is being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let message = - [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + two_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 1..(u8::max_value() as usize) * 2 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[300]).unwrap().id(); @@ -677,38 +914,73 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_none_if_message_was_not_fully_received() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message1 = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments1 = split_and_prepare_payloads(&message1); + let message1 = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments1: Vec<_> = message_chunker + .split_message(&message1) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments1[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments1[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments1[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments1[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments1[255]).unwrap().id(); assert!(reconstructor.find_starting_set_id(second_set_id).is_none()); - let message2 = [43u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments2 = split_and_prepare_payloads(&message2); + let message2 = + vec![ + 43u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments2: Vec<_> = message_chunker + .split_message(&message2) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments2[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments2[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments2[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments2[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments2[255]).unwrap().id(); @@ -717,20 +989,38 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_expected_starting_id() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -785,15 +1075,32 @@ mod message_reconstructor { #[test] #[should_panic] fn getting_previous_linked_set_id_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -828,15 +1135,32 @@ mod message_reconstructor { #[test] #[should_panic] fn getting_next_linked_set_id_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -871,15 +1195,32 @@ mod message_reconstructor { #[test] #[should_panic] fn extracting_set_payload_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -888,14 +1229,23 @@ mod message_reconstructor { #[test] fn extracting_set_payload_is_returns_entire_set_data() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); - let mut message = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -914,14 +1264,23 @@ mod message_reconstructor { #[test] fn reconstructing_message_for_single_set_is_equivalent_to_extracting_set_payload() { // we're inserting this via the buffer approach as not to trigger immediate re-assembly + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); - let mut message = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -930,25 +1289,39 @@ mod message_reconstructor { reconstructor.reconstructed_sets.insert(set_id, set_buf); let mut reconstructor_clone = reconstructor.clone(); + let reconstructed_message = reconstructor_clone.reconstruct_message(set_id); assert_eq!( reconstructor.extract_set_payload(set_id), - reconstructor_clone.reconstruct_message(set_id) + reconstructed_message.0 ); + assert_eq!(reconstructed_message.1.len(), 1); + assert_eq!(reconstructed_message.1[0], set_id); } #[test] fn reconstructing_message_for_two_sets_is_equivalent_to_combining_results_of_extracting_set_payload( ) { + let mut message_chunker = MessageChunker::test_fixture(); + // we're inserting this via the buffer approach as not to trigger immediate re-assembly let mut reconstructor = MessageReconstructor::new(); let mut set_buf1 = ReconstructionBuffer::new(u8::max_value()); let mut set_buf2 = ReconstructionBuffer::new(1); let mut rng = thread_rng(); - let mut message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; + let mut message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { set_buf1.insert_fragment(Fragment::try_from_bytes(&raw_fragments[i]).unwrap()); } @@ -968,34 +1341,50 @@ mod message_reconstructor { let manually_combined_message = [extracted_set1, extracted_set2].concat(); + let reconstructed_message1 = reconstructor_clone.reconstruct_message(set_id1); + let reconstructed_message2 = reconstructor_clone2.reconstruct_message(set_id2); + + assert_eq!(reconstructed_message1.1.len(), 2); + assert_eq!(reconstructed_message1.1, vec![set_id1, set_id2]); + + assert_eq!(reconstructed_message2.1.len(), 2); + assert_eq!(reconstructed_message2.1, vec![set_id1, set_id2]); + // make sure we can use any id that is part of the message - assert_eq!( - reconstructor_clone.reconstruct_message(set_id1), - manually_combined_message - ); - assert_eq!( - reconstructor_clone2.reconstruct_message(set_id2), - manually_combined_message - ); + assert_eq!(reconstructed_message1.0, manually_combined_message); + assert_eq!(reconstructed_message2.0, manually_combined_message); } #[test] fn adding_invalid_fragment_does_not_change_reconstructor_state() { - let mut empty_reconstructor = MessageReconstructor::new(); + let mut message_chunker = MessageChunker::test_fixture(); + let empty_reconstructor = MessageReconstructor::new(); assert!(empty_reconstructor - .new_fragment([24u8; 43].to_vec()) - .is_none()); + .recover_fragment([24u8; 43].to_vec()) + .is_err()); assert_eq!(empty_reconstructor, MessageReconstructor::new()); let mut reconstructor_with_data = MessageReconstructor::new(); - let dummy_message = [24u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 30]; - let mut fragments = split_and_prepare_payloads(&dummy_message); - reconstructor_with_data.new_fragment(fragments.pop().unwrap()); + let dummy_message = + vec![ + 24u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + 30 + ]; + let mut fragments: Vec<_> = message_chunker + .split_message(&dummy_message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + reconstructor_with_data.insert_new_fragment( + reconstructor_with_data + .recover_fragment(fragments.pop().unwrap()) + .unwrap(), + ); let reconstructor_clone = reconstructor_with_data.clone(); assert!(empty_reconstructor - .new_fragment([24u8; 43].to_vec()) - .is_none()); + .recover_fragment([24u8; 43].to_vec()) + .is_err()); assert_eq!(reconstructor_with_data, reconstructor_clone); } } @@ -1003,133 +1392,224 @@ mod message_reconstructor { #[cfg(test)] mod message_reconstruction { use super::*; - use crate::chunking::split_and_prepare_payloads; + use crate::MessageChunker; use rand::seq::SliceRandom; use rand::{thread_rng, RngCore}; #[cfg(test)] mod single_set_split { use super::*; - use crate::chunking::fragment::{ - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - }; - use crate::chunking::set::MAX_UNLINKED_SET_PAYLOAD_LENGTH; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::max_unlinked_set_payload_length; #[test] fn it_reconstructs_unfragmented_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 20]; + let mut message = vec![ + 0u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) - 20 + ]; rng.fill_bytes(&mut message); - let fragment = split_and_prepare_payloads(&message); + let fragment: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragment.len(), 1); let mut message_reconstructor = MessageReconstructor::new(); - assert_eq!( - message_reconstructor - .new_fragment(fragment[0].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragment[0].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_unfragmented_message_of_max_length() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message); - let fragment = split_and_prepare_payloads(&message); + let fragment: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragment.len(), 1); let mut message_reconstructor = MessageReconstructor::new(); - assert_eq!( - message_reconstructor - .new_fragment(fragment[0].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragment[0].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_2_max_lenghts() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 2 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 2 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 2); let mut message_reconstructor = MessageReconstructor::new(); assert!(message_reconstructor - .new_fragment(fragments[0].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[0].clone()) + .unwrap() + ) .is_none()); - assert_eq!( - message_reconstructor - .new_fragment(fragments[1].clone()) - .unwrap(), - message - ) + + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[1].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_with_non_max_tail() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 2 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 42]; + let mut message = vec![ + 0u8; + 2 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) - 42 + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 2); let mut message_reconstructor = MessageReconstructor::new(); assert!(message_reconstructor - .new_fragment(fragments[0].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[0].clone()) + .unwrap() + ) .is_none()); - assert_eq!( - message_reconstructor - .new_fragment(fragments[1].clone()) - .unwrap(), - message - ) + + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[1].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_30_fragments() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 30); let mut message_reconstructor = MessageReconstructor::new(); for i in 0..29 { assert!(message_reconstructor - .new_fragment(fragments[i].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[i].clone()) + .unwrap() + ) .is_none()); } - assert_eq!( - message_reconstructor - .new_fragment(fragments[29].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[29].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_not_in_order_of_30_fragments() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 30); // shuffle the fragments @@ -1138,33 +1618,52 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for i in 0..29 { assert!(message_reconstructor - .new_fragment(fragments[i].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[i].clone()) + .unwrap() + ) .is_none()); } - assert_eq!( - message_reconstructor - .new_fragment(fragments[29].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[29].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_two_different_fragmented_messages_not_in_order_of_30_fragments_each() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message1 = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message1 = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message1); - let mut message2 = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message2 = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), 30); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), 30); // combine and shuffle fragments @@ -1175,10 +1674,15 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + assert_eq!(reconstructed_msg.1.len(), 1); + match reconstructed_msg.0[0] { + 1 => assert_eq!(reconstructed_msg.0, message1), + 2 => assert_eq!(reconstructed_msg.0, message2), _ => panic!("Unknown message!"), } } @@ -1187,19 +1691,28 @@ mod message_reconstruction { #[test] fn it_reconstructs_two_different_messages_not_in_order_of_maximum_single_set_size_each() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message1 = vec![0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut message1 = + vec![ + 0u8; + max_unlinked_set_payload_length(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message1); - let mut message2 = vec![0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut message2 = + vec![ + 0u8; + max_unlinked_set_payload_length(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), u8::max_value() as usize); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), u8::max_value() as usize); // combine and shuffle fragments @@ -1210,10 +1723,15 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments.into_iter() { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + assert_eq!(reconstructed_msg.1.len(), 1); + match reconstructed_msg.0[0] { + 1 => assert_eq!(reconstructed_msg.0, message1), + 2 => assert_eq!(reconstructed_msg.0, message2), _ => panic!("Unknown message!"), } } @@ -1224,18 +1742,28 @@ mod message_reconstruction { #[cfg(test)] mod multiple_sets_split { use super::*; - use crate::chunking::set::{ - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH, TWO_WAY_LINKED_SET_PAYLOAD_LENGTH, + use crate::set::{ + max_one_way_linked_set_payload_length, two_way_linked_set_payload_length, }; #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_two_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 12345]; + let mut message = vec![ + 0u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 12345 + ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1247,8 +1775,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 2); finished_reconstruction = true; } } @@ -1256,17 +1787,24 @@ mod message_reconstruction { #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_four_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 12345 + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 12345 ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1278,8 +1816,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 4); finished_reconstruction = true; } } @@ -1287,16 +1828,24 @@ mod message_reconstruction { #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_four_full_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 4 * (u8::max_value() as usize)); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1309,8 +1858,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 4); finished_reconstruction = true; } } @@ -1318,27 +1870,34 @@ mod message_reconstruction { #[test] fn it_reconstructs_two_fragmented_messages_not_in_order_split_into_four_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message1 = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message1); let mut message2 = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), 4 * (u8::max_value() as usize)); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), 4 * (u8::max_value() as usize)); // combine and shuffle fragments @@ -1349,10 +1908,20 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments.into_iter() { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + match msg.0[0] { + 1 => { + assert_eq!(msg.0, message1); + assert_eq!(msg.1.len(), 4); + } + 2 => { + assert_eq!(msg.0, message2); + assert_eq!(msg.1.len(), 4); + } _ => panic!("Unknown message!"), } } diff --git a/common/nymsphinx/src/chunking/set.rs b/common/nymsphinx/chunking/src/set.rs similarity index 55% rename from common/nymsphinx/src/chunking/set.rs rename to common/nymsphinx/chunking/src/set.rs index 68f5d20d36..9cbbe82e33 100644 --- a/common/nymsphinx/src/chunking/set.rs +++ b/common/nymsphinx/chunking/src/set.rs @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::chunking::fragment::{ - Fragment, LINKED_FRAGMENTED_HEADER_LEN, LINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_HEADER_LEN, - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, +use crate::fragment::{ + linked_fragment_payload_max_len, unlinked_fragment_payload_max_len, Fragment, + LINKED_FRAGMENTED_HEADER_LEN, UNLINKED_FRAGMENTED_HEADER_LEN, }; use rand::Rng; /// In the simplest case of message being divided into a single set, the set has the upper bound /// on its payload length of the maximum number of `Fragment`s multiplied by their maximum, /// fragmented, length. -pub const MAX_UNLINKED_SET_PAYLOAD_LENGTH: usize = - u8::max_value() as usize * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; +pub const fn max_unlinked_set_payload_length(max_plaintext_size: usize) -> usize { + u8::max_value() as usize * unlinked_fragment_payload_max_len(max_plaintext_size) +} /// If the set is being linked to another one, by either being the very first set, or the very last, /// one of its `Fragment`s must be changed from "unlinked" into "linked" to compensate for a tiny @@ -31,16 +31,20 @@ pub const MAX_UNLINKED_SET_PAYLOAD_LENGTH: usize = /// Note that the "MAX" prefix only applies to if the set is the last one as it does not have /// a lower bound on its length. If the set is one way linked and a first one, it *must have* /// this exact payload length instead. -pub const MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH: usize = MAX_UNLINKED_SET_PAYLOAD_LENGTH - - (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN); +pub const fn max_one_way_linked_set_payload_length(max_plaintext_size: usize) -> usize { + max_unlinked_set_payload_length(max_plaintext_size) + - (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN) +} /// If the set is being linked two others sets by being stuck in the middle of divided message, /// two of its `Fragment`s (first and final one) must be changed from /// "unlinked" into "linked" to compensate for data overhead. /// Note that this constant no longer has a "MAX" prefix, this is because each set being stuck /// between different sets, *must* have this exact payload length. -pub const TWO_WAY_LINKED_SET_PAYLOAD_LENGTH: usize = MAX_UNLINKED_SET_PAYLOAD_LENGTH - - 2 * (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN); +pub const fn two_way_linked_set_payload_length(max_plaintext_size: usize) -> usize { + max_unlinked_set_payload_length(max_plaintext_size) + - 2 * (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN) +} /// `FragmentSet` is an ordered collection of 1 to 255 `Fragment`s, each with the same ID /// that can be used to produce original message, assuming no linking took place. @@ -69,7 +73,7 @@ pub(crate) type FragmentSet = Vec; /// This approach saves whole byte per `Fragment`, which while may seem insignificant and /// introduces extra complexity, quickly adds up when faced with sphinx packet encapsulation for longer /// messages. -/// Finally, the reason 0 id is not allowed is to explicitly distinguish it from unfragmented +/// Finally, the reason 0 id is not allowed is to explicitly distinguish it from `COVER_FRAG_ID` /// `Fragment`s thus allowing for some additional optimizations by letting it skip /// certain procedures when reconstructing. fn generate_set_id(rng: &mut R) -> i32 { @@ -83,18 +87,16 @@ fn generate_set_id(rng: &mut R) -> i32 { } } -/// The simplest case of splitting underlying message - when it fits into a single -/// `Fragment` thus requiring no linking or even a set id. -/// For obvious reasons the most efficient approach. -fn prepare_unfragmented_set(message: &[u8]) -> FragmentSet { - vec![Fragment::try_new_unfragmented(&message).unwrap()] -} - /// Splits underlying message into multiple `Fragment`s while all of them fit in a single /// `Set` (number of `Fragment`s <= 255) -fn prepare_unlinked_fragmented_set(message: &[u8], id: i32) -> FragmentSet { - let pre_casted_frags = - (message.len() as f64 / UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN as f64).ceil() as usize; +fn prepare_unlinked_fragmented_set( + message: &[u8], + id: i32, + max_plaintext_size: usize, +) -> FragmentSet { + let pre_casted_frags = (message.len() as f64 + / unlinked_fragment_payload_max_len(max_plaintext_size) as f64) + .ceil() as usize; debug_assert!(pre_casted_frags <= u8::max_value() as usize); let num_fragments = pre_casted_frags as u8; @@ -103,14 +105,22 @@ fn prepare_unlinked_fragmented_set(message: &[u8], id: i32) -> FragmentSet { for i in 1..(pre_casted_frags + 1) { // we can't use u8 directly here as upper (NON-INCLUSIVE, so it would always fit) bound could be u8::max_value() + 1 - let lb = (i as usize - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; + let lb = (i as usize - 1) * unlinked_fragment_payload_max_len(max_plaintext_size); let ub = usize::min( message.len(), - i as usize * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, + i as usize * unlinked_fragment_payload_max_len(max_plaintext_size), ); fragments.push( - Fragment::try_new_fragmented(&message[lb..ub], id, num_fragments, i as u8, None, None) - .unwrap(), + Fragment::try_new( + &message[lb..ub], + id, + num_fragments, + i as u8, + None, + None, + max_plaintext_size, + ) + .unwrap(), ) } @@ -126,19 +136,21 @@ fn prepare_linked_fragment_set( id: i32, previous_link_id: Option, next_link_id: Option, + max_plaintext_size: usize, ) -> FragmentSet { // determine number of fragments in the set: let num_frags_usize = if next_link_id.is_some() { u8::max_value() as usize } else { // we know this set is linked, if it's not post-linked then it MUST BE pre-linked - let tail_len = if message.len() >= LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - message.len() - LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + let tail_len = if message.len() >= linked_fragment_payload_max_len(max_plaintext_size) { + message.len() - linked_fragment_payload_max_len(max_plaintext_size) } else { 0 }; - let pre_casted_frags = - 1 + (tail_len as f64 / UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN as f64).ceil() as usize; + let pre_casted_frags = 1 + + (tail_len as f64 / unlinked_fragment_payload_max_len(max_plaintext_size) as f64) + .ceil() as usize; if pre_casted_frags > u8::max_value() as usize { panic!("message would produce too many fragments!") }; @@ -148,16 +160,19 @@ fn prepare_linked_fragment_set( // determine bounds for the first fragment which depends on whether set is pre-linked let mut lb = 0; let mut ub = if previous_link_id.is_some() { - usize::min(message.len(), LINKED_FRAGMENTED_PAYLOAD_MAX_LEN) + usize::min( + message.len(), + linked_fragment_payload_max_len(max_plaintext_size), + ) } else { // the set might be linked, but fragment itself is not (i.e. the set is linked at the tail) - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + unlinked_fragment_payload_max_len(max_plaintext_size) }; let mut fragments = Vec::with_capacity(num_frags_usize); for i in 1..(num_frags_usize + 1) { // we can't use u8 directly here as upper (NON-INCLUSIVE, so i would always fit) bound could be u8::max_value() + 1 - let fragment = Fragment::try_new_fragmented( + let fragment = Fragment::try_new( &message[lb..ub], id, num_frags_usize as u8, @@ -168,30 +183,37 @@ fn prepare_linked_fragment_set( } else { None }, + max_plaintext_size, ) .unwrap(); fragments.push(fragment); // update bounds for the next fragment lb = ub; - ub = usize::min(message.len(), ub + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN); + ub = usize::min( + message.len(), + ub + unlinked_fragment_payload_max_len(max_plaintext_size), + ); } fragments } /// Based on total message length, determines the number of sets into which it is going to be split. -fn total_number_of_sets(message_len: usize) -> usize { - if message_len <= MAX_UNLINKED_SET_PAYLOAD_LENGTH { +fn total_number_of_sets(message_len: usize, max_plaintext_size: usize) -> usize { + if message_len <= max_unlinked_set_payload_length(max_plaintext_size) { 1 - } else if message_len > MAX_UNLINKED_SET_PAYLOAD_LENGTH - && message_len <= 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + } else if message_len > max_unlinked_set_payload_length(max_plaintext_size) + && message_len <= 2 * max_one_way_linked_set_payload_length(max_plaintext_size) { 2 } else { - let len_without_edges = message_len - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; + let len_without_edges = + message_len - 2 * max_one_way_linked_set_payload_length(max_plaintext_size); // every set in between edges must be two way linked - (len_without_edges as f64 / TWO_WAY_LINKED_SET_PAYLOAD_LENGTH as f64).ceil() as usize + 2 + (len_without_edges as f64 / two_way_linked_set_payload_length(max_plaintext_size) as f64) + .ceil() as usize + + 2 } } @@ -202,38 +224,50 @@ fn prepare_fragment_set( id: i32, previous_link_id: Option, next_link_id: Option, + max_plaintext_size: usize, ) -> FragmentSet { if previous_link_id.is_some() || next_link_id.is_some() { - prepare_linked_fragment_set(message, id, previous_link_id, next_link_id) - } else if message.len() > UNFRAGMENTED_PAYLOAD_MAX_LEN { + prepare_linked_fragment_set( + message, + id, + previous_link_id, + next_link_id, + max_plaintext_size, + ) + } else { // the bounds on whether the message fits in an unlinked set should have been done by the callee // when determining ids of other sets - prepare_unlinked_fragmented_set(message, id) - } else { - prepare_unfragmented_set(message) + prepare_unlinked_fragmented_set(message, id, max_plaintext_size) } } /// Entry point for splitting whole message into possibly multiple `Set`s. -pub(crate) fn split_into_sets(message: &[u8]) -> Vec { - use rand::thread_rng; - - let mut rng = thread_rng(); - let num_of_sets = total_number_of_sets(message.len()); +pub(crate) fn split_into_sets( + rng: &mut R, + message: &[u8], + max_plaintext_size: usize, +) -> Vec { + let num_of_sets = total_number_of_sets(message.len(), max_plaintext_size); if num_of_sets == 1 { - let set_id = generate_set_id(&mut rng); - vec![prepare_fragment_set(message, set_id, None, None)] + let set_id = generate_set_id(rng); + vec![prepare_fragment_set( + message, + set_id, + None, + None, + max_plaintext_size, + )] } else { let mut sets = Vec::with_capacity(num_of_sets); // pre-generate all ids for the sets let set_ids: Vec<_> = std::iter::repeat(()) - .map(|_| generate_set_id(&mut rng)) + .map(|_| generate_set_id(rng)) .take(num_of_sets) .collect(); // initial bounds for the set payloads let mut lb = 0; - let mut ub = MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; + let mut ub = max_one_way_linked_set_payload_length(max_plaintext_size); for i in 0..num_of_sets { let fragment_set = prepare_fragment_set( @@ -245,6 +279,7 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { } else { Some(set_ids[i + 1]) }, + max_plaintext_size, ); sets.push(fragment_set); @@ -252,9 +287,15 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { lb = ub; ub = if i == num_of_sets - 2 { // we're going to go into the last iteration now, hence the last set will be one-way linked - usize::min(message.len(), ub + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH) + usize::min( + message.len(), + ub + max_one_way_linked_set_payload_length(max_plaintext_size), + ) } else { - usize::min(message.len(), ub + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH) + usize::min( + message.len(), + ub + two_way_linked_set_payload_length(max_plaintext_size), + ) } } @@ -266,13 +307,21 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { #[cfg(test)] mod tests { use super::*; + use nymsphinx_params::packet_sizes::PacketSize; + + fn max_plaintext_size() -> usize { + PacketSize::default().plaintext_size() - PacketSize::ACKPacket.size() + } fn verify_unlinked_set_payload(mut set: FragmentSet, payload: &[u8]) { for i in (0..set.len()).rev() { assert_eq!( set.pop().unwrap().extract_payload(), - payload[i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - ..usize::min(payload.len(), (i + 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN)] + payload[i * unlinked_fragment_payload_max_len(max_plaintext_size()) + ..usize::min( + payload.len(), + (i + 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + )] .to_vec() ) } @@ -283,11 +332,13 @@ mod tests { let lb = if i == 0 { 0 } else { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; let ub = usize::min( payload.len(), - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN, + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()), ); assert_eq!( @@ -299,11 +350,12 @@ mod tests { fn verify_post_linked_set_payload(mut set: FragmentSet, payload: &[u8]) { for i in (0..set.len()).rev() { - let lb = i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; + let lb = i * unlinked_fragment_payload_max_len(max_plaintext_size()); let ub = if i == (u8::max_value() as usize - 1) { - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) } else { - (i + 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i + 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) }; assert_eq!( @@ -318,13 +370,15 @@ mod tests { let lb = if i == 0 { 0 } else { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; let ub = if i == (u8::max_value() as usize - 1) { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 2 * LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + 2 * linked_fragment_payload_max_len(max_plaintext_size()) } else { - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; assert_eq!( @@ -334,7 +388,7 @@ mod tests { } } - fn verfiy_correct_link(left: &FragmentSet, right: &FragmentSet) { + fn verify_correct_link(left: &FragmentSet, right: &FragmentSet) { let first_id = left[0].id(); let post_id = left[254].next_fragments_set_id().unwrap(); @@ -345,31 +399,6 @@ mod tests { assert_eq!(second_id, post_id); } - #[cfg(test)] - mod preparing_unfragmented_set { - use super::*; - - #[test] - fn makes_set_with_single_unfragmented_element_for_valid_message_lengths() { - let mut set = prepare_unfragmented_set(&[1]); - assert_eq!(1, set.len()); - assert_eq!(set.pop().unwrap().extract_payload(), [1].to_vec()); - - let mut set = prepare_unfragmented_set(&[1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]); - assert_eq!(1, set.len()); - assert_eq!( - set.pop().unwrap().extract_payload(), - [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN].to_vec() - ); - } - - #[test] - #[should_panic] - fn panics_for_too_long_payload() { - prepare_unfragmented_set(&[1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 1]); - } - } - #[cfg(test)] mod preparing_unlinked_set { // remember this this is only called for a sole set with <= 255 fragments @@ -381,30 +410,46 @@ mod tests { let id = 12345; let mut rng = thread_rng(); - let mut two_element_set_payload = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; + let mut two_element_set_payload = + vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size()) + 1]; rng.fill_bytes(&mut two_element_set_payload); - let two_element_set = prepare_unlinked_fragmented_set(&two_element_set_payload, id); + let two_element_set = + prepare_unlinked_fragmented_set(&two_element_set_payload, id, max_plaintext_size()); assert_eq!(2, two_element_set.len()); verify_unlinked_set_payload(two_element_set, &two_element_set_payload); let mut forty_two_element_set_payload = - [0u8; 41 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 42]; + vec![0u8; 41 * unlinked_fragment_payload_max_len(max_plaintext_size()) + 42]; rng.fill_bytes(&mut forty_two_element_set_payload); - let forty_two_element_set = - prepare_unlinked_fragmented_set(&forty_two_element_set_payload, id); + let forty_two_element_set = prepare_unlinked_fragmented_set( + &forty_two_element_set_payload, + id, + max_plaintext_size(), + ); assert_eq!(42, forty_two_element_set.len()); verify_unlinked_set_payload(forty_two_element_set, &forty_two_element_set_payload); let mut max_fragments_set_payload = - [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; // last fragment should have a single byte of data + vec![ + 0u8; + max_unlinked_set_payload_length(max_plaintext_size()) + - unlinked_fragment_payload_max_len(max_plaintext_size()) + + 1 + ]; // last fragment should have a single byte of data rng.fill_bytes(&mut max_fragments_set_payload); - let max_fragment_set = prepare_unlinked_fragmented_set(&max_fragments_set_payload, id); + let max_fragment_set = prepare_unlinked_fragmented_set( + &max_fragments_set_payload, + id, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, max_fragment_set.len()); verify_unlinked_set_payload(max_fragment_set, &max_fragments_set_payload); - let mut full_set_payload = [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_unlinked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = prepare_unlinked_fragmented_set(&full_set_payload, id); + let full_fragment_set = + prepare_unlinked_fragmented_set(&full_set_payload, id, max_plaintext_size()); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_unlinked_set_payload(full_fragment_set, &full_set_payload); } @@ -412,7 +457,11 @@ mod tests { #[test] #[should_panic] fn panics_for_too_long_payload() { - prepare_unlinked_fragmented_set(&[0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH + 1], 12345); + prepare_unlinked_fragmented_set( + &vec![0u8; max_unlinked_set_payload_length(max_plaintext_size()) + 1], + 12345, + max_plaintext_size(), + ); } } @@ -427,39 +476,66 @@ mod tests { let link_id = 1234; let mut rng = thread_rng(); - let mut two_element_set_payload = [0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; + let mut two_element_set_payload = + vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) + 1]; rng.fill_bytes(&mut two_element_set_payload); - let two_element_set = - prepare_linked_fragment_set(&two_element_set_payload, id, Some(link_id), None); + let two_element_set = prepare_linked_fragment_set( + &two_element_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(2, two_element_set.len()); verify_pre_linked_set_payload(two_element_set, &two_element_set_payload); - let mut forty_two_element_set_payload = [0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 40 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 42]; + let mut forty_two_element_set_payload = + vec![ + 0u8; + linked_fragment_payload_max_len(max_plaintext_size()) + + 40 * unlinked_fragment_payload_max_len(max_plaintext_size()) + + 42 + ]; rng.fill_bytes(&mut forty_two_element_set_payload); let forty_two_element_set = prepare_linked_fragment_set( &forty_two_element_set_payload, id, Some(link_id), None, + max_plaintext_size(), ); assert_eq!(42, forty_two_element_set.len()); verify_pre_linked_set_payload(forty_two_element_set, &forty_two_element_set_payload); let mut max_fragments_set_payload = - [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; // last fragment should have a single byte of data + vec![ + 0u8; + max_unlinked_set_payload_length(max_plaintext_size()) + - linked_fragment_payload_max_len(max_plaintext_size()) + + 1 + ]; // last fragment should have a single byte of data rng.fill_bytes(&mut max_fragments_set_payload); - let max_fragment_set = - prepare_linked_fragment_set(&max_fragments_set_payload, id, Some(link_id), None); + let max_fragment_set = prepare_linked_fragment_set( + &max_fragments_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, max_fragment_set.len()); verify_pre_linked_set_payload(max_fragment_set, &max_fragments_set_payload); - let mut full_set_payload = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = - prepare_linked_fragment_set(&full_set_payload, id, Some(link_id), None); + let full_fragment_set = prepare_linked_fragment_set( + &full_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_pre_linked_set_payload(full_fragment_set, &full_set_payload); } @@ -468,10 +544,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_pre_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, Some(1234), None, + max_plaintext_size(), ); } @@ -482,10 +559,16 @@ mod tests { let mut rng = thread_rng(); // if set is post-linked, there is only a single valid case - full length payload - let mut full_set_payload = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = - prepare_linked_fragment_set(&full_set_payload, id, None, Some(link_id)); + let full_fragment_set = prepare_linked_fragment_set( + &full_set_payload, + id, + None, + Some(link_id), + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_post_linked_set_payload(full_fragment_set, &full_set_payload); } @@ -494,10 +577,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_post_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, None, Some(1234), + max_plaintext_size(), ); } @@ -505,10 +589,11 @@ mod tests { #[should_panic] fn panics_for_too_short_payload_for_post_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) - 1], 12345, None, Some(1234), + max_plaintext_size(), ); } @@ -521,13 +606,15 @@ mod tests { let post_link_id = 123456; let mut rng = thread_rng(); - let mut full_set_payload = [0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; two_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); let full_fragment_set = prepare_linked_fragment_set( &full_set_payload, id, Some(pre_link_id), Some(post_link_id), + max_plaintext_size(), ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_two_way_linked_set_payload(full_fragment_set, &full_set_payload); @@ -537,10 +624,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_two_way_linked_set() { prepare_linked_fragment_set( - &[0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; two_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, Some(123456), Some(1234), + max_plaintext_size(), ); } @@ -548,10 +636,11 @@ mod tests { #[should_panic] fn panics_for_too_short_payload_for_two_way_linked_set() { prepare_linked_fragment_set( - &[0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - 1], + &vec![0u8; two_way_linked_set_payload_length(max_plaintext_size()) - 1], 12345, Some(123456), Some(1234), + max_plaintext_size(), ); } } @@ -561,37 +650,14 @@ mod tests { use super::*; use rand::{thread_rng, RngCore}; - #[test] - fn correctly_creates_set_with_single_unfragmented_element_when_expected() { - let mut rng = thread_rng(); - let tiny_message = [1, 2, 3, 4, 5]; - let mut max_unfragmented_message = [0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut max_unfragmented_message); - - let mut sets = split_into_sets(&tiny_message); - assert_eq!(1, sets.len()); - assert_eq!(1, sets[0].len()); - assert_eq!( - tiny_message.to_vec(), - sets.pop().unwrap().pop().unwrap().extract_payload() - ); - - let mut sets = split_into_sets(&max_unfragmented_message); - assert_eq!(1, sets.len()); - assert_eq!(1, sets[0].len()); - assert_eq!( - max_unfragmented_message.to_vec(), - sets.pop().unwrap().pop().unwrap().extract_payload() - ); - } - #[test] fn correctly_creates_single_fragmented_set_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - 2345]; + let mut message = + vec![0u8; max_unlinked_set_payload_length(max_plaintext_size()) - 2345]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(1, sets.len()); verify_unlinked_set_payload(sets.pop().unwrap(), &message); } @@ -602,127 +668,141 @@ mod tests { fn correctly_creates_two_singly_linked_sets_with_second_set_containing_data_fitting_in_unfragmented_payload( ) { let mut rng = thread_rng(); - let mut message = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; + let mut message = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 123]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } #[test] fn correctly_creates_two_singly_linked_sets_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2345]; + let mut message = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 2345]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); - let mut message = [0u8; 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut message = + vec![0u8; 2 * max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } #[test] fn correctly_creates_four_correctly_formed_sets_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2345]; + let mut message = vec![ + 0u8; + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2345 + ]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(4, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); assert_eq!(sets[2].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); - verfiy_correct_link(&sets[1], &sets[2]); - verfiy_correct_link(&sets[2], &sets[3]); + verify_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[1], &sets[2]); + verify_correct_link(&sets[2], &sets[3]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + ..2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[max_one_way_linked_set_payload_length(max_plaintext_size()) + ..two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); - let mut message = [0u8; 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut message = + vec![ + 0u8; + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + ]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(4, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); assert_eq!(sets[2].len(), u8::max_value() as usize); assert_eq!(sets[3].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); - verfiy_correct_link(&sets[1], &sets[2]); - verfiy_correct_link(&sets[2], &sets[3]); + verify_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[1], &sets[2]); + verify_correct_link(&sets[2], &sets[3]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + ..2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[max_one_way_linked_set_payload_length(max_plaintext_size()) + ..two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } } @@ -735,66 +815,89 @@ mod tests { fn total_number_of_sets() { assert_eq!( 1, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH - 1) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()) - 1, + max_plaintext_size() + ) ); assert_eq!( 1, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()), + max_plaintext_size() + ) ); assert_eq!( 2, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH + 1) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()) + 1, + max_plaintext_size() + ) ); assert_eq!( 2, - super::total_number_of_sets(2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH) - ); - assert_eq!( - 3, - super::total_number_of_sets(2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1) - ); - assert_eq!( - 3, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - - 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 3, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + 1, + max_plaintext_size() + ) + ); + assert_eq!( + 3, + super::total_number_of_sets( + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()) + - 1, + max_plaintext_size() + ) + ); + assert_eq!( + 3, + super::total_number_of_sets( + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()) + + 1, + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - - 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + - 1, + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 5, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + 1, + max_plaintext_size() ) ); } diff --git a/common/nymsphinx/cover/Cargo.toml b/common/nymsphinx/cover/Cargo.toml new file mode 100644 index 0000000000..ccf8eb6005 --- /dev/null +++ b/common/nymsphinx/cover/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nymsphinx-cover" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = { version = "0.7.3", features = ["wasm-bindgen"] } + +nymsphinx-acknowledgements = { path = "../acknowledgements" } +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-chunking = { path = "../chunking" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs new file mode 100644 index 0000000000..fe50b44956 --- /dev/null +++ b/common/nymsphinx/cover/src/lib.rs @@ -0,0 +1,116 @@ +// 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 nymsphinx_acknowledgements::surb_ack::SURBAck; +use nymsphinx_acknowledgements::AckAes128Key; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx_chunking::fragment::COVER_FRAG_ID; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{delays, Destination, Error as SphinxError, SphinxPacket}; +use rand::{CryptoRng, RngCore}; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::time; +use topology::{NymTopology, NymTopologyError}; + +pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; + +#[derive(Debug)] +pub enum CoverMessageError { + NoValidProvidersError, + InvalidTopologyError, + SphinxError(SphinxError), + InvalidFirstMixAddress, +} + +impl From for CoverMessageError { + fn from(err: SphinxError) -> Self { + CoverMessageError::SphinxError(err) + } +} + +impl From for CoverMessageError { + fn from(_: NymNodeRoutingAddressError) -> Self { + use CoverMessageError::*; + InvalidFirstMixAddress + } +} + +impl From for CoverMessageError { + fn from(_: NymTopologyError) -> Self { + CoverMessageError::InvalidTopologyError + } +} + +pub fn generate_loop_cover_surb_ack( + rng: &mut R, + topology: &T, + ack_key: &AckAes128Key, + full_address: &Recipient, + average_ack_delay: time::Duration, +) -> Result +where + R: RngCore + CryptoRng, + T: NymTopology, +{ + Ok(SURBAck::construct( + rng, + full_address, + ack_key, + &COVER_FRAG_ID.to_bytes(), + average_ack_delay, + topology, + )?) +} + +pub fn generate_loop_cover_packet( + rng: &mut R, + topology: &T, + ack_key: &AckAes128Key, + full_address: &Recipient, + average_ack_delay: time::Duration, + average_packet_delay: time::Duration, +) -> Result<(SocketAddr, SphinxPacket), CoverMessageError> +where + R: RngCore + CryptoRng, + T: NymTopology, +{ + // we don't care about total ack delay - we will not be retransmitting it anyway + let (_, ack_bytes) = + generate_loop_cover_surb_ack(rng, topology, ack_key, full_address, average_ack_delay)? + .prepare_for_sending(); + + let cover_payload: Vec<_> = ack_bytes + .into_iter() + .chain(LOOP_COVER_MESSAGE_PAYLOAD.into_iter().cloned()) + .collect(); + + let route = topology.random_route_to_gateway(&full_address.gateway())?; + let delays = delays::generate_from_average_duration(route.len(), average_packet_delay); + // in our design we don't care about SURB_ID + let destination = Destination::new(full_address.destination(), Default::default()); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let packet = SphinxPacketBuilder::new() + .with_payload_size(PacketSize::default().payload_size()) + .build_packet(cover_payload, &route, &destination, &delays) + .unwrap(); + + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok((first_hop_address.into(), packet)) +} diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml new file mode 100644 index 0000000000..6e645bd246 --- /dev/null +++ b/common/nymsphinx/framing/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nymsphinx-framing" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytes = "0.5" +tokio-util = { version = "0.3.1", features = ["codec"] } + +nymsphinx-types = { path = "../types" } +nymsphinx-params = { path = "../params" } diff --git a/common/nymsphinx/src/framing/mod.rs b/common/nymsphinx/framing/src/lib.rs similarity index 97% rename from common/nymsphinx/src/framing/mod.rs rename to common/nymsphinx/framing/src/lib.rs index 4add544ffd..d86eaf918a 100644 --- a/common/nymsphinx/src/framing/mod.rs +++ b/common/nymsphinx/framing/src/lib.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::packets::{InvalidPacketSize, PacketSize}; use bytes::{Buf, BufMut, BytesMut}; -use sphinx::SphinxPacket; +use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; +use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; use std::io; use tokio_util::codec::{Decoder, Encoder}; diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml new file mode 100644 index 0000000000..b00828e908 --- /dev/null +++ b/common/nymsphinx/params/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nymsphinx-params" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nymsphinx-types = { path = "../types" } \ No newline at end of file diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs new file mode 100644 index 0000000000..17543c20d3 --- /dev/null +++ b/common/nymsphinx/params/src/lib.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 packet_sizes; diff --git a/common/nymsphinx/src/packets.rs b/common/nymsphinx/params/src/packet_sizes.rs similarity index 80% rename from common/nymsphinx/src/packets.rs rename to common/nymsphinx/params/src/packet_sizes.rs index 34b22071a7..5c8969797c 100644 --- a/common/nymsphinx/src/packets.rs +++ b/common/nymsphinx/params/src/packet_sizes.rs @@ -12,22 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. +use nymsphinx_types::header::HEADER_SIZE; +use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE; use std::convert::TryFrom; // it's up to the smart people to figure those values out : ) -const REGULAR_PACKET_SIZE: usize = 2 * 1024; -const ACK_PACKET_SIZE: usize = 512; -const EXTENDED_PACKET_SIZE: usize = 32 * 1024; +const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024; +const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 24; +const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; pub struct InvalidPacketSize; #[repr(u8)] +#[derive(Clone, Copy, Debug)] pub enum PacketSize { RegularPacket = 1, // for example instant messaging use case ACKPacket = 2, // for sending SURB-ACKs ExtendedPacket = 3, // for example for streaming fast and furious in uncompressed 10bit 4K HDR quality - - PreSURBChanges = 0, } impl TryFrom for PacketSize { @@ -38,7 +39,6 @@ impl TryFrom for PacketSize { _ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket), _ if value == (PacketSize::ACKPacket as u8) => Ok(Self::ACKPacket), _ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket), - _ if value == (PacketSize::PreSURBChanges as u8) => Ok(Self::PreSURBChanges), _ => Err(InvalidPacketSize), } } @@ -50,10 +50,17 @@ impl PacketSize { PacketSize::RegularPacket => REGULAR_PACKET_SIZE, PacketSize::ACKPacket => ACK_PACKET_SIZE, PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE, - PacketSize::PreSURBChanges => crate::PACKET_SIZE, } } + pub fn plaintext_size(&self) -> usize { + self.size() - HEADER_SIZE - PAYLOAD_OVERHEAD_SIZE + } + + pub fn payload_size(&self) -> usize { + self.size() - HEADER_SIZE + } + pub fn get_type(size: usize) -> std::result::Result { if PacketSize::RegularPacket.size() == size { Ok(PacketSize::RegularPacket) @@ -61,8 +68,6 @@ impl PacketSize { Ok(PacketSize::ACKPacket) } else if PacketSize::ExtendedPacket.size() == size { Ok(PacketSize::ExtendedPacket) - } else if PacketSize::PreSURBChanges.size() == size { - Ok(PacketSize::PreSURBChanges) } else { Err(InvalidPacketSize) } diff --git a/common/nymsphinx/src/chunking/fragment.rs b/common/nymsphinx/src/chunking/fragment.rs deleted file mode 100644 index ddb315f479..0000000000 --- a/common/nymsphinx/src/chunking/fragment.rs +++ /dev/null @@ -1,1157 +0,0 @@ -// 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::chunking::ChunkingError; -use std::convert::TryInto; - -/// The entire marshaled `Fragment` can never be longer than the maximum length of the plaintext -/// data we can put into a Sphinx packet. -pub const MAXIMUM_FRAGMENT_LENGTH: usize = sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH; - -/// The minimum data overhead required for message fitting into a single `Fragment`. The single byte -/// used to literally indicate "this message is not fragmented". -pub const UNFRAGMENTED_HEADER_LEN: usize = 1; - -/// When the underlying message has to be split into multiple Fragments, but still manages to fit -/// into a single `FragmentSet`, each `FragmentHeader` needs to hold additional information to allow -/// for correct message reconstruction: 4 bytes for set id, 1 byte to represent total number -/// of fragments in the set, 1 byte to represent position of the current fragment in the set -/// and finally an extra byte to indicate the fragment has no links to other sets. -pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7; - -/// Logically almost identical to `UNLINKED_FRAGMENTED_HEADER_LEN`, however, the extra three -/// bytes are due to changing the final byte that used to indicate the `Fragment` is not linked -/// into 4 byte id of either previous or the next set. -/// Note that the linked headers can potentially be used only for very first and very last -/// `Fragment` in a `FragmentSet`. -pub const LINKED_FRAGMENTED_HEADER_LEN: usize = 10; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const UNFRAGMENTED_PAYLOAD_MAX_LEN: usize = MAXIMUM_FRAGMENT_LENGTH - UNFRAGMENTED_HEADER_LEN; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN: usize = - MAXIMUM_FRAGMENT_LENGTH - UNLINKED_FRAGMENTED_HEADER_LEN; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const LINKED_FRAGMENTED_PAYLOAD_MAX_LEN: usize = - MAXIMUM_FRAGMENT_LENGTH - LINKED_FRAGMENTED_HEADER_LEN; - -/// The basic unit of division of underlying bytes message sent through the mix network. -/// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. -/// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional -/// header used to reconstruct the message after being received. -#[derive(PartialEq, Clone, Debug)] -pub(crate) struct Fragment { - header: FragmentHeader, - payload: Vec, -} - -impl Fragment { - /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. - /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata - /// is malformed or self-contradictory, for example if current_fragment > total_fragments. - pub(crate) fn try_new_fragmented( - payload: &[u8], - id: i32, - total_fragments: u8, - current_fragment: u8, - previous_fragments_set_id: Option, - next_fragments_set_id: Option, - ) -> Result { - let header = FragmentHeader::try_new_fragmented( - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - )?; - - // check for whether payload has expected length, which depend on whether fragment is linked - // and if it's the only one or the last one in the set (then lower bound is removed) - if previous_fragments_set_id.is_some() { - if total_fragments > 1 { - if payload.len() != LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if payload.len() > LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } - } else if next_fragments_set_id.is_some() { - if payload.len() != LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if total_fragments != current_fragment { - if payload.len() != UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if payload.len() > UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } - } - - Ok(Fragment { - header, - payload: payload.to_vec(), - }) - } - - /// The most efficient way of representing underlying message incurring the least data overhead - /// for as long as the message can fit into a single, unfragmented, `Fragment`. - pub(crate) fn try_new_unfragmented(payload: &[u8]) -> Result { - if payload.len() > UNFRAGMENTED_PAYLOAD_MAX_LEN { - Err(ChunkingError::InvalidPayloadLengthError) - } else { - Ok(Fragment { - header: FragmentHeader::new_unfragmented(), - payload: payload.to_vec(), - }) - } - } - - /// Convert this `Fragment` into vector of bytes which can be put into a sphinx packet. - pub(crate) fn into_bytes(self) -> Vec { - self.header - .to_bytes() - .into_iter() - .chain(self.payload.into_iter()) - .collect() - } - - /// Extracts id of this `Fragment`. - pub(crate) fn id(&self) -> i32 { - self.header.id - } - - /// Extracts total number of fragments associated with this particular `Fragment` (belonging to - /// the same `FragmentSet`). - pub(crate) fn total_fragments(&self) -> u8 { - self.header.total_fragments - } - - /// Extracts position of this `Fragment` in a `FragmentSet`. - pub(crate) fn current_fragment(&self) -> u8 { - self.header.current_fragment - } - - /// Extracts information regarding id of pre-linked `FragmentSet` - pub(crate) fn previous_fragments_set_id(&self) -> Option { - self.header.previous_fragments_set_id - } - - /// Extracts information regarding id of post-linked `FragmentSet` - pub(crate) fn next_fragments_set_id(&self) -> Option { - self.header.next_fragments_set_id - } - - /// Consumes `Self` to obtain payload (i.e. part of original message) associated with this - /// `Fragment`. - pub(crate) fn extract_payload(self) -> Vec { - self.payload - } - - /// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet. - /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata - /// is malformed or self-contradictory, for example if current_fragment > total_fragments. - pub(crate) fn try_from_bytes(b: &[u8]) -> Result { - let (header, n) = FragmentHeader::try_from_bytes(b)?; - - // determine what's our expected payload size bound and whether the message fits in this - let is_payload_in_range = if header.is_fragmented { - if header.is_linked() { - if header.is_final() { - (b.len() - n) <= LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } else { - (b.len() - n) == LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } - } else { - if header.is_final() { - (b.len() - n) <= UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } else { - (b.len() - n) == UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } - } - } else { - (b.len() - n) <= UNFRAGMENTED_PAYLOAD_MAX_LEN - }; - - if !is_payload_in_range { - return Err(ChunkingError::MalformedFragmentData); - } - - Ok(Fragment { - header, - payload: b[n..].to_vec(), - }) - } -} - -/// In order to be able to re-assemble fragmented message sent through a mix-network, some -/// metadata is attached alongside the actual message payload. The idea is to include as little -/// of that data as possible due to computationally very costly process of sphinx encapsulation. -/// -/// The generic `FragmentHeader` is represented as follows: -/// IF flag || 31 bit ID || TotalFragments || CurrentFragment || LID flag || 31 bit Linked ID -/// note that LID is a valid flag only for first and -/// last fragment (if TotalFragments == CurrentFragment == 255) in given set. -/// -/// further note that if IF (isFragmented) is not set, -/// then the remaining bytes of the header are used as payload and also note that if LID is not set, -/// then the Linked ID bytes in the header are used as payload. -/// -/// Hence after marshaling `FragmentHeader` into bytes, -/// the following three alternatives are possible: -/// -/// Single byte representing that the `Fragment` is the only one into which the message was split. -/// '0'byte -/// -/// 7 byte long sequence representing that this `Fragment` is one of multiple ones in the set. -/// However, the set is not linked to any other sets: -/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '0'byte -/// -/// 10 byte sequence representing first (or last) fragment in the set, -/// where the set is linked to either preceding data (TF == 1) or proceeding data (TF == CF == 255) -/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '1'bit || 31-bit LID -/// -/// And hence for messages smaller than sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH, -/// there is only a single byte of overhead; -/// for messages larger than sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH but small enough -/// to avoid set division (which happens if message has to be fragmented into more than 255 fragments) -/// there is 7 bytes of overhead inside each sphinx packet sent -/// and finally for the longest messages, without upper bound, there is usually also only 7 bytes -/// of overhead apart from first and last fragments in each set that instead have 10 bytes of overhead. -#[derive(PartialEq, Clone, Debug)] -pub(crate) struct FragmentHeader { - /// Flag used to indicate whether this `Fragment` is the only one into which original - /// message was split. - /// If so, rest of the fields are meaningless as they are set to their default values - /// the whole time and serve no purpose when marshaling or unmarshaling a `FragmentHeader` - is_fragmented: bool, - - /// ID associated with `FragmentSet` to which this particular `Fragment` belongs. - /// Its value is restricted to (0, i32::max_value()]. - /// Note that it *excludes* 0, but *includes* i32::max_value(). - /// This allows the field to be represented using 31 bits. - id: i32, - - /// Total number of `Fragment`s in `FragmentSet` used to be able to determine if entire - /// set was fully received as well as to perform bound checks. - total_fragments: u8, - - /// Since message is always fragmented into payloads of constant lengths - /// (apart from possibly the last one), there's no need to use offsets like ipv4/ipv6 - /// and we can just simply enumerate the fragments to later reconstruct the message. - current_fragment: u8, - - /// Optional ID of previous `FragmentSet` into which the original message was split. - /// Note, this option is only valid of `current_fragment == 1` - previous_fragments_set_id: Option, - - /// Optional ID of next `FragmentSet` into which the original message was split. - /// Note, this option is only valid of `current_fragment == total_fragments == u8::max_value()` - next_fragments_set_id: Option, -} - -impl FragmentHeader { - /// Tries to create a new `FragmentHeader` using provided metadata. Bunch of logical - /// checks are performed to see if the data is not self-contradictory, - /// for example if current_fragment > total_fragments. - fn try_new_fragmented( - id: i32, - total_fragments: u8, - current_fragment: u8, - previous_fragments_set_id: Option, - next_fragments_set_id: Option, - ) -> Result { - if id <= 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if total_fragments < current_fragment { - return Err(ChunkingError::MalformedHeaderError); - } - if total_fragments == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if current_fragment == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if let Some(pfid) = previous_fragments_set_id { - if pfid <= 0 || current_fragment != 1 || pfid == id { - return Err(ChunkingError::MalformedHeaderError); - } - } - if let Some(nfid) = next_fragments_set_id { - if nfid <= 0 || current_fragment != total_fragments || nfid == id { - return Err(ChunkingError::MalformedHeaderError); - } - } - - Ok(FragmentHeader { - is_fragmented: true, - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - }) - } - - /// When unfragmented header is created, no checks need to happen as there is no variability - /// in the values of its fields. All of them always have the same, default, values. - fn new_unfragmented() -> Self { - FragmentHeader { - is_fragmented: false, - id: 0, - total_fragments: 1, - current_fragment: 1, - previous_fragments_set_id: None, - next_fragments_set_id: None, - } - } - - /// Tries to recover `FragmentHeader` from slice of bytes extracted from received sphinx packet. - /// If successful, returns `Self` and number of bytes used, as those can differ based on the - /// type of header (unfragmented, unlinked, linked). - fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { - if b.is_empty() { - return Err(ChunkingError::TooShortFragmentData); - } - // check if it's fragmented - if it's not - the whole first byte is set to 0 - // otherwise first bit is set to 1 - if b[0] == 0 { - Ok((Self::new_unfragmented(), 1)) - } else { - // if it's fragmented, it needs to be at least 7 bytes long - if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); - } - let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); - // sanity check for the fragmentation flag - if ((frag_id >> 31) & 1) == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - - let id = frag_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly - let total_fragments = b[4]; - let current_fragment = b[5]; - - if total_fragments == 0 || current_fragment == 0 || current_fragment > total_fragments { - return Err(ChunkingError::MalformedHeaderError); - } - - let mut previous_fragments_set_id = None; - let mut next_fragments_set_id = None; - - // check if the linking id flag might be set - let read_bytes = if b[6] != 0 { - // there's linking ID supposedly attached, make sure we have enough bytes to parse - if b.len() < LINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); - } - let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); - - // sanity check for the linked flag - if ((flagged_linked_id >> 31) & 1) == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - - let linked_id = flagged_linked_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly - - if current_fragment == 1 { - previous_fragments_set_id = Some(linked_id); - } else if total_fragments == current_fragment && current_fragment == u8::max_value() - { - next_fragments_set_id = Some(linked_id); - } else { - return Err(ChunkingError::MalformedHeaderError); - } - - 10 - } else { - 7 - }; - - Ok(( - Self::try_new_fragmented( - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - )?, - read_bytes, - )) - } - } - - /// Helper method to determine if this `FragmentHeader` is used to represent a linked `Fragment`. - fn is_linked(&self) -> bool { - self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some() - } - - /// Helper method to determine if this `FragmentHeader` is used to represent a `Fragment` that - /// is a final one in some `FragmentSet`. - fn is_final(&self) -> bool { - self.total_fragments == self.current_fragment - } - - /// Marshal this `FragmentHeader` into vector of bytes which can be put into a sphinx packet. - fn to_bytes(&self) -> Vec { - if self.is_fragmented { - let frag_id = self.id | (1 << 31); - let frag_id_bytes = frag_id.to_be_bytes(); - let bytes_prefix_iter = frag_id_bytes - .iter() - .cloned() - .chain(std::iter::once(self.total_fragments)) - .chain(std::iter::once(self.current_fragment)); - - let is_linked = - self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some(); - if is_linked { - let linked_id = self - .previous_fragments_set_id - .unwrap_or_else(|| self.next_fragments_set_id.unwrap()); - let linked_id_entry = linked_id | (1 << 31); - let linked_id_bytes = linked_id_entry.to_be_bytes(); - bytes_prefix_iter - .chain(linked_id_bytes.iter().cloned()) - .collect() - } else { - bytes_prefix_iter.chain(std::iter::once(0)).collect() - } - } else { - [0].to_vec() - } - } -} - -// everything below are tests - -#[cfg(test)] -mod fragment { - use rand::{thread_rng, RngCore}; - - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_unfragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNFRAGMENTED_PAYLOAD_MAX_LEN - 20; - let mut valid_message = vec![0u8; mlen]; - rng.fill_bytes(&mut valid_message); - - let valid_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: valid_message, - }; - let packet_bytes = valid_unfragmented_packet.clone().into_bytes(); - assert_eq!( - valid_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let empty_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: Vec::new(), - }; - let packet_bytes = empty_unfragmented_packet.clone().into_bytes(); - assert_eq!( - empty_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut full_message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut full_message); - - let full_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: full_message, - }; - let packet_bytes = full_unfragmented_packet.clone().into_bytes(); - assert_eq!( - full_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_unfragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNFRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_unlinked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let non_last_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = non_last_packet.clone().into_bytes(); - assert_eq!( - non_last_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let last_full_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 10, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = last_full_packet.clone().into_bytes(); - assert_eq!( - last_full_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let last_non_full_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 10, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = last_non_full_packet.clone().into_bytes(); - assert_eq!( - last_non_full_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_unlinked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_short_fragmented_payload_if_not_last() { - let mut rng = thread_rng(); - - let mlen = UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_pre_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 1, 1, Some(1234), None).unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_pre_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_post_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_post_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn unfragmented_fragment_can_be_created_with_payload_of_valid_length() { - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 1]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 20]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - } - - #[test] - fn unfragmented_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 1]; - assert!(Fragment::try_new_unfragmented(&payload).is_err()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 20]; - assert!(Fragment::try_new_unfragmented(&payload).is_err()); - } - - #[test] - fn unlinked_fragment_can_be_created_with_payload_of_valid_length() { - let id = 12345; - let full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - let non_full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 1, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 5, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 1, 1, None, None).is_ok()); - - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 1, 1, None, None).is_ok()); - - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 1, 1, None, None).is_ok()); - } - - #[test] - fn unlinked_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let id = 12345; - let non_full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - let too_much_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; - - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 5, None, None).is_err()); - - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 10, 5, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 1, 1, None, None).is_err()); - - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 5, None, None).is_err()); - } - - #[test] - fn linked_fragment_can_be_created_with_payload_of_valid_length() { - let id = 12345; - let link_id = 1234; - let full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - let non_full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - - assert!( - Fragment::try_new_fragmented(&full_payload, id, 10, 1, Some(link_id), None).is_ok() - ); - assert!(Fragment::try_new_fragmented(&full_payload, id, 1, 1, Some(link_id), None).is_ok()); - assert!( - Fragment::try_new_fragmented(&non_full_payload, id, 1, 1, Some(link_id), None).is_ok() - ); - assert!( - Fragment::try_new_fragmented(&non_full_payload2, id, 1, 1, Some(link_id), None).is_ok() - ); - - assert!(Fragment::try_new_fragmented( - &full_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_ok()); - } - - #[test] - fn linked_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let id = 12345; - let link_id = 1234; - let non_full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - let too_much_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; - - assert!( - Fragment::try_new_fragmented(&non_full_payload, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&non_full_payload2, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&too_much_payload, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&too_much_payload, id, 1, 1, Some(link_id), None).is_err() - ); - - assert!(Fragment::try_new_fragmented( - &non_full_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - assert!(Fragment::try_new_fragmented( - &non_full_payload2, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - - assert!(Fragment::try_new_fragmented( - &too_much_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - } -} - -#[cfg(test)] -mod fragment_header { - use super::*; - - #[cfg(test)] - mod unfragmented_payload { - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let header_bytes = unfragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(unfragmented_header, recovered_header); - assert_eq!(UNFRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let mut header_bytes = unfragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(unfragmented_header, recovered_header); - assert_eq!(UNFRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn retrieval_from_bytes_fail_for_empty_slice() { - let empty_vec = Vec::new(); - - assert!(FragmentHeader::try_from_bytes(&empty_vec).is_err()) - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let mut header_bytes = unfragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - // set the fragmentation flag - header_bytes[0] |= 1 << 7; - - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - } - - #[cfg(test)] - mod unlinked_fragmented_payload { - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn retrieval_from_bytes_fail_for_insufficient_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let header_bytes = &header_bytes[..header_bytes.len() - 1]; - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()) - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { - let fragmented_header = - FragmentHeader::try_new_fragmented(10, 10, 5, None, None).unwrap(); - - let mut header_bytes_low = fragmented_header.to_bytes(); - - // clear the fragmentation flag - header_bytes_low[0] &= !(1 << 7); - - let mut header_bytes_high = header_bytes_low.clone(); - // make sure first byte of id is non-empty (apart from the fragmentation flag) - // note for anyone reading this test in the future: choice of '3' here is arbitrary. - header_bytes_high[0] |= 1 << 3; - - // this will cause it to be parsed as 'unfragmented' header due to whole byte being set to 0 - // there isn't a really a good way of preventing that apart from adding even data overhead - assert_eq!( - FragmentHeader::new_unfragmented(), - FragmentHeader::try_from_bytes(&header_bytes_low).unwrap().0 - ); - - // however, this will have cause an error as there will be a value in the first byte - assert!(FragmentHeader::try_from_bytes(&header_bytes_high).is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_link_flag() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - // set linked flag - header_bytes[6] |= 1 << 7; - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - - #[test] - fn creation_of_header_fails_if_current_fragment_is_higher_than_total() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 11, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_current_fragment_is_zero() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 0, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_total_fragments_is_zero() { - assert!(FragmentHeader::try_new_fragmented(12345, 0, 0, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_id_is_negative() { - assert!(FragmentHeader::try_new_fragmented(-10, 10, 5, None, None).is_err()); - } - - #[test] - fn fragmented_header_cannot_be_created_with_zero_id() { - assert!(FragmentHeader::try_new_fragmented(0, 10, 5, None, None).is_err()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 5, Some(0), None).is_err()); - assert!(FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(0) - ) - .is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_if_current_fragment_is_higher_than_total() { - // manually create header to overwrite any constructor checks - let header = FragmentHeader { - is_fragmented: true, - id: 1234, - total_fragments: 10, - current_fragment: 11, - previous_fragments_set_id: None, - next_fragments_set_id: None, - }; - let header_bytes = header.to_bytes(); - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_if_current_or_total_fragment_is_zero() { - // manually create header to overwrite any constructor checks - let header = FragmentHeader { - is_fragmented: true, - id: 1234, - total_fragments: 0, - current_fragment: 0, - previous_fragments_set_id: None, - next_fragments_set_id: None, - }; - let header_bytes = header.to_bytes(); - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - } - - #[cfg(test)] - mod linked_fragmented_payload { - use super::*; - - #[test] - fn cannot_be_linked_to_itself() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 1, Some(12345), None).is_err()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 10, None, Some(12345)).is_err()); - } - - #[test] - fn can_only_be_pre_linked_for_first_fragment() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).is_ok()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 2, Some(1234), None).is_err()); - } - - #[test] - fn can_only_be_post_linked_for_last_fragment() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 10, None, Some(1234)).is_ok()); - assert!(FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .is_ok()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 2, Some(1234), None).is_err()); - } - - #[test] - fn pre_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn pre_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn pre_linked_is_successfully_recovered_if_its_both_first_and_final_fragment() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 1, 1, Some(1234), None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn post_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn post_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - } -} diff --git a/common/nymsphinx/src/chunking/mod.rs b/common/nymsphinx/src/chunking/mod.rs deleted file mode 100644 index 5f0238bace..0000000000 --- a/common/nymsphinx/src/chunking/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -// 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::chunking::set::split_into_sets; -use crate::packets::PacketSize; - -pub mod fragment; -pub mod reconstruction; -pub mod set; - -/// The idea behind the process of chunking is to incur as little data overhead as possible due -/// to very computationally costly sphinx encapsulation procedure. -/// -/// To achieve this, the underlying message is split into so-called "sets", which are further -/// subdivided into the base unit of "fragment" that is directly encapsulated by a Sphinx packet. -/// This allows to encapsulate messages of arbitrary length. -/// -/// Each message, regardless of its size, consists of at least a single `Set` that has at least -/// a single `Fragment`. -/// -/// Each `Fragment` can have variable, yet fully deterministic, length, -/// that depends on its position in the set as well as total number of sets. This is further -/// explained in `fragment.rs` file. -/// -/// Similarly, each `Set` can have a variable number of `Fragment`s inside. However, that -/// value is more restrictive: if it's the last set into which the message was split -/// (or implicitly the only one), it has no lower bound on the number of `Fragment`s. -/// (Apart from the restriction of containing at least a single one). If the set is located -/// somewhere in the middle, *it must be* full. Finally, regardless of its position, it must also be -/// true that it contains no more than `u8::max_value()`, i.e. 255 `Fragment`s. -/// Again, the reasoning for this is further explained in `set.rs` file. However, you might -/// also want to look at `fragment.rs` to understand the full context behind that design choice. -/// -/// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` -/// are further explained in the respective files. - -#[derive(PartialEq, Debug)] -pub enum ChunkingError { - InvalidPayloadLengthError, - TooBigMessageToSplit, - MalformedHeaderError, - NoValidProvidersError, - NoValidRoutesAvailableError, - InvalidTopologyError, - TooShortFragmentData, - MalformedFragmentData, - UnexpectedFragmentCount, -} - -pub struct MessageChunker { - packet_size: PacketSize, - reply_surbs: bool, - surb_acks: bool, -} - -impl MessageChunker { - pub fn new() -> Self { - Default::default() - } - - pub fn with_reply_surbs(mut self, reply_surbs: bool) -> Self { - self.reply_surbs = reply_surbs; - self - } - - pub fn with_surb_acks(mut self, surb_acks: bool) -> Self { - self.surb_acks = surb_acks; - self - } - - pub fn with_packet_size(mut self, packet_size: PacketSize) -> Self { - self.packet_size = packet_size; - self - } - - pub fn finalize() -> Vec> { - todo!() - } - - pub fn attach_surb_acks() {} - - pub fn attach_reply_surbs() {} -} - -impl Default for MessageChunker { - fn default() -> Self { - MessageChunker { - packet_size: Default::default(), - reply_surbs: false, - surb_acks: false, - } - } -} - -/// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets -/// directly. After receiving they can be combined using `reconstruction::MessageReconstructor` -/// to obtain the original message back. -pub fn split_and_prepare_payloads(message: &[u8]) -> Vec> { - let fragmented_messages = split_into_sets(message); - fragmented_messages - .into_iter() - .flat_map(|fragment_set| fragment_set.into_iter()) - .map(|fragment| fragment.into_bytes()) - .collect() -} diff --git a/common/nymsphinx/src/lib.rs b/common/nymsphinx/src/lib.rs index e3ec920bb9..b660507af9 100644 --- a/common/nymsphinx/src/lib.rs +++ b/common/nymsphinx/src/lib.rs @@ -12,31 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod addressing; -pub mod chunking; -pub mod framing; -pub mod packets; pub mod utils; -// Future consideration: currently in a lot of places, the payloads have randomised content -// which is not a perfect testing strategy as it might not detect some edge cases I never would -// have assumed could be possible. A better approach would be to research some Fuzz testing -// library like: https://github.com/rust-fuzz/afl.rs and use that instead for the inputs. - -// perhaps it might be useful down the line for interaction testing between client,mixes,etc? - -// re-exporting types and constants available in sphinx -pub use sphinx::{ - constants::{ - DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, - }, - header::{delays, delays::Delay, ProcessedHeader, SphinxHeader}, - payload::Payload, - route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, - Error, ProcessedPacket, Result, SphinxPacket, PACKET_SIZE, -}; - -// re-exporting this separately to remember to put special attention to below -// modules/types/constants when refactoring sphinx crate itself -// TODO: replace with sphinx::PublicKey once merged -pub use sphinx::key; +// re-export sub-crates +pub use nymsphinx_acknowledgements as acknowledgements; +pub use nymsphinx_addressing as addressing; +pub use nymsphinx_chunking as chunking; +pub use nymsphinx_cover as cover; +pub use nymsphinx_framing as framing; +pub use nymsphinx_params as params; +pub use nymsphinx_types::*; diff --git a/common/nymsphinx/src/utils/encapsulation.rs b/common/nymsphinx/src/utils/encapsulation.rs deleted file mode 100644 index 7b229a7e7e..0000000000 --- a/common/nymsphinx/src/utils/encapsulation.rs +++ /dev/null @@ -1,76 +0,0 @@ -// 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::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; -use crate::{delays, Destination, DestinationAddressBytes, SphinxPacket}; -use crate::{Error as SphinxError, Node as SphinxNode}; -use std::convert::TryFrom; -use std::net::SocketAddr; -use std::time; - -pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; - -#[derive(Debug)] -pub enum SphinxPacketEncapsulationError { - NoValidProvidersError, - InvalidTopologyError, - SphinxError(SphinxError), - InvalidFirstMixAddress, -} - -impl From for SphinxPacketEncapsulationError { - fn from(err: SphinxError) -> Self { - SphinxPacketEncapsulationError::SphinxError(err) - } -} - -impl From for SphinxPacketEncapsulationError { - fn from(_: NymNodeRoutingAddressError) -> Self { - use SphinxPacketEncapsulationError::*; - InvalidFirstMixAddress - } -} - -pub fn loop_cover_message_route( - our_address: DestinationAddressBytes, - route: Vec, - average_delay: time::Duration, -) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { - encapsulate_message_route( - our_address, - LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), - route, - average_delay, - ) -} - -pub fn encapsulate_message_route( - destination: DestinationAddressBytes, - message: Vec, - route: Vec, - average_delay: time::Duration, -) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { - // in our design we don't care about SURB_ID - let destination = Destination::new(destination, Default::default()); - - let delays = delays::generate_from_average_duration(route.len(), average_delay); - - // build the packet - let packet = SphinxPacket::new(message, &route[..], &destination, &delays, None)?; - - let first_node_address = - NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone())?; - - Ok((first_node_address.into(), packet)) -} diff --git a/common/nymsphinx/src/utils/mod.rs b/common/nymsphinx/src/utils/mod.rs index 3235d6d540..b3baf8710b 100644 --- a/common/nymsphinx/src/utils/mod.rs +++ b/common/nymsphinx/src/utils/mod.rs @@ -1,2 +1,33 @@ -pub mod encapsulation; -pub mod poisson; +// 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 rand::Rng; +use rand_distr::{Distribution, Exp}; +use std::time; + +// TODO: ask @AP why we are actually using Distribution::Exp(1/L) rather than just +// Distribution::Poisson(L) directly? + +// TODO: should we put an extra trait bound on this to require `CryptoRng`? Could there be any attacks +// because of weak rng used? +pub fn sample_poisson_duration( + rng: &mut R, + average_duration: time::Duration, +) -> time::Duration { + // this is our internal code used by our traffic streams + // the error is only thrown if average delay is less than 0, which will never happen + // so call to unwrap is perfectly safe here + let exp = Exp::new(1.0 / average_duration.as_nanos() as f64).unwrap(); + time::Duration::from_nanos(exp.sample(rng).round() as u64) +} diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml new file mode 100644 index 0000000000..4ac06239d3 --- /dev/null +++ b/common/nymsphinx/types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nymsphinx-types" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sphinx = { git = "https://github.com/nymtech/sphinx", rev="cd9f09b85de33c60030ba6d7fae613c42cbe1faf" } +#sphinx = { path = "../../../../sphinx"} \ No newline at end of file diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs new file mode 100644 index 0000000000..8d16ba194c --- /dev/null +++ b/common/nymsphinx/types/src/lib.rs @@ -0,0 +1,27 @@ +// 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. + +// re-exporting types and constants available in sphinx +pub use sphinx::{ + constants::{ + self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, + }, + crypto::{public_key_from_bytes, PublicKey, SecretKey}, + header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE}, + packet::builder::{self, DEFAULT_PAYLOAD_SIZE}, + payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, + route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, + surb::{SURBMaterial, SURB}, + Error, ProcessedPacket, Result, SphinxPacket, +}; diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 46186e1714..17a568b979 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -16,5 +16,6 @@ rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } ## internal -nymsphinx = {path = "../nymsphinx"} +nymsphinx-addressing = {path = "../nymsphinx/addressing"} +nymsphinx-types = {path = "../nymsphinx/types"} version-checker = {path = "../version-checker" } diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index c67b95ffe1..d514b7ae62 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -61,7 +61,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index e9231c1496..c519572b1f 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -15,7 +15,7 @@ use crate::filter::VersionFilterable; use futures::future::BoxFuture; use itertools::Itertools; -use nymsphinx::Node as SphinxNode; +use nymsphinx_types::{Node as SphinxNode, NodeAddressBytes}; use rand::seq::IteratorRandom; use std::cmp::max; use std::collections::HashMap; @@ -48,7 +48,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let mut highest_layer = 0; for mix in self.mix_nodes() { // we need to have extra space for provider - if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 { + if mix.layer > nymsphinx_types::MAX_PATH_LENGTH as u64 { return Err(NymTopologyError::InvalidMixLayerError); } highest_layer = max(highest_layer, mix.layer); @@ -90,15 +90,31 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { Ok(route) } - // Sets up a route to a specific gateway + fn gateway_exists(&self, gateway_address: &NodeAddressBytes) -> bool { + let b58_address = gateway_address.to_base58_string(); + self.gateways() + .iter() + .find(|&gateway| gateway.pub_key == b58_address) + .is_some() + } + fn random_route_to_gateway( &self, - gateway_node: SphinxNode, + gateway_address: &NodeAddressBytes, ) -> Result, NymTopologyError> { + let b58_address = gateway_address.to_base58_string(); + + let gateway = self + .gateways() + .iter() + .find(|&gateway| gateway.pub_key == b58_address) + .ok_or_else(|| NymTopologyError::NonExistentGatewayError)? + .clone(); + Ok(self .random_mix_route()? .into_iter() - .chain(std::iter::once(gateway_node)) + .chain(std::iter::once(gateway.into())) .collect()) } @@ -159,4 +175,5 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { pub enum NymTopologyError { InvalidMixLayerError, MissingLayerError(Vec), + NonExistentGatewayError, } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 68241ee1a5..588938c69b 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -46,7 +46,7 @@ impl Into for Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.host).try_into().unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/provider.rs b/common/topology/src/provider.rs index c869a88637..de1a722a97 100644 --- a/common/topology/src/provider.rs +++ b/common/topology/src/provider.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -54,7 +54,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index a5c93d8198..8454722fd5 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-gateway" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index de7672aff2..1a5a221743 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::auth_token::AuthToken; -use crate::types::BinaryRequest::ForwardSphinx; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; use serde::{Deserialize, Serialize}; use std::{ @@ -27,7 +27,7 @@ use tokio_tungstenite::tungstenite::protocol::Message; #[derive(Debug)] pub enum GatewayRequestsError { IncorrectlyEncodedAddress, - RequestOfInvalidSize(usize, usize), + RequestOfInvalidSize(usize), MalformedSphinxPacket, } @@ -38,10 +38,10 @@ impl fmt::Display for GatewayRequestsError { use GatewayRequestsError::*; match self { IncorrectlyEncodedAddress => write!(f, "address field was incorrectly encoded"), - RequestOfInvalidSize(actual, expected) => write!( + RequestOfInvalidSize(actual) => write!( f, - "received request had invalid size. (actual: {}, expected: {})", - actual, expected + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", + actual, PacketSize::ACKPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), } @@ -164,18 +164,18 @@ impl BinaryRequest { let address = NymNodeRoutingAddress::try_from_bytes(&raw_req)?; let addr_offset = address.bytes_min_len(); - if raw_req[addr_offset..].len() != nymsphinx::PACKET_SIZE { - Err(GatewayRequestsError::RequestOfInvalidSize( - raw_req[addr_offset..].len(), - nymsphinx::PACKET_SIZE, - )) + let packet_size = raw_req[addr_offset..].len(); + if let Err(_) = PacketSize::get_type(packet_size) { + // TODO: should this allow AckPacket sizes? + + Err(GatewayRequestsError::RequestOfInvalidSize(packet_size)) } else { let sphinx_packet = match SphinxPacket::from_bytes(&raw_req[addr_offset..]) { Ok(packet) => packet, Err(_) => return Err(GatewayRequestsError::MalformedSphinxPacket), }; - Ok(ForwardSphinx { + Ok(BinaryRequest::ForwardSphinx { address: address.into(), sphinx_packet, }) @@ -201,6 +201,7 @@ impl BinaryRequest { } } + // TODO: this will be encrypted, etc. pub fn new_forward_request(address: SocketAddr, sphinx_packet: SphinxPacket) -> BinaryRequest { BinaryRequest::ForwardSphinx { address, diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 51f23d93db..3389e774ad 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -61,6 +61,7 @@ where while let Some(sphinx_packet) = self.framed_connection.next().await { match sphinx_packet { Ok(sphinx_packet) => { + // rather important TODO: // we *really* need a worker pool here, because if we receive too many packets, // we will spawn too many tasks and starve CPU due to context switching. // (because presumably tokio has some concept of context switching in its diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 8142724aa5..a6345b5a99 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -16,15 +16,17 @@ use crate::node::client_handling::clients_handler::{ ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, }; use crate::node::client_handling::websocket::message_receiver::MixMessageSender; +use crate::node::mixnet_handling::sender::OutboundMixMessageSender; use crate::node::storage::inboxes::{ClientStorage, StoreData}; use crypto::encryption; use futures::channel::oneshot; use futures::lock::Mutex; use log::*; -use nymsphinx::{ - utils::encapsulation::LOOP_COVER_MESSAGE_PAYLOAD, DestinationAddressBytes, - Error as SphinxError, ProcessedPacket, SphinxPacket, -}; +use nymsphinx::acknowledgements::surb_ack::{SURBAck, SURBAckRecoveryError}; +use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; +use nymsphinx::params::packet_sizes::PacketSize; +use nymsphinx::{DestinationAddressBytes, Error as SphinxError, ProcessedPacket, SphinxPacket}; + use std::collections::HashMap; use std::io; use std::ops::Deref; @@ -34,9 +36,10 @@ use std::sync::Arc; pub enum MixProcessingError { ReceivedForwardHopError, NonMatchingRecipient, - InvalidPayload, + UnsupportedSphinxPacketSize(usize), SphinxProcessingError(SphinxError), - IOError(String), + IncorrectlyFormattedSURBAck(SURBAckRecoveryError), + IOError(io::Error), } impl From for MixProcessingError { @@ -52,7 +55,15 @@ impl From for MixProcessingError { fn from(e: io::Error) -> Self { use MixProcessingError::*; - IOError(e.to_string()) + IOError(e) + } +} + +impl From for MixProcessingError { + fn from(err: SURBAckRecoveryError) -> Self { + use MixProcessingError::*; + + IncorrectlyFormattedSURBAck(err) } } @@ -65,6 +76,7 @@ pub struct PacketProcessor { available_socket_senders_cache: Arc>>, client_store: ClientStorage, clients_handler_sender: ClientsHandlerRequestSender, + ack_sender: OutboundMixMessageSender, } impl PacketProcessor { @@ -72,12 +84,14 @@ impl PacketProcessor { secret_key: Arc, clients_handler_sender: ClientsHandlerRequestSender, client_store: ClientStorage, + ack_sender: OutboundMixMessageSender, ) -> Self { PacketProcessor { available_socket_senders_cache: Arc::new(Mutex::new(HashMap::new())), clients_handler_sender, client_store, secret_key, + ack_sender, } } @@ -85,10 +99,16 @@ impl PacketProcessor { &self, sender_channel: Option, message: Vec, - ) -> bool { + ) -> Result<(), Vec> { match sender_channel { - None => false, - Some(sender_channel) => sender_channel.unbounded_send(vec![message]).is_ok(), + None => Err(message), + Some(sender_channel) => { + sender_channel + .unbounded_send(vec![message]) + // right now it's a "simpler" case here as we're only ever sending 1 message + // at the time, but the channel itself could accept arbitrary many messages at once + .map_err(|try_send_err| try_send_err.into_inner().pop().unwrap()) + } } } @@ -140,7 +160,7 @@ impl PacketProcessor { client_address: DestinationAddressBytes, message: Vec, ) -> io::Result<()> { - trace!( + debug!( "Storing received packet for {:?} on the disk...", client_address.to_base58_string() ); @@ -148,6 +168,9 @@ impl PacketProcessor { // not cause our sfw-provider to run out of disk space too quickly. // Eventually this is going to get removed and be replaced by a quota system described in: // https://github.com/nymtech/nym/issues/137 + + // JS: I think this would never get called anyway, because if loop cover messages are sent + // it means client is online and hence all his messages should be pushed directly to him? if message == LOOP_COVER_MESSAGE_PAYLOAD { debug!("Received a loop cover message - not going to store it"); return Ok(()); @@ -163,15 +186,15 @@ impl PacketProcessor { ) -> Result<(DestinationAddressBytes, Vec), MixProcessingError> { match packet.process(self.secret_key.deref().inner()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(_, _, _)) => { - warn!("Received a forward hop message - those are not implemented for providers"); + warn!("Received a forward hop message - those are not implemented for gateways"); Err(MixProcessingError::ReceivedForwardHopError) } Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, _surb_id, payload)) => { // in our current design, we do not care about the 'surb_id' in the header // as it will always be empty anyway - let (payload_destination, message) = payload - .try_recover_destination_and_plaintext() - .ok_or_else(|| MixProcessingError::InvalidPayload)?; + let (payload_destination, message) = + payload.try_recover_destination_and_plaintext()?; + // TODO: @AP, does that check still make sense? if client_address != payload_destination { return Err(MixProcessingError::NonMatchingRecipient); } @@ -184,28 +207,100 @@ impl PacketProcessor { } } + fn split_plaintext_into_ack_and_message( + &self, + mut extracted_plaintext: Vec, + ) -> (Vec, Vec) { + if extracted_plaintext.len() < SURBAck::len() { + // TODO: + // TODO: + // this is mostly for dev purposes to see if we receive something we did not mean to send + // but in an actual system, what should we do? abandon the whole packet? + // store client's data regardless? + // I'm going to leave this question open for until I've implemented reply SURBs + // as they will change the communication between client and gateway so this + // if statement might no longer make any sense + panic!("received packet without an ack"); + } + + let plaintext = extracted_plaintext.split_off(SURBAck::len()); + let ack_data = extracted_plaintext; + (ack_data, plaintext) + } + pub(crate) async fn process_sphinx_packet( &mut self, sphinx_packet: SphinxPacket, ) -> Result<(), MixProcessingError> { + // see if what we got now is an ack or normal packet + let packet_len = sphinx_packet.len(); + // TODO: micro-optimisations: + // 1. don't even try to unwrap the packet if it's not one of `PacketSize` variants + // 2. if client_address doesn't exist at this gateway, don't do any other work here + // (as stupid as this sounds, there's currently no easy way of directly checking if the + // client exists here) let (client_address, plaintext) = self.unwrap_sphinx_packet(sphinx_packet)?; + let (routable_ack, plaintext) = match packet_len { + n if n == PacketSize::ACKPacket.size() => { + trace!("received an ack packet!"); + (None, plaintext) + } + n if n == PacketSize::RegularPacket.size() + || n == PacketSize::ExtendedPacket.size() => + { + trace!("received a normal packet!"); + let (ack_data, plaintext) = self.split_plaintext_into_ack_and_message(plaintext); + let (ack_first_hop, ack_packet) = SURBAck::try_recover_first_hop_packet(&ack_data)?; + (Some((ack_first_hop, ack_packet)), plaintext) + } + n => return Err(MixProcessingError::UnsupportedSphinxPacketSize(n)), + }; let client_sender = self .try_to_obtain_client_ws_message_sender(client_address.clone()) .await; - // TODO: think of a way to prevent having to clone the plaintext here, perhaps make channels use references? - // this will, again, take slightly more time, so it's an issue for later - if !self.try_push_message_to_client(client_sender, plaintext.clone()) { - Ok(self - .store_processed_packet_payload(client_address, plaintext) - .await?) + if let Err(unsent_plaintext) = self.try_push_message_to_client(client_sender, plaintext) { + // means we failed to push message directly to the client (it might be offline) + // but we don't want to store an ack message for him - he won't be able to decode + // it anyway. + // TODO: after keybase discussion we *might* want to store them after all + if routable_ack.is_none() { + trace!("Received an ack for offline client - won't try storing it"); + return Ok(()); + } + + if let Err(io_err) = self + .store_processed_packet_payload(client_address.clone(), unsent_plaintext) + .await + { + return Err(io_err)?; + } else { + trace!( + "Managed to store packet for {:?} on the disk", + client_address.to_base58_string() + ); + } } else { trace!( "Managed to push received packet for {:?} to websocket connection!", client_address.to_base58_string() ); - Ok(()) } + + // if we managed to either push message directly to the [online] client or store it at + // it's inbox, it means that it must exist at this gateway, hence we can send the + // received ack back into the network + if let Some((ack_first_hop, ack_packet)) = routable_ack { + trace!( + "Sending an ack back into the network. The first hop is {:?}", + ack_first_hop + ); + self.ack_sender + .unbounded_send((ack_first_hop.into(), ack_packet)) + .unwrap(); + } + + Ok(()) } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 87e289a90d..fc1bfe8c85 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -66,13 +66,18 @@ impl Gateway { } } - fn start_mix_socket_listener(&self, clients_handler_sender: ClientsHandlerRequestSender) { + fn start_mix_socket_listener( + &self, + clients_handler_sender: ClientsHandlerRequestSender, + ack_sender: OutboundMixMessageSender, + ) { info!("Starting mix socket listener..."); let packet_processor = mixnet_handling::PacketProcessor::new( Arc::new(self.sphinx_keypair.private_key().clone()), clients_handler_sender, self.client_inbox_storage.clone(), + ack_sender, ); mixnet_handling::Listener::new(self.config.get_mix_listening_address()) @@ -180,7 +185,7 @@ impl Gateway { let mix_forwarding_channel = self.start_packet_forwarder(); let clients_handler_sender = self.start_clients_handler(); - self.start_mix_socket_listener(clients_handler_sender.clone()); + self.start_mix_socket_listener(clients_handler_sender.clone(), mix_forwarding_channel.clone()); self.start_client_websocket_listener(mix_forwarding_channel, clients_handler_sender); self.start_presence_notifier(); diff --git a/gateway/src/node/storage/inboxes.rs b/gateway/src/node/storage/inboxes.rs index bd165e5a81..9c835c133b 100644 --- a/gateway/src/node/storage/inboxes.rs +++ b/gateway/src/node/storage/inboxes.rs @@ -118,9 +118,10 @@ impl ClientStorage { let full_store_path = full_store_dir.join(Self::generate_random_file_name( inner_data.filename_length as usize, )); - debug!( + trace!( "going to store: {:?} in file: {:?}", - store_data.message, full_store_path + store_data.message, + full_store_path ); let mut file = File::create(full_store_path).await?; diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index cec01d1bfb..28b811eecc 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-mixnode" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 359d21c809..0472a694e9 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-validator" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] edition = "2018"