Skip to content

Commit

Permalink
feat: allow tls1.2 for v3 protocol non-strict mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ihciah committed Feb 14, 2023
1 parent ffe5000 commit 3467ad8
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 55 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.15"
version = "0.2.16"

[dependencies]
monoio = {version = "0.0.9"}
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ENV PASSWORD=""
ENV ALPN=""
ENV DISABLE_NODELAY=""
ENV V3=""
ENV STRICT=""

COPY ./entrypoint.sh /
RUN chmod +x /entrypoint.sh && apk add --no-cache ca-certificates
Expand Down
6 changes: 4 additions & 2 deletions docs/protocol-v3-en.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ In addition, I also mentioned in [this blog](https://www.ihcblog.com/a-better-tl
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.
The V3 protocol only supports handshake servers using TLS1.3 in strict mode. You can use `openssl s_client -tls1_3 -connect example.com:443` to detect whether a server supports TLS1.3.

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.
If you want to support TLS1.2, you need to perceive more details of the TLS protocol, and the implementation will be more complicated; since TLS1.3 is already used by many manufacturers, we decided to only support TLS1.3 in strict mode.

Considering compatibility and some scenarios that require less protection against connection hijacking (such as using a specific SNI to bypass the billing system), TLS1.2 is allowed in non-strict mode.

# 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
6 changes: 4 additions & 2 deletions docs/protocol-v3-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ V2 版本目前工作良好,在日常使用中我没有遇到被封锁等问
4. 保持简单:仅作为 TCP 流代理,不重复造轮子。

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

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

考虑到兼容性和部分对防御连接劫持需求较低的场景(如使用特定 SNI 绕过计费系统),在非严格模式下允许使用 TLS1.2。

# 握手流程
这部分协议设计借鉴 [restls](https://github.com/3andne/restls) 但存在一定差别:弱化了对 TLS 细节的感知,更易于实现。
Expand Down
5 changes: 5 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ then
parameter="$parameter --v3"
fi

if [ ! -z "$STRICT" ]
then
parameter="$parameter --strict"
fi

if [ "$MODE" = "server" ]
then
parameter="$parameter $MODE"
Expand Down
86 changes: 47 additions & 39 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use rustls_fork_shadow_tls::{OwnedTrustAnchor, RootCertStore, ServerName};

use crate::{
helper_v2::{copy_with_application_data, copy_without_application_data, HashedReadStream},
util::{kdf, mod_tcp_conn, prelude::*, verified_relay, xor_slice, Hmac},
util::{kdf, mod_tcp_conn, prelude::*, verified_relay, xor_slice, Hmac, V3Mode},
};

const FAKE_REQUEST_LENGTH_RANGE: (usize, usize) = (16, 64);
Expand All @@ -31,7 +31,7 @@ pub struct ShadowTlsClient<LA, TA> {
tls_names: Arc<TlsNames>,
password: Arc<String>,
nodelay: bool,
v3: bool,
v3: V3Mode,
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -116,7 +116,7 @@ impl<LA, TA> ShadowTlsClient<LA, TA> {
tls_ext_config: TlsExtConfig,
password: String,
nodelay: bool,
v3: bool,
v3: V3Mode,
) -> anyhow::Result<Self> {
let mut root_store = RootCertStore::empty();
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
Expand Down Expand Up @@ -166,7 +166,7 @@ impl<LA, TA> ShadowTlsClient<LA, TA> {
let client = shared.clone();
mod_tcp_conn(&mut conn, true, shared.nodelay);
monoio::spawn(async move {
let _ = match client.v3 {
let _ = match client.v3.enabled() {
false => client.relay_v2(conn).await,
true => client.relay_v3(conn).await,
};
Expand Down Expand Up @@ -220,30 +220,31 @@ impl<LA, TA> ShadowTlsClient<LA, TA> {
.await?;
tracing::debug!("handshake success");
let (stream, session) = tls_stream.into_parts();
let server_random = stream.authorized();
let authorized = stream.authorized();
let maybe_srh = stream
.state()
.as_ref()
.map(|s| (s.server_random, s.hmac.to_owned()));
let stream = stream.into_inner();

// stage2:
match server_random {
None => {
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 or TLS1.3 is not supported, fake request fail: {e}");
}
bail!("traffic hijacked or TLS1.3 is not supported, but fake request success");
}
Some((sr, hmac_sr)) => {
drop(session);
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, Some(hmac_sr)).await;
Ok(())
if maybe_srh.is_none() || !authorized && self.v3.strict() {
tracing::warn!("V3 strict enabled: 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 or TLS1.3 is not supported, fake request fail: {e}");
}
bail!("traffic hijacked or TLS1.3 is not supported, but fake request success");
}

drop(session);
let (sr, hmac_sr) = maybe_srh.unwrap();
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, Some(hmac_sr)).await;
Ok(())
}

/// Connect remote, do handshaking and calculate HMAC.
Expand Down Expand Up @@ -282,11 +283,18 @@ struct StreamWrapper<S> {

read_buf: Option<Vec<u8>>,
read_pos: usize,
read_server_random: Option<[u8; TLS_RANDOM_SIZE]>,
read_hmac_key: Option<(Hmac, Vec<u8>)>,

read_state: Option<State>,
read_authorized: bool,
}

#[derive(Clone)]
struct State {
server_random: [u8; TLS_RANDOM_SIZE],
hmac: Hmac,
key: Vec<u8>,
}

impl<S> StreamWrapper<S> {
fn new(raw: S, password: &str) -> Self {
Self {
Expand All @@ -295,21 +303,18 @@ impl<S> StreamWrapper<S> {

read_buf: Some(Vec::new()),
read_pos: 0,
read_server_random: None,
read_hmac_key: None,

read_state: None,
read_authorized: false,
}
}

/// Return None for unauthorized,
/// return Some(server_random) for authorized.
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()))
}
fn authorized(&self) -> bool {
self.read_authorized
}

fn state(&self) -> &Option<State> {
&self.read_state
}

fn into_inner(self) -> S {
Expand Down Expand Up @@ -370,16 +375,19 @@ impl<S: AsyncReadRent> StreamWrapper<S> {
)
}
tracing::debug!("ServerRandom extracted: {server_random:?}");
self.read_server_random = Some(server_random);
let hmac = Hmac::new(&self.password, (&server_random, &[]));
let key = kdf(&self.password, &server_random);
self.read_hmac_key = Some((hmac, key));
self.read_state = Some(State {
server_random,
hmac,
key,
});
}
}
APPLICATION_DATA => {
self.read_authorized = false;
if buf.len() > TLS_HMAC_HEADER_SIZE {
if let Some((hmac, key)) = self.read_hmac_key.as_mut() {
if let Some(State { hmac, key, .. }) = self.read_state.as_mut() {
hmac.update(&buf[TLS_HMAC_HEADER_SIZE..]);
if hmac.finalize() == buf[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] {
xor_slice(&mut buf[TLS_HMAC_HEADER_SIZE..], key);
Expand Down
17 changes: 13 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter};
use crate::{
client::{parse_client_names, ShadowTlsClient, TlsExtConfig, TlsNames},
server::{parse_server_addrs, ShadowTlsServer, TlsAddrs},
util::V3Mode,
};

#[derive(Parser, Debug)]
Expand All @@ -40,6 +41,8 @@ struct Opts {
disable_nodelay: bool,
#[clap(long, help = "Use v3 protocol")]
v3: bool,
#[clap(long, help = "Strict mode(only for v3 protocol)")]
strict: bool,
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -104,20 +107,26 @@ enum RunningArgs {
tls_ext: TlsExtConfig,
password: String,
nodelay: bool,
v3: bool,
v3: V3Mode,
},
Server {
listen_addr: String,
target_addr: String,
tls_addr: TlsAddrs,
password: String,
nodelay: bool,
v3: bool,
v3: V3Mode,
},
}

impl From<Args> for RunningArgs {
fn from(args: Args) -> Self {
let v3 = match (args.opts.v3, args.opts.strict) {
(true, true) => V3Mode::Strict,
(true, false) => V3Mode::Lossy,
(false, _) => V3Mode::Disabled,
};

match args.cmd {
Commands::Client {
listen,
Expand All @@ -132,7 +141,7 @@ impl From<Args> for RunningArgs {
tls_ext: TlsExtConfig::from(alpn),
password,
nodelay: !args.opts.disable_nodelay,
v3: args.opts.v3,
v3,
},
Commands::Server {
listen,
Expand All @@ -145,7 +154,7 @@ impl From<Args> for RunningArgs {
tls_addr,
password,
nodelay: !args.opts.disable_nodelay,
v3: args.opts.v3,
v3,
},
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::{
},
util::{
copy_bidirectional, copy_until_eof, kdf, mod_tcp_conn, prelude::*, verified_relay,
xor_slice, Hmac,
xor_slice, Hmac, V3Mode,
},
};

Expand All @@ -38,7 +38,7 @@ pub struct ShadowTlsServer<LA, TA> {
tls_addr: Arc<TlsAddrs>,
password: Arc<String>,
nodelay: bool,
v3: bool,
v3: V3Mode,
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -128,7 +128,7 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
tls_addr: TlsAddrs,
password: String,
nodelay: bool,
v3: bool,
v3: V3Mode,
) -> Self {
Self {
listen_addr: Arc::new(listen_addr),
Expand Down Expand Up @@ -158,7 +158,7 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
let server = shared.clone();
mod_tcp_conn(&mut conn, true, shared.nodelay);
monoio::spawn(async move {
let _ = match server.v3 {
let _ = match server.v3.enabled() {
false => server.relay_v2(conn).await,
true => server.relay_v3(conn).await,
};
Expand Down Expand Up @@ -283,8 +283,10 @@ impl<LA, TA> ShadowTlsServer<LA, TA> {
};
tracing::debug!("Client authenticated. ServerRandom extracted: {server_random:?}");

if !support_tls13(&first_server_frame) {
tracing::error!("TLS 1.3 is not supported, will copy bidirectional");
if self.v3.strict() && !support_tls13(&first_server_frame) {
tracing::error!(
"V3 strict enabled and TLS 1.3 is not supported, will copy bidirectional"
);
copy_bidirectional(&mut in_stream, &mut handshake_stream).await;
return Ok(());
}
Expand Down
30 changes: 30 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,35 @@ pub mod prelude {
pub const HMAC_SIZE: usize = 4;
}

#[derive(Copy, Clone, Debug)]
pub enum V3Mode {
Disabled,
Lossy,
Strict,
}

impl std::fmt::Display for V3Mode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
V3Mode::Disabled => write!(f, "disabled"),
V3Mode::Lossy => write!(f, "enabled(lossy)"),
V3Mode::Strict => write!(f, "enabled(strict)"),
}
}
}

impl V3Mode {
#[inline]
pub fn enabled(&self) -> bool {
!matches!(self, V3Mode::Disabled)
}

#[inline]
pub fn strict(&self) -> bool {
matches!(self, V3Mode::Strict)
}
}

pub async fn copy_until_eof<R, W>(mut read_half: R, mut write_half: W) -> std::io::Result<()>
where
R: monoio::io::AsyncReadRent,
Expand Down Expand Up @@ -67,6 +96,7 @@ pub fn mod_tcp_conn(conn: &mut TcpStream, keepalive: bool, nodelay: bool) {
let _ = conn.set_nodelay(nodelay);
}

#[derive(Clone)]
pub struct Hmac(hmac::Hmac<sha1::Sha1>);

impl Hmac {
Expand Down

0 comments on commit 3467ad8

Please sign in to comment.