diff --git a/Cargo.lock b/Cargo.lock index 2d30574..2d4b5ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -644,7 +644,7 @@ dependencies = [ [[package]] name = "shadow-tls" -version = "0.2.15" +version = "0.2.16" dependencies = [ "anyhow", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index fc7d6f5..570adc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"} diff --git a/Dockerfile b/Dockerfile index 7664a1c..a863f6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/docs/protocol-v3-en.md b/docs/protocol-v3-en.md index 7e0413e..1766a21 100644 --- a/docs/protocol-v3-en.md +++ b/docs/protocol-v3-en.md @@ -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. diff --git a/docs/protocol-v3-zh.md b/docs/protocol-v3-zh.md index 40cacb8..caab408 100644 --- a/docs/protocol-v3-zh.md +++ b/docs/protocol-v3-zh.md @@ -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 细节的感知,更易于实现。 diff --git a/entrypoint.sh b/entrypoint.sh index 5693304..256f4c8 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -15,6 +15,11 @@ then parameter="$parameter --v3" fi +if [ ! -z "$STRICT" ] +then + parameter="$parameter --strict" +fi + if [ "$MODE" = "server" ] then parameter="$parameter $MODE" diff --git a/src/client.rs b/src/client.rs index edc0d8a..fdf3ae1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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); @@ -31,7 +31,7 @@ pub struct ShadowTlsClient { tls_names: Arc, password: Arc, nodelay: bool, - v3: bool, + v3: V3Mode, } #[derive(Clone, Debug, PartialEq)] @@ -116,7 +116,7 @@ impl ShadowTlsClient { tls_ext_config: TlsExtConfig, password: String, nodelay: bool, - v3: bool, + v3: V3Mode, ) -> anyhow::Result { let mut root_store = RootCertStore::empty(); root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { @@ -166,7 +166,7 @@ impl ShadowTlsClient { 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, }; @@ -220,30 +220,31 @@ impl ShadowTlsClient { .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. @@ -282,11 +283,18 @@ struct StreamWrapper { read_buf: Option>, read_pos: usize, - read_server_random: Option<[u8; TLS_RANDOM_SIZE]>, - read_hmac_key: Option<(Hmac, Vec)>, + + read_state: Option, read_authorized: bool, } +#[derive(Clone)] +struct State { + server_random: [u8; TLS_RANDOM_SIZE], + hmac: Hmac, + key: Vec, +} + impl StreamWrapper { fn new(raw: S, password: &str) -> Self { Self { @@ -295,21 +303,18 @@ impl StreamWrapper { 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 { + &self.read_state } fn into_inner(self) -> S { @@ -370,16 +375,19 @@ impl StreamWrapper { ) } 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); diff --git a/src/main.rs b/src/main.rs index a102f2b..61d7db6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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)] @@ -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)] @@ -104,7 +107,7 @@ enum RunningArgs { tls_ext: TlsExtConfig, password: String, nodelay: bool, - v3: bool, + v3: V3Mode, }, Server { listen_addr: String, @@ -112,12 +115,18 @@ enum RunningArgs { tls_addr: TlsAddrs, password: String, nodelay: bool, - v3: bool, + v3: V3Mode, }, } impl From 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, @@ -132,7 +141,7 @@ impl From for RunningArgs { tls_ext: TlsExtConfig::from(alpn), password, nodelay: !args.opts.disable_nodelay, - v3: args.opts.v3, + v3, }, Commands::Server { listen, @@ -145,7 +154,7 @@ impl From for RunningArgs { tls_addr, password, nodelay: !args.opts.disable_nodelay, - v3: args.opts.v3, + v3, }, } } diff --git a/src/server.rs b/src/server.rs index fb5993a..1ffe507 100644 --- a/src/server.rs +++ b/src/server.rs @@ -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, }, }; @@ -38,7 +38,7 @@ pub struct ShadowTlsServer { tls_addr: Arc, password: Arc, nodelay: bool, - v3: bool, + v3: V3Mode, } #[derive(Clone, Debug, PartialEq)] @@ -128,7 +128,7 @@ impl ShadowTlsServer { tls_addr: TlsAddrs, password: String, nodelay: bool, - v3: bool, + v3: V3Mode, ) -> Self { Self { listen_addr: Arc::new(listen_addr), @@ -158,7 +158,7 @@ impl ShadowTlsServer { 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, }; @@ -283,8 +283,10 @@ impl ShadowTlsServer { }; 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(()); } diff --git a/src/util.rs b/src/util.rs index 4fe39c7..c7f0880 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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(mut read_half: R, mut write_half: W) -> std::io::Result<()> where R: monoio::io::AsyncReadRent, @@ -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); impl Hmac {