Skip to content

Commit

Permalink
feat: disable tls1.2 for V3 protocol; fix client data bug
Browse files Browse the repository at this point in the history
  • Loading branch information
ihciah committed Feb 12, 2023
1 parent c535a35 commit 3e6420d
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "MIT/Apache-2.0"
name = "shadow-tls"
readme = "README.md"
repository = "https://github.com/ihciah/shadow-tls"
version = "0.2.14"
version = "0.2.15"

[dependencies]
monoio = {version = "0.0.9"}
Expand Down
4 changes: 4 additions & 0 deletions docs/protocol-v3-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ In addition, I also mentioned in [this blog](https://www.ihcblog.com/a-better-tl
3. Be as weakly aware of the TLS protocol itself as possible, so implementers do not need to hack the TLS library, let alone implement the TLS protocol themselves.
4. Keep it simple: only act as a TCP flow proxy, no duplicate wheel building.

## About support for TLS 1.2
The V3 protocol only supports handshake servers that use TLS1.3. You can use `openssl s_client -tls1_3 -connect example.com:443` to probe a server for TLS1.3 support.

To support TLS1.2 would require more awareness of TLS protocol details and would be more complex to implement; given that TLS1.3 is already used by more vendors, we decided to support only TLS1.3.

# Handshake
This part of the protocol design is based on [restls](https://github.com/3andne/restls), but there are some differences: it is less aware of the details of TLS and easier to implement.
Expand Down
5 changes: 5 additions & 0 deletions docs/protocol-v3-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ V2 版本目前工作良好,在日常使用中我没有遇到被封锁等问
3. 尽可能地弱感知 TLS 协议本身,实现者无需 Hack TLS 库,更不需要自行实现 TLS 协议。
4. 保持简单:仅作为 TCP 流代理,不重复造轮子。

## 关于对 TLS 1.2 的支持
V3 协议仅支持使用 TLS1.3 的握手服务器。你可以使用 `openssl s_client -tls1_3 -connect example.com:443` 来探测一个服务器是否支持 TLS1.3。

如果要支持 TLS1.2,需要感知更多 TLS 协议细节,实现起来会更加复杂;鉴于 TLS1.3 已经有较多厂商使用,我们决定仅支持 TLS1.3。

# 握手流程
这部分协议设计借鉴 [restls](https://github.com/3andne/restls) 但存在一定差别:弱化了对 TLS 细节的感知,更易于实现。

Expand Down
18 changes: 9 additions & 9 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,21 @@ impl<LA, TA> ShadowTlsClient<LA, TA> {
// stage2:
match server_random {
None => {
tracing::warn!("traffic hijacking detected");
tracing::warn!("traffic hijacked or TLS1.3 is not supported");
let tls_stream =
monoio_rustls_fork_shadow_tls::ClientTlsStream::new(stream, session);
if let Err(e) = fake_request(tls_stream).await {
bail!("traffic hijacked, fake request fail: {e}");
bail!("traffic hijacked or TLS1.3 is not supported, fake request fail: {e}");
}
bail!("traffic hijacked, but fake request success");
bail!("traffic hijacked or TLS1.3 is not supported, but fake request success");
}
Some(sr) => {
Some((sr, hmac_sr)) => {
drop(session);
tracing::debug!("ServerRandom extracted: {sr:?}");
tracing::debug!("Authorized, ServerRandom extracted: {sr:?}");
let hmac_sr_s = Hmac::new(&self.password, (&sr, b"S"));
let hmac_sr_c = Hmac::new(&self.password, (&sr, b"C"));

verified_relay(in_stream, stream, hmac_sr_c, hmac_sr_s).await;
verified_relay(in_stream, stream, hmac_sr_c, hmac_sr_s, Some(hmac_sr)).await;
Ok(())
}
}
Expand Down Expand Up @@ -303,11 +303,12 @@ impl<S> StreamWrapper<S> {

/// Return None for unauthorized,
/// return Some(server_random) for authorized.
fn authorized(&self) -> Option<[u8; 32]> {
fn authorized(&self) -> Option<([u8; 32], Hmac)> {
if !self.read_authorized {
None
} else {
self.read_server_random
.map(|x| (x, self.read_hmac_key.as_ref().unwrap().0.to_owned()))
}
}

Expand All @@ -322,6 +323,7 @@ impl<S: AsyncReadRent> StreamWrapper<S> {

// read header
unsafe { buf.set_init(0) };
self.read_pos = 0;
buf.reserve(TLS_HEADER_SIZE);
let (res, buf) = self.raw.read_exact(buf.slice_mut(0..TLS_HEADER_SIZE)).await;
match res {
Expand Down Expand Up @@ -380,7 +382,6 @@ impl<S: AsyncReadRent> StreamWrapper<S> {
if let Some((hmac, key)) = self.read_hmac_key.as_mut() {
hmac.update(&buf[TLS_HMAC_HEADER_SIZE..]);
if hmac.finalize() == buf[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] {
tracing::debug!("app data verification success");
xor_slice(&mut buf[TLS_HMAC_HEADER_SIZE..], key);
unsafe {
copy(
Expand All @@ -405,7 +406,6 @@ impl<S: AsyncReadRent> StreamWrapper<S> {

// set buffer
let buf_len = buf.len();
self.read_pos = 0;
self.read_buf = Some(buf);
Ok(buf_len)
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ fn main() {
let mut rt = monoio::RuntimeBuilder::<monoio::FusionDriver>::new()
.enable_timer()
.build()
.expect("unable to build monoio runtime");
.expect("unable to build monoio runtime(please refer to: https://github.com/ihciah/shadow-tls/wiki/How-to-Run#common-issues)");
let _ = rt.block_on(runnable_clone.serve());
});
threads.push(t);
Expand Down
72 changes: 64 additions & 8 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
res?;
if !client_hello_pass {
// if client verify failed, bidirectional copy and return
tracing::debug!("ClientHello verify failed, will copy bidirectional");
tracing::warn!("ClientHello verify failed, will work as a SNI proxy");
copy_bidirectional(&mut in_stream, &mut handshake_stream).await;
return Ok(());
}
Expand All @@ -276,13 +276,19 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
Some(sr) => sr,
None => {
// we cannot extract server random, bidirectional copy and return
tracing::debug!("ServerRandom extract failed, will copy bidirectional");
tracing::warn!("ServerRandom extract failed, will copy bidirectional");
copy_bidirectional(&mut in_stream, &mut handshake_stream).await;
return Ok(());
}
};
tracing::debug!("ServerRandom extracted: {server_random:?}");

if !support_tls13(&first_server_frame) {
tracing::error!("TLS 1.3 is not supported, will copy bidirectional");
copy_bidirectional(&mut in_stream, &mut handshake_stream).await;
return Ok(());
}

// stage 1.3.1: create HMAC_ServerRandomC and HMAC_ServerRandom
let mut hmac_sr_c = Hmac::new(&self.password, (&server_random, b"C"));
let hmac_sr_s = Hmac::new(&self.password, (&server_random, b"S"));
Expand Down Expand Up @@ -334,7 +340,7 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
mod_tcp_conn(&mut data_stream, true, self.nodelay);
let (res, _) = data_stream.write_all(pure_data).await;
res?;
verified_relay(data_stream, in_stream, hmac_sr_s, hmac_sr_c).await;
verified_relay(data_stream, in_stream, hmac_sr_s, hmac_sr_c, None).await;
Ok(())
}
}
Expand Down Expand Up @@ -626,7 +632,7 @@ async fn extract_sni_v2<R: AsyncReadRent>(mut r: R) -> std::io::Result<(Vec<u8>,
read_ok!(cursor.skip_by_u16(), data);
continue;
}
tracing::debug!("found SNI extension");
tracing::debug!("found server_name extension");
let _ext_len = read_ok!(cursor.read_u16::<BigEndian>(), data);
let _sni_len = read_ok!(cursor.read_u16::<BigEndian>(), data);
// must be host_name
Expand All @@ -652,17 +658,18 @@ async fn read_exact_frame_into(
mut r: impl AsyncReadRent,
mut buffer: Vec<u8>,
) -> std::io::Result<Vec<u8>> {
unsafe { buffer.set_len(0) };
buffer.reserve(TLS_HEADER_SIZE);
let (res, header) = r.read_exact(buffer.slice_mut(..TLS_HEADER_SIZE)).await;
res?;
let mut buffer = header.into_inner();

// read tls frame length
let mut size: [u8; 2] = Default::default();
size.copy_from_slice(&header[3..5]);
size.copy_from_slice(&buffer[3..5]);
let data_size = u16::from_be_bytes(size) as usize;

// read tls frame body
let mut buffer = header.into_inner();
buffer.reserve(data_size);
let (res, data_slice) = r
.read_exact(buffer.slice_mut(TLS_HEADER_SIZE..TLS_HEADER_SIZE + data_size))
Expand Down Expand Up @@ -725,7 +732,7 @@ fn verified_extract_sni(frame: &[u8], password: &str) -> (bool, Option<Vec<u8>>)
read_ok!(cursor.skip_by_u16());
continue;
}
tracing::debug!("found SNI extension");
tracing::debug!("found server_name extension");
let _ext_len = read_ok!(cursor.read_u16::<BigEndian>());
let _sni_len = read_ok!(cursor.read_u16::<BigEndian>());
// must be host_name
Expand Down Expand Up @@ -773,12 +780,22 @@ async fn copy_by_frame_until_hmac_matches(
let mut g_buffer = Vec::new();

loop {
tracing::debug!("copy_by_frame_until_hmac_matches getting frame");
let buffer = read_exact_frame_into(&mut read, g_buffer).await?;
tracing::debug!("copy_by_frame_until_hmac_matches get a frame: {buffer:?}",);
if buffer.len() > 9 && buffer[0] == APPLICATION_DATA {
// check hmac
let mut tmp_hmac = hmac.to_owned();
tmp_hmac.update(&buffer[TLS_HMAC_HEADER_SIZE..]);
if buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] == tmp_hmac.finalize() {
let h = tmp_hmac.finalize();

tracing::debug!(
"tmp hmac({:?}) = {h:?}, raw = {:?}",
&buffer[TLS_HMAC_HEADER_SIZE..],
&buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE]
);

if buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] == h {
hmac.update(&buffer[TLS_HMAC_HEADER_SIZE..]);
hmac.update(&buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE]);
return Ok(buffer[TLS_HMAC_HEADER_SIZE..].to_vec());
Expand Down Expand Up @@ -812,6 +829,7 @@ async fn copy_by_frame_with_modification(
monoio::select! {
// this function can be stopped by a channel when reading.
_ = &mut stop => {
tracing::debug!("copy_by_frame_with_modification recv stop");
return Ok(());
},
buffer_res = read_exact_frame_into(&mut read, g_buffer) => {
Expand Down Expand Up @@ -845,6 +863,44 @@ async fn copy_by_frame_with_modification(
}
}

/// Parse ServerHello and return if tls1.3 is supported.
fn support_tls13(frame: &[u8]) -> bool {
if frame.len() < SESSION_ID_LEN_IDX {
return false;
}
let mut cursor = std::io::Cursor::new(&frame[SESSION_ID_LEN_IDX..]);
macro_rules! read_ok {
($res: expr) => {
match $res {
Ok(r) => r,
Err(_) => {
return false;
}
}
};
}

// skip session id
read_ok!(cursor.skip_by_u8());
// skip cipher suites
read_ok!(cursor.skip(3));
// skip ext length
let cnt = read_ok!(cursor.read_u16::<BigEndian>());

for _ in 0..cnt {
let ext_type = read_ok!(cursor.read_u16::<BigEndian>());
if ext_type != SUPPORTED_VERSIONS_TYPE {
read_ok!(cursor.skip_by_u16());
continue;
}
tracing::debug!("found supported_versions extension");
let ext_len = read_ok!(cursor.read_u16::<BigEndian>());
let ext_val = read_ok!(cursor.read_u16::<BigEndian>());
return ext_len == 2 && ext_val == TLS_13;
}
false
}

/// A helper trait for fast read and skip.
trait CursorExt {
fn read_by_u16(&mut self) -> std::io::Result<Vec<u8>>;
Expand Down
37 changes: 24 additions & 13 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ pub mod prelude {
pub const TLS_MAJOR: u8 = 0x03;
pub const TLS_MINOR: (u8, u8) = (0x03, 0x01);
pub const SNI_EXT_TYPE: u16 = 0;
pub const SUPPORTED_VERSIONS_TYPE: u16 = 43;
pub const TLS_RANDOM_SIZE: usize = 32;
pub const TLS_HEADER_SIZE: usize = 5;
pub const TLS_SESSION_ID_SIZE: usize = 32;
pub const TLS_13: u16 = 0x0304;

pub const CLIENT_HELLO: u8 = 0x01;
pub const SERVER_HELLO: u8 = 0x02;
Expand Down Expand Up @@ -119,6 +121,7 @@ pub async fn verified_relay(
mut tls: TcpStream,
mut hmac_add: Hmac,
mut hmac_verify: Hmac,
mut hmac_ignore: Option<Hmac>,
) {
tracing::debug!("verified relay started");
let (mut tls_read, mut tls_write) = tls.split();
Expand All @@ -130,6 +133,7 @@ pub async fn verified_relay(
&mut tls_read,
&mut raw_write,
&mut hmac_verify,
&mut hmac_ignore,
&mut notifier,
)
.await;
Expand All @@ -147,7 +151,8 @@ pub async fn verified_relay(
async fn copy_remove_appdata_and_verify(
read: impl AsyncReadRent,
mut write: impl AsyncWriteRent,
hmac: &mut Hmac,
hmac_verify: &mut Hmac,
hmac_ignore: &mut Option<Hmac>,
alert_notifier: &mut Receiver<()>,
) {
const INIT_BUFFER_SIZE: usize = 2048;
Expand Down Expand Up @@ -175,7 +180,18 @@ async fn copy_remove_appdata_and_verify(
return;
}
APPLICATION_DATA => {
if verify_appdata(frame, hmac) {
if let Some(hi) = hmac_ignore.as_mut() {
if verify_appdata(frame, hi, false) {
// we can ignore the data
tracing::debug!("useless data skipped");
continue;
} else {
tracing::debug!("useless data detector disabled");
hmac_ignore.take();
}
}

if verify_appdata(frame, hmac_verify, true) {
let (res, _) = write
.write_all(unsafe {
monoio::buf::RawBuf::new(
Expand All @@ -190,6 +206,7 @@ async fn copy_remove_appdata_and_verify(
return;
}
} else {
tracing::debug!("buffer hmac validate failed");
alert_notifier.close();
return;
}
Expand Down Expand Up @@ -250,22 +267,16 @@ async fn copy_add_appdata(
}
}

fn verify_appdata(frame: &[u8], hmac: &mut Hmac) -> bool {
fn verify_appdata(frame: &[u8], hmac: &mut Hmac, sep: bool) -> bool {
if frame[1] != TLS_MAJOR || frame[2] != TLS_MINOR.0 || frame.len() < TLS_HMAC_HEADER_SIZE {
return false;
}
hmac.update(&frame[TLS_HMAC_HEADER_SIZE..]);
let mut hmac_val = [0; HMAC_SIZE];
unsafe {
copy_nonoverlapping(
frame.as_ptr().add(TLS_HEADER_SIZE),
hmac_val.as_mut_ptr(),
HMAC_SIZE,
)
}
let hmac_real = hmac.finalize();
hmac.update(&hmac_real);
hmac_val == hmac_real
if sep {
hmac.update(&hmac_real);
}
frame[TLS_HEADER_SIZE..TLS_HEADER_SIZE + HMAC_SIZE] == hmac_real
}

async fn send_alert(mut w: impl AsyncWriteRent) {
Expand Down

0 comments on commit 3e6420d

Please sign in to comment.