Skip to content

Commit 4d348d9

Browse files
committed
remove AEAD-UDP nonce replay check
1 parent f97ab97 commit 4d348d9

File tree

9 files changed

+92
-191
lines changed

9 files changed

+92
-191
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ aead-cipher-extra = ["shadowsocks-service/aead-cipher-extra"]
129129
# Enable AEAD 2022
130130
aead-cipher-2022 = ["shadowsocks-service/aead-cipher-2022"]
131131

132-
# Enable detection against replay attack
132+
# Enable detection against replay attack (Stream / AEAD)
133133
security-replay-attack-detect = ["shadowsocks-service/security-replay-attack-detect"]
134134
replay-attack-detect = ["security-replay-attack-detect"] # Backward compatibility. DO NOT USE.
135135
# Enable IV printable prefix

crates/shadowsocks-service/src/local/loadbalancing/ping_balancer.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use shadowsocks::{
2323
relay::{
2424
socks5::Address,
2525
tcprelay::proxy_stream::ProxyClientStream,
26-
udprelay::{proxy_socket::ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE},
26+
udprelay::{options::UdpSocketControlData, proxy_socket::ProxySocket, MAXIMUM_UDP_PAYLOAD_SIZE},
2727
},
2828
ServerConfig,
2929
};
@@ -882,7 +882,13 @@ impl PingChecker {
882882
self.context.connect_opts_ref(),
883883
)
884884
.await?;
885-
client.send(&addr, DNS_QUERY).await?;
885+
886+
let control = UdpSocketControlData {
887+
client_session_id: rand::random::<u64>(),
888+
server_session_id: 0,
889+
packet_id: 1,
890+
};
891+
client.send_with_ctrl(&addr, &control, DNS_QUERY).await?;
886892

887893
let mut buffer = [0u8; MAXIMUM_UDP_PAYLOAD_SIZE];
888894
let (n, ..) = client.recv(&mut buffer).await?;

crates/shadowsocks-service/src/server/udprelay.rs

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -313,31 +313,16 @@ impl UdpAssociation {
313313
}
314314
}
315315

316-
struct ClientContext {
317-
packet_window_filter: PacketWindowFilter,
318-
}
319-
320316
struct ClientSessionContext {
321317
client_session_id: u64,
322-
client_context_map: LruCache<SocketAddr, ClientContext>,
318+
packet_window_filter: PacketWindowFilter,
323319
}
324320

325321
impl ClientSessionContext {
326322
fn new(client_session_id: u64) -> ClientSessionContext {
327-
// Client shouldn't be remembered too long.
328-
// If a client was switching between networks (like Wi-Fi and Cellular),
329-
// when it switched back from another, the packet filter window will be too old.
330-
const CLIENT_SESSION_REMEMBER_DURATION: Duration = Duration::from_secs(60);
331-
332-
// Wi-Fi & Cellular network device, so it is 2 for most users
333-
const CLIENT_SESSION_REMEMBER_COUNT: usize = 2;
334-
335323
ClientSessionContext {
336324
client_session_id,
337-
client_context_map: LruCache::with_expiry_duration_and_capacity(
338-
CLIENT_SESSION_REMEMBER_DURATION,
339-
CLIENT_SESSION_REMEMBER_COUNT,
340-
),
325+
packet_window_filter: PacketWindowFilter::new(),
341326
}
342327
}
343328
}
@@ -524,21 +509,10 @@ impl UdpAssociationContext {
524509
if let Some(control) = control {
525510
// Check if Packet ID is in the window
526511

527-
let session = self
512+
let session_context = self
528513
.client_session
529514
.get_or_insert_with(|| ClientSessionContext::new(control.client_session_id));
530515

531-
let session_context = session.client_context_map.entry(self.peer_addr).or_insert_with(|| {
532-
trace!(
533-
"udp client {} with session {} created",
534-
self.peer_addr,
535-
control.client_session_id
536-
);
537-
ClientContext {
538-
packet_window_filter: PacketWindowFilter::new(),
539-
}
540-
});
541-
542516
let packet_id = control.packet_id;
543517
if !session_context
544518
.packet_window_filter

crates/shadowsocks/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ stream-cipher = ["shadowsocks-crypto/v1-stream"]
3232
aead-cipher-extra = ["shadowsocks-crypto/v1-aead-extra"]
3333

3434
# Enable AEAD 2022
35-
aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache", "spin"]
35+
aead-cipher-2022 = ["shadowsocks-crypto/v2", "rand/small_rng", "aes", "lru_time_cache"]
3636

3737
# Enable detection against replay attack
38-
security-replay-attack-detect = ["bloomfilter", "spin"]
38+
security-replay-attack-detect = ["bloomfilter"]
3939
# Enable IV printable prefix
4040
security-iv-printable-prefix = ["rand"]
4141

@@ -54,7 +54,7 @@ byte_string = "1.0"
5454
base64 = "0.13"
5555
url = "2.2"
5656
once_cell = "1.8"
57-
spin = { version = "0.9", features = ["std"], optional = true }
57+
spin = { version = "0.9", features = ["std"] }
5858
pin-project = "1.0"
5959
bloomfilter = { version = "1.0.8", optional = true }
6060
thiserror = "1.0"

crates/shadowsocks/src/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl Context {
3535
pub fn new(config_type: ServerType) -> Context {
3636
Context {
3737
replay_protector: ReplayProtector::new(config_type),
38-
replay_policy: ReplayAttackPolicy::Ignore,
38+
replay_policy: ReplayAttackPolicy::Reject,
3939
dns_resolver: Arc::new(DnsResolver::system_resolver()),
4040
ipv6_first: false,
4141
}

crates/shadowsocks/src/relay/udprelay/aead_2022.rs

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,10 @@ use aes::{
6363
};
6464
use byte_string::ByteStr;
6565
use bytes::{Buf, BufMut, BytesMut};
66-
use log::{error, trace, warn};
66+
use log::trace;
6767
use lru_time_cache::LruCache;
68-
use once_cell::sync::Lazy;
69-
use spin::Mutex as SpinMutex;
7068

7169
use crate::{
72-
config::ReplayAttackPolicy,
7370
context::Context,
7471
crypto::{
7572
v2::udp::{ChaCha20Poly1305Cipher, UdpCipher},
@@ -159,57 +156,6 @@ fn get_cipher(method: CipherKind, key: &[u8], session_id: u64) -> Rc<UdpCipher>
159156
})
160157
}
161158

162-
fn check_and_record_nonce(method: CipherKind, key: &[u8], session_id: u64, nonce: &[u8]) -> bool {
163-
static REPLAY_FILTER_RECORDER: Lazy<SpinMutex<LruCache<CipherKey, LruCache<Vec<u8>, ()>>>> = Lazy::new(|| {
164-
SpinMutex::new(LruCache::with_expiry_duration_and_capacity(
165-
CIPHER_CACHE_DURATION,
166-
CIPHER_CACHE_LIMIT,
167-
))
168-
});
169-
170-
let cache_key = CipherKey {
171-
method,
172-
// The key is stored in ServerConfig structure, so the address of it won't change.
173-
key: key.as_ptr() as usize,
174-
session_id,
175-
};
176-
177-
const REPLAY_DETECT_NONCE_EXPIRE_DURATION: Duration = Duration::from_secs(SERVER_PACKET_TIMESTAMP_MAX_DIFF);
178-
179-
let mut session_map = REPLAY_FILTER_RECORDER.lock();
180-
181-
let session_nonce_map = session_map
182-
.entry(cache_key)
183-
.or_insert_with(|| LruCache::with_expiry_duration(REPLAY_DETECT_NONCE_EXPIRE_DURATION));
184-
185-
if session_nonce_map.get(nonce).is_some() {
186-
return true;
187-
}
188-
189-
session_nonce_map.insert(nonce.to_vec(), ());
190-
false
191-
}
192-
193-
#[inline]
194-
fn check_nonce_replay(context: &Context, method: CipherKind, key: &[u8], session_id: u64, nonce: &[u8]) -> bool {
195-
match context.replay_attack_policy() {
196-
ReplayAttackPolicy::Ignore => false,
197-
ReplayAttackPolicy::Detect => {
198-
if check_and_record_nonce(method, key, session_id, nonce) {
199-
warn!("detected repeated nonce salt {:?}", ByteStr::new(nonce));
200-
}
201-
false
202-
}
203-
ReplayAttackPolicy::Reject => {
204-
let replayed = check_and_record_nonce(method, key, session_id, nonce);
205-
if replayed {
206-
error!("detected repeated nonce salt {:?}", ByteStr::new(nonce));
207-
}
208-
replayed
209-
}
210-
}
211-
}
212-
213159
fn encrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &mut BytesMut, session_id: u64) {
214160
unsafe {
215161
packet.advance_mut(method.tag_len());
@@ -255,7 +201,7 @@ fn encrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &
255201
}
256202
}
257203

258-
fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &mut [u8]) -> bool {
204+
fn decrypt_message(_context: &Context, method: CipherKind, key: &[u8], packet: &mut [u8]) -> bool {
259205
match method {
260206
CipherKind::AEAD2022_BLAKE3_CHACHA20_POLY1305 => {
261207
// 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
272218
u64::from_be(session_id_slice[0])
273219
};
274220

275-
if check_nonce_replay(context, method, key, session_id, nonce) {
276-
error!("detected replayed nonce: {:?}", ByteStr::new(nonce));
277-
return false;
278-
}
279-
280221
let cipher = get_cipher(method, key, session_id);
281222

282223
if !cipher.decrypt_packet(nonce, message) {
@@ -316,14 +257,7 @@ fn decrypt_message(context: &Context, method: CipherKind, key: &[u8], packet: &m
316257

317258
let nonce = &packet_header[4..16];
318259

319-
let cipher = {
320-
if check_nonce_replay(context, method, key, session_id, nonce) {
321-
error!("detected replayed nonce: {:?}", ByteStr::new(nonce));
322-
return false;
323-
}
324-
325-
get_cipher(method, key, session_id)
326-
};
260+
let cipher = get_cipher(method, key, session_id);
327261

328262
if !cipher.decrypt_packet(nonce, message) {
329263
return false;

crates/shadowsocks/src/security/replay/dummy.rs

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,75 @@
1+
#[cfg(feature = "aead-cipher-2022")]
2+
use std::time::Duration;
3+
14
use cfg_if::cfg_if;
5+
#[cfg(feature = "aead-cipher-2022")]
6+
use lru_time_cache::LruCache;
7+
#[allow(dead_code)]
8+
use spin::Mutex as SpinMutex;
9+
10+
#[cfg(feature = "aead-cipher-2022")]
11+
use crate::relay::tcprelay::proxy_stream::protocol::v2::SERVER_STREAM_TIMESTAMP_MAX_DIFF;
12+
use crate::{config::ServerType, crypto::CipherKind};
13+
14+
#[cfg(feature = "security-replay-attack-detect")]
15+
use self::ppbloom::PingPongBloom;
16+
17+
#[cfg(feature = "security-replay-attack-detect")]
18+
mod ppbloom;
19+
20+
/// A Bloom Filter based protector against replay attach
21+
pub struct ReplayProtector {
22+
// Check for duplicated IV/Nonce, for prevent replay attack
23+
// https://github.com/shadowsocks/shadowsocks-org/issues/44
24+
#[cfg(feature = "security-replay-attack-detect")]
25+
nonce_ppbloom: SpinMutex<PingPongBloom>,
26+
27+
// AEAD 2022 specific filter.
28+
// AEAD 2022 TCP protocol has a timestamp, which can already reject most of the replay requests,
29+
// so we only need to remember nonce that are in the valid time range
30+
#[cfg(feature = "aead-cipher-2022")]
31+
nonce_set: SpinMutex<LruCache<Vec<u8>, ()>>,
32+
}
33+
34+
impl ReplayProtector {
35+
/// Create a new ReplayProtector
36+
pub fn new(config_type: ServerType) -> ReplayProtector {
37+
ReplayProtector {
38+
#[cfg(feature = "security-replay-attack-detect")]
39+
nonce_ppbloom: SpinMutex::new(PingPongBloom::new(config_type)),
40+
#[cfg(feature = "aead-cipher-2022")]
41+
nonce_set: SpinMutex::new(LruCache::with_expiry_duration(Duration::from_secs(
42+
SERVER_STREAM_TIMESTAMP_MAX_DIFF,
43+
))),
44+
}
45+
}
46+
47+
/// Check if nonce exist or not
48+
#[inline(always)]
49+
pub fn check_nonce_and_set(&self, method: CipherKind, nonce: &[u8]) -> bool {
50+
// Plain cipher doesn't have a nonce
51+
// Always treated as non-duplicated
52+
if nonce.is_empty() {
53+
return false;
54+
}
55+
56+
#[cfg(feature = "aead-cipher-2022")]
57+
if method.is_aead_2022() {
58+
let mut set = self.nonce_set.lock();
59+
if set.get(nonce).is_some() {
60+
return true;
61+
}
62+
set.insert(nonce.to_vec(), ());
63+
return false;
64+
}
265

3-
cfg_if! {
4-
if #[cfg(feature = "security-replay-attack-detect")] {
5-
mod ppbloom;
6-
pub use self::ppbloom::*;
7-
} else {
8-
mod dummy;
9-
pub use self::dummy::*;
66+
cfg_if! {
67+
if #[cfg(feature = "security-replay-attack-detect")] {
68+
let mut ppbloom = self.nonce_ppbloom.lock();
69+
ppbloom.check_and_set(nonce)
70+
} else {
71+
false
72+
}
73+
}
1074
}
1175
}

0 commit comments

Comments
 (0)