diff --git a/Cargo.toml b/Cargo.toml index 77fc88c0c38f..8b3bd3139001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,7 +129,7 @@ aead-cipher-extra = ["shadowsocks-service/aead-cipher-extra"] # Enable AEAD 2022 aead-cipher-2022 = ["shadowsocks-service/aead-cipher-2022"] -# Enable detection against replay attack +# Enable detection against replay attack (Stream / AEAD) security-replay-attack-detect = ["shadowsocks-service/security-replay-attack-detect"] replay-attack-detect = ["security-replay-attack-detect"] # Backward compatibility. DO NOT USE. # Enable IV printable prefix diff --git a/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs b/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs index eafc4e9d3b7c..a28361d91336 100644 --- a/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs +++ b/crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs @@ -23,7 +23,7 @@ use shadowsocks::{ relay::{ socks5::Address, tcprelay::proxy_stream::ProxyClientStream, - udprelay::{proxy_socket::ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, + udprelay::{options::UdpSocketControlData, proxy_socket::ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE}, }, ServerConfig, }; @@ -882,7 +882,13 @@ impl PingChecker { self.context.connect_opts_ref(), ) .await?; - client.send(&addr, DNS_QUERY).await?; + + let control = UdpSocketControlData { + client_session_id: rand::random::(), + server_session_id: 0, + packet_id: 1, + }; + client.send_with_ctrl(&addr, &control, DNS_QUERY).await?; let mut buffer = [0u8; MAXIMUM_UDP_PAYLOAD_SIZE]; let (n, ..) = client.recv(&mut buffer).await?; diff --git a/crates/shadowsocks-service/src/server/udprelay.rs b/crates/shadowsocks-service/src/server/udprelay.rs index e048fda1b37a..aaa68f3d0162 100644 --- a/crates/shadowsocks-service/src/server/udprelay.rs +++ b/crates/shadowsocks-service/src/server/udprelay.rs @@ -313,31 +313,16 @@ impl UdpAssociation { } } -struct ClientContext { - packet_window_filter: PacketWindowFilter, -} - struct ClientSessionContext { client_session_id: u64, - client_context_map: LruCache, + packet_window_filter: PacketWindowFilter, } impl ClientSessionContext { fn new(client_session_id: u64) -> ClientSessionContext { - // Client shouldn't be remembered too long. - // If a client was switching between networks (like Wi-Fi and Cellular), - // when it switched back from another, the packet filter window will be too old. - const CLIENT_SESSION_REMEMBER_DURATION: Duration = Duration::from_secs(60); - - // Wi-Fi & Cellular network device, so it is 2 for most users - const CLIENT_SESSION_REMEMBER_COUNT: usize = 2; - ClientSessionContext { client_session_id, - client_context_map: LruCache::with_expiry_duration_and_capacity( - CLIENT_SESSION_REMEMBER_DURATION, - CLIENT_SESSION_REMEMBER_COUNT, - ), + packet_window_filter: PacketWindowFilter::new(), } } } @@ -524,21 +509,10 @@ impl UdpAssociationContext { if let Some(control) = control { // Check if Packet ID is in the window - let session = self + let session_context = self .client_session .get_or_insert_with(|| ClientSessionContext::new(control.client_session_id)); - let session_context = session.client_context_map.entry(self.peer_addr).or_insert_with(|| { - trace!( - "udp client {} with session {} created", - self.peer_addr, - control.client_session_id - ); - ClientContext { - packet_window_filter: PacketWindowFilter::new(), - } - }); - let packet_id = control.packet_id; if !session_context .packet_window_filter diff --git a/crates/shadowsocks/Cargo.toml b/crates/shadowsocks/Cargo.toml index bef11d42fd9c..9ff865cb23aa 100644 --- a/crates/shadowsocks/Cargo.toml +++ b/crates/shadowsocks/Cargo.toml @@ -32,10 +32,10 @@ stream-cipher = ["shadowsocks-crypto/v1-stream"] aead-cipher-extra = ["shadowsocks-crypto/v1-aead-extra"] # Enable AEAD 2022 -aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache", "spin"] +aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache"] # Enable detection against replay attack -security-replay-attack-detect = ["bloomfilter", "spin"] +security-replay-attack-detect = ["bloomfilter"] # Enable IV printable prefix security-iv-printable-prefix = ["rand"] @@ -54,7 +54,7 @@ byte_string = "1.0" base64 = "0.13" url = "2.2" once_cell = "1.8" -spin = { version = "0.9", features = ["std"], optional = true } +spin = { version = "0.9", features = ["std"] } pin-project = "1.0" bloomfilter = { version = "1.0.8", optional = true } thiserror = "1.0" diff --git a/crates/shadowsocks/src/context.rs b/crates/shadowsocks/src/context.rs index d1d43172501e..5eda7226df76 100644 --- a/crates/shadowsocks/src/context.rs +++ b/crates/shadowsocks/src/context.rs @@ -35,7 +35,7 @@ impl Context { pub fn new(config_type: ServerType) -> Context { Context { replay_protector: ReplayProtector::new(config_type), - replay_policy: ReplayAttackPolicy::Ignore, + replay_policy: ReplayAttackPolicy::Reject, dns_resolver: Arc::new(DnsResolver::system_resolver()), ipv6_first: false, } diff --git a/crates/shadowsocks/src/relay/udprelay/aead_2022.rs b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs index 379199d5a169..6641d397c68c 100644 --- a/crates/shadowsocks/src/relay/udprelay/aead_2022.rs +++ b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs @@ -63,13 +63,10 @@ use aes::{ }; use byte_string::ByteStr; use bytes::{Buf, BufMut, BytesMut}; -use log::{error, trace, warn}; +use log::trace; use lru_time_cache::LruCache; -use once_cell::sync::Lazy; -use spin::Mutex as SpinMutex; use crate::{ - config::ReplayAttackPolicy, context::Context, crypto::{ v2::udp::{ChaCha20Poly1305Cipher, UdpCipher}, @@ -159,57 +156,6 @@ fn get_cipher(method: CipherKind, key: &[u8], session_id: u64) -> Rc }) } -fn check_and_record_nonce(method: CipherKind, key: &[u8], session_id: u64, nonce: &[u8]) -> bool { - static REPLAY_FILTER_RECORDER: Lazy, ()>>>> = Lazy::new(|| { - SpinMutex::new(LruCache::with_expiry_duration_and_capacity( - CIPHER_CACHE_DURATION, - CIPHER_CACHE_LIMIT, - )) - }); - - let cache_key = CipherKey { - method, - // The key is stored in ServerConfig structure, so the address of it won't change. - key: key.as_ptr() as usize, - session_id, - }; - - const REPLAY_DETECT_NONCE_EXPIRE_DURATION: Duration = Duration::from_secs(SERVER_PACKET_TIMESTAMP_MAX_DIFF); - - let mut session_map = REPLAY_FILTER_RECORDER.lock(); - - let session_nonce_map = session_map - .entry(cache_key) - .or_insert_with(|| LruCache::with_expiry_duration(REPLAY_DETECT_NONCE_EXPIRE_DURATION)); - - if session_nonce_map.get(nonce).is_some() { - return true; - } - - session_nonce_map.insert(nonce.to_vec(), ()); - false -} - -#[inline] -fn check_nonce_replay(context: &Context, method: CipherKind, key: &[u8], session_id: u64, nonce: &[u8]) -> bool { - match context.replay_attack_policy() { - ReplayAttackPolicy::Ignore => false, - ReplayAttackPolicy::Detect => { - if check_and_record_nonce(method, key, session_id, nonce) { - warn!("detected repeated nonce salt {:?}", ByteStr::new(nonce)); - } - false - } - ReplayAttackPolicy::Reject => { - let replayed = check_and_record_nonce(method, key, session_id, nonce); - if replayed { - error!("detected repeated nonce salt {:?}", ByteStr::new(nonce)); - } - replayed - } - } -} - fn encrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &mut BytesMut, session_id: u64) { unsafe { packet.advance_mut(method.tag_len()); @@ -255,7 +201,7 @@ fn encrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: & } } -fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &mut [u8]) -> bool { +fn decrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &mut [u8]) -> bool { match method { CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305 => { // ChaCha20-Poly1305 uses PSK as key, prepended nonce in packet @@ -272,11 +218,6 @@ fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &m u64::from_be(session_id_slice[0]) }; - if check_nonce_replay(context, method, key, session_id, nonce) { - error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); - return false; - } - let cipher = get_cipher(method, key, session_id); if !cipher.decrypt_packet(nonce, message) { @@ -316,14 +257,7 @@ fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &m let nonce = &packet_header[4..16]; - let cipher = { - if check_nonce_replay(context, method, key, session_id, nonce) { - error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); - return false; - } - - get_cipher(method, key, session_id) - }; + let cipher = get_cipher(method, key, session_id); if !cipher.decrypt_packet(nonce, message) { return false; diff --git a/crates/shadowsocks/src/security/replay/dummy.rs b/crates/shadowsocks/src/security/replay/dummy.rs deleted file mode 100644 index 4d21ff09baa4..000000000000 --- a/crates/shadowsocks/src/security/replay/dummy.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{config::ServerType, crypto::CipherKind}; - -/// A dummy protector against replay attack -/// -/// It is dummy because it can protect nothing. -pub struct ReplayProtector; - -impl ReplayProtector { - /// Create a new ReplayProtector - #[inline(always)] - pub fn new(_: ServerType) -> ReplayProtector { - ReplayProtector - } - - /// Check if nonce exist or not - #[inline(always)] - pub fn check_nonce_and_set(&self, _method: CipherKind, _nonce: &[u8]) -> bool { - false - } -} diff --git a/crates/shadowsocks/src/security/replay/mod.rs b/crates/shadowsocks/src/security/replay/mod.rs index 0eb58b90a4eb..a70a7931ffd5 100644 --- a/crates/shadowsocks/src/security/replay/mod.rs +++ b/crates/shadowsocks/src/security/replay/mod.rs @@ -1,11 +1,75 @@ +#[cfg(feature = "aead-cipher-2022")] +use std::time::Duration; + use cfg_if::cfg_if; +#[cfg(feature = "aead-cipher-2022")] +use lru_time_cache::LruCache; +#[allow(dead_code)] +use spin::Mutex as SpinMutex; + +#[cfg(feature = "aead-cipher-2022")] +use crate::relay::tcprelay::proxy_stream::protocol::v2::SERVER_STREAM_TIMESTAMP_MAX_DIFF; +use crate::{config::ServerType, crypto::CipherKind}; + +#[cfg(feature = "security-replay-attack-detect")] +use self::ppbloom::PingPongBloom; + +#[cfg(feature = "security-replay-attack-detect")] +mod ppbloom; + +/// A Bloom Filter based protector against replay attach +pub struct ReplayProtector { + // Check for duplicated IV/Nonce, for prevent replay attack + // https://github.com/shadowsocks/shadowsocks-org/issues/44 + #[cfg(feature = "security-replay-attack-detect")] + nonce_ppbloom: SpinMutex, + + // AEAD 2022 specific filter. + // AEAD 2022 TCP protocol has a timestamp, which can already reject most of the replay requests, + // so we only need to remember nonce that are in the valid time range + #[cfg(feature = "aead-cipher-2022")] + nonce_set: SpinMutex, ()>>, +} + +impl ReplayProtector { + /// Create a new ReplayProtector + pub fn new(config_type: ServerType) -> ReplayProtector { + ReplayProtector { + #[cfg(feature = "security-replay-attack-detect")] + nonce_ppbloom: SpinMutex::new(PingPongBloom::new(config_type)), + #[cfg(feature = "aead-cipher-2022")] + nonce_set: SpinMutex::new(LruCache::with_expiry_duration(Duration::from_secs( + SERVER_STREAM_TIMESTAMP_MAX_DIFF, + ))), + } + } + + /// Check if nonce exist or not + #[inline(always)] + pub fn check_nonce_and_set(&self, method: CipherKind, nonce: &[u8]) -> bool { + // Plain cipher doesn't have a nonce + // Always treated as non-duplicated + if nonce.is_empty() { + return false; + } + + #[cfg(feature = "aead-cipher-2022")] + if method.is_aead_2022() { + let mut set = self.nonce_set.lock(); + if set.get(nonce).is_some() { + return true; + } + set.insert(nonce.to_vec(), ()); + return false; + } -cfg_if! { - if #[cfg(feature = "security-replay-attack-detect")] { - mod ppbloom; - pub use self::ppbloom::*; - } else { - mod dummy; - pub use self::dummy::*; + cfg_if! { + if #[cfg(feature = "security-replay-attack-detect")] { + let mut ppbloom = self.nonce_ppbloom.lock(); + ppbloom.check_and_set(nonce) + } else { + false + } + } } } diff --git a/crates/shadowsocks/src/security/replay/ppbloom.rs b/crates/shadowsocks/src/security/replay/ppbloom.rs index 0a15a49bd997..cf0be9c82c22 100644 --- a/crates/shadowsocks/src/security/replay/ppbloom.rs +++ b/crates/shadowsocks/src/security/replay/ppbloom.rs @@ -1,15 +1,7 @@ -#[cfg(feature = "aead-cipher-2022")] -use std::time::Duration; - use bloomfilter::Bloom; use log::debug; -#[cfg(feature = "aead-cipher-2022")] -use lru_time_cache::LruCache; -use spin::Mutex as SpinMutex; -#[cfg(feature = "aead-cipher-2022")] -use crate::relay::tcprelay::proxy_stream::protocol::v2::SERVER_STREAM_TIMESTAMP_MAX_DIFF; -use crate::{config::ServerType, crypto::CipherKind}; +use crate::config::ServerType; // Entries for server's bloom filter // @@ -35,7 +27,7 @@ const BF_ERROR_RATE_FOR_CLIENT: f64 = 1e-15; // // It contains 2 bloom filters and each one holds 1/2 entries. // Use them as a ring buffer. -struct PingPongBloom { +pub struct PingPongBloom { blooms: [Bloom<[u8]>; 2], bloom_count: [usize; 2], item_count: usize, @@ -99,52 +91,3 @@ impl PingPongBloom { false } } - -/// A Bloom Filter based protector against replay attach -pub struct ReplayProtector { - // Check for duplicated IV/Nonce, for prevent replay attack - // https://github.com/shadowsocks/shadowsocks-org/issues/44 - nonce_ppbloom: SpinMutex, - - // AEAD 2022 specific filter. - // AEAD 2022 TCP protocol has a timestamp, which can already reject most of the replay requests, - // so we only need to remember nonce that are in the valid time range - #[cfg(feature = "aead-cipher-2022")] - nonce_set: SpinMutex, ()>>, -} - -impl ReplayProtector { - /// Create a new ReplayProtector - pub fn new(config_type: ServerType) -> ReplayProtector { - ReplayProtector { - nonce_ppbloom: SpinMutex::new(PingPongBloom::new(config_type)), - #[cfg(feature = "aead-cipher-2022")] - nonce_set: SpinMutex::new(LruCache::with_expiry_duration(Duration::from_secs( - SERVER_STREAM_TIMESTAMP_MAX_DIFF, - ))), - } - } - - /// Check if nonce exist or not - #[inline(always)] - pub fn check_nonce_and_set(&self, method: CipherKind, nonce: &[u8]) -> bool { - // Plain cipher doesn't have a nonce - // Always treated as non-duplicated - if nonce.is_empty() { - return false; - } - - #[cfg(feature = "aead-cipher-2022")] - if method.is_aead_2022() { - let mut set = self.nonce_set.lock(); - if set.get(nonce).is_some() { - return true; - } - set.insert(nonce.to_vec(), ()); - return false; - } - - let mut ppbloom = self.nonce_ppbloom.lock(); - ppbloom.check_and_set(nonce) - } -}