replay filter for TCP & UDP in AEAD-2022

This commit is contained in:
zonyitoo
2022-04-20 00:10:53 +08:00
parent 50d2a01289
commit c90da4e340
14 changed files with 156 additions and 77 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@
/udp_echo_server.py
/.idea
/.devcontainer
.DS_Store

View File

@@ -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"]

View File

@@ -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
}
}

View File

@@ -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<SmallRng> = RefCell::new(SmallRng::from_entropy());
}
if payload.is_empty() {
PADDING_RNG.with(|rng| rng.borrow_mut().gen::<usize>() % AEAD2022_MAX_PADDING_SIZE)
} else {
0
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -169,13 +169,13 @@ impl<S> CryptoStream<S> {
#[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<S> CryptoStream<S> {
#[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
}

View File

@@ -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<S>(
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<SmallRng> = RefCell::new(SmallRng::from_entropy());
}
let padding_size = if buf.is_empty() {
PADDING_RNG.with(|rng| rng.borrow_mut().gen::<usize>() % 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);

View File

@@ -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));

View File

@@ -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));
}

View File

@@ -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<SmallRng> = RefCell::new(SmallRng::from_entropy());
static CIPHER_CACHE: RefCell<LruCache<CipherKey, Rc<UdpCipher>>> =
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::<usize>() % MAX_PADDING_SIZE)
} else {
0
}
thread_local! {
static CIPHER_CACHE: RefCell<LruCache<CipherKey, Rc<UdpCipher>>> =
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<UdpCipher>
})
}
fn check_and_record_nonce(method: CipherKind, key: &[u8], session_id: u64, nonce: &[u8]) -> bool {
static REPLAY_FILTER_RECORDER: Lazy<SpinMutex<LruCache<CipherKey, LruCache<Vec<u8>, ()>>>> = 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));
}

View File

@@ -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));
}

View File

@@ -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
}
}

View File

@@ -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<PingPongBloom>,
// 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<LruCache<Vec<u8>, ()>>,
}
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)
}