Skip to content

Commit

Permalink
remove AEAD-UDP nonce replay check
Browse files Browse the repository at this point in the history
  • Loading branch information
zonyitoo committed Apr 19, 2022
1 parent f97ab97 commit e1b5518
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 191 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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::<u64>(),
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?;
Expand Down
32 changes: 3 additions & 29 deletions crates/shadowsocks-service/src/server/udprelay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,31 +313,16 @@ impl UdpAssociation {
}
}

struct ClientContext {
packet_window_filter: PacketWindowFilter,
}

struct ClientSessionContext {
client_session_id: u64,
client_context_map: LruCache<SocketAddr, ClientContext>,
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(),
}
}
}
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions crates/shadowsocks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/shadowsocks/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
72 changes: 3 additions & 69 deletions crates/shadowsocks/src/relay/udprelay/aead_2022.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -159,57 +156,6 @@ 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());
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 0 additions & 20 deletions crates/shadowsocks/src/security/replay/dummy.rs

This file was deleted.

79 changes: 72 additions & 7 deletions crates/shadowsocks/src/security/replay/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,76 @@
#[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<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 {
/// Create a new ReplayProtector
#[allow(unused_variables)]
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
}
}
}
}
Loading

0 comments on commit e1b5518

Please sign in to comment.