diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 2172b1c..e2dcd7a 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -6,6 +6,7 @@ on: env: IMAGE_NAME: shadow-tls + CARGO_NET_GIT_FETCH_WITH_CLI: true jobs: build: diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 9d2fe51..913553c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -6,6 +6,7 @@ on: env: CARGO_TERM_COLOR: always + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: build-cross: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c0d6b6..0827f36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ on: env: RUST_TOOLCHAIN: nightly TOOLCHAIN_PROFILE: minimal + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse jobs: lints: diff --git a/Dockerfile b/Dockerfile index a863f6f..2f4c1d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /usr/src/shadow-tls RUN apk add --no-cache musl-dev libressl-dev COPY . . -RUN RUSTFLAGS="" cargo build --bin shadow-tls --release +RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse RUSTFLAGS="" cargo build --bin shadow-tls --release FROM alpine:latest diff --git a/src/client.rs b/src/client.rs index fe1747d..be4e0be 100644 --- a/src/client.rs +++ b/src/client.rs @@ -229,7 +229,7 @@ impl ShadowTlsClient { let stream = stream.into_inner(); // stage2: - if maybe_srh.is_none() || !authorized && self.v3.strict() { + 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 { diff --git a/tests/sni.rs b/tests/sni.rs index 0b5112d..6825e2e 100644 --- a/tests/sni.rs +++ b/tests/sni.rs @@ -1,10 +1,16 @@ use std::time::Duration; -use monoio::net::TcpStream; +use monoio::{ + io::{AsyncReadRentExt, AsyncWriteRentExt}, + net::TcpStream, +}; use monoio_rustls_fork_shadow_tls::TlsConnector; use rustls_fork_shadow_tls::{OwnedTrustAnchor, RootCertStore, ServerName}; use shadow_tls::{RunningArgs, TlsAddrs, V3Mode}; +const FEISHU_HTTP_REQUEST: &[u8; 48] = b"GET / HTTP/1.1\r\nHost: feishu.cn\r\nAccept: */*\r\n\r\n"; +const FEISHU_CN_HTTP_RESP: &[u8; 30] = b"HTTP/1.1 301 Moved Permanently"; + #[monoio::test(enable_timer = true)] async fn sni() { // construct tls connector @@ -24,7 +30,7 @@ async fn sni() { // run server let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20012".to_string(), + listen_addr: "127.0.0.1:32000".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("feishu.cn").unwrap(), password: "test".to_string(), @@ -35,14 +41,25 @@ async fn sni() { monoio::time::sleep(Duration::from_secs(1)).await; // connect and handshake - assert!(tls_connector + let mut feishu_conn = tls_connector .connect( ServerName::try_from("feishu.cn").unwrap(), - TcpStream::connect("127.0.0.1:20012").await.unwrap() + TcpStream::connect("127.0.0.1:32000").await.unwrap(), ) .await - .is_ok()); - let conn = TcpStream::connect("127.0.0.1:20012").await.unwrap(); + .expect("unable to connect feishu.cn"); + feishu_conn + .write_all(FEISHU_HTTP_REQUEST.to_vec()) + .await + .0 + .unwrap(); + let (res, buf) = feishu_conn + .read_exact(vec![0; FEISHU_CN_HTTP_RESP.len()]) + .await; + assert!(res.is_ok()); + assert_eq!(&buf, FEISHU_CN_HTTP_RESP); + + let conn = TcpStream::connect("127.0.0.1:32000").await.unwrap(); assert!(tls_connector .connect(ServerName::try_from("t.cn").unwrap(), conn) .await diff --git a/tests/tls12.rs b/tests/tls12.rs index 57f1d35..5939d63 100644 --- a/tests/tls12.rs +++ b/tests/tls12.rs @@ -9,8 +9,8 @@ use utils::*; #[test] fn tls12_v2() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20000".to_string(), - target_addr: "127.0.0.1:20001".to_string(), + listen_addr: "127.0.0.1:30000".to_string(), + target_addr: "127.0.0.1:30001".to_string(), tls_names: TlsNames::try_from("t.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -18,7 +18,7 @@ fn tls12_v2() { v3: V3Mode::Disabled, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20001".to_string(), + listen_addr: "127.0.0.1:30001".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("t.cn").unwrap(), password: "test".to_string(), @@ -34,8 +34,8 @@ fn tls12_v2() { #[test] fn tls12_v3_lossy() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20002".to_string(), - target_addr: "127.0.0.1:20003".to_string(), + listen_addr: "127.0.0.1:30002".to_string(), + target_addr: "127.0.0.1:30003".to_string(), tls_names: TlsNames::try_from("t.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -43,7 +43,7 @@ fn tls12_v3_lossy() { v3: V3Mode::Lossy, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20003".to_string(), + listen_addr: "127.0.0.1:30003".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("t.cn").unwrap(), password: "test".to_string(), @@ -61,8 +61,8 @@ fn tls12_v3_lossy() { #[should_panic] fn tls12_v3_strict() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20004".to_string(), - target_addr: "127.0.0.1:20005".to_string(), + listen_addr: "127.0.0.1:30004".to_string(), + target_addr: "127.0.0.1:30005".to_string(), tls_names: TlsNames::try_from("t.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -70,7 +70,7 @@ fn tls12_v3_strict() { v3: V3Mode::Strict, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20005".to_string(), + listen_addr: "127.0.0.1:30005".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("t.cn").unwrap(), password: "test".to_string(), @@ -79,3 +79,40 @@ fn tls12_v3_strict() { }; utils::test_ok(client, server, T_CN_HTTP_REQUEST, T_CN_HTTP_RESP); } + +// protocol: v2 +// Note: v2 can not defend against hijack attack. +// The interceptor will see TLS Alert. +// But it will not cause data error since the connection will be closed. +#[test] +fn tls12_v2_hijack() { + let client = RunningArgs::Client { + listen_addr: "127.0.0.1:30006".to_string(), + target_addr: "qq.com:443".to_string(), + tls_names: TlsNames::try_from("qq.com").unwrap(), + tls_ext: TlsExtConfig::new(None), + password: "test".to_string(), + nodelay: true, + v3: V3Mode::Disabled, + }; + test_hijack(client); +} + +// protocol: v3 lossy +// (v3 strict can not work with tls1.2) +// Note: tls1.2 with v3 lossy can not defend against hijack attack. +// The interceptor will see TLS Alert. +// But it will not cause data error since the connection will be closed. +#[test] +fn tls12_v3_lossy_hijack() { + let client = RunningArgs::Client { + listen_addr: "127.0.0.1:30007".to_string(), + target_addr: "qq.com:443".to_string(), + tls_names: TlsNames::try_from("qq.com").unwrap(), + tls_ext: TlsExtConfig::new(None), + password: "test".to_string(), + nodelay: true, + v3: V3Mode::Lossy, + }; + test_hijack(client); +} diff --git a/tests/tls13.rs b/tests/tls13.rs index b8f7360..cf097f6 100644 --- a/tests/tls13.rs +++ b/tests/tls13.rs @@ -9,8 +9,8 @@ use utils::*; #[test] fn tls13_v2() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20006".to_string(), - target_addr: "127.0.0.1:20007".to_string(), + listen_addr: "127.0.0.1:31000".to_string(), + target_addr: "127.0.0.1:31001".to_string(), tls_names: TlsNames::try_from("feishu.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -18,7 +18,7 @@ fn tls13_v2() { v3: V3Mode::Disabled, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20007".to_string(), + listen_addr: "127.0.0.1:31001".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("feishu.cn").unwrap(), password: "test".to_string(), @@ -34,8 +34,8 @@ fn tls13_v2() { #[test] fn tls13_v3_lossy() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20008".to_string(), - target_addr: "127.0.0.1:20009".to_string(), + listen_addr: "127.0.0.1:31002".to_string(), + target_addr: "127.0.0.1:31003".to_string(), tls_names: TlsNames::try_from("feishu.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -43,7 +43,7 @@ fn tls13_v3_lossy() { v3: V3Mode::Lossy, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20009".to_string(), + listen_addr: "127.0.0.1:31003".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("feishu.cn").unwrap(), password: "test".to_string(), @@ -59,8 +59,8 @@ fn tls13_v3_lossy() { #[test] fn tls13_v3_strict() { let client = RunningArgs::Client { - listen_addr: "127.0.0.1:20010".to_string(), - target_addr: "127.0.0.1:20011".to_string(), + listen_addr: "127.0.0.1:31004".to_string(), + target_addr: "127.0.0.1:31005".to_string(), tls_names: TlsNames::try_from("feishu.cn").unwrap(), tls_ext: TlsExtConfig::new(None), password: "test".to_string(), @@ -68,7 +68,7 @@ fn tls13_v3_strict() { v3: V3Mode::Strict, }; let server = RunningArgs::Server { - listen_addr: "127.0.0.1:20011".to_string(), + listen_addr: "127.0.0.1:31005".to_string(), target_addr: "t.cn:80".to_string(), tls_addr: TlsAddrs::try_from("feishu.cn").unwrap(), password: "test".to_string(), @@ -77,3 +77,54 @@ fn tls13_v3_strict() { }; utils::test_ok(client, server, T_CN_HTTP_REQUEST, T_CN_HTTP_RESP); } + +// protocol: v2 +// Note: v2 can not defend against hijack attack. +// The interceptor will not see TLS Alert. +// But it will cause data error. +#[test] +#[should_panic] +fn tls13_v2_hijack() { + let client = RunningArgs::Client { + listen_addr: "127.0.0.1:31006".to_string(), + target_addr: "feishu.cn:443".to_string(), + tls_names: TlsNames::try_from("feishu.cn").unwrap(), + tls_ext: TlsExtConfig::new(None), + password: "test".to_string(), + nodelay: true, + v3: V3Mode::Disabled, + }; + test_hijack(client); +} + +// protocol: v3 lossy +// tls1.3 with v3 protocol defends against hijack attack. +#[test] +fn tls13_v3_lossy_hijack() { + let client = RunningArgs::Client { + listen_addr: "127.0.0.1:31007".to_string(), + target_addr: "feishu.cn:443".to_string(), + tls_names: TlsNames::try_from("feishu.cn").unwrap(), + tls_ext: TlsExtConfig::new(None), + password: "test".to_string(), + nodelay: true, + v3: V3Mode::Lossy, + }; + test_hijack(client); +} + +// protocol: v3 strict +// tls1.3 with v3 protocol defends against hijack attack. +#[test] +fn tls13_v3_strict_hijack() { + let client = RunningArgs::Client { + listen_addr: "127.0.0.1:31008".to_string(), + target_addr: "feishu.cn:443".to_string(), + tls_names: TlsNames::try_from("feishu.cn").unwrap(), + tls_ext: TlsExtConfig::new(None), + password: "test".to_string(), + nodelay: true, + v3: V3Mode::Strict, + }; + test_hijack(client); +} diff --git a/tests/utils.rs b/tests/utils.rs index 0f56f3b..98dcef1 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -33,3 +33,19 @@ pub fn test_ok( conn.read_exact(&mut buf).unwrap(); assert_eq!(&buf, http_response); } + +pub fn test_hijack(client: RunningArgs) { + let client_listen = match &client { + RunningArgs::Client { listen_addr, .. } => listen_addr.clone(), + RunningArgs::Server { .. } => panic!("not valid client args"), + }; + client.build().expect("build client failed").start(1); + + // sleep 1s to make sure client and server have started + std::thread::sleep(Duration::from_secs(1)); + let mut conn = TcpStream::connect(client_listen).unwrap(); + conn.write_all(b"dummy").unwrap(); + conn.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + let mut dummy_buf = [0; 1]; + assert!(!matches!(conn.read(&mut dummy_buf), Ok(1))); +}