diff --git a/.gitignore b/.gitignore index 1e9a6131..6e6dbd43 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /udp_echo_server.py /.idea /.devcontainer +.DS_Store diff --git a/crates/shadowsocks/Cargo.toml b/crates/shadowsocks/Cargo.toml index c7f4e18c..bef11d42 100644 --- a/crates/shadowsocks/Cargo.toml +++ b/crates/shadowsocks/Cargo.toml @@ -32,7 +32,7 @@ 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"] +aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache", "spin"] # Enable detection against replay attack security-replay-attack-detect = ["bloomfilter", "spin"] diff --git a/crates/shadowsocks/src/context.rs b/crates/shadowsocks/src/context.rs index 586e04d0..d1d43172 100644 --- a/crates/shadowsocks/src/context.rs +++ b/crates/shadowsocks/src/context.rs @@ -7,7 +7,7 @@ use log::warn; use crate::{ config::{ReplayAttackPolicy, ServerType}, - crypto::v1::random_iv_or_salt, + crypto::{v1::random_iv_or_salt, CipherKind}, dns_resolver::DnsResolver, security::replay::ReplayProtector, }; @@ -50,15 +50,15 @@ impl Context { /// /// If not, set into the current bloom filter #[inline(always)] - fn check_nonce_and_set(&self, nonce: &[u8]) -> bool { + fn check_nonce_and_set(&self, method: CipherKind, nonce: &[u8]) -> bool { match self.replay_policy { ReplayAttackPolicy::Ignore => false, - _ => self.replay_protector.check_nonce_and_set(nonce), + _ => self.replay_protector.check_nonce_and_set(method, nonce), } } /// Generate nonce (IV or SALT) - pub fn generate_nonce(&self, nonce: &mut [u8], unique: bool) { + pub fn generate_nonce(&self, method: CipherKind, nonce: &mut [u8], unique: bool) { if nonce.is_empty() { return; } @@ -86,7 +86,7 @@ impl Context { } // Salt already exists, generate a new one. - if unique && self.check_nonce_and_set(nonce) { + if unique && self.check_nonce_and_set(method, nonce) { continue; } @@ -95,7 +95,7 @@ impl Context { } /// Check nonce replay - pub fn check_nonce_replay(&self, nonce: &[u8]) -> io::Result<()> { + pub fn check_nonce_replay(&self, method: CipherKind, nonce: &[u8]) -> io::Result<()> { if nonce.is_empty() { return Ok(()); } @@ -103,13 +103,13 @@ impl Context { match self.replay_policy { ReplayAttackPolicy::Ignore => Ok(()), ReplayAttackPolicy::Detect => { - if self.replay_protector.check_nonce_and_set(nonce) { + if self.replay_protector.check_nonce_and_set(method, nonce) { warn!("detected repeated nonce (iv/salt) {:?}", ByteStr::new(nonce)); } Ok(()) } ReplayAttackPolicy::Reject => { - if self.replay_protector.check_nonce_and_set(nonce) { + if self.replay_protector.check_nonce_and_set(method, nonce) { let err = io::Error::new(io::ErrorKind::Other, "detected repeated nonce (iv/salt)"); Err(err) } else { @@ -151,4 +151,9 @@ impl Context { pub fn set_replay_attack_policy(&mut self, replay_policy: ReplayAttackPolicy) { self.replay_policy = replay_policy; } + + /// Get policy against replay attach + pub fn replay_attack_policy(&self) -> ReplayAttackPolicy { + self.replay_policy + } } diff --git a/crates/shadowsocks/src/relay/mod.rs b/crates/shadowsocks/src/relay/mod.rs index cac1b18c..52b05a84 100644 --- a/crates/shadowsocks/src/relay/mod.rs +++ b/crates/shadowsocks/src/relay/mod.rs @@ -5,3 +5,25 @@ pub use self::socks5::Address; pub mod socks5; pub mod tcprelay; pub mod udprelay; + +/// AEAD 2022 maximum padding length +#[cfg(feature = "aead-cipher-2022")] +const AEAD2022_MAX_PADDING_SIZE: usize = 900; + +/// Get a properly AEAD 2022 padding size according to payload's length +#[cfg(feature = "aead-cipher-2022")] +fn get_aead_2022_padding_size(payload: &[u8]) -> usize { + use std::cell::RefCell; + + use rand::{rngs::SmallRng, Rng, SeedableRng}; + + thread_local! { + static PADDING_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); + } + + if payload.is_empty() { + PADDING_RNG.with(|rng| rng.borrow_mut().gen::() % AEAD2022_MAX_PADDING_SIZE) + } else { + 0 + } +} diff --git a/crates/shadowsocks/src/relay/tcprelay/aead.rs b/crates/shadowsocks/src/relay/tcprelay/aead.rs index 6dc3461d..423af8dd 100644 --- a/crates/shadowsocks/src/relay/tcprelay/aead.rs +++ b/crates/shadowsocks/src/relay/tcprelay/aead.rs @@ -224,7 +224,7 @@ impl DecryptedReader { // Check repeated salt after first successful decryption #442 if let Some(ref salt) = self.salt { - context.check_nonce_replay(salt)?; + context.check_nonce_replay(self.method, salt)?; } // Remote TAG diff --git a/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs b/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs index ce78dce0..40b03f8e 100644 --- a/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs +++ b/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs @@ -222,7 +222,7 @@ impl DecryptedReader { // Check repeated salt after first successful decryption #442 if let Some(ref salt) = self.salt { - context.check_nonce_replay(salt)?; + context.check_nonce_replay(self.method, salt)?; } // Remote TAG diff --git a/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs b/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs index e31a8e7c..cd010bd3 100644 --- a/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs +++ b/crates/shadowsocks/src/relay/tcprelay/crypto_io.rs @@ -169,13 +169,13 @@ impl CryptoStream { #[cfg(feature = "stream-cipher")] CipherCategory::Stream => { let mut local_iv = vec![0u8; prev_len]; - context.generate_nonce(&mut local_iv, true); + context.generate_nonce(method, &mut local_iv, true); trace!("generated Stream cipher IV {:?}", ByteStr::new(&local_iv)); local_iv } CipherCategory::Aead => { let mut local_salt = vec![0u8; prev_len]; - context.generate_nonce(&mut local_salt, true); + context.generate_nonce(method, &mut local_salt, true); trace!("generated AEAD cipher salt {:?}", ByteStr::new(&local_salt)); local_salt } @@ -183,7 +183,7 @@ impl CryptoStream { #[cfg(feature = "aead-cipher-2022")] CipherCategory::Aead2022 => { let mut local_salt = vec![0u8; prev_len]; - context.generate_nonce(&mut local_salt, true); + context.generate_nonce(method, &mut local_salt, true); trace!("generated AEAD cipher salt {:?}", ByteStr::new(&local_salt)); local_salt } diff --git a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs index d77a9e1d..2bd85a8c 100644 --- a/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs +++ b/crates/shadowsocks/src/relay/tcprelay/proxy_stream/client.rs @@ -16,8 +16,6 @@ use tokio::{ time, }; -#[cfg(feature = "aead-cipher-2022")] -use crate::context::Context; use crate::{ config::ServerConfig, context::SharedContext, @@ -28,6 +26,8 @@ use crate::{ tcprelay::crypto_io::{CryptoRead, CryptoStream, CryptoWrite}, }, }; +#[cfg(feature = "aead-cipher-2022")] +use crate::{context::Context, relay::get_aead_2022_padding_size}; enum ProxyClientStreamWriteState { Connect(Address), @@ -197,8 +197,8 @@ fn poll_read_aead_2022_header( where S: AsyncRead + AsyncWrite + Unpin, { + use super::protocol::v2::{get_now_timestamp, Aead2022TcpStreamType, SERVER_STREAM_TIMESTAMP_MAX_DIFF}; use bytes::Buf; - use std::time::SystemTime; // AEAD 2022 TCP Response Header // @@ -208,9 +208,6 @@ where // | Request SALT (Variable ...) // +-------+-------+-------+-------+-------+-------+-------+-------+-------+ - const SERVER_STREAM_TYPE: u8 = 1; - const SERVER_STREAM_TIMESTAMP_MAX_DIFF: u64 = 30; - // Initialize buffer let method = stream.method(); if header_buf.is_empty() { @@ -230,7 +227,7 @@ where // Done reading TCP header, check all the fields let stream_type = header_buf.get_u8(); - if stream_type != SERVER_STREAM_TYPE { + if stream_type != Aead2022TcpStreamType::Server as u8 { return Err(io::Error::new( ErrorKind::Other, format!("received TCP response header with wrong type {}", stream_type), @@ -239,10 +236,7 @@ where } let timestamp = header_buf.get_u64(); - let now = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(n) => n.as_secs(), - Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), - }; + let now = get_now_timestamp(); if now.abs_diff(timestamp) > SERVER_STREAM_TIMESTAMP_MAX_DIFF { return Err(io::Error::new( @@ -317,30 +311,14 @@ fn make_first_packet_buffer(method: CipherKind, addr: &Address, buf: &[u8]) -> B // // Client -> Server TYPE=0 - use rand::{rngs::SmallRng, Rng, SeedableRng}; - use std::{cell::RefCell, time::SystemTime}; + use super::protocol::v2::{get_now_timestamp, Aead2022TcpStreamType}; - const CLIENT_STREAM_TYPE: u8 = 0; - const MAX_PADDING_SIZE: usize = 900; - - thread_local! { - static PADDING_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); - } - - let padding_size = if buf.is_empty() { - PADDING_RNG.with(|rng| rng.borrow_mut().gen::() % MAX_PADDING_SIZE) - } else { - // If handshake with data buffer, then padding is not required and should be 0 for letting TFO work properly. - 0 - }; + let padding_size = get_aead_2022_padding_size(buf); buffer.reserve(1 + 8 + addr_length + 2 + padding_size); - buffer.put_u8(CLIENT_STREAM_TYPE); + buffer.put_u8(Aead2022TcpStreamType::Client as u8); - let timestamp = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { - Ok(n) => n.as_secs(), - Err(_) => panic!("SystemTime::now() is before UNIX Epoch!"), - }; + let timestamp = get_now_timestamp(); buffer.put_u64(timestamp); addr.write_to_buf(&mut buffer); diff --git a/crates/shadowsocks/src/relay/tcprelay/stream.rs b/crates/shadowsocks/src/relay/tcprelay/stream.rs index e91f3d7d..bad6d240 100644 --- a/crates/shadowsocks/src/relay/tcprelay/stream.rs +++ b/crates/shadowsocks/src/relay/tcprelay/stream.rs @@ -119,7 +119,7 @@ impl DecryptedReader { } let iv = &self.buffer[..iv_len]; - context.check_nonce_replay(iv)?; + context.check_nonce_replay(self.method, iv)?; trace!("got stream iv {:?}", ByteStr::new(iv)); diff --git a/crates/shadowsocks/src/relay/udprelay/aead.rs b/crates/shadowsocks/src/relay/udprelay/aead.rs index fd16be20..367e245a 100644 --- a/crates/shadowsocks/src/relay/udprelay/aead.rs +++ b/crates/shadowsocks/src/relay/udprelay/aead.rs @@ -43,7 +43,7 @@ pub fn encrypt_payload_aead( let salt = &mut dst[..salt_len]; if salt_len > 0 { - context.generate_nonce(salt, false); + context.generate_nonce(method, salt, false); trace!("UDP packet generated aead salt {:?}", ByteStr::new(salt)); } diff --git a/crates/shadowsocks/src/relay/udprelay/aead_2022.rs b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs index 1a8335d2..379199d5 100644 --- a/crates/shadowsocks/src/relay/udprelay/aead_2022.rs +++ b/crates/shadowsocks/src/relay/udprelay/aead_2022.rs @@ -63,24 +63,25 @@ use aes::{ }; use byte_string::ByteStr; use bytes::{Buf, BufMut, BytesMut}; -use log::{error, trace}; +use log::{error, trace, warn}; use lru_time_cache::LruCache; -use rand::{rngs::SmallRng, Rng, SeedableRng}; +use once_cell::sync::Lazy; +use spin::Mutex as SpinMutex; use crate::{ + config::ReplayAttackPolicy, context::Context, crypto::{ v2::udp::{ChaCha20Poly1305Cipher, UdpCipher}, CipherKind, }, - relay::socks5::Address, + relay::{get_aead_2022_padding_size, socks5::Address}, }; use super::options::UdpSocketControlData; const CLIENT_SOCKET_TYPE: u8 = 0; const SERVER_SOCKET_TYPE: u8 = 1; -const MAX_PADDING_SIZE: usize = 900; const SERVER_PACKET_TIMESTAMP_MAX_DIFF: u64 = 30; #[derive(PartialEq, Eq, Hash, Clone, Debug)] @@ -124,19 +125,12 @@ impl Ord for CipherKey { } } -thread_local! { - static PADDING_RNG: RefCell = RefCell::new(SmallRng::from_entropy()); - static CIPHER_CACHE: RefCell>> = - RefCell::new(LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60), 102400)); -} +const CIPHER_CACHE_DURATION: Duration = Duration::from_secs(30); +const CIPHER_CACHE_LIMIT: usize = 102400; -#[inline] -fn get_padding_size(payload: &[u8]) -> usize { - if payload.is_empty() { - PADDING_RNG.with(|rng| rng.borrow_mut().gen::() % MAX_PADDING_SIZE) - } else { - 0 - } +thread_local! { + static CIPHER_CACHE: RefCell>> = + RefCell::new(LruCache::with_expiry_duration_and_capacity(CIPHER_CACHE_DURATION, CIPHER_CACHE_LIMIT)); } #[inline] @@ -165,6 +159,57 @@ 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()); @@ -217,10 +262,6 @@ fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &m let nonce_size = ChaCha20Poly1305Cipher::nonce_size(); let (nonce, message) = packet.split_at_mut(nonce_size); - if let Err(..) = context.check_nonce_replay(nonce) { - error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); - return false; - } // NOTE: ChaCha20-Poly1305's session_id is not required because it uses PSK directly // @@ -231,6 +272,11 @@ 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) { @@ -271,7 +317,7 @@ fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &m let nonce = &packet_header[4..16]; let cipher = { - if let Err(..) = context.check_nonce_replay(nonce) { + if check_nonce_replay(context, method, key, session_id, nonce) { error!("detected replayed nonce: {:?}", ByteStr::new(nonce)); return false; } @@ -308,7 +354,7 @@ pub fn encrypt_client_payload_aead_2022( payload: &[u8], dst: &mut BytesMut, ) { - let padding_size = get_padding_size(payload); + let padding_size = get_aead_2022_padding_size(payload); let nonce_size = get_nonce_len(method); dst.reserve( @@ -322,7 +368,7 @@ pub fn encrypt_client_payload_aead_2022( } let nonce = &mut dst[..nonce_size]; - context.generate_nonce(nonce, false); + context.generate_nonce(method, nonce, false); trace!("UDP packet generated aead nonce {:?}", ByteStr::new(nonce)); } @@ -414,7 +460,7 @@ pub fn encrypt_server_payload_aead_2022( payload: &[u8], dst: &mut BytesMut, ) { - let padding_size = get_padding_size(payload); + let padding_size = get_aead_2022_padding_size(payload); let nonce_size = get_nonce_len(method); dst.reserve( @@ -428,7 +474,7 @@ pub fn encrypt_server_payload_aead_2022( } let nonce = &mut dst[..nonce_size]; - context.generate_nonce(nonce, false); + context.generate_nonce(method, nonce, false); trace!("UDP packet generated aead nonce {:?}", ByteStr::new(nonce)); } diff --git a/crates/shadowsocks/src/relay/udprelay/stream.rs b/crates/shadowsocks/src/relay/udprelay/stream.rs index 34c74119..75834c81 100644 --- a/crates/shadowsocks/src/relay/udprelay/stream.rs +++ b/crates/shadowsocks/src/relay/udprelay/stream.rs @@ -41,7 +41,7 @@ pub fn encrypt_payload_stream( let iv = &mut dst[..iv_len]; if iv_len > 0 { - context.generate_nonce(iv, false); + context.generate_nonce(method, iv, false); trace!("UDP packet generated stream iv {:?}", ByteStr::new(iv)); } diff --git a/crates/shadowsocks/src/security/replay/dummy.rs b/crates/shadowsocks/src/security/replay/dummy.rs index 9439b814..4d21ff09 100644 --- a/crates/shadowsocks/src/security/replay/dummy.rs +++ b/crates/shadowsocks/src/security/replay/dummy.rs @@ -1,4 +1,4 @@ -use crate::config::ServerType; +use crate::{config::ServerType, crypto::CipherKind}; /// A dummy protector against replay attack /// @@ -14,7 +14,7 @@ impl ReplayProtector { /// Check if nonce exist or not #[inline(always)] - pub fn check_nonce_and_set(&self, _nonce: &[u8]) -> bool { + pub fn check_nonce_and_set(&self, _method: CipherKind, _nonce: &[u8]) -> bool { false } } diff --git a/crates/shadowsocks/src/security/replay/ppbloom.rs b/crates/shadowsocks/src/security/replay/ppbloom.rs index 51672d06..0a15a49b 100644 --- a/crates/shadowsocks/src/security/replay/ppbloom.rs +++ b/crates/shadowsocks/src/security/replay/ppbloom.rs @@ -1,8 +1,15 @@ +#[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; -use crate::config::ServerType; +#[cfg(feature = "aead-cipher-2022")] +use crate::relay::tcprelay::proxy_stream::protocol::v2::SERVER_STREAM_TIMESTAMP_MAX_DIFF; +use crate::{config::ServerType, crypto::CipherKind}; // Entries for server's bloom filter // @@ -98,6 +105,12 @@ 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 { @@ -105,18 +118,32 @@ impl 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, nonce: &[u8]) -> bool { + 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) }