diff --git a/Cargo.lock b/Cargo.lock index 5b1cd3706b..dea8579e54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1703,6 +1703,17 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "cuckoofilter" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b810a8449931679f64cd7eef1bbd0fa315801b6d5d9cdc1ace2804d6529eee18" +dependencies = [ + "byteorder", + "fnv", + "rand 0.7.3", +] + [[package]] name = "cupid" version = "0.6.1" @@ -2598,6 +2609,23 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastbloom-rs" +version = "0.5.4" +source = "git+https://github.com/simonwicky/fastbloom#9733fce76e68f193ce749cc455557b09620abf32" +dependencies = [ + "cuckoofilter", + "fastmurmur3", + "xorfilter-rs", + "xxhash-rust", +] + +[[package]] +name = "fastmurmur3" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d922f481ae01f2a3f1fff7b9e0e789f18f0c755a38ec983a3e6f37762cdcc2a2" + [[package]] name = "fastrand" version = "1.9.0" @@ -5764,6 +5792,7 @@ dependencies = [ "bytes", "cfg-if", "cpu-cycles", + "fastbloom-rs", "futures", "humantime-serde", "log", @@ -5928,7 +5957,7 @@ dependencies = [ "tracing", "utoipa", "utoipa-swagger-ui", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.1", ] [[package]] @@ -6496,7 +6525,7 @@ dependencies = [ "thiserror", "ts-rs", "url", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.1", ] [[package]] @@ -6628,7 +6657,7 @@ dependencies = [ "nym-task", "nym-wireguard-types", "tokio", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.1", ] [[package]] @@ -6646,7 +6675,7 @@ dependencies = [ "sha2 0.10.8", "thiserror", "utoipa", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.1", ] [[package]] @@ -9032,8 +9061,7 @@ dependencies = [ [[package]] name = "sphinx-packet" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc43eda802856ee82a7555c7b75ceb9e07451741c7a2f5f23d036020e01189d4" +source = "git+https://github.com/nymtech/sphinx.git?branch=simon/replay_tag#91a64ab1aaf3a7bb358af0a5b1a89f0564a03d38" dependencies = [ "aes 0.7.5", "arrayref", @@ -10917,7 +10945,7 @@ dependencies = [ "tokio", "webpki 0.21.4", "webrtc-util", - "x25519-dalek 2.0.0", + "x25519-dalek 2.0.1", "x509-parser 0.13.2", ] @@ -11304,9 +11332,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek 4.1.1", "rand_core 0.6.4", @@ -11360,6 +11388,18 @@ dependencies = [ "libc", ] +[[package]] +name = "xorfilter-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f9da296a88b6bc150b896d17770a62d4dc6f63ecf0ed10a9c08a1cb3d12f24" + +[[package]] +name = "xxhash-rust" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" + [[package]] name = "yamux" version = "0.10.2" diff --git a/common/mixnode-common/Cargo.toml b/common/mixnode-common/Cargo.toml index 9cf2c51cf0..5ee12e5766 100644 --- a/common/mixnode-common/Cargo.toml +++ b/common/mixnode-common/Cargo.toml @@ -24,6 +24,7 @@ tokio = { version = "1.24.1", features = [ tokio-util = { workspace = true, features = ["codec"] } url = { workspace = true } thiserror = { workspace = true } +fastbloom-rs = { git = "https://github.com/simonwicky/fastbloom"} ## tracing tracing = { version = "0.1.37", optional = true } diff --git a/common/mixnode-common/src/packet_processor/error.rs b/common/mixnode-common/src/packet_processor/error.rs index 405e10bb91..cc8788baf5 100644 --- a/common/mixnode-common/src/packet_processor/error.rs +++ b/common/mixnode-common/src/packet_processor/error.rs @@ -28,4 +28,7 @@ pub enum MixProcessingError { #[error("failed to process received outfox packet: {0}")] OutfoxProcessingError(#[from] OutfoxError), + + #[error("this packet was already processed, it's a replay")] + ReplayedPacketDetected, } diff --git a/common/mixnode-common/src/packet_processor/mod.rs b/common/mixnode-common/src/packet_processor/mod.rs index 3d2efa6b2c..90075f9939 100644 --- a/common/mixnode-common/src/packet_processor/mod.rs +++ b/common/mixnode-common/src/packet_processor/mod.rs @@ -3,3 +3,4 @@ pub mod error; pub mod processor; +pub mod replay_detection; diff --git a/common/mixnode-common/src/packet_processor/replay_detection.rs b/common/mixnode-common/src/packet_processor/replay_detection.rs new file mode 100644 index 0000000000..5d44906847 --- /dev/null +++ b/common/mixnode-common/src/packet_processor/replay_detection.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::packet_processor::error::MixProcessingError; +use fastbloom_rs::{BloomFilter, FilterBuilder, Membership}; +use std::sync::{Arc, Mutex}; + +const BLOOM_FILTER_SIZE: u64 = 10_000_000; +const FP_RATE: f64 = 1e-4; + +//alias for convenience +type ReplayTag = [u8]; + +#[derive(Clone, Debug)] +pub struct ReplayDetector(Arc>); + +impl ReplayDetector { + pub fn new() -> Self { + ReplayDetector(Arc::new(Mutex::new(ReplayDetectorInner::new()))) + } + + //check if secret has been seen already + //if no, return Ok + //if yes, add the secret to the list, then return an error + pub fn handle_replay_tag(&self, replay_tag: &ReplayTag) -> Result<(), MixProcessingError> { + match self.0.lock() { + Ok(mut inner) => { + if !inner.lookup_then_insert(replay_tag) { + Ok(()) + } else { + Err(MixProcessingError::ReplayedPacketDetected) + } + } + Err(err) => { + log::warn!("Failed to handle replay_tag : {err}"); + Ok(()) //what is the sensible thing to do, if the lock is poisoned? Reset the filter ? + } + } + } +} + +impl Default for ReplayDetector { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug)] +struct ReplayDetectorInner { + filter: BloomFilter, +} + +impl ReplayDetectorInner { + pub fn new() -> Self { + ReplayDetectorInner { + filter: FilterBuilder::new(BLOOM_FILTER_SIZE, FP_RATE).build_bloom_filter(), + } + } + + pub fn lookup_then_insert(&mut self, replay_tag: &ReplayTag) -> bool { + self.filter.contains_then_add(replay_tag) + } +} + +#[cfg(test)] +mod replay_detector_test { + use super::*; + + #[test] + fn handle_replay_tag_correctly_detects_replay() { + let replay_detector = ReplayDetector::new(); + let replay_tag = b"Hello World!"; + assert!(replay_detector.handle_replay_tag(replay_tag).is_ok()); //first insert is fine + assert!(replay_detector.handle_replay_tag(replay_tag).is_err()); //second is not + } +}