├── .dockerignore ├── .github ├── resources │ └── shadow-tls.png └── workflows │ ├── build-release.yml │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.action ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── protocol-en.md ├── protocol-v3-en.md ├── protocol-v3-zh.md ├── protocol-zh.md └── quick-start.md ├── entrypoint.sh ├── examples ├── client_config.json └── server_config.json ├── rust-toolchain ├── src ├── client.rs ├── helper_v2.rs ├── lib.rs ├── main.rs ├── server.rs ├── sip003.rs └── util.rs └── tests ├── sni.rs ├── tls12.rs ├── tls13.rs └── utils.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.git/ -------------------------------------------------------------------------------- /.github/resources/shadow-tls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ihciah/shadow-tls/02dd0bc7bae8a2011729f95021690e694fd8e43e/.github/resources/shadow-tls.png -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Releases 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | env: 8 | IMAGE_NAME: shadow-tls 9 | CARGO_TERM_COLOR: always 10 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 11 | 12 | jobs: 13 | build-cross: 14 | runs-on: ubuntu-latest 15 | env: 16 | RUST_BACKTRACE: full 17 | strategy: 18 | matrix: 19 | target: 20 | - x86_64-unknown-linux-musl 21 | - aarch64-unknown-linux-musl 22 | - armv7-unknown-linux-musleabihf 23 | - arm-unknown-linux-musleabi 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install Rust 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | target: ${{ matrix.target }} 31 | toolchain: nightly 32 | default: true 33 | override: true 34 | - name: Install cross 35 | run: cargo install cross 36 | - name: Build ${{ matrix.target }} 37 | timeout-minutes: 120 38 | run: | 39 | cross build --release --target ${{ matrix.target }} && 40 | cp target/${{ matrix.target }}/release/shadow-tls target/${{ matrix.target }}/release/shadow-tls-${{ matrix.target }} 41 | - name: Upload Github Assets 42 | uses: softprops/action-gh-release@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | files: target/${{ matrix.target }}/release/shadow-tls-${{ matrix.target }} 47 | prerelease: ${{ contains(github.ref, '-') }} 48 | - name: Set up QEMU 49 | uses: docker/setup-qemu-action@v2 50 | - name: Setup Docker Buildx 51 | uses: docker/setup-buildx-action@v1 52 | - name: Login to Dockerhub 53 | uses: docker/login-action@v1 54 | with: 55 | username: ${{ secrets.DOCKERHUB_USERNAME }} 56 | password: ${{ secrets.DOCKERHUB_TOKEN }} 57 | - name: Login to GitHub Container Registry 58 | uses: docker/login-action@v1 59 | with: 60 | registry: ghcr.io 61 | username: ${{ github.repository_owner }} 62 | password: ${{ secrets.GITHUB_TOKEN }} 63 | - name: Login to Gitlab Container Registry 64 | uses: docker/login-action@v1 65 | with: 66 | registry: registry.gitlab.com 67 | username: ${{ secrets.GITLAB_USERNAME }} 68 | password: ${{ secrets.GITLAB_TOKEN }} 69 | - name: Generate App Version 70 | run: echo VERSIONED_TAG=`git describe --tags --always` >> $GITHUB_ENV 71 | - name: Build and release Docker images 72 | uses: docker/build-push-action@v3 73 | with: 74 | file: Dockerfile.action 75 | build-args: "DOWNLOAD_BASE=https://github.com/${{ github.repository }}/releases/download/${{ env.VERSIONED_TAG }}" 76 | platforms: linux/amd64,linux/arm64,linux/arm/v7 77 | tags: | 78 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest 79 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSIONED_TAG }} 80 | ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest 81 | ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ env.VERSIONED_TAG }} 82 | registry.gitlab.com/${{ secrets.GITLAB_USERNAME }}/${{ env.IMAGE_NAME }}:latest 83 | registry.gitlab.com/${{ secrets.GITLAB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSIONED_TAG }} 84 | push: true 85 | 86 | build-unix: 87 | runs-on: macos-latest 88 | env: 89 | RUST_BACKTRACE: full 90 | strategy: 91 | matrix: 92 | target: 93 | - x86_64-apple-darwin 94 | - aarch64-apple-darwin 95 | steps: 96 | - uses: actions/checkout@v2 97 | - name: Install Rust 98 | uses: actions-rs/toolchain@v1 99 | with: 100 | profile: minimal 101 | target: ${{ matrix.target }} 102 | toolchain: nightly 103 | default: true 104 | override: true 105 | - name: Build release 106 | shell: bash 107 | run: | 108 | cargo build --release --target ${{ matrix.target }} && 109 | mv target/${{ matrix.target }}/release/shadow-tls target/${{ matrix.target }}/release/shadow-tls-${{ matrix.target }} 110 | - name: Upload Github Assets 111 | uses: softprops/action-gh-release@v1 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | with: 115 | files: target/${{ matrix.target }}/release/shadow-tls-${{ matrix.target }} 116 | prerelease: ${{ contains(github.ref, '-') }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | - '**.png' 8 | pull_request: 9 | paths-ignore: 10 | - '**.md' 11 | - '**.png' 12 | 13 | env: 14 | RUST_TOOLCHAIN: nightly 15 | TOOLCHAIN_PROFILE: minimal 16 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse 17 | 18 | jobs: 19 | lints: 20 | name: Run cargo fmt and cargo clippy 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout sources 24 | uses: actions/checkout@v2 25 | - name: Install toolchain 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | profile: ${{ env.TOOLCHAIN_PROFILE }} 29 | toolchain: ${{ env.RUST_TOOLCHAIN }} 30 | override: true 31 | components: rustfmt, clippy 32 | - name: Cache 33 | uses: Swatinem/rust-cache@v1 34 | - name: Run cargo fmt 35 | uses: actions-rs/cargo@v1 36 | with: 37 | command: fmt 38 | args: --all -- --check 39 | - name: Run cargo check with no default features 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: check 43 | args: --no-default-features 44 | - name: Run cargo check with all features 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: check 48 | args: --all-features 49 | - name: Run cargo clippy 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: clippy 53 | args: -- -D warnings 54 | test: 55 | name: Run cargo test 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Checkout sources 59 | uses: actions/checkout@v2 60 | - name: Install toolchain 61 | uses: actions-rs/toolchain@v1 62 | with: 63 | profile: ${{ env.TOOLCHAIN_PROFILE }} 64 | toolchain: ${{ env.RUST_TOOLCHAIN }} 65 | override: true 66 | - name: Cache 67 | uses: Swatinem/rust-cache@v1 68 | - name: Run cargo test --no-run 69 | uses: actions-rs/cargo@v1 70 | with: 71 | command: test 72 | args: --all-features --no-run 73 | - name: Run cargo test 74 | run: sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && sudo -u runner RUSTUP_TOOLCHAIN=nightly /home/runner/.cargo/bin/cargo test --all-features" 75 | 76 | test-macos: 77 | name: Run cargo test on macos 78 | runs-on: macos-latest 79 | steps: 80 | - name: Checkout sources 81 | uses: actions/checkout@v2 82 | - name: Install toolchain 83 | uses: actions-rs/toolchain@v1 84 | with: 85 | profile: ${{ env.TOOLCHAIN_PROFILE }} 86 | toolchain: ${{ env.RUST_TOOLCHAIN }} 87 | override: true 88 | - name: Cache 89 | uses: Swatinem/rust-cache@v1 90 | - name: Run cargo test 91 | uses: actions-rs/cargo@v1 92 | with: 93 | command: test 94 | args: --all-features 95 | toolchain: ${{ env.RUST_TOOLCHAIN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.5" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys 0.52.0", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys 0.52.0", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.75" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 67 | 68 | [[package]] 69 | name = "auto-const-array" 70 | version = "0.2.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "62f7df18977a1ee03650ee4b31b4aefed6d56bac188760b6e37610400fe8d4bb" 73 | dependencies = [ 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "1.1.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 84 | 85 | [[package]] 86 | name = "bitflags" 87 | version = "1.3.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "2.4.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 96 | 97 | [[package]] 98 | name = "block-buffer" 99 | version = "0.10.4" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 102 | dependencies = [ 103 | "generic-array", 104 | ] 105 | 106 | [[package]] 107 | name = "bumpalo" 108 | version = "3.14.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 111 | 112 | [[package]] 113 | name = "byteorder" 114 | version = "1.5.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 117 | 118 | [[package]] 119 | name = "bytes" 120 | version = "1.5.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 123 | 124 | [[package]] 125 | name = "cc" 126 | version = "1.0.83" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 129 | dependencies = [ 130 | "libc", 131 | ] 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "clap" 141 | version = "4.4.11" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" 144 | dependencies = [ 145 | "clap_builder", 146 | "clap_derive", 147 | ] 148 | 149 | [[package]] 150 | name = "clap_builder" 151 | version = "4.4.11" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" 154 | dependencies = [ 155 | "anstream", 156 | "anstyle", 157 | "clap_lex", 158 | "strsim", 159 | ] 160 | 161 | [[package]] 162 | name = "clap_derive" 163 | version = "4.4.7" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" 166 | dependencies = [ 167 | "heck", 168 | "proc-macro2", 169 | "quote", 170 | "syn", 171 | ] 172 | 173 | [[package]] 174 | name = "clap_lex" 175 | version = "0.6.0" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" 178 | 179 | [[package]] 180 | name = "colorchoice" 181 | version = "1.0.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 184 | 185 | [[package]] 186 | name = "cpufeatures" 187 | version = "0.2.11" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 190 | dependencies = [ 191 | "libc", 192 | ] 193 | 194 | [[package]] 195 | name = "crypto-common" 196 | version = "0.1.6" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 199 | dependencies = [ 200 | "generic-array", 201 | "typenum", 202 | ] 203 | 204 | [[package]] 205 | name = "ctrlc" 206 | version = "3.4.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "82e95fbd621905b854affdc67943b043a0fbb6ed7385fd5a25650d19a8a6cfdf" 209 | dependencies = [ 210 | "nix 0.27.1", 211 | "windows-sys 0.48.0", 212 | ] 213 | 214 | [[package]] 215 | name = "digest" 216 | version = "0.10.7" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 219 | dependencies = [ 220 | "block-buffer", 221 | "crypto-common", 222 | "subtle", 223 | ] 224 | 225 | [[package]] 226 | name = "flume" 227 | version = "0.10.14" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" 230 | dependencies = [ 231 | "futures-core", 232 | "futures-sink", 233 | "nanorand", 234 | "pin-project", 235 | "spin 0.9.8", 236 | ] 237 | 238 | [[package]] 239 | name = "futures-core" 240 | version = "0.3.29" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 243 | 244 | [[package]] 245 | name = "futures-sink" 246 | version = "0.3.29" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 249 | 250 | [[package]] 251 | name = "futures-task" 252 | version = "0.3.29" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 255 | 256 | [[package]] 257 | name = "futures-util" 258 | version = "0.3.29" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 261 | dependencies = [ 262 | "futures-core", 263 | "futures-task", 264 | "pin-project-lite", 265 | "pin-utils", 266 | ] 267 | 268 | [[package]] 269 | name = "fxhash" 270 | version = "0.2.1" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 273 | dependencies = [ 274 | "byteorder", 275 | ] 276 | 277 | [[package]] 278 | name = "generic-array" 279 | version = "0.14.7" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 282 | dependencies = [ 283 | "typenum", 284 | "version_check", 285 | ] 286 | 287 | [[package]] 288 | name = "getrandom" 289 | version = "0.2.11" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 292 | dependencies = [ 293 | "cfg-if", 294 | "js-sys", 295 | "libc", 296 | "wasi", 297 | "wasm-bindgen", 298 | ] 299 | 300 | [[package]] 301 | name = "heck" 302 | version = "0.4.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 305 | 306 | [[package]] 307 | name = "hermit-abi" 308 | version = "0.3.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 311 | 312 | [[package]] 313 | name = "hmac" 314 | version = "0.12.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 317 | dependencies = [ 318 | "digest", 319 | ] 320 | 321 | [[package]] 322 | name = "io-uring" 323 | version = "0.6.2" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "460648e47a07a43110fbfa2e0b14afb2be920093c31e5dccc50e49568e099762" 326 | dependencies = [ 327 | "bitflags 1.3.2", 328 | "libc", 329 | ] 330 | 331 | [[package]] 332 | name = "itoa" 333 | version = "1.0.10" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 336 | 337 | [[package]] 338 | name = "js-sys" 339 | version = "0.3.66" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" 342 | dependencies = [ 343 | "wasm-bindgen", 344 | ] 345 | 346 | [[package]] 347 | name = "lazy_static" 348 | version = "1.4.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 351 | 352 | [[package]] 353 | name = "libc" 354 | version = "0.2.151" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" 357 | 358 | [[package]] 359 | name = "local-sync" 360 | version = "0.1.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "3865cc3ed16dd99f3b89098478647a558a5a2f6dc5f0b45d7867d75da80050ef" 363 | dependencies = [ 364 | "futures-core", 365 | "futures-sink", 366 | "futures-util", 367 | ] 368 | 369 | [[package]] 370 | name = "lock_api" 371 | version = "0.4.11" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 374 | dependencies = [ 375 | "autocfg", 376 | "scopeguard", 377 | ] 378 | 379 | [[package]] 380 | name = "log" 381 | version = "0.4.20" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 384 | 385 | [[package]] 386 | name = "matchers" 387 | version = "0.1.0" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 390 | dependencies = [ 391 | "regex-automata 0.1.10", 392 | ] 393 | 394 | [[package]] 395 | name = "memchr" 396 | version = "2.6.4" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 399 | 400 | [[package]] 401 | name = "memoffset" 402 | version = "0.7.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 405 | dependencies = [ 406 | "autocfg", 407 | ] 408 | 409 | [[package]] 410 | name = "mio" 411 | version = "0.8.10" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" 414 | dependencies = [ 415 | "libc", 416 | "log", 417 | "wasi", 418 | "windows-sys 0.48.0", 419 | ] 420 | 421 | [[package]] 422 | name = "monoio" 423 | version = "0.2.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "c91a9bcc2622991bc92f3b6d7dc495329c4863e4dc530d1748529b009bb2170a" 426 | dependencies = [ 427 | "auto-const-array", 428 | "bytes", 429 | "flume", 430 | "fxhash", 431 | "io-uring", 432 | "libc", 433 | "mio", 434 | "monoio-macros", 435 | "nix 0.26.4", 436 | "pin-project-lite", 437 | "socket2", 438 | "threadpool", 439 | "windows-sys 0.48.0", 440 | ] 441 | 442 | [[package]] 443 | name = "monoio-io-wrapper" 444 | version = "0.1.1" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "4bcfaa76e5daf87cc4d31b4d1b6bc93c12db59c19df50b9200afdbde42077655" 447 | dependencies = [ 448 | "monoio", 449 | ] 450 | 451 | [[package]] 452 | name = "monoio-macros" 453 | version = "0.1.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "176a5f5e69613d9e88337cf2a65e11135332b4efbcc628404a7c555e4452084c" 456 | dependencies = [ 457 | "proc-macro2", 458 | "quote", 459 | "syn", 460 | ] 461 | 462 | [[package]] 463 | name = "monoio-rustls-fork-shadow-tls" 464 | version = "0.3.0-mod.2" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "6e24e3f4f85c39e9d009ab7feb304fdf8e8465d8039e59335fd73be80141d930" 467 | dependencies = [ 468 | "bytes", 469 | "monoio", 470 | "monoio-io-wrapper", 471 | "rustls-fork-shadow-tls", 472 | "thiserror", 473 | ] 474 | 475 | [[package]] 476 | name = "nanorand" 477 | version = "0.7.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 480 | dependencies = [ 481 | "getrandom", 482 | ] 483 | 484 | [[package]] 485 | name = "nix" 486 | version = "0.26.4" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 489 | dependencies = [ 490 | "bitflags 1.3.2", 491 | "cfg-if", 492 | "libc", 493 | "memoffset", 494 | "pin-utils", 495 | ] 496 | 497 | [[package]] 498 | name = "nix" 499 | version = "0.27.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 502 | dependencies = [ 503 | "bitflags 2.4.1", 504 | "cfg-if", 505 | "libc", 506 | ] 507 | 508 | [[package]] 509 | name = "nu-ansi-term" 510 | version = "0.46.0" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 513 | dependencies = [ 514 | "overload", 515 | "winapi", 516 | ] 517 | 518 | [[package]] 519 | name = "num_cpus" 520 | version = "1.16.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 523 | dependencies = [ 524 | "hermit-abi", 525 | "libc", 526 | ] 527 | 528 | [[package]] 529 | name = "once_cell" 530 | version = "1.19.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 533 | 534 | [[package]] 535 | name = "overload" 536 | version = "0.1.1" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 539 | 540 | [[package]] 541 | name = "pin-project" 542 | version = "1.1.3" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 545 | dependencies = [ 546 | "pin-project-internal", 547 | ] 548 | 549 | [[package]] 550 | name = "pin-project-internal" 551 | version = "1.1.3" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 554 | dependencies = [ 555 | "proc-macro2", 556 | "quote", 557 | "syn", 558 | ] 559 | 560 | [[package]] 561 | name = "pin-project-lite" 562 | version = "0.2.13" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 565 | 566 | [[package]] 567 | name = "pin-utils" 568 | version = "0.1.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 571 | 572 | [[package]] 573 | name = "ppv-lite86" 574 | version = "0.2.17" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 577 | 578 | [[package]] 579 | name = "proc-macro2" 580 | version = "1.0.70" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 583 | dependencies = [ 584 | "unicode-ident", 585 | ] 586 | 587 | [[package]] 588 | name = "quote" 589 | version = "1.0.33" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 592 | dependencies = [ 593 | "proc-macro2", 594 | ] 595 | 596 | [[package]] 597 | name = "rand" 598 | version = "0.8.5" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 601 | dependencies = [ 602 | "libc", 603 | "rand_chacha", 604 | "rand_core", 605 | ] 606 | 607 | [[package]] 608 | name = "rand_chacha" 609 | version = "0.3.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 612 | dependencies = [ 613 | "ppv-lite86", 614 | "rand_core", 615 | ] 616 | 617 | [[package]] 618 | name = "rand_core" 619 | version = "0.6.4" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 622 | dependencies = [ 623 | "getrandom", 624 | ] 625 | 626 | [[package]] 627 | name = "regex" 628 | version = "1.10.2" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" 631 | dependencies = [ 632 | "aho-corasick", 633 | "memchr", 634 | "regex-automata 0.4.3", 635 | "regex-syntax 0.8.2", 636 | ] 637 | 638 | [[package]] 639 | name = "regex-automata" 640 | version = "0.1.10" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 643 | dependencies = [ 644 | "regex-syntax 0.6.29", 645 | ] 646 | 647 | [[package]] 648 | name = "regex-automata" 649 | version = "0.4.3" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 652 | dependencies = [ 653 | "aho-corasick", 654 | "memchr", 655 | "regex-syntax 0.8.2", 656 | ] 657 | 658 | [[package]] 659 | name = "regex-syntax" 660 | version = "0.6.29" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 663 | 664 | [[package]] 665 | name = "regex-syntax" 666 | version = "0.8.2" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 669 | 670 | [[package]] 671 | name = "ring" 672 | version = "0.16.20" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 675 | dependencies = [ 676 | "cc", 677 | "libc", 678 | "once_cell", 679 | "spin 0.5.2", 680 | "untrusted 0.7.1", 681 | "web-sys", 682 | "winapi", 683 | ] 684 | 685 | [[package]] 686 | name = "ring" 687 | version = "0.17.7" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" 690 | dependencies = [ 691 | "cc", 692 | "getrandom", 693 | "libc", 694 | "spin 0.9.8", 695 | "untrusted 0.9.0", 696 | "windows-sys 0.48.0", 697 | ] 698 | 699 | [[package]] 700 | name = "rustc-hash" 701 | version = "1.1.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 704 | 705 | [[package]] 706 | name = "rustls-fork-shadow-tls" 707 | version = "0.20.9-mod.2" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "a095c00ed7b7606a456667abd022d50437f66bd2b6db1d90c93227e8f329dec8" 710 | dependencies = [ 711 | "log", 712 | "ring 0.16.20", 713 | "sct", 714 | "webpki", 715 | ] 716 | 717 | [[package]] 718 | name = "rustls-pki-types" 719 | version = "1.0.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "e7673e0aa20ee4937c6aacfc12bb8341cfbf054cdd21df6bec5fd0629fe9339b" 722 | 723 | [[package]] 724 | name = "ryu" 725 | version = "1.0.16" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 728 | 729 | [[package]] 730 | name = "scopeguard" 731 | version = "1.2.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 734 | 735 | [[package]] 736 | name = "sct" 737 | version = "0.7.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 740 | dependencies = [ 741 | "ring 0.17.7", 742 | "untrusted 0.9.0", 743 | ] 744 | 745 | [[package]] 746 | name = "serde" 747 | version = "1.0.193" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 750 | dependencies = [ 751 | "serde_derive", 752 | ] 753 | 754 | [[package]] 755 | name = "serde_derive" 756 | version = "1.0.193" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 759 | dependencies = [ 760 | "proc-macro2", 761 | "quote", 762 | "syn", 763 | ] 764 | 765 | [[package]] 766 | name = "serde_json" 767 | version = "1.0.108" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 770 | dependencies = [ 771 | "itoa", 772 | "ryu", 773 | "serde", 774 | ] 775 | 776 | [[package]] 777 | name = "sha1" 778 | version = "0.10.6" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 781 | dependencies = [ 782 | "cfg-if", 783 | "cpufeatures", 784 | "digest", 785 | ] 786 | 787 | [[package]] 788 | name = "sha2" 789 | version = "0.10.8" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 792 | dependencies = [ 793 | "cfg-if", 794 | "cpufeatures", 795 | "digest", 796 | ] 797 | 798 | [[package]] 799 | name = "shadow-tls" 800 | version = "0.2.25" 801 | dependencies = [ 802 | "anyhow", 803 | "byteorder", 804 | "clap", 805 | "ctrlc", 806 | "hmac", 807 | "local-sync", 808 | "monoio", 809 | "monoio-rustls-fork-shadow-tls", 810 | "pin-project-lite", 811 | "rand", 812 | "rustc-hash", 813 | "rustls-fork-shadow-tls", 814 | "serde", 815 | "serde_json", 816 | "sha1", 817 | "sha2", 818 | "tracing", 819 | "tracing-subscriber", 820 | "webpki-roots", 821 | ] 822 | 823 | [[package]] 824 | name = "sharded-slab" 825 | version = "0.1.7" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 828 | dependencies = [ 829 | "lazy_static", 830 | ] 831 | 832 | [[package]] 833 | name = "smallvec" 834 | version = "1.11.2" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 837 | 838 | [[package]] 839 | name = "socket2" 840 | version = "0.5.5" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 843 | dependencies = [ 844 | "libc", 845 | "windows-sys 0.48.0", 846 | ] 847 | 848 | [[package]] 849 | name = "spin" 850 | version = "0.5.2" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 853 | 854 | [[package]] 855 | name = "spin" 856 | version = "0.9.8" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 859 | dependencies = [ 860 | "lock_api", 861 | ] 862 | 863 | [[package]] 864 | name = "strsim" 865 | version = "0.10.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 868 | 869 | [[package]] 870 | name = "subtle" 871 | version = "2.5.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 874 | 875 | [[package]] 876 | name = "syn" 877 | version = "2.0.40" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e" 880 | dependencies = [ 881 | "proc-macro2", 882 | "quote", 883 | "unicode-ident", 884 | ] 885 | 886 | [[package]] 887 | name = "thiserror" 888 | version = "1.0.50" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 891 | dependencies = [ 892 | "thiserror-impl", 893 | ] 894 | 895 | [[package]] 896 | name = "thiserror-impl" 897 | version = "1.0.50" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 900 | dependencies = [ 901 | "proc-macro2", 902 | "quote", 903 | "syn", 904 | ] 905 | 906 | [[package]] 907 | name = "thread_local" 908 | version = "1.1.7" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 911 | dependencies = [ 912 | "cfg-if", 913 | "once_cell", 914 | ] 915 | 916 | [[package]] 917 | name = "threadpool" 918 | version = "1.8.1" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" 921 | dependencies = [ 922 | "num_cpus", 923 | ] 924 | 925 | [[package]] 926 | name = "tracing" 927 | version = "0.1.40" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 930 | dependencies = [ 931 | "pin-project-lite", 932 | "tracing-attributes", 933 | "tracing-core", 934 | ] 935 | 936 | [[package]] 937 | name = "tracing-attributes" 938 | version = "0.1.27" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 941 | dependencies = [ 942 | "proc-macro2", 943 | "quote", 944 | "syn", 945 | ] 946 | 947 | [[package]] 948 | name = "tracing-core" 949 | version = "0.1.32" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 952 | dependencies = [ 953 | "once_cell", 954 | "valuable", 955 | ] 956 | 957 | [[package]] 958 | name = "tracing-log" 959 | version = "0.2.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 962 | dependencies = [ 963 | "log", 964 | "once_cell", 965 | "tracing-core", 966 | ] 967 | 968 | [[package]] 969 | name = "tracing-subscriber" 970 | version = "0.3.18" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 973 | dependencies = [ 974 | "matchers", 975 | "nu-ansi-term", 976 | "once_cell", 977 | "regex", 978 | "sharded-slab", 979 | "smallvec", 980 | "thread_local", 981 | "tracing", 982 | "tracing-core", 983 | "tracing-log", 984 | ] 985 | 986 | [[package]] 987 | name = "typenum" 988 | version = "1.17.0" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 991 | 992 | [[package]] 993 | name = "unicode-ident" 994 | version = "1.0.12" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 997 | 998 | [[package]] 999 | name = "untrusted" 1000 | version = "0.7.1" 1001 | source = "registry+https://github.com/rust-lang/crates.io-index" 1002 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 1003 | 1004 | [[package]] 1005 | name = "untrusted" 1006 | version = "0.9.0" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1009 | 1010 | [[package]] 1011 | name = "utf8parse" 1012 | version = "0.2.1" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1015 | 1016 | [[package]] 1017 | name = "valuable" 1018 | version = "0.1.0" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1021 | 1022 | [[package]] 1023 | name = "version_check" 1024 | version = "0.9.4" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1027 | 1028 | [[package]] 1029 | name = "wasi" 1030 | version = "0.11.0+wasi-snapshot-preview1" 1031 | source = "registry+https://github.com/rust-lang/crates.io-index" 1032 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1033 | 1034 | [[package]] 1035 | name = "wasm-bindgen" 1036 | version = "0.2.89" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" 1039 | dependencies = [ 1040 | "cfg-if", 1041 | "wasm-bindgen-macro", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "wasm-bindgen-backend" 1046 | version = "0.2.89" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" 1049 | dependencies = [ 1050 | "bumpalo", 1051 | "log", 1052 | "once_cell", 1053 | "proc-macro2", 1054 | "quote", 1055 | "syn", 1056 | "wasm-bindgen-shared", 1057 | ] 1058 | 1059 | [[package]] 1060 | name = "wasm-bindgen-macro" 1061 | version = "0.2.89" 1062 | source = "registry+https://github.com/rust-lang/crates.io-index" 1063 | checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" 1064 | dependencies = [ 1065 | "quote", 1066 | "wasm-bindgen-macro-support", 1067 | ] 1068 | 1069 | [[package]] 1070 | name = "wasm-bindgen-macro-support" 1071 | version = "0.2.89" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" 1074 | dependencies = [ 1075 | "proc-macro2", 1076 | "quote", 1077 | "syn", 1078 | "wasm-bindgen-backend", 1079 | "wasm-bindgen-shared", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "wasm-bindgen-shared" 1084 | version = "0.2.89" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" 1087 | 1088 | [[package]] 1089 | name = "web-sys" 1090 | version = "0.3.66" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" 1093 | dependencies = [ 1094 | "js-sys", 1095 | "wasm-bindgen", 1096 | ] 1097 | 1098 | [[package]] 1099 | name = "webpki" 1100 | version = "0.22.4" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" 1103 | dependencies = [ 1104 | "ring 0.17.7", 1105 | "untrusted 0.9.0", 1106 | ] 1107 | 1108 | [[package]] 1109 | name = "webpki-roots" 1110 | version = "0.26.0" 1111 | source = "registry+https://github.com/rust-lang/crates.io-index" 1112 | checksum = "0de2cfda980f21be5a7ed2eadb3e6fe074d56022bea2cdeb1a62eb220fc04188" 1113 | dependencies = [ 1114 | "rustls-pki-types", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "winapi" 1119 | version = "0.3.9" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1122 | dependencies = [ 1123 | "winapi-i686-pc-windows-gnu", 1124 | "winapi-x86_64-pc-windows-gnu", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "winapi-i686-pc-windows-gnu" 1129 | version = "0.4.0" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1132 | 1133 | [[package]] 1134 | name = "winapi-x86_64-pc-windows-gnu" 1135 | version = "0.4.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1138 | 1139 | [[package]] 1140 | name = "windows-sys" 1141 | version = "0.48.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1144 | dependencies = [ 1145 | "windows-targets 0.48.5", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "windows-sys" 1150 | version = "0.52.0" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1153 | dependencies = [ 1154 | "windows-targets 0.52.0", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "windows-targets" 1159 | version = "0.48.5" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1162 | dependencies = [ 1163 | "windows_aarch64_gnullvm 0.48.5", 1164 | "windows_aarch64_msvc 0.48.5", 1165 | "windows_i686_gnu 0.48.5", 1166 | "windows_i686_msvc 0.48.5", 1167 | "windows_x86_64_gnu 0.48.5", 1168 | "windows_x86_64_gnullvm 0.48.5", 1169 | "windows_x86_64_msvc 0.48.5", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "windows-targets" 1174 | version = "0.52.0" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 1177 | dependencies = [ 1178 | "windows_aarch64_gnullvm 0.52.0", 1179 | "windows_aarch64_msvc 0.52.0", 1180 | "windows_i686_gnu 0.52.0", 1181 | "windows_i686_msvc 0.52.0", 1182 | "windows_x86_64_gnu 0.52.0", 1183 | "windows_x86_64_gnullvm 0.52.0", 1184 | "windows_x86_64_msvc 0.52.0", 1185 | ] 1186 | 1187 | [[package]] 1188 | name = "windows_aarch64_gnullvm" 1189 | version = "0.48.5" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1192 | 1193 | [[package]] 1194 | name = "windows_aarch64_gnullvm" 1195 | version = "0.52.0" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 1198 | 1199 | [[package]] 1200 | name = "windows_aarch64_msvc" 1201 | version = "0.48.5" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1204 | 1205 | [[package]] 1206 | name = "windows_aarch64_msvc" 1207 | version = "0.52.0" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 1210 | 1211 | [[package]] 1212 | name = "windows_i686_gnu" 1213 | version = "0.48.5" 1214 | source = "registry+https://github.com/rust-lang/crates.io-index" 1215 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1216 | 1217 | [[package]] 1218 | name = "windows_i686_gnu" 1219 | version = "0.52.0" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 1222 | 1223 | [[package]] 1224 | name = "windows_i686_msvc" 1225 | version = "0.48.5" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1228 | 1229 | [[package]] 1230 | name = "windows_i686_msvc" 1231 | version = "0.52.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 1234 | 1235 | [[package]] 1236 | name = "windows_x86_64_gnu" 1237 | version = "0.48.5" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1240 | 1241 | [[package]] 1242 | name = "windows_x86_64_gnu" 1243 | version = "0.52.0" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 1246 | 1247 | [[package]] 1248 | name = "windows_x86_64_gnullvm" 1249 | version = "0.48.5" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1252 | 1253 | [[package]] 1254 | name = "windows_x86_64_gnullvm" 1255 | version = "0.52.0" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 1258 | 1259 | [[package]] 1260 | name = "windows_x86_64_msvc" 1261 | version = "0.48.5" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1264 | 1265 | [[package]] 1266 | name = "windows_x86_64_msvc" 1267 | version = "0.52.0" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 1270 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["ihciah "] 3 | description = "A proxy to expose real tls handshake to the MITM." 4 | edition = "2021" 5 | keywords = ["proxy", "tls", "shadowsocks"] 6 | license = "MIT/Apache-2.0" 7 | name = "shadow-tls" 8 | readme = "README.md" 9 | repository = "https://github.com/ihciah/shadow-tls" 10 | version = "0.2.25" 11 | 12 | [dependencies] 13 | monoio = { version = "0.2.0", features = ["sync"] } 14 | monoio-rustls-fork-shadow-tls = { version = "0.3.0-mod.2" } 15 | rustls-fork-shadow-tls = { version = "0.20.9-mod.2", default-features = false } 16 | 17 | anyhow = "1" 18 | byteorder = "1" 19 | clap = { version = "4", features = ["derive"] } 20 | ctrlc = { version = "3", features = ["termination"] } 21 | hmac = "0.12" 22 | local-sync = "0.1.0" 23 | pin-project-lite = "0.2" 24 | rand = "0.8" 25 | rustc-hash = "1" 26 | sha1 = "0.10" 27 | sha2 = "0.10" 28 | tracing = "0.1" 29 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 30 | webpki-roots = "0.26" 31 | serde = { version = "1", features = ["derive"] } 32 | serde_json = "1" 33 | 34 | [profile.release] 35 | lto = true 36 | opt-level = 3 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.74-alpine as builder 2 | WORKDIR /usr/src/shadow-tls 3 | RUN apk add --no-cache musl-dev libressl-dev 4 | 5 | COPY . . 6 | RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse RUSTFLAGS="" cargo build --bin shadow-tls --release 7 | 8 | FROM alpine:latest 9 | 10 | ENV MODE="" 11 | ENV LISTEN="" 12 | ENV SERVER="" 13 | ENV TLS="" 14 | ENV THREADS="" 15 | ENV PASSWORD="" 16 | ENV ALPN="" 17 | ENV DISABLE_NODELAY="" 18 | ENV FASTOPEN="" 19 | ENV V3="" 20 | ENV STRICT="" 21 | 22 | COPY ./entrypoint.sh / 23 | RUN chmod +x /entrypoint.sh && apk add --no-cache ca-certificates 24 | COPY --from=builder /usr/src/shadow-tls/target/release/shadow-tls /usr/local/bin/shadow-tls 25 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /Dockerfile.action: -------------------------------------------------------------------------------- 1 | FROM alpine:latest as builder 2 | 3 | ARG DOWNLOAD_BASE 4 | RUN ARCH=$(uname -m | sed -e "s/armv7l/armv7-unknown-linux-musleabihf/g" | sed -e "s/aarch64/aarch64-unknown-linux-musl/g" | sed -e "s/x86_64/x86_64-unknown-linux-musl/g") && \ 5 | apk add --no-cache curl && \ 6 | curl -L "${DOWNLOAD_BASE}/shadow-tls-${ARCH}" -o /shadow-tls && \ 7 | chmod +x /shadow-tls 8 | 9 | FROM alpine:latest 10 | 11 | ENV MODE="" 12 | ENV LISTEN="" 13 | ENV SERVER="" 14 | ENV TLS="" 15 | ENV THREADS="" 16 | ENV PASSWORD="" 17 | ENV ALPN="" 18 | ENV DISABLE_NODELAY="" 19 | ENV FASTOPEN="" 20 | ENV V3="" 21 | ENV STRICT="" 22 | 23 | COPY ./entrypoint.sh / 24 | RUN chmod +x /entrypoint.sh && apk add --no-cache ca-certificates 25 | COPY --from=builder /shadow-tls /usr/local/bin/shadow-tls 26 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 ihciah 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadow TLS 2 | [![Build Releases](https://github.com/ihciah/shadow-tls/actions/workflows/build-release.yml/badge.svg)](https://github.com/ihciah/shadow-tls/releases) [![Crates.io](https://img.shields.io/crates/v/shadow-tls.svg)](https://crates.io/crates/shadow-tls) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fihciah%2Fshadow-tls.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fihciah%2Fshadow-tls?ref=badge_shield) 3 | 4 | 一个**可以使用别人的受信证书**的 TLS 伪装代理。 5 | 6 | 它和 [trojan](https://github.com/trojan-gfw/trojan) 的表现类似,但它在做真实 TLS 握手的同时,可以直接使用别人的受信证书(如某些大公司或机构的域名),而不需要自己签发证书。当直接使用浏览器打开时,可以正常显示对应可信域名的网页内容。 7 | 8 | --- 9 | 10 | A proxy to expose real tls handshake to the firewall. 11 | 12 | It works like [trojan](https://github.com/trojan-gfw/trojan) but it does not require signing certificate. The firewall will see **real** tls handshake with **valid certificate** that you choose. 13 | 14 | ## How to Use It 15 | 这个服务需要双边部署,并且它一般需要搭配一个加密代理(因为本项目不包含数据加密和代理请求封装功能,这不是我们的目标)。 16 | 17 | 通常,你可以在同机部署 shadowsocks-server 和 shadowtls-server;之后在防火墙的另一端部署 shadowsocks-client 和 shadowtls-client。 18 | 19 | 有两种方式部署这个服务。 20 | 1. 使用 Docker + Docker Compose 21 | 22 | 修改 `docker-compose.yml` 后直接 `docker-compose up -d`。 23 | 2. 使用预编译的二进制 24 | 25 | 从 [Release 页面](https://github.com/ihciah/shadow-tls/releases)下载对应平台的二进制文件, 然后运行即可。运行指南可以 `./shadow-tls client --help` 或 `./shadow-tls server --help` 看到。 26 | 27 | 更详细的使用指南请参考 [Wiki](https://github.com/ihciah/shadow-tls/wiki/How-to-Run)。 28 | 29 | --- 30 | 31 | Normally you need to deploy this service on both sides of the firewall. And it is usually used with an encryption proxy (because this project does not include encryption and proxy request encapsulation, which is not our goal). 32 | 33 | 1. Run with Docker + Docker Compose 34 | Modfy `docker-compose.yml` and run `docker-compose up -d`. 35 | 36 | 2. Use prebuilt binary 37 | Download the binary from [Release page](https://github.com/ihciah/shadow-tls/releases) and run it. 38 | 39 | For more detailed usage guide, please refer to [Wiki](https://github.com/ihciah/shadow-tls/wiki/How-to-Run). 40 | 41 | ## How it Works 42 | On client side, just do tls handshake. And for server, we have to relay data as well as parsing tls handshake to handshaking server which will provide valid certificate. We need to know when the tls handshaking is finished. Once finished, we can relay data to our real server. 43 | 44 | Full design doc is here: [v2](./docs/protocol-en.md) | [v3](./docs/protocol-v3-en.md). 45 | 46 | 完整的协议设计: [v2](./docs/protocol-zh.md) | [v3](./docs/protocol-v3-zh.md). 47 | 48 | ## Note 49 | This project relies on [Monoio](https://github.com/bytedance/monoio) which is a high performance rust async runtime with io_uring. However, it does not support windows yet. So this project does not support windows. 50 | 51 | However, if this project is used widely, we will support it by conditional compiling. 52 | 53 | Also, you may need to [modify some system limitations](https://github.com/bytedance/monoio/blob/master/docs/en/memlock.md) to make it work. If it does not work, you can add environ `MONOIO_FORCE_LEGACY_DRIVER=1` to use epoll instead of io_uring. 54 | 55 | 你可能需要修改某些系统设置来让它工作,[参考这里](https://github.com/bytedance/monoio/blob/master/docs/en/memlock.md)。如果它不起作用,您可以添加环境变量 `MONOIO_FORCE_LEGACY_DRIVER=1` 以使用 epoll 而不是 io_uring。 56 | 57 | ## License 58 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fihciah%2Fshadow-tls.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fihciah%2Fshadow-tls?ref=badge_large) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | shadow-tls: 4 | image: ghcr.io/ihciah/shadow-tls:latest 5 | container_name: shadow-tls 6 | restart: always 7 | network_mode: "host" 8 | environment: 9 | - MODE= 10 | - LISTEN= 11 | - SERVER= 12 | - TLS= 13 | - PASSWORD= 14 | 15 | # Available environs: 16 | # MODE: client or server 17 | # LISTEN: local listen address with port 18 | # SERVER: remote address with port 19 | # TLS: domain name in sni for client mode(like xxx.com.cn) 20 | # shadow-tls server address with port for server mode(like xxx.com.cn:443) 21 | # PASSWORD: shadow-tls password 22 | # ALPN(optional): set alpns(like http/1.1, http/1.1;h2, recommend to leave it blank if you don't know it) 23 | # THREADS(optional): set threads number(recommend to leave it blank) 24 | # DISABLE_NODELAY(optional): disable TCP_NODELAY(recommend to leave it blank) 25 | # FASTOPEN(optional): enable TCP_FASTOPEN 26 | # WILDCARD_SNI: Use sni:443 as handshake server(off/authed/all) 27 | 28 | # Note: 29 | # Multiple SNIs is supported now. 30 | # For full help, see https://github.com/ihciah/shadow-tls/wiki/How-to-Run#parameters 31 | # Server side example: cloudflare.com:1.1.1.1:443;captive.apple.com;cloud.tencent.com 32 | # Client side example: captive.apple.com;cloud.tencent.com 33 | # If server is configured with multiple SNIs, server will extract ServerName and find 34 | # it in mapping, then use the corresponding backend or the fallback one. 35 | # If client is configured with multiple SNIs, client will pick one in random. -------------------------------------------------------------------------------- /docs/protocol-en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ShadowTLS Protocol Design 3 | date: 2022-10-02 11:00:00 4 | author: ihciah 5 | --- 6 | 7 | # Protocol 8 | 9 | There are two versions of protocol. 10 | 11 | The first version is a simple protocol which can only expose real tls handshaking to the man-in-the-middle, but can not defend against active detection, also the traffic after tls handshake is easy to distinguish. 12 | 13 | The second version is a more complex one, but it can defend against active detection, and the traffic after tls handshake is hard to distinguish. If attackers access the server with browser, it will work like real web servers. 14 | 15 | ![protocol design](../.github/resources/shadow-tls.png) 16 | 17 | ## V1 18 | In this version, we just want to expose the tls handshaking to the man-in-the-middle, with someone else's certificates. 19 | 20 | The man-in-the-middle will see traffics with valid and trusted certificates. Under the assumption that the man-in-the-middle will not perform any active detection, or analysis traffic after tls handshaking finished, this version can be used to defend against: 21 | 1. Blocking based on traffic characteristics: We looks like normal TLS traffic. 22 | 2. Blocking based on SNI: We use trusted certificates, so the SNI will be valid. 23 | 24 | The latest V1 implementation is [v0.1.4](https://github.com/ihciah/shadow-tls/releases/tag/v0.1.4). 25 | 26 | ### Client 27 | Clients connect server and do tls handshaking. After the handshaking, all client traffic will be sent to server without modification(including encrypting and data packing). 28 | 29 | 1. Do handshaking with example.trusted-server.domain. 30 | 2. Relay traffic without modification. 31 | 32 | However, real traffic after tls handshaking is packet with application data protocol. So the traffic is easy to distinguish. 33 | 34 | Now the traffic looks like a tls connection with trusted servers(since we can use trusted domains and certificates), but in fact its not a http service. There are far fewer servers using TLS but not http. 35 | 36 | ### Server 37 | Servers relay data between client and 2 backends. One backend is tls handshaking server which provide valid certificates and not belonging to us. The other backend is our real server. After the handshaking finished, server will relay traffic between client and real server. 38 | 39 | 1. Accept connections. 40 | 2. Relay traffic between client and tls handshaking server(like example.trusted-server.domain:443). Also it will watch the traffic to see if the handshaking finishes. 41 | 3. Relay traffic between client and our real server after handshaking finishes. 42 | 43 | Since we need to sense when the handshaking finishes, we can only use TLS1.2 for this version. 44 | 45 | ## V2 46 | This version is designed to solve the issues about traffic analysis and active detection. Also, it makes tls1.3 available. 47 | 48 | ### Client 49 | Clients connect server and do tls handshaking. After the handshaking: 50 | 1. All data will be packed within tls application data. 51 | 2. A 8-byte hmac is inserted in the front of the first application data packet. 52 | 53 | The hmac is calculated with all of the data that server sent, which can be used as a challenge. The hmac can be treated as a response to the challenge, used to identify. 54 | 55 | ### Server 56 | Servers relay data between client and 2 backends like V1. The different is: We do not watch the traffic to see if the handshaking finishes. Instead, we use the hmac. So its easier for us to parse the traffic and switch backends. Also, TLS1.3 or other versions are supported. 57 | 58 | 1. Accept connections. 59 | 2. Relay traffic between client and tls handshaking server(like example.trusted-server.domain:443). All data server sent will be used to calculate hmac. If some application data packet that client sent has valid hmac in its front, we will switch the traffic to our real server. 60 | 3. If not switched, the traffic will be relayed to tls handshaking server. Users with browser accessing the server will be able to access the http service on handshaking server. For efficiency, we can only proform hmac checking for the first N packets. 61 | -------------------------------------------------------------------------------- /docs/protocol-v3-en.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ShadowTLS V3 Protocol 3 | date: 2023-02-06 11:00:00 4 | updated: 2023-02-11 20:00:00 5 | author: ihciah 6 | --- 7 | 8 | # Version Evolution 9 | In August 2022 I implemented the first version of the ShadowTLS protocol. The goal of the V1 protocol was simple: to evade man-in-the-middle traffic discrimination by simply proxying the TLS handshake. v1 assumed that the man-in-the-middle would only observe handshake traffic, not subsequent traffic, not active probes, and not traffic hijacking. 10 | 11 | However, this assumption does not hold true. In order to defend against active probing, the V2 version of the protocol added a mechanism to verify the identity of the client by challenge-response; and added Application Data encapsulation to better disguise the traffic. 12 | 13 | The V2 version works well so far, and I have not encountered any problem of being blocked in daily use. After implementing support for multiple SNIs, it can even work as an SNI Proxy, which doesn't look like a proxy for data smuggling at all. 14 | 15 | But the V2 protocol still assumes that the middleman will not do traffic hijacking (refer to [issue](https://github.com/ihciah/shadow-tls/issues/30)). The cost of traffic hijacking is relatively high, and it is not widely used at present. The means of man-in-the-middle are still mainly bypass observation and injection, and active detection. However, this does not mean that traffic hijacking will not be used on a large scale in the future, and protocols designed to resist traffic hijacking must be a better solution. One of the biggest problems faced is that it is difficult for the server side to identify itself covertly. 16 | 17 | The [restls](https://github.com/3andne/restls) proposed in this [issue](https://github.com/ihciah/shadow-tls/issues/66) provides a very innovative idea. With this idea we can solve the server-side identity problem. 18 | 19 | In addition, I also mentioned in [this blog](https://www.ihcblog.com/a-better-tls-obfs-proxy/) some possible hijacking attacks against data encapsulation, which must be addressed by the V3 protocol. 20 | 21 | 22 | # V3 Protocol Principle 23 | 1. Capable of defending against traffic signature detection, active detection and traffic hijacking. 24 | 2. Easier to implement correctly. 25 | 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. 26 | 4. Keep it simple: only act as a TCP flow proxy, no duplicate wheel building. 27 | 28 | ## About support for TLS 1.2 29 | 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. 30 | 31 | 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. 32 | 33 | 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. 34 | 35 | # Handshake 36 | 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. 37 | 38 | The client's TLS Client constructs the ClientHello, which generates a custom SessionID. The length of the SessionID must be 32, the first 28 bits are random values, and the last 4 bits are the HMAC signature data of the ClientHello frame (without the 5-byte header of the TLS frame, the 4 bytes after the SessionID are filled with 0). The HMAC instance is for one-time use only, and the instance is created directly using the password. A Read Wrapper is also needed to extract the ServerRandom from ServerHello and forward the subsequent streams. 2. 39 | When the server receives the packet, it will authenticate the ClientHello, and if the authentication fails, it will continue the TCP relay with the handshake server. If the identification is successful, it will also forward it to the handshake server and continuously hijack the return stream from the handshake server. The server side will. 40 | 1. log the ServerRandom in the forwarded ServerHello. 41 | 2. do the following with the content portion of all ApplicationData frames. 42 | 1. transform the data to XOR SHA256 (PreSharedKey + ServerRandom). 2. 43 | 2. Add the 4 byte prefix `HMAC_ServerRandom(processed frame data)`, the HMAC instance should be filled with ServerRandom as the initial value, and this HMAC instance should be reused for subsequent ApplicationData forwarded from the handshake server. Note that the frame length needs to be + 4 at the same time. 3. 44 | The client's ReadWrapper needs to parse the ApplicationData frame and determine the first 4 byte HMAC: 1. 45 | 1. If `HMAC_ServerRandom(frame data)` is met, the server is proven to be reliable. These frames need to be filtered out after the handshake is complete. 2. 46 | 2. If `HMAC_ServerRandomS(frame data)` is met, it proves that the data has finished switching. The content part needs to be forwarded to the user side. 47 | 3. If none of them match, the traffic may have been hijacked and the handshake needs to be continued (or stopped if the handshake fails) and a random length HTTP request (muddled request) sent after a successful handshake and the connection closed properly after the response is read. 48 | 49 | ## Security Verification 50 | 1. When traffic is hijacked, Server will return data without doing XOR and Client will go straight to the muddling process. 51 | 2. ClientHello may be replayed but cannot use its correct handshake ([discussion of restls](https://github.com/3andne/restls/blob/main/Restls%3A%20%E5%AF%B9TLS%E6%8F%A1%E6%89%8B%E7%9A%84%E5%AE%8C%E7%BE%8E%E4%BC%AA%E8%A3%85.md)), so there is no way to identify whether the XOR data we return with a prefix is decodable. 52 | 2. If Client pretends the data is decrypted successfully and sends the data directly, it will not be able to pass because of the data frame checksum. 53 | 54 | # Data Encapsulation 55 | The V2 version of the data encapsulation protocol is in fact not resistant to traffic hijacking, e.g., the middleman may tamper with this part of the data after the handshake is completed, and we need to be able to respond to Alert; the middleman may also split one ApplicationData package into two as in the V2 protocol, which can also be used to identify the protocol if the connection is normal. 56 | 57 | To deal with traffic hijacking, in addition to optimizing the handshake process, the data encapsulation part also needs to be redesigned. We need to be able to authenticate the data stream and resist attacks such as replay, data tampering, data slicing, and data disorder. 58 | 59 | In addition to continuing to use ApplicationData encapsulation for the outermost layer of data, we added a 4 byte HMAC computed value to the inner layer. After we create the HMAC instance with the preshared key, we fill in `ServerRandom+"C"` or `ServerRandom+"S"` as the initial value, the former corresponds to the sent data stream of the Client, the latter corresponds to the sent data stream of the Server (the purpose is to prevent the man-in-the-middle from sending back the data we sent, or replaying (the purpose is to prevent the middleman from sending back the data we sent or replaying the data from different connections). In the forwarding process, the pure data is first filled into the HMAC instance, and then the 4 byte value is calculated and placed at the top of the pure data. The encapsulated data frame format: (5B tls frame header)(4B HMAC)(data). After encapsulation, the 4 byte data is fed into the HMAC instance (to avoid man-in-the-middle cut splicing requests). 60 | 61 | When the data checksum fails, we need to send a TLS Alert immediately to close the connection properly. We also need to be able to close the connection correctly when it is broken. 62 | 63 | ## Security Verification 64 | 1. For man-in-the-middle data tampering, HMAC will directly verify it and will respond to Alert. 65 | 2. For man-in-the-middle disorder attack, HMAC will directly verify it and respond to Alert. 66 | 3. For cut and splice attack (merging two AppData), although HMAC is processing the data stream, we can interrupt two consecutive streams to defend against this attack because we update in an additional 4 byte value after the processing is completed. 67 | 68 | # Implementation Guide 69 | ## Client 70 | The client is responsible for TLS handshaking, switching and doing data encapsulation and decapsulation after the switch. 71 | 72 | The client needs to have a built-in TLS Client and a Read Wrapper on the read side of the network stream: TLSClient <- ReadWrapper <- TCPStream; similarly, a Write Wrapper needs to be attached to the write data link: TLSClient -> WriteWrapper --> TCPStream. 73 | 74 | Stage1: TLS handshake 75 | Construct and sign a custom SessionID from the TLS library. 2. 76 | ReadWrapper. 1: 77 | 1. Extract ServerRandom from ServerHello; create `HMAC_ServerRandom`. 2. 78 | 2. Use `HMAC_ServerRandom` for ApplicationData to determine if the HMAC of the frame content (without the 4byte HMAC) matches the first 4 bytes. If it matches, rewrite the data frame content to its XOR SHA256(PreSharedKey + ServerRandom) and remove the first 4 byte HMAC value. If it does not match, no changes are made and the connection is marked as hijacked and a muddled request is sent after a successful handshake; if the handshake fails, no processing is done. 79 | 80 | Stage2: Data forwarding (this process does not rely on TLS library) 81 | 1. Create `HMAC_ServerRandomC` and `HMAC_ServerRandomS`. 2. 82 | 2. Parse Application Data wrapping when reading the connection and verify the first 4 bytes of data using `HMAC_ServerRandomS` and `HMAC_ServerRandom`. 83 | 1. `HMAC_ServerRandom` passes the validation, which means this is the residual handshake data, so it is ignored. 84 | 2. `HMAC_ServerRandomS` passes the validation, it means this is our own data encapsulation, after that `HMAC_ServerRandom` branch is disabled and the data is forwarded to the user (without HMAC) 85 | 3. if none of them pass, the data will be handled as Alert bad_record_mac. 4. 86 | Note: Here the Server is allowed to send residual TLS frames to the Client after the Client has finished switching, and the Client is responsible for filtering them out. This is because Server does not strongly sense the end of TLS handshake, it only senses the data sent by Client after the switchover. 3. 87 | 3. add Application Data and HMAC when writing connection, HMAC is calculated by `HMAC_ServerRandomC`. 88 | 89 | ## Server-side 90 | The server is responsible for forwarding the TLS handshake, determining the timing of the switch, and encapsulating and decapsulating the data after the switch, without relying on the TLS library. 91 | 92 | Stage1: Forwarding the TLS handshake 93 | 1. Read ClientHello: extract and identify the SessionID in ClientHello, if it does not pass, mark it as active detection traffic and start TCP forwarding directly (if it implements multiple SNIs, it also needs to resolve the SNIs and do the corresponding splitting and forwarding); if it passes, it also forwards the data frame and continues to the second step. 94 | 2. Read ServerHello from the other side: extract ServerRandom. 95 | Start two-way forwarding (with Handshake Server). 96 | 1. Create `HMAC_ServerRandomC` and `HMAC_ServerRandom`. 97 | 2. ShadowTLS Client -> Handshake Server: Forward directly until a frame matching the first 4 bytes of ApplicationData matches the `HMAC_ServerRandomC` signature is encountered, then stop forwarding in both directions (but ensure the integrity of the frames remaining in transit). 98 | 3. Handshake Server -> ShadowTLS Client: modify the Application Data frame by doing XOR SHA256 (PreSharedKey + ServerRandom) on the data and adding 4 byte HMAC in the header (calculated by `HMAC_ ServerRandom`). 99 | 100 | Stage2: Data forwarding (with Data Server) 101 | 1. Create `HMAC_ServerRandomS`. 102 | 2. ShadowTLS Client -> Data Server: Parse Application Data encapsulation and verify the first 4 bytes of data with `HMAC_ServerRandomC`. If it fails, it will be treated as Alert bad_record_mac. After validation is complete, the 4 bytes of data are also entered into the `HMAC_ServerRandomC` instance. 103 | 3. Data Server -> ShadowTLS Client: Add Application Data with HMAC, which is calculated by `HMAC_ServerRandomS`. The 4 bytes of data is then also entered into the `HMAC_ServerRandomS` instance. -------------------------------------------------------------------------------- /docs/protocol-v3-zh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ShadowTLS V3 协议设计 3 | date: 2023-02-06 11:00:00 4 | updated: 2023-02-06 11:00:00 5 | author: ihciah 6 | --- 7 | 8 | # 版本演进 9 | 在 2022 年 8 月的时候我实现了第一版 ShadowTLS 协议。当时 V1 协议的目标非常简单,仅仅通过代理 TLS 握手来逃避中间人对流量的判别。V1 协议假定中间人只会观察握手流量,不会观察后续流量、不会做主动探测,也不会做流量劫持。 10 | 11 | 但这个假设并不成立。为了防御主动探测,在 V2 版本的协议中添加了通过 challenge-response 方式来验证客户端身份的机制;并新增了 Application Data 封装来更好地伪装流量。 12 | 13 | V2 版本目前工作良好,在日常使用中我没有遇到被封锁等问题。在实现了对多 SNI 的支持后,它甚至可以作为一个 SNI Proxy 工作,看起来完全不像是一个偷渡数据用的代理。 14 | 15 | 但是 V2 协议仍假设中间人不会对流量做劫持(参考 [issue](https://github.com/ihciah/shadow-tls/issues/30))。流量劫持成本比较高,目前没有被广泛应用,目前中间人的手段仍以旁路观测和注入以及主动探测为主。但这并不意味这未来流量劫持不会被大规模使用,协议设计上能够抵御流量劫持一定是更好的方案。面临的最大的一个问题是,服务端很难隐蔽地表明身份。 16 | 17 | 这个 [issue](https://github.com/ihciah/shadow-tls/issues/66) 提出的 [restls](https://github.com/3andne/restls) 提供了一个极具创新性的思路。借鉴这个思路我们可以解决服务端身份鉴定问题。 18 | 19 | 除此之外,我在[这篇博客](https://www.ihcblog.com/a-better-tls-obfs-proxy/)里也提到了一些针对数据封装的可能的劫持攻击,这也是 V3 协议必须解决的问题。 20 | 21 | # V3 协议目标 22 | 1. 能够防御流量特征检测、主动探测和流量劫持。 23 | 2. 更易于正确实现。 24 | 3. 尽可能地弱感知 TLS 协议本身,实现者无需 Hack TLS 库,更不需要自行实现 TLS 协议。 25 | 4. 保持简单:仅作为 TCP 流代理,不重复造轮子。 26 | 27 | ## 关于对 TLS 1.2 的支持 28 | V3 协议在严格模式下仅支持使用 TLS1.3 的握手服务器。你可以使用 `openssl s_client -tls1_3 -connect example.com:443` 来探测一个服务器是否支持 TLS1.3。 29 | 30 | 如果要支持 TLS1.2,需要感知更多 TLS 协议细节,实现起来会更加复杂;鉴于 TLS1.3 已经有较多厂商使用,我们决定在严格模式下仅支持 TLS1.3。 31 | 32 | 考虑到兼容性和部分对防御连接劫持需求较低的场景(如使用特定 SNI 绕过计费系统),在非严格模式下允许使用 TLS1.2。 33 | 34 | # 握手流程 35 | 这部分协议设计借鉴 [restls](https://github.com/3andne/restls) 但存在一定差别:弱化了对 TLS 细节的感知,更易于实现。 36 | 37 | 1. 客户端的 TLS Client 构造 ClientHello,ClientHello 需要生成自定义的 SessionID。SessionID 长度需为 32,前 28 位是随机值,后 4 位是 ClientHello 帧(不含 TLS 帧的 5 字节头,SessionID 后 4 byte 填充 0)的 HMAC 签名数据。HMAC 实例仅为一次性使用,直接使用密码创建实例。同时需要一个 Read Wrapper 负责提取 ServerHello 中的 ServerRandom 并转发后续流。 38 | 2. 服务端收到包后,会对 ClientHello 做鉴定,如果鉴定失败则直接持续性与握手服务器进行 TCP 中继。如果鉴定成功,也会将其转发至握手服务器,并持续劫持握手服务器的返回数据流。服务端会: 39 | 1. 记录转发的 ServerHello 中的 ServerRandom。 40 | 2. 对所有 ApplicationData 帧的内容部分做处理: 41 | 1. 对数据做变换,将其 XOR SHA256(PreSharedKey + ServerRandom)。 42 | 2. 添加 4 byte 前缀 `HMAC_ServerRandom(处理后的帧数据)`,HMAC 实例需事先灌入 ServerRandom 作为初始值,对此后从握手服务器转发的 ApplicationData 需要复用这个 HMAC 实例。注意帧长度需要同时 + 4。 43 | 3. 客户端的 ReadWrapper 需要解析 ApplicationData 帧,判定前 4 byte HMAC: 44 | 1. 符合 `HMAC_ServerRandom(帧数据)`,则证明服务端是可靠的。在握手完成后这类帧需要过滤掉。 45 | 2. 符合 `HMAC_ServerRandomS(帧数据)`,则证明数据已经完成切换。需要将内容部分转发至用户侧。 46 | 3. 都不符合,此时可能流量已被劫持,需要继续握手(握手失败则作罢),并在握手成功后发送一个长度随机的 HTTP 请求(糊弄性请求),在读取完响应后正确关闭连接。 47 | 48 | ## 安全性验证 49 | 1. 流量劫持时,Server 会返回没有做 XOR 的数据,Client 会直接进入糊弄流程。 50 | 2. ClientHello 可能会被重放,但无法使用其正确握手([restls 的讨论](https://github.com/3andne/restls/blob/main/Restls%3A%20%E5%AF%B9TLS%E6%8F%A1%E6%89%8B%E7%9A%84%E5%AE%8C%E7%BE%8E%E4%BC%AA%E8%A3%85.md)),所以无法鉴别我们返回的带前缀的 XOR 数据是否可解密。 51 | 2. 若 Client 假装数据解密成功,直接发送数据,由于存在数据帧校验,其也无法通过。 52 | 53 | # 数据封装 54 | V2 版本的数据封装协议事实上也无法抵御流量劫持,如中间人可能会在握手完成后对这部分数据做篡改,我们需要能够响应 Alert;中间人也可能会按照 V2 协议的样子将一个 ApplicationData 封装拆成两个,如果连接正常,则也可以用于识别协议。 55 | 56 | 要应对流量劫持,除了要优化握手流程,数据封装部分也要重新设计。我们需要能够对数据流做验证,并且抵御重放、数据篡改、数据切分、数据乱序等攻击。 57 | 58 | 数据除了最外层继续使用 ApplicationData 封装外,内层添加了 4 byte 的 HMAC 计算值。我们在使用 preshared key 创建 HMAC 实例后,会灌入 `ServerRandom+"C"` 或 `ServerRandom+"S"` 作为初始值,前者对应 Client 的发送数据流,后者对应 Server 的发送数据流(目的是防止中间人将我们发送的数据发回来,或者将不同连接的数据重放)。在转发过程中,首先将纯数据灌入 HMAC 实例,之后计算 4 byte 值后放于纯数据最前面。封装出的数据帧格式:(5B tls frame header)(4B HMAC)(data)。封装结束后将 4 byte 数据输入 HMAC 实例(避免中间人剪切拼接请求)。 59 | 60 | 当数据校验失败时,我们需要立刻发送 TLS Alert 正确关闭连接。在连接断开时也需要能够正确关闭。 61 | 62 | ## 安全性验证 63 | 1. 对于中间人的数据篡改,HMAC 会直接验证出来,会响应 Alert。 64 | 2. 对于中间人乱序攻击,HMAC 会直接验证出来,会响应 Alert。 65 | 3. 对于剪切拼接攻击(合并两个 AppData),虽然 HMAC 处理的是数据流,但是由于我们在处理完成后又额外 update 进去一个 4 byte 的值,所以可以打断两个连续的流,防御这种攻击。 66 | 67 | # 实现指南 68 | ## 客户端 69 | 客户端负责 TLS 握手、切换并在切换后做数据封装和解封装。 70 | 71 | 客户端需要内置一个 TLS Client,并在其对网络流读时加一层 Read Wrapper:TLSClient <- ReadWrapper <- TCPStream;同样,在写数据链路上也需要附加一个 Write Wrapper:TLSClient -> WriteWrapper -> TCPStream。 72 | 73 | Stage1: TLS 握手 74 | 1. 通过 TLS 库构造自定义 SessionID 并签名。 75 | 2. ReadWrapper: 76 | 1. 提取 ServerHello 中的 ServerRandom;创建 `HMAC_ServerRandom`。 77 | 2. 对 ApplicationData 使用 `HMAC_ServerRandom` 判定帧内容(不含 4byte HMAC)的 HMAC 与前 4 byte 是否匹配。若匹配则重写数据帧内容为其 XOR SHA256(PreSharedKey + ServerRandom),并去掉前 4 byte HMAC 值。若不匹配则不做修改,并标记该连接被劫持,握手成功后发送糊弄性请求;握手失败则不做处理。 78 | 79 | Stage2: 数据转发(该过程不依赖 TLS 库) 80 | 1. 创建 `HMAC_ServerRandomC` 和 `HMAC_ServerRandomS`。 81 | 2. 读连接时 Parse Application Data 封装,并利用 `HMAC_ServerRandomS` 和 `HMAC_ServerRandom` 验证数据前 4 byte。 82 | 1. `HMAC_ServerRandom` 通过验证则表示这个是握手残留数据,直接忽略。 83 | 2. `HMAC_ServerRandomS` 通过验证则表示这个是我们自己的数据封装,此后禁用 `HMAC_ServerRandom` 分支判定,并将数据转发至用户(不含 HMAC) 84 | 3. 均未通过,则按照 Alert bad_record_mac 处理。 85 | 4. 说明:这里允许 Server 在 Client 完成切换后向 Client 发送残留的 TLS 帧,客户端需要负责过滤掉。因为 Server 并不强感知 TLS 握手结束,其仅感知 Client 发送切换后的数据。 86 | 3. 写连接时添加 Application Data 与 HMAC,HMAC 通过 `HMAC_ServerRandomC` 计算得到。 87 | 88 | ## 服务端 89 | 服务端负责转发 TLS 握手、判定切换时机并在切换后做数据封装和解封装,它不依赖 TLS 库。 90 | 91 | Stage1: 转发 TLS 握手 92 | 1. 读 ClientHello: 提取并鉴定 ClientHello 中的 SessionID,若未通过则标记为主动探测流量,后续直接启动 TCP 转发(实现多 SNI 的话还需要解析 SNI 并做对应分流转发);若通过也会转发该数据帧,并继续第二步。 93 | 2. 从另一侧读 ServerHello: 提取 ServerRandom。 94 | 3. 启动双向转发(with Handshake Server): 95 | 1. 创建 `HMAC_ServerRandomC` 和 `HMAC_ServerRandom`。 96 | 2. ShadowTLS Client -> Handshake Server: 直接转发,直到遇到符合 ApplicationData 前 4 byte 符合 `HMAC_ServerRandomC` 签名结果的数据帧,此时停止双向转发(但需要保证残留正在发送中的帧的完整性)。 97 | 3. Handshake Server -> ShadowTLS Client: 对 Application Data 帧做修改,对数据做 XOR SHA256(PreSharedKey + ServerRandom) 之后在头部添加 4 byte HMAC(由 `HMAC_ServerRandom` 计算)。 98 | 99 | Stage2: 数据转发(with Data Server) 100 | 1. 创建 `HMAC_ServerRandomS`。 101 | 2. ShadowTLS Client -> Data Server: Parse Application Data 封装,并利用 `HMAC_ServerRandomC` 验证数据前 4 byte。若未通过,则按照 Alert bad_record_mac 处理。验证完成后,将该 4 字节数据也输入到 `HMAC_ServerRandomC` 实例中。 102 | 3. Data Server -> ShadowTLS Client: 添加 Application Data 与 HMAC,HMAC 通过 `HMAC_ServerRandomS` 计算得到。之后将该 4 字节数据也输入到 `HMAC_ServerRandomS` 实例中。 103 | -------------------------------------------------------------------------------- /docs/protocol-zh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ShadowTLS 协议设计 3 | date: 2022-10-02 11:00:00 4 | author: ihciah 5 | --- 6 | 7 | # 协议 8 | 9 | ShadowTLS 协议有两个版本。 10 | 11 | 第一个版本是一个简单的协议,只能将真正的 TLS 握手暴露给中间人,但不能防御主动检测,而且 TLS 握手后的流量也很容易区分。 12 | 13 | 第二个版本比较复杂,但是可以防御主动检测,tls握手后的流量很难区分。如果攻击者使用浏览器访问服务端,它能将像真正的 Web 服务器一样工作。 14 | 15 | ![protocol design](../.github/resources/shadow-tls.png) 16 | 17 | ## V1 18 | 在这个版本中,我们只想让中间人观测到 TLS 握手,并且握手使用其他人的合法证书。这是我们最初的目标。 19 | 20 | 中间人将看到有效的受信任证书的握手流量。如果假设中间人不会主动探测,并且不会分析在 TLS 握手完成后的流量,这个版本可以用来防御: 21 | 1. 基于流量特征的封锁:我们看起来像正常的 TLS 流量,而不是 shadowsocks 那样的无法理解的随机数据。 22 | 2. 基于 SNI 的封锁:我们使用合法且受信任的证书(比如可能某些大型公司或政府机构的域名是被标记为受信任的),因此会被认为是合法数据。 23 | 24 | V1 版本的最后一个实现是 [v0.1.4](https://github.com/ihciah/shadow-tls/releases/tag/v0.1.4). 25 | 26 | ### 客户端 27 | 客户端连接服务器并进行 TLS 握手。握手后,所有客户端流量将不加修改地发送到服务器(包括加密和数据封装等)。 28 | 29 | 1. 与 example.trusted-server.domain 进行 TLS 握手。 30 | 2. 中继流量(不加修改)。 31 | 32 | 但是,TLS 握手后的实际流量是带有 Application Data 封装的数据包,所以流量很容易区分。 33 | 34 | 现在流量看起来像是与受信任服务器的 TLS 连接(因为我们可以使用受信任的域和证书),但实际上它不是 http 服务,而使用 TLS 但不使用 http 的服务器非常少,这可能也会是一个特征。 35 | 36 | ### 服务端 37 | 服务器在客户端和 2 个后端之间中继数据。一个后端是 TLS 握手服务器,它提供有效的证书(不属于我们);另一个后端是我们的数据服务器。握手完成后,服务器将在客户端和数据服务器之间中继流量。 38 | 39 | 1. 接受连接。 40 | 2. 在客户端和 TLS 握手服务器之间中继流量(例如 example.trusted-server.domain:443)。它还将监视流量以查看握手是否完成。 41 | 3. 握手完成后在客户端和我们的真实服务器之间中继流量。 42 | 43 | 由于我们需要感知握手结束,所以这个版本我们只能使用 TLS1.2。 44 | 45 | ## V2 46 | 该版本旨在解决流量分析和主动检测问题。此外,它可以支持 TLS 1.3。 47 | 48 | ### 客户端 49 | 客户端连接服务器并进行 TLS 握手。 握手后: 50 | 1. 所有数据都将打包在 TLS Application Data 中。 51 | 2. 在第一个 Application Data 包的数据的最前面插入一个 8 字节的 hmac。 52 | 53 | hmac 是使用服务器发送的所有数据计算的(服务器发送的所有数据被用作 challenge,因为客户端无法完全控制它,并且有一定随机性)。hmac 可以被视为对 challenge 的响应,用于识别。 54 | 55 | ### 服务端 56 | 服务器在客户端和 2 个后端(模式和 V1 一样)之间中继数据。 不同的是:我们不会观察流量来查看握手是否完成;相反,我们使用 hmac。所以我们更容易解析流量和切换后端。此外,还支持 TLS1.3 或其他版本。 57 | 58 | 1. 接受连接。 59 | 2. 在客户端和 tls 握手服务器之间中继流量(例如 example.trusted-server.domain:443)。服务器发送的所有数据将用于计算 hmac。如果客户端发送的某个 Application Data 包的前缀有有效的hmac,我们会将流量切换到我们的数据服务器。 60 | 3. 如果不切换,流量将被中继到 TLS 握手服务器。使用浏览器访问服务器的用户将能够访问握手服务器上的 http 服务。为了效率,我们只能对前 N 个数据包进行 hmac 检查(在我们的实现中 N = 3,hmac 算法为 HMAC-SHA1)。 61 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | date: 2022-10-02 11:00:00 4 | author: ihciah 5 | --- 6 | 7 | TODO:待完善 8 | 9 | ## 参考的 docker-compose 文件 10 | 命名为 `docker-compose.yml` 后可以在该文件夹下 `docker-compose up -d` 启动。 11 | 12 | Server: 13 | ```yaml 14 | version: '2.4' 15 | services: 16 | shadowsocks: 17 | image: shadowsocks/shadowsocks-libev 18 | container_name: shadowsocks-raw 19 | restart: always 20 | network_mode: "host" 21 | environment: 22 | - SERVER_PORT=24000 23 | - SERVER_ADDR=127.0.0.1 24 | - METHOD=chacha20-ietf-poly1305 25 | - PASSWORD=EXAMPLE_PASSWORD_CHANGE_IT 26 | shadow-tls: 27 | image: ghcr.io/ihciah/shadow-tls:latest 28 | restart: always 29 | network_mode: "host" 30 | environment: 31 | - MODE=server 32 | - LISTEN=0.0.0.0:8443 33 | - SERVER=127.0.0.1:24000 34 | - TLS=cloud.tencent.com:443 35 | - PASSWORD=CHANGE_IT_123321 36 | ``` 37 | 38 | Client: 39 | ```yaml 40 | version: '2.4' 41 | services: 42 | shadow-tls: 43 | image: ghcr.io/ihciah/shadow-tls:latest 44 | restart: always 45 | network_mode: "host" 46 | environment: 47 | - MODE=client 48 | - LISTEN=0.0.0.0:24000 49 | - SERVER=your_vps_ip:8443 50 | - TLS=cloud.tencent.com 51 | - PASSWORD=CHANGE_IT_123321 52 | ``` 53 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | parameter="" 3 | if [ ! -z "$THREADS" ] 4 | then 5 | parameter="$parameter --threads $THREADS" 6 | fi 7 | 8 | if [ ! -z "$DISABLE_NODELAY" ] 9 | then 10 | parameter="$parameter --disable-nodelay" 11 | fi 12 | 13 | if [ ! -z "$FASTOPEN" ] 14 | then 15 | parameter="$parameter --fastopen" 16 | fi 17 | 18 | if [ ! -z "$V3" ] 19 | then 20 | parameter="$parameter --v3" 21 | fi 22 | 23 | if [ ! -z "$STRICT" ] 24 | then 25 | parameter="$parameter --strict" 26 | fi 27 | 28 | if [ "$MODE" = "server" ] 29 | then 30 | parameter="$parameter $MODE" 31 | 32 | if [ ! -z "$TLS" ] 33 | then 34 | parameter="$parameter --tls $TLS" 35 | fi 36 | 37 | if [ ! -z "$WILDCARD_SNI" ] 38 | then 39 | parameter="$parameter --wildcard-sni $WILDCARD_SNI" 40 | fi 41 | fi 42 | 43 | if [ "$MODE" = "client" ] 44 | then 45 | parameter="$parameter $MODE" 46 | 47 | if [ ! -z "$TLS" ] 48 | then 49 | parameter="$parameter --sni $TLS" 50 | fi 51 | 52 | if [ ! -z "$ALPN" ] 53 | then 54 | parameter="$parameter --alpn $ALPN" 55 | fi 56 | fi 57 | 58 | if [ ! -z "$SERVER" ] 59 | then 60 | parameter="$parameter --server $SERVER" 61 | fi 62 | 63 | if [ ! -z "$LISTEN" ] 64 | then 65 | parameter="$parameter --listen $LISTEN" 66 | fi 67 | 68 | if [ ! -z "$PASSWORD" ] 69 | then 70 | parameter="$parameter --password $PASSWORD" 71 | fi 72 | 73 | exec shadow-tls $parameter -------------------------------------------------------------------------------- /examples/client_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "disable_nodelay": false, 3 | "fastopen": true, 4 | "v3": true, 5 | "strict": true, 6 | "client": { 7 | "listen": "localhost:8080", 8 | "server_addr": "256.256.256.256:443", 9 | "tls_names": "captive.apple.com;cloudflare.com", 10 | "password": "12345678" 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /examples/server_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "disable_nodelay": false, 3 | "fastopen": true, 4 | "v3": true, 5 | "strict": true, 6 | "server": { 7 | "listen": "localhost:8443", 8 | "server_addr": "localhost:24000", 9 | "tls_addr": { 10 | "wildcard_sni": "off", 11 | "dispatch": { 12 | "cloudflare.com": "1.1.1.1:443", 13 | "captive.apple.com": "captive.apple.com:443" 14 | }, 15 | "fallback": "cloud.tencent.com:443" 16 | }, 17 | "password": "12345678", 18 | "wildcard_sni": "authed" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | nightly -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | future::Future, 3 | ptr::{copy, copy_nonoverlapping}, 4 | rc::Rc, 5 | sync::Arc, 6 | }; 7 | 8 | use anyhow::bail; 9 | use byteorder::{BigEndian, WriteBytesExt}; 10 | use monoio::{ 11 | buf::IoBufMut, 12 | io::{AsyncReadRent, AsyncReadRentExt, AsyncWriteRent, AsyncWriteRentExt, Splitable}, 13 | net::{TcpConnectOpts, TcpStream}, 14 | BufResult, 15 | }; 16 | use monoio_rustls_fork_shadow_tls::TlsConnector; 17 | use rand::{prelude::Distribution, seq::SliceRandom, Rng}; 18 | use rustls_fork_shadow_tls::{OwnedTrustAnchor, RootCertStore, ServerName}; 19 | use serde::{de::Visitor, Deserialize}; 20 | 21 | use crate::{ 22 | helper_v2::{copy_with_application_data, copy_without_application_data, HashedReadStream}, 23 | util::{ 24 | bind_with_pretty_error, kdf, mod_tcp_conn, prelude::*, resolve, support_tls13, 25 | verified_relay, xor_slice, Hmac, V3Mode, 26 | }, 27 | }; 28 | 29 | const FAKE_REQUEST_LENGTH_RANGE: (usize, usize) = (16, 64); 30 | 31 | /// ShadowTlsClient. 32 | #[derive(Clone)] 33 | pub struct ShadowTlsClient { 34 | listen_addr: Arc, 35 | target_addr: Arc, 36 | tls_connector: TlsConnector, 37 | tls_names: Arc, 38 | password: Arc, 39 | nodelay: bool, 40 | fastopen: bool, 41 | v3: V3Mode, 42 | } 43 | 44 | #[derive(Clone, Debug, PartialEq)] 45 | pub struct TlsNames(Vec); 46 | 47 | impl TlsNames { 48 | #[inline] 49 | pub fn random_choose(&self) -> &ServerName { 50 | self.0.choose(&mut rand::thread_rng()).unwrap() 51 | } 52 | } 53 | 54 | impl TryFrom<&str> for TlsNames { 55 | type Error = anyhow::Error; 56 | 57 | fn try_from(value: &str) -> Result { 58 | let v: Result, _> = value.trim().split(';').map(ServerName::try_from).collect(); 59 | let v = v.map_err(Into::into).and_then(|v| { 60 | if v.is_empty() { 61 | Err(anyhow::anyhow!("empty tls names")) 62 | } else { 63 | Ok(v) 64 | } 65 | })?; 66 | Ok(Self(v)) 67 | } 68 | } 69 | 70 | struct TlsNamesVisitor; 71 | 72 | impl<'de> Visitor<'de> for TlsNamesVisitor { 73 | type Value = TlsNames; 74 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 75 | formatter.write_str("a semicolon seperated list of domains and ip addresses") 76 | } 77 | 78 | fn visit_str(self, v: &str) -> Result 79 | where 80 | E: serde::de::Error, 81 | { 82 | match Self::Value::try_from(v) { 83 | Err(e) => Err(E::custom(e.to_string())), 84 | Ok(u) => Ok(u), 85 | } 86 | } 87 | 88 | fn visit_string(self, v: String) -> Result 89 | where 90 | E: serde::de::Error, 91 | { 92 | Self.visit_str(&v) 93 | } 94 | } 95 | 96 | impl<'de> Deserialize<'de> for TlsNames { 97 | fn deserialize(deserializer: D) -> Result 98 | where 99 | D: serde::Deserializer<'de>, 100 | { 101 | deserializer.deserialize_string(TlsNamesVisitor) 102 | } 103 | } 104 | 105 | impl std::fmt::Display for TlsNames { 106 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 107 | write!(f, "{:?}", self.0) 108 | } 109 | } 110 | 111 | #[derive(Default, Debug)] 112 | pub struct TlsExtConfig { 113 | alpn: Option>>, 114 | } 115 | 116 | impl TlsExtConfig { 117 | #[allow(unused)] 118 | #[inline] 119 | pub fn new(alpn: Option>>) -> TlsExtConfig { 120 | TlsExtConfig { alpn } 121 | } 122 | } 123 | 124 | impl From>> for TlsExtConfig { 125 | fn from(maybe_alpns: Option>) -> Self { 126 | Self { 127 | alpn: maybe_alpns.map(|alpns| alpns.into_iter().map(Into::into).collect()), 128 | } 129 | } 130 | } 131 | 132 | impl std::fmt::Display for TlsExtConfig { 133 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 134 | match self.alpn.as_ref() { 135 | Some(alpns) => { 136 | write!(f, "ALPN(Some(")?; 137 | for alpn in alpns.iter() { 138 | write!(f, "{},", String::from_utf8_lossy(alpn))?; 139 | } 140 | write!(f, "))")?; 141 | } 142 | None => { 143 | write!(f, "ALPN(None)")?; 144 | } 145 | } 146 | Ok(()) 147 | } 148 | } 149 | 150 | impl ShadowTlsClient { 151 | /// Create new ShadowTlsClient. 152 | #[allow(clippy::too_many_arguments)] 153 | pub fn new( 154 | listen_addr: String, 155 | target_addr: String, 156 | tls_names: TlsNames, 157 | tls_ext_config: TlsExtConfig, 158 | password: String, 159 | nodelay: bool, 160 | fastopen: bool, 161 | v3: V3Mode, 162 | ) -> anyhow::Result { 163 | let mut root_store = RootCertStore::empty(); 164 | root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| { 165 | OwnedTrustAnchor::from_subject_spki_name_constraints( 166 | ta.subject.as_ref(), 167 | ta.subject_public_key_info.as_ref(), 168 | ta.name_constraints.as_ref().map(|n| n.as_ref()), 169 | ) 170 | })); 171 | // TLS 1.2 and TLS 1.3 is enabled. 172 | let mut tls_config = rustls_fork_shadow_tls::ClientConfig::builder() 173 | .with_safe_defaults() 174 | .with_root_certificates(root_store) 175 | .with_no_client_auth(); 176 | 177 | // Set tls config 178 | if let Some(alpn) = tls_ext_config.alpn { 179 | tls_config.alpn_protocols = alpn; 180 | } 181 | 182 | let tls_connector = TlsConnector::from(tls_config); 183 | 184 | Ok(Self { 185 | listen_addr: Arc::new(listen_addr), 186 | target_addr: Arc::new(target_addr), 187 | tls_connector, 188 | tls_names: Arc::new(tls_names), 189 | password: Arc::new(password), 190 | nodelay, 191 | fastopen, 192 | v3, 193 | }) 194 | } 195 | 196 | /// Serve a raw connection. 197 | pub async fn serve(self) -> anyhow::Result<()> { 198 | let listener = bind_with_pretty_error(self.listen_addr.as_ref(), self.fastopen)?; 199 | let shared = Rc::new(self); 200 | loop { 201 | match listener.accept().await { 202 | Ok((mut conn, addr)) => { 203 | tracing::info!("Accepted a connection from {addr}"); 204 | let client = shared.clone(); 205 | mod_tcp_conn(&mut conn, true, shared.nodelay); 206 | monoio::spawn(async move { 207 | let _ = match client.v3.enabled() { 208 | false => client.relay_v2(conn).await, 209 | true => client.relay_v3(conn).await, 210 | }; 211 | tracing::info!("Relay for {addr} finished"); 212 | }); 213 | } 214 | Err(e) => { 215 | tracing::error!("Accept failed: {e}"); 216 | } 217 | } 218 | } 219 | } 220 | 221 | /// Main relay for V2 protocol. 222 | async fn relay_v2(&self, in_stream: TcpStream) -> anyhow::Result<()> { 223 | let (out_stream, hash, session) = self.connect_v2().await?; 224 | let mut hash_8b = [0; 8]; 225 | unsafe { std::ptr::copy_nonoverlapping(hash.as_ptr(), hash_8b.as_mut_ptr(), 8) }; 226 | let (out_r, mut out_w) = out_stream.into_split(); 227 | let (mut in_r, mut in_w) = in_stream.into_split(); 228 | let mut session_filtered_out_r = crate::helper_v2::SessionFilterStream::new(session, out_r); 229 | let (a, b) = monoio::join!( 230 | copy_without_application_data(&mut session_filtered_out_r, &mut in_w), 231 | copy_with_application_data(&mut in_r, &mut out_w, Some(hash_8b)) 232 | ); 233 | let (_, _) = (a?, b?); 234 | Ok(()) 235 | } 236 | 237 | /// Main relay for V3 protocol. 238 | async fn relay_v3(&self, in_stream: TcpStream) -> anyhow::Result<()> { 239 | let addr = resolve(&self.target_addr).await?; 240 | let mut stream = TcpStream::connect_addr_with_config( 241 | addr, 242 | &TcpConnectOpts::default().tcp_fast_open(self.fastopen), 243 | ) 244 | .await?; 245 | mod_tcp_conn(&mut stream, true, self.nodelay); 246 | tracing::debug!("tcp connected, start handshaking"); 247 | 248 | // stage1: handshake with wrapper 249 | let hamc_sr = Hmac::new(&self.password, (&[], &[])); 250 | let stream = StreamWrapper::new(stream, &self.password); 251 | let sni = self.tls_names.random_choose().clone(); 252 | let tls_stream = self 253 | .tls_connector 254 | .connect_with_session_id_generator(sni, stream, move |data| { 255 | generate_session_id(&hamc_sr, data) 256 | }) 257 | .await?; 258 | tracing::debug!("handshake success"); 259 | let (stream, session) = tls_stream.into_parts(); 260 | let authorized = stream.authorized(); 261 | let tls13 = stream.tls13; 262 | let maybe_srh = stream 263 | .state() 264 | .as_ref() 265 | .map(|s| (s.server_random, s.hmac.to_owned())); 266 | let stream = stream.into_inner(); 267 | 268 | // stage2: 269 | if (maybe_srh.is_none() || !authorized) && self.v3.strict() { 270 | tracing::warn!("V3 strict enabled: traffic hijacked or TLS1.3 is not supported"); 271 | let tls_stream = monoio_rustls_fork_shadow_tls::ClientTlsStream::new(stream, session); 272 | if let Err(e) = fake_request(tls_stream).await { 273 | bail!("traffic hijacked or TLS1.3 is not supported, fake request fail: {e}"); 274 | } 275 | bail!("traffic hijacked or TLS1.3 is not supported, but fake request success"); 276 | } 277 | 278 | drop(session); 279 | let (sr, hmac_sr) = maybe_srh.unwrap(); 280 | tracing::debug!("Authorized, ServerRandom extracted: {sr:?}"); 281 | let hmac_sr_s = Hmac::new(&self.password, (&sr, b"S")); 282 | let hmac_sr_c = Hmac::new(&self.password, (&sr, b"C")); 283 | 284 | verified_relay( 285 | in_stream, 286 | stream, 287 | hmac_sr_c, 288 | hmac_sr_s, 289 | Some(hmac_sr), 290 | !tls13, 291 | ) 292 | .await; 293 | Ok(()) 294 | } 295 | 296 | /// Connect remote, do handshaking and calculate HMAC. 297 | /// 298 | /// Only used by V2 protocol. 299 | async fn connect_v2( 300 | &self, 301 | ) -> anyhow::Result<( 302 | TcpStream, 303 | [u8; 20], 304 | rustls_fork_shadow_tls::ClientConnection, 305 | )> { 306 | let addr = resolve(&self.target_addr).await?; 307 | let mut stream = TcpStream::connect_addr_with_config( 308 | addr, 309 | &TcpConnectOpts::default().tcp_fast_open(self.fastopen), 310 | ) 311 | .await?; 312 | mod_tcp_conn(&mut stream, true, self.nodelay); 313 | tracing::debug!("tcp connected, start handshaking"); 314 | let stream = HashedReadStream::new(stream, self.password.as_bytes())?; 315 | let sni = self.tls_names.random_choose().clone(); 316 | let tls_stream = self.tls_connector.connect(sni, stream).await?; 317 | let (io, session) = tls_stream.into_parts(); 318 | let hash = io.hash(); 319 | tracing::debug!("tls handshake finished, signed hmac: {:?}", hash); 320 | let stream = io.into_inner(); 321 | Ok((stream, hash, session)) 322 | } 323 | } 324 | 325 | /// A wrapper for doing data extraction and modification. 326 | /// 327 | /// Only used by V3 protocol. 328 | struct StreamWrapper { 329 | raw: S, 330 | password: String, 331 | 332 | read_buf: Option>, 333 | read_pos: usize, 334 | 335 | read_state: Option, 336 | read_authorized: bool, 337 | tls13: bool, 338 | } 339 | 340 | #[derive(Clone)] 341 | struct State { 342 | server_random: [u8; TLS_RANDOM_SIZE], 343 | hmac: Hmac, 344 | key: Vec, 345 | } 346 | 347 | impl StreamWrapper { 348 | fn new(raw: S, password: &str) -> Self { 349 | Self { 350 | raw, 351 | password: password.to_string(), 352 | 353 | read_buf: Some(Vec::new()), 354 | read_pos: 0, 355 | 356 | read_state: None, 357 | read_authorized: false, 358 | tls13: false, 359 | } 360 | } 361 | 362 | fn authorized(&self) -> bool { 363 | self.read_authorized 364 | } 365 | 366 | fn state(&self) -> &Option { 367 | &self.read_state 368 | } 369 | 370 | fn into_inner(self) -> S { 371 | self.raw 372 | } 373 | } 374 | 375 | impl StreamWrapper { 376 | async fn feed_data(&mut self) -> std::io::Result { 377 | let mut buf = self.read_buf.take().unwrap(); 378 | 379 | // read header 380 | unsafe { buf.set_init(0) }; 381 | self.read_pos = 0; 382 | buf.reserve(TLS_HEADER_SIZE); 383 | let (res, buf) = self.raw.read_exact(buf.slice_mut(0..TLS_HEADER_SIZE)).await; 384 | match res { 385 | Ok(_) => (), 386 | Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { 387 | tracing::debug!("stream wrapper eof"); 388 | self.read_buf = Some(buf.into_inner()); 389 | return Ok(0); 390 | } 391 | Err(e) => { 392 | tracing::error!("stream wrapper unable to read tls header: {e}"); 393 | self.read_buf = Some(buf.into_inner()); 394 | return Err(e); 395 | } 396 | } 397 | let mut buf: Vec = buf.into_inner(); 398 | let mut size: [u8; 2] = Default::default(); 399 | size.copy_from_slice(&buf[3..5]); 400 | let data_size = u16::from_be_bytes(size) as usize; 401 | 402 | // read body 403 | buf.reserve(data_size); 404 | let (res, buf) = self 405 | .raw 406 | .read_exact(buf.slice_mut(TLS_HEADER_SIZE..TLS_HEADER_SIZE + data_size)) 407 | .await; 408 | if let Err(e) = res { 409 | self.read_buf = Some(buf.into_inner()); 410 | return Err(e); 411 | } 412 | let mut buf: Vec = buf.into_inner(); 413 | 414 | // do extraction and modification 415 | match buf[0] { 416 | HANDSHAKE => { 417 | if buf.len() > SERVER_RANDOM_IDX + TLS_RANDOM_SIZE && buf[5] == SERVER_HELLO { 418 | // we can read server random 419 | let mut server_random = [0; TLS_RANDOM_SIZE]; 420 | unsafe { 421 | copy_nonoverlapping( 422 | buf.as_ptr().add(SERVER_RANDOM_IDX), 423 | server_random.as_mut_ptr(), 424 | TLS_RANDOM_SIZE, 425 | ) 426 | } 427 | tracing::debug!("ServerRandom extracted: {server_random:?}"); 428 | let hmac = Hmac::new(&self.password, (&server_random, &[])); 429 | let key = kdf(&self.password, &server_random); 430 | self.read_state = Some(State { 431 | server_random, 432 | hmac, 433 | key, 434 | }); 435 | self.tls13 = support_tls13(&buf); 436 | } 437 | } 438 | APPLICATION_DATA => { 439 | self.read_authorized = false; 440 | if buf.len() > TLS_HMAC_HEADER_SIZE { 441 | if let Some(State { hmac, key, .. }) = self.read_state.as_mut() { 442 | hmac.update(&buf[TLS_HMAC_HEADER_SIZE..]); 443 | if hmac.finalize() == buf[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] { 444 | xor_slice(&mut buf[TLS_HMAC_HEADER_SIZE..], key); 445 | unsafe { 446 | copy( 447 | buf.as_ptr().add(TLS_HMAC_HEADER_SIZE), 448 | buf.as_mut_ptr().add(5), 449 | buf.len() - 9, 450 | ) 451 | }; 452 | (&mut buf[3..5]) 453 | .write_u16::(data_size as u16 - HMAC_SIZE as u16) 454 | .unwrap(); 455 | unsafe { buf.set_init(buf.len() - HMAC_SIZE) }; 456 | self.read_authorized = true; 457 | } else { 458 | tracing::debug!("app data verification failed"); 459 | } 460 | } 461 | } 462 | } 463 | _ => {} 464 | } 465 | 466 | // set buffer 467 | let buf_len = buf.len(); 468 | self.read_buf = Some(buf); 469 | Ok(buf_len) 470 | } 471 | } 472 | 473 | impl AsyncWriteRent for StreamWrapper { 474 | #[inline] 475 | fn write( 476 | &mut self, 477 | buf: T, 478 | ) -> impl Future> { 479 | self.raw.write(buf) 480 | } 481 | #[inline] 482 | fn writev( 483 | &mut self, 484 | buf_vec: T, 485 | ) -> impl Future> { 486 | self.raw.writev(buf_vec) 487 | } 488 | #[inline] 489 | fn flush(&mut self) -> impl Future> { 490 | self.raw.flush() 491 | } 492 | #[inline] 493 | fn shutdown(&mut self) -> impl Future> { 494 | self.raw.shutdown() 495 | } 496 | } 497 | 498 | impl AsyncReadRent for StreamWrapper { 499 | // uncancelable 500 | async fn read(&mut self, mut buf: T) -> BufResult { 501 | loop { 502 | let owned_buf = self.read_buf.as_mut().unwrap(); 503 | let data_len = owned_buf.len() - self.read_pos; 504 | // there is enough data to copy 505 | if data_len > 0 { 506 | let to_copy = buf.bytes_total().min(data_len); 507 | unsafe { 508 | copy_nonoverlapping( 509 | owned_buf.as_ptr().add(self.read_pos), 510 | buf.write_ptr(), 511 | to_copy, 512 | ); 513 | buf.set_init(to_copy); 514 | }; 515 | self.read_pos += to_copy; 516 | return (Ok(to_copy), buf); 517 | } 518 | 519 | // no data now 520 | match self.feed_data().await { 521 | Ok(0) => return (Ok(0), buf), 522 | Ok(_) => continue, 523 | Err(e) => return (Err(e), buf), 524 | } 525 | } 526 | } 527 | 528 | async fn readv(&mut self, mut buf: T) -> BufResult { 529 | let slice = match monoio::buf::IoVecWrapperMut::new(buf) { 530 | Ok(slice) => slice, 531 | Err(buf) => return (Ok(0), buf), 532 | }; 533 | 534 | let (result, slice) = self.read(slice).await; 535 | buf = slice.into_inner(); 536 | if let Ok(n) = result { 537 | unsafe { buf.set_init(n) }; 538 | } 539 | (result, buf) 540 | } 541 | } 542 | 543 | /// Doing fake request. 544 | /// 545 | /// Only used by V3 protocol. 546 | async fn fake_request( 547 | mut stream: monoio_rustls_fork_shadow_tls::ClientTlsStream, 548 | ) -> std::io::Result<()> { 549 | const HEADER: &[u8; 207] = b"GET / HTTP/1.1\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36\nAccept: gzip, deflate, br\nConnection: Close\nCookie: sessionid="; 550 | let cnt = 551 | rand::thread_rng().gen_range(FAKE_REQUEST_LENGTH_RANGE.0..FAKE_REQUEST_LENGTH_RANGE.1); 552 | let mut buffer = Vec::with_capacity(cnt + HEADER.len() + 1); 553 | 554 | buffer.extend_from_slice(HEADER); 555 | rand::distributions::Alphanumeric 556 | .sample_iter(rand::thread_rng()) 557 | .take(cnt) 558 | .for_each(|c| buffer.push(c)); 559 | buffer.push(b'\n'); 560 | 561 | let (res, mut buf) = stream.write_all(buffer).await; 562 | res?; 563 | let _ = stream.shutdown().await; 564 | 565 | // read until eof 566 | loop { 567 | let (res, b) = stream.read(buf).await; 568 | buf = b; 569 | if res? == 0 { 570 | return Ok(()); 571 | } 572 | } 573 | } 574 | 575 | /// Take a slice of tls frame[5..] and returns signed session id. 576 | /// 577 | /// Only used by V3 protocol. 578 | fn generate_session_id(hmac: &Hmac, buf: &[u8]) -> [u8; TLS_SESSION_ID_SIZE] { 579 | /// Note: SESSION_ID_START does not include 5 TLS_HEADER_SIZE. 580 | const SESSION_ID_START: usize = 1 + 3 + 2 + TLS_RANDOM_SIZE + 1; 581 | 582 | if buf.len() < SESSION_ID_START + TLS_SESSION_ID_SIZE { 583 | tracing::warn!("unexpected client hello length"); 584 | return [0; TLS_SESSION_ID_SIZE]; 585 | } 586 | 587 | let mut session_id = [0; TLS_SESSION_ID_SIZE]; 588 | rand::thread_rng().fill(&mut session_id[..TLS_SESSION_ID_SIZE - HMAC_SIZE]); 589 | let mut hmac = hmac.to_owned(); 590 | hmac.update(&buf[0..SESSION_ID_START]); 591 | hmac.update(&session_id); 592 | hmac.update(&buf[SESSION_ID_START + TLS_SESSION_ID_SIZE..]); 593 | let hmac_val = hmac.finalize(); 594 | unsafe { 595 | copy_nonoverlapping( 596 | hmac_val.as_ptr(), 597 | session_id.as_mut_ptr().add(TLS_SESSION_ID_SIZE - HMAC_SIZE), 598 | HMAC_SIZE, 599 | ) 600 | } 601 | tracing::debug!("ClientHello before sign: {buf:?}, session_id {session_id:?}"); 602 | session_id 603 | } 604 | -------------------------------------------------------------------------------- /src/helper_v2.rs: -------------------------------------------------------------------------------- 1 | //! Stream wrapper to calculate hmac. 2 | //! All structs in this file is used by V2 protocol only. 3 | 4 | use std::{ 5 | cell::RefCell, 6 | future::Future, 7 | io::Read, 8 | marker::PhantomData, 9 | pin::Pin, 10 | ptr::copy_nonoverlapping, 11 | rc::Rc, 12 | task::{Context, Poll}, 13 | }; 14 | 15 | use hmac::Mac; 16 | use monoio::{ 17 | buf::{IoBuf, IoBufMut, IoVecWrapper, IoVecWrapperMut}, 18 | io::{ 19 | as_fd::{AsReadFd, AsWriteFd}, 20 | AsyncReadRent, AsyncReadRentExt, AsyncWriteRent, AsyncWriteRentExt, 21 | }, 22 | BufResult, 23 | }; 24 | 25 | use crate::util::prelude::*; 26 | 27 | pub(crate) const HMAC_SIZE_V2: usize = 8; 28 | 29 | #[allow(unused)] 30 | pub(crate) trait HashedStream { 31 | fn hash_stream(&self) -> [u8; 20]; 32 | } 33 | 34 | pub(crate) struct HashedReadStream { 35 | raw: S, 36 | hmac: hmac::Hmac, 37 | } 38 | 39 | // # Safety 40 | // Here we does not make read and write related, so if S is Split, Self is Split. 41 | unsafe impl monoio::io::Split for HashedReadStream {} 42 | 43 | impl HashedReadStream { 44 | pub(crate) fn new(raw: S, password: &[u8]) -> Result { 45 | Ok(Self { 46 | raw, 47 | hmac: hmac::Hmac::new_from_slice(password)?, 48 | }) 49 | } 50 | 51 | pub(crate) fn into_inner(self) -> S { 52 | self.raw 53 | } 54 | 55 | pub(crate) fn hash(&self) -> [u8; 20] { 56 | self.hmac 57 | .clone() 58 | .finalize() 59 | .into_bytes() 60 | .as_slice() 61 | .try_into() 62 | .expect("unexpected digest length") 63 | } 64 | } 65 | 66 | impl HashedStream for HashedReadStream { 67 | fn hash_stream(&self) -> [u8; 20] { 68 | self.hash() 69 | } 70 | } 71 | 72 | pub struct HashedWriteStream { 73 | raw: S, 74 | hmac: Rc)>>, 75 | } 76 | 77 | // # Safety 78 | // Here we does not make read and write related, so if S is Split, Self is Split. 79 | unsafe impl monoio::io::Split for HashedWriteStream {} 80 | unsafe impl monoio::io::Split for &mut HashedWriteStream {} 81 | 82 | impl AsReadFd for HashedWriteStream { 83 | fn as_reader_fd(&mut self) -> &monoio::io::as_fd::SharedFdWrapper { 84 | self.raw.as_reader_fd() 85 | } 86 | } 87 | 88 | impl AsWriteFd for HashedWriteStream { 89 | fn as_writer_fd(&mut self) -> &monoio::io::as_fd::SharedFdWrapper { 90 | self.raw.as_writer_fd() 91 | } 92 | } 93 | 94 | impl HashedWriteStream { 95 | pub(crate) fn new(raw: S, password: &[u8]) -> Result { 96 | Ok(Self { 97 | raw, 98 | hmac: Rc::new(RefCell::new((true, hmac::Hmac::new_from_slice(password)?))), 99 | }) 100 | } 101 | 102 | #[allow(unused)] 103 | pub(crate) fn hash(&self) -> [u8; 20] { 104 | self.hmac 105 | .borrow() 106 | .clone() 107 | .1 108 | .finalize() 109 | .into_bytes() 110 | .as_slice() 111 | .try_into() 112 | .expect("unexpected digest length") 113 | } 114 | 115 | pub(crate) fn hmac_handler(&self) -> HmacHandler { 116 | HmacHandler(self.hmac.clone()) 117 | } 118 | } 119 | 120 | pub(crate) struct HmacHandler(Rc)>>); 121 | 122 | impl HmacHandler { 123 | pub(crate) fn hash(&self) -> [u8; 20] { 124 | self.0 125 | .borrow() 126 | .clone() 127 | .1 128 | .finalize() 129 | .into_bytes() 130 | .as_slice() 131 | .try_into() 132 | .expect("unexpected digest length") 133 | } 134 | 135 | pub(crate) fn disable(&mut self) { 136 | self.0.borrow_mut().0 = false; 137 | } 138 | } 139 | 140 | impl HashedStream for HashedWriteStream { 141 | fn hash_stream(&self) -> [u8; 20] { 142 | self.hash() 143 | } 144 | } 145 | 146 | impl AsyncReadRent for HashedReadStream { 147 | async fn read(&mut self, mut buf: T) -> BufResult { 148 | let ptr = buf.write_ptr(); 149 | let (result, buf) = self.raw.read(buf).await; 150 | if let Ok(n) = result { 151 | // Safety: we can make sure the ptr and n are valid. 152 | self.hmac 153 | .update(unsafe { std::slice::from_raw_parts(ptr, n) }); 154 | } 155 | (result, buf) 156 | } 157 | 158 | async fn readv(&mut self, mut buf: T) -> BufResult { 159 | let slice = match IoVecWrapperMut::new(buf) { 160 | Ok(slice) => slice, 161 | Err(buf) => return (Ok(0), buf), 162 | }; 163 | 164 | let (result, slice) = self.read(slice).await; 165 | buf = slice.into_inner(); 166 | if let Ok(n) = result { 167 | unsafe { buf.set_init(n) }; 168 | } 169 | (result, buf) 170 | } 171 | } 172 | 173 | impl AsyncWriteRent for HashedReadStream { 174 | fn write( 175 | &mut self, 176 | buf: T, 177 | ) -> impl Future> { 178 | self.raw.write(buf) 179 | } 180 | 181 | fn writev( 182 | &mut self, 183 | buf_vec: T, 184 | ) -> impl Future> { 185 | self.raw.writev(buf_vec) 186 | } 187 | 188 | fn flush(&mut self) -> impl Future> { 189 | self.raw.flush() 190 | } 191 | 192 | fn shutdown(&mut self) -> impl Future> { 193 | self.raw.shutdown() 194 | } 195 | } 196 | 197 | impl AsyncReadRent for HashedWriteStream { 198 | #[inline] 199 | fn read( 200 | &mut self, 201 | buf: T, 202 | ) -> impl Future> { 203 | self.raw.read(buf) 204 | } 205 | 206 | #[inline] 207 | fn readv( 208 | &mut self, 209 | buf: T, 210 | ) -> impl Future> { 211 | self.raw.readv(buf) 212 | } 213 | } 214 | 215 | impl AsyncWriteRent for HashedWriteStream { 216 | async fn write(&mut self, buf: T) -> BufResult { 217 | let ptr = buf.read_ptr(); 218 | let (result, buf) = self.raw.write(buf).await; 219 | if let Ok(n) = result { 220 | let mut eh = self.hmac.borrow_mut(); 221 | if eh.0 { 222 | // Safety: we can make sure the ptr and n are valid. 223 | eh.1.update(unsafe { std::slice::from_raw_parts(ptr, n) }); 224 | } 225 | } 226 | (result, buf) 227 | } 228 | 229 | async fn writev(&mut self, buf: T) -> BufResult { 230 | let slice = match IoVecWrapper::new(buf) { 231 | Ok(slice) => slice, 232 | Err(buf) => return (Ok(0), buf), 233 | }; 234 | 235 | let (result, slice) = self.write(slice).await; 236 | (result, slice.into_inner()) 237 | } 238 | 239 | fn flush(&mut self) -> impl Future> { 240 | self.raw.flush() 241 | } 242 | 243 | fn shutdown(&mut self) -> impl Future> { 244 | self.raw.shutdown() 245 | } 246 | } 247 | 248 | /// Read tls frame and check with tls session. 249 | /// If checking pass, ignore the data. 250 | /// If not, return the data out(and the later data). 251 | /// This wrapper is used to fix a v2 protocol bug: 252 | /// In v2 protocol server may relay application data 253 | /// from handshake server even when client side 254 | /// finished switching. Client must be able to filter 255 | /// out these packets. 256 | pub(crate) struct SessionFilterStream { 257 | session: C, 258 | stream: S, 259 | direct: bool, 260 | direct_buffer: Option<(Vec, usize)>, 261 | } 262 | 263 | impl SessionFilterStream { 264 | pub(crate) fn new(session: C, stream: S) -> Self { 265 | Self { 266 | session, 267 | stream, 268 | direct: false, 269 | direct_buffer: None, 270 | } 271 | } 272 | } 273 | 274 | impl AsyncReadRent for SessionFilterStream 275 | where 276 | S: AsyncReadRent, 277 | C: std::ops::DerefMut 278 | + std::ops::Deref> 279 | + 'static, 280 | { 281 | fn read( 282 | &mut self, 283 | mut buf: T, 284 | ) -> impl Future> { 285 | const HEADER_BUF_SIZE: usize = 5; 286 | 287 | async move { 288 | if self.direct { 289 | return self.stream.read(buf).await; 290 | } 291 | if let Some((buffer, copied)) = &mut self.direct_buffer { 292 | if buffer.len() == *copied { 293 | self.direct = true; 294 | return self.stream.read(buf).await; 295 | } else { 296 | let cnt = buf.bytes_total().min(buffer.len() - *copied); 297 | unsafe { 298 | copy_nonoverlapping(buffer.as_ptr().add(*copied), buf.write_ptr(), cnt) 299 | }; 300 | unsafe { buf.set_init(cnt) }; 301 | *copied += cnt; 302 | return (Ok(cnt), buf); 303 | } 304 | } 305 | let slice = 306 | unsafe { std::slice::from_raw_parts_mut(buf.write_ptr(), buf.bytes_total()) }; 307 | 308 | loop { 309 | match self.session.reader().read(slice) { 310 | Ok(n) => { 311 | unsafe { buf.set_init(n) }; 312 | return (Ok(n), buf); 313 | } 314 | // need more data, read something 315 | Err(ref err) if err.kind() == std::io::ErrorKind::WouldBlock => (), 316 | Err(_) => { 317 | self.direct = true; 318 | return self.stream.read(buf).await; 319 | } 320 | } 321 | 322 | // read header and body 323 | let header = vec![0; HEADER_BUF_SIZE]; 324 | let (res, header) = self.stream.read_exact(header).await; 325 | if res.is_err() { 326 | return (res, buf); 327 | } 328 | let mut size: [u8; 2] = Default::default(); 329 | size.copy_from_slice(&header[3..5]); 330 | let data_size = u16::from_be_bytes(size); 331 | tracing::debug!("session filter read tls frame header, body size is {data_size}"); 332 | let mut data = vec![0; data_size as usize + HEADER_BUF_SIZE]; 333 | unsafe { copy_nonoverlapping(header.as_ptr(), data.as_mut_ptr(), HEADER_BUF_SIZE) }; 334 | let (res, data_slice) = self.stream.read_exact(data.slice_mut(5..)).await; 335 | if res.is_err() { 336 | return (res, buf); 337 | } 338 | let data = data_slice.into_inner(); 339 | tracing::debug!("session filter read full tls frame of size {}", data.len()); 340 | 341 | let mut cursor = std::io::Cursor::new(&data); 342 | let _ = self.session.read_tls(&mut cursor); 343 | 344 | if self.session.process_new_packets().is_err() { 345 | let cnt = data.len().min(slice.len()); 346 | unsafe { copy_nonoverlapping(data.as_ptr(), slice.as_mut_ptr(), cnt) }; 347 | unsafe { buf.set_init(cnt) }; 348 | self.direct_buffer = Some((data, cnt)); 349 | return (Ok(cnt), buf); 350 | } 351 | } 352 | } 353 | } 354 | 355 | async fn readv(&mut self, mut buf: T) -> BufResult { 356 | let slice = match IoVecWrapperMut::new(buf) { 357 | Ok(slice) => slice, 358 | Err(buf) => return (Ok(0), buf), 359 | }; 360 | 361 | let (result, slice) = self.read(slice).await; 362 | buf = slice.into_inner(); 363 | if let Ok(n) = result { 364 | unsafe { buf.set_init(n) }; 365 | } 366 | (result, buf) 367 | } 368 | } 369 | 370 | pin_project_lite::pin_project! { 371 | /// ErrGroup works like ErrGroup in golang. 372 | /// If the two futures all finished with Ok, self is finished with Ok. 373 | /// If any one of them finished with Err, self is finished with Err. 374 | pub(crate) struct ErrGroup { 375 | #[pin] 376 | future_a: FA, 377 | #[pin] 378 | future_b: FB, 379 | slot_a: Option, 380 | slot_b: Option, 381 | marker: PhantomData, 382 | } 383 | } 384 | 385 | impl ErrGroup 386 | where 387 | FA: Future>, 388 | FB: Future>, 389 | { 390 | pub(crate) fn new(future_a: FA, future_b: FB) -> Self { 391 | Self { 392 | future_a, 393 | future_b, 394 | slot_a: None, 395 | slot_b: None, 396 | marker: Default::default(), 397 | } 398 | } 399 | } 400 | 401 | impl Future for ErrGroup 402 | where 403 | FA: Future>, 404 | FB: Future>, 405 | { 406 | type Output = Result<(A, B), E>; 407 | 408 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 409 | let this = self.project(); 410 | if this.slot_a.is_none() { 411 | if let Poll::Ready(r) = this.future_a.poll(cx) { 412 | match r { 413 | Ok(a) => *this.slot_a = Some(a), 414 | Err(e) => return Poll::Ready(Err(e)), 415 | } 416 | } 417 | } 418 | if this.slot_b.is_none() { 419 | if let Poll::Ready(r) = this.future_b.poll(cx) { 420 | match r { 421 | Ok(b) => *this.slot_b = Some(b), 422 | Err(e) => return Poll::Ready(Err(e)), 423 | } 424 | } 425 | } 426 | if this.slot_a.is_some() && this.slot_b.is_some() { 427 | return Poll::Ready(Ok(( 428 | this.slot_a.take().unwrap(), 429 | this.slot_b.take().unwrap(), 430 | ))); 431 | } 432 | Poll::Pending 433 | } 434 | } 435 | 436 | pin_project_lite::pin_project! { 437 | /// FirstRetGroup returns if the first future is ready. 438 | pub(crate) struct FirstRetGroup { 439 | #[pin] 440 | future_a: FA, 441 | #[pin] 442 | future_b: Option, 443 | slot_b: Option, 444 | marker: PhantomData, 445 | } 446 | } 447 | 448 | pub(crate) enum FutureOrOutput 449 | where 450 | F: Future>, 451 | { 452 | Future(F), 453 | Output(R), 454 | } 455 | 456 | impl FirstRetGroup 457 | where 458 | FA: Future>, 459 | FB: Future>, 460 | { 461 | pub(crate) fn new(future_a: FA, future_b: FB) -> Self { 462 | Self { 463 | future_a, 464 | future_b: Some(future_b), 465 | slot_b: None, 466 | marker: Default::default(), 467 | } 468 | } 469 | } 470 | 471 | impl Future for FirstRetGroup 472 | where 473 | FA: Future>, 474 | FB: Future> + Unpin, 475 | { 476 | type Output = Result<(A, FutureOrOutput), E>; 477 | 478 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 479 | let this = self.project(); 480 | if let Poll::Ready(r) = this.future_a.poll(cx) { 481 | let b = if let Some(output) = this.slot_b.take() { 482 | FutureOrOutput::Output(output) 483 | } else { 484 | FutureOrOutput::Future(this.future_b.get_mut().take().unwrap()) 485 | }; 486 | return Poll::Ready(r.map(|r| (r, b))); 487 | } 488 | if this.slot_b.is_none() { 489 | if let Poll::Ready(r) = this.future_b.as_pin_mut().unwrap().poll(cx) { 490 | match r { 491 | Ok(r) => *this.slot_b = Some(r), 492 | Err(e) => return Poll::Ready(Err(e)), 493 | } 494 | } 495 | } 496 | Poll::Pending 497 | } 498 | } 499 | 500 | pub(crate) async fn copy_with_application_data<'a, const N: usize, R, W>( 501 | reader: &'a mut R, 502 | writer: &'a mut W, 503 | write_prefix: Option<[u8; N]>, 504 | ) -> std::io::Result 505 | where 506 | R: monoio::io::AsyncReadRent + ?Sized, 507 | W: monoio::io::AsyncWriteRent + ?Sized, 508 | { 509 | let mut buf: Vec = vec![0; COPY_BUF_SIZE]; 510 | buf[0] = APPLICATION_DATA; 511 | // 0x03, 0x03: tls 1.2 512 | buf[1] = TLS_MAJOR; 513 | buf[2] = TLS_MINOR.0; 514 | // prefix 515 | let mut buf = if let Some(prefix) = write_prefix { 516 | assert!(N + TLS_HEADER_SIZE <= COPY_BUF_SIZE); 517 | unsafe { 518 | std::ptr::copy_nonoverlapping(prefix.as_ptr(), buf.as_mut_ptr().add(TLS_HEADER_SIZE), N) 519 | }; 520 | unsafe { buf.set_init(TLS_HEADER_SIZE + N) }; 521 | tracing::debug!("create buf slice with {} bytes prefix", TLS_HEADER_SIZE + N); 522 | buf.slice_mut(TLS_HEADER_SIZE + N..) 523 | } else { 524 | unsafe { buf.set_init(TLS_HEADER_SIZE) }; 525 | tracing::debug!("create buf slice with {} bytes prefix", TLS_HEADER_SIZE); 526 | buf.slice_mut(TLS_HEADER_SIZE..) 527 | }; 528 | 529 | let mut transfered: u64 = 0; 530 | loop { 531 | let (read_res, buf_read) = reader.read(buf).await; 532 | match read_res { 533 | Ok(0) => { 534 | // read closed 535 | break; 536 | } 537 | Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => { 538 | // retry 539 | buf = buf_read; 540 | continue; 541 | } 542 | Err(e) => { 543 | // should return error 544 | return Err(e); 545 | } 546 | Ok(n) => { 547 | // go write data 548 | tracing::debug!("copy_with_application_data: read {n} bytes data"); 549 | } 550 | } 551 | let mut raw_buf = buf_read.into_inner(); 552 | // convert n to u16 is safe since the buffer will not be resized. 553 | let n_u16 = (raw_buf.len() - TLS_HEADER_SIZE) as u16; 554 | // set 3-4 byte of raw_buf as data size 555 | let size_data = n_u16.to_be_bytes(); 556 | // # Safety 557 | // We can make sure there are spaces inside the buffer. 558 | unsafe { 559 | std::ptr::copy_nonoverlapping(size_data.as_ptr(), raw_buf.as_mut_ptr().add(3), 2); 560 | } 561 | 562 | tracing::debug!( 563 | "copy_with_application_data: write {} bytes data", 564 | raw_buf.len() 565 | ); 566 | let (write_res, buf_) = writer.write_all(raw_buf).await; 567 | let n = write_res?; 568 | transfered += n as u64; 569 | buf = buf_.slice_mut(TLS_HEADER_SIZE..); 570 | } 571 | let _ = writer.shutdown().await; 572 | Ok(transfered) 573 | } 574 | 575 | pub(crate) async fn copy_without_application_data<'a, R, W>( 576 | reader: &'a mut R, 577 | writer: &'a mut W, 578 | ) -> std::io::Result 579 | where 580 | R: monoio::io::AsyncReadRent + ?Sized, 581 | W: monoio::io::AsyncWriteRent + ?Sized, 582 | { 583 | let mut buf: Vec = vec![0; COPY_BUF_SIZE]; 584 | let mut to_copy = 0; 585 | let mut transfered: u64 = 0; 586 | 587 | unsafe { buf.set_init(0) }; 588 | let mut buf = buf.slice_mut(0..); 589 | #[allow(unused_labels)] 590 | 'r: loop { 591 | let (read_res, buf_read) = reader.read(buf).await; 592 | match read_res { 593 | Ok(0) => { 594 | // read closed 595 | break; 596 | } 597 | Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => { 598 | // retry 599 | buf = buf_read; 600 | continue; 601 | } 602 | Err(e) => { 603 | // should return error 604 | return Err(e); 605 | } 606 | Ok(_) => { 607 | // go write data or read again if data is not enough 608 | } 609 | }; 610 | let mut raw_buf = buf_read.into_inner(); 611 | let mut read_index = 0; 612 | 613 | loop { 614 | // check if we know how much data to copy 615 | while to_copy == 0 { 616 | // we should parse header to get its size 617 | let initialized_length = raw_buf.len(); 618 | if initialized_length < read_index + TLS_HEADER_SIZE { 619 | // if the data is not enough for decoding length, 620 | // we will move the data left to the front of the buffer. 621 | for idx in read_index..initialized_length { 622 | raw_buf[idx - read_index] = raw_buf[idx]; 623 | } 624 | // we have to read again because its not enough to parse 625 | buf = raw_buf.slice_mut(initialized_length - read_index..); 626 | continue 'r; 627 | } 628 | // now there is enough data to parse 629 | if raw_buf[read_index] != APPLICATION_DATA { 630 | return Err(std::io::Error::new( 631 | std::io::ErrorKind::InvalidData, 632 | "unexpected tls content type", 633 | )); 634 | } 635 | let mut size = [0; 2]; 636 | size[0] = raw_buf[read_index + 3]; 637 | size[1] = raw_buf[read_index + 4]; 638 | to_copy = u16::from_be_bytes(size) as usize; 639 | // TODO: check how to handle application data with zero size in other libraries? 640 | // If needed, maybe we should throw an error here. 641 | read_index += TLS_HEADER_SIZE; 642 | } 643 | 644 | // now we know how much data to copy 645 | let initialized = raw_buf.len() - read_index; 646 | if initialized == 0 { 647 | // there is no data to copy, we should do read 648 | buf = raw_buf.slice_mut(0..); 649 | continue 'r; 650 | } 651 | let copy_size = to_copy.min(initialized); 652 | let write_slice = raw_buf.slice(read_index..read_index + copy_size); 653 | 654 | let (write_res, buf_) = writer.write_all(write_slice).await; 655 | let n = write_res?; 656 | read_index += n; 657 | to_copy -= n; 658 | transfered += n as u64; 659 | raw_buf = buf_.into_inner(); 660 | } 661 | } 662 | let _ = writer.shutdown().await; 663 | Ok(transfered) 664 | } 665 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(impl_trait_in_assoc_type)] 2 | 3 | mod client; 4 | mod helper_v2; 5 | mod server; 6 | pub mod sip003; 7 | mod util; 8 | 9 | use std::{fmt::Display, thread::JoinHandle}; 10 | 11 | pub use crate::{ 12 | client::{ShadowTlsClient, TlsExtConfig, TlsNames}, 13 | server::{ShadowTlsServer, TlsAddrs}, 14 | util::{V3Mode, WildcardSNI}, 15 | }; 16 | 17 | pub enum RunningArgs { 18 | Client { 19 | listen_addr: String, 20 | target_addr: String, 21 | tls_names: TlsNames, 22 | tls_ext: TlsExtConfig, 23 | password: String, 24 | nodelay: bool, 25 | fastopen: bool, 26 | v3: V3Mode, 27 | }, 28 | Server { 29 | listen_addr: String, 30 | target_addr: String, 31 | tls_addr: TlsAddrs, 32 | password: String, 33 | nodelay: bool, 34 | fastopen: bool, 35 | v3: V3Mode, 36 | }, 37 | } 38 | 39 | impl RunningArgs { 40 | #[inline] 41 | pub fn build(self) -> anyhow::Result { 42 | match self { 43 | RunningArgs::Client { 44 | listen_addr, 45 | target_addr, 46 | tls_names, 47 | tls_ext, 48 | password, 49 | nodelay, 50 | fastopen, 51 | v3, 52 | } => Ok(Runnable::Client(ShadowTlsClient::new( 53 | listen_addr, 54 | target_addr, 55 | tls_names, 56 | tls_ext, 57 | password, 58 | nodelay, 59 | fastopen, 60 | v3, 61 | )?)), 62 | RunningArgs::Server { 63 | listen_addr, 64 | target_addr, 65 | tls_addr, 66 | password, 67 | nodelay, 68 | fastopen, 69 | v3, 70 | } => Ok(Runnable::Server(ShadowTlsServer::new( 71 | listen_addr, 72 | target_addr, 73 | tls_addr, 74 | password, 75 | nodelay, 76 | fastopen, 77 | v3, 78 | ))), 79 | } 80 | } 81 | } 82 | 83 | impl Display for RunningArgs { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | match self { 86 | Self::Client { 87 | listen_addr, 88 | target_addr, 89 | tls_names, 90 | tls_ext, 91 | nodelay, 92 | fastopen, 93 | v3, 94 | .. 95 | } => { 96 | write!(f, "Client with:\nListen address: {listen_addr}\nTarget address: {target_addr}\nTLS server names: {tls_names}\nTLS Extension: {tls_ext}\nTCP_NODELAY: {nodelay}\nTCP_FASTOPEN:{fastopen}\nV3 Protocol: {v3}") 97 | } 98 | Self::Server { 99 | listen_addr, 100 | target_addr, 101 | tls_addr, 102 | nodelay, 103 | fastopen, 104 | v3, 105 | .. 106 | } => { 107 | write!(f, "Server with:\nListen address: {listen_addr}\nTarget address: {target_addr}\nTLS server address: {tls_addr}\nTCP_NODELAY: {nodelay}\nTCP_FASTOPEN:{fastopen}\nV3 Protocol: {v3}") 108 | } 109 | } 110 | } 111 | } 112 | 113 | #[derive(Clone)] 114 | pub enum Runnable { 115 | Client(ShadowTlsClient), 116 | Server(ShadowTlsServer), 117 | } 118 | 119 | impl Runnable { 120 | async fn serve(self) -> anyhow::Result<()> { 121 | match self { 122 | Runnable::Client(c) => c.serve().await, 123 | Runnable::Server(s) => s.serve().await, 124 | } 125 | } 126 | 127 | pub fn start(&self, parallelism: usize) -> Vec>> { 128 | // 8 threads are enough for resolving domains 129 | const MAX_BLOCKING_THREADS: usize = 8; 130 | 131 | let mut threads = Vec::new(); 132 | let shared_pool = 133 | monoio::blocking::DefaultThreadPool::new(MAX_BLOCKING_THREADS.min(2 * parallelism)); 134 | for _ in 0..parallelism { 135 | let runnable_clone = self.clone(); 136 | let shared_pool = Box::new(shared_pool.clone()); 137 | let t = std::thread::spawn(move || { 138 | let mut rt = monoio::RuntimeBuilder::::new() 139 | .attach_thread_pool(shared_pool) 140 | .enable_timer() 141 | .build() 142 | .expect("unable to build monoio runtime(please refer to: https://github.com/ihciah/shadow-tls/wiki/How-to-Run#common-issues)"); 143 | rt.block_on(runnable_clone.serve()) 144 | }); 145 | threads.push(t); 146 | } 147 | threads 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(type_alias_impl_trait)] 2 | 3 | use std::{collections::HashMap, path::PathBuf, process::exit}; 4 | 5 | use clap::{Parser, Subcommand, ValueEnum}; 6 | use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter}; 7 | 8 | use shadow_tls::{ 9 | sip003::parse_sip003_options, RunningArgs, TlsAddrs, TlsExtConfig, TlsNames, V3Mode, 10 | WildcardSNI, 11 | }; 12 | 13 | use serde::Deserialize; 14 | 15 | #[derive(Parser, Debug, Deserialize)] 16 | #[clap( 17 | author, 18 | version, 19 | about, 20 | long_about = "A proxy to expose real tls handshake to the firewall.\nGithub: github.com/ihciah/shadow-tls" 21 | )] 22 | struct Args { 23 | #[clap(subcommand)] 24 | #[serde(flatten)] 25 | cmd: Commands, 26 | #[clap(flatten)] 27 | #[serde(flatten)] 28 | opts: Opts, 29 | } 30 | 31 | macro_rules! default_function { 32 | ($name: ident, $type: ident, $val: expr) => { 33 | fn $name() -> $type { 34 | $val 35 | } 36 | }; 37 | } 38 | 39 | // default_function!(default_true, bool, true); 40 | default_function!(default_false, bool, false); 41 | default_function!(default_8080, String, "[::1]:8080".to_string()); 42 | default_function!(default_443, String, "[::]:443".to_string()); 43 | default_function!(default_wildcard_sni, WildcardSNI, WildcardSNI::Off); 44 | 45 | #[derive(Parser, Debug, Default, Clone, Deserialize)] 46 | struct Opts { 47 | #[clap(short, long, help = "Set parallelism manually")] 48 | threads: Option, 49 | #[serde(default = "default_false")] 50 | #[clap(long, help = "Disable TCP_NODELAY")] 51 | disable_nodelay: bool, 52 | #[serde(default = "default_false")] 53 | #[clap(long, help = "Enable TCP_FASTOPEN")] 54 | fastopen: bool, 55 | #[serde(default = "default_false")] 56 | #[clap(long, help = "Use v3 protocol")] 57 | v3: bool, 58 | #[serde(default = "default_false")] 59 | #[clap(long, help = "Strict mode(only for v3 protocol)")] 60 | strict: bool, 61 | } 62 | 63 | #[derive(Subcommand, Debug, Deserialize)] 64 | enum Commands { 65 | #[clap(about = "Run client side")] 66 | #[serde(rename = "client")] 67 | Client { 68 | #[clap( 69 | long = "listen", 70 | default_value = "[::1]:8080", 71 | help = "Shadow-tls client listen address(like \"[::1]:8080\")" 72 | )] 73 | #[serde(default = "default_8080")] 74 | listen: String, 75 | #[clap( 76 | long = "server", 77 | help = "Your shadow-tls server address(like \"1.2.3.4:443\")" 78 | )] 79 | server_addr: String, 80 | #[clap( 81 | long = "sni", 82 | help = "TLS handshake SNIs(like \"cloud.tencent.com\", \"captive.apple.com;cloud.tencent.com\")", 83 | value_parser = parse_client_names 84 | )] 85 | tls_names: TlsNames, 86 | #[clap(long = "password", help = "Password")] 87 | password: String, 88 | #[clap( 89 | long = "alpn", 90 | help = "Application-Layer Protocol Negotiation list(like \"http/1.1\", \"http/1.1;h2\")", 91 | value_delimiter = ';' 92 | )] 93 | alpn: Option>, 94 | }, 95 | #[clap(about = "Run server side")] 96 | #[serde(rename = "server")] 97 | Server { 98 | #[clap( 99 | long = "listen", 100 | default_value = "[::]:443", 101 | help = "Shadow-tls server listen address(like \"[::]:443\")" 102 | )] 103 | #[serde(default = "default_443")] 104 | listen: String, 105 | #[clap( 106 | long = "server", 107 | help = "Your data server address(like \"127.0.0.1:8080\")" 108 | )] 109 | server_addr: String, 110 | #[clap( 111 | long = "tls", 112 | help = "TLS handshake server address(like \"cloud.tencent.com:443\", \"cloudflare.com:1.1.1.1:443;captive.apple.com;cloud.tencent.com\")", 113 | value_parser = parse_server_addrs 114 | )] 115 | tls_addr: TlsAddrs, 116 | #[clap(long = "password", help = "Password")] 117 | password: String, 118 | #[clap( 119 | long = "wildcard-sni", 120 | default_value = "off", 121 | help = "Use sni:443 as handshake server without predefining mapping(useful for bypass billing system like airplane wifi without modifying server config)" 122 | )] 123 | #[serde(default = "default_wildcard_sni")] 124 | wildcard_sni: WildcardSNI, 125 | }, 126 | #[serde(skip)] 127 | Config { 128 | #[serde(skip)] 129 | #[clap(short, long, value_name = "FILE", help = "Path to config file")] 130 | config: PathBuf, 131 | }, 132 | } 133 | 134 | fn parse_client_names(addrs: &str) -> anyhow::Result { 135 | TlsNames::try_from(addrs) 136 | } 137 | 138 | fn parse_server_addrs(arg: &str) -> anyhow::Result { 139 | TlsAddrs::try_from(arg) 140 | } 141 | 142 | fn read_config_file(filename: String) -> Args { 143 | let file = std::fs::File::open(filename); 144 | match file { 145 | Err(e) => { 146 | tracing::error!("cannot open config file: {}", e); 147 | exit(-1); 148 | } 149 | Ok(f) => match serde_json::from_reader(f) { 150 | Err(e) => { 151 | tracing::error!("cannot read config file: {}", e); 152 | exit(-1); 153 | } 154 | Ok(res) => res, 155 | }, 156 | } 157 | } 158 | 159 | impl From for RunningArgs { 160 | fn from(args: Args) -> Self { 161 | let v3 = match (args.opts.v3, args.opts.strict) { 162 | (true, true) => V3Mode::Strict, 163 | (true, false) => V3Mode::Lossy, 164 | (false, _) => V3Mode::Disabled, 165 | }; 166 | 167 | match args.cmd { 168 | Commands::Client { 169 | listen, 170 | server_addr, 171 | tls_names, 172 | password, 173 | alpn, 174 | } => Self::Client { 175 | listen_addr: listen, 176 | target_addr: server_addr, 177 | tls_names, 178 | tls_ext: TlsExtConfig::from(alpn), 179 | password, 180 | nodelay: !args.opts.disable_nodelay, 181 | fastopen: args.opts.fastopen, 182 | v3, 183 | }, 184 | Commands::Server { 185 | listen, 186 | server_addr, 187 | mut tls_addr, 188 | password, 189 | wildcard_sni, 190 | } => { 191 | tls_addr.set_wildcard_sni(wildcard_sni); 192 | Self::Server { 193 | listen_addr: listen, 194 | target_addr: server_addr, 195 | tls_addr, 196 | password, 197 | nodelay: !args.opts.disable_nodelay, 198 | fastopen: args.opts.fastopen, 199 | v3, 200 | } 201 | } 202 | Commands::Config { config: _ } => { 203 | unreachable!() 204 | } 205 | } 206 | } 207 | } 208 | 209 | // SIP003 [https://shadowsocks.org/en/wiki/Plugin.html](https://shadowsocks.org/en/wiki/Plugin.html) 210 | pub(crate) fn get_sip003_arg() -> Option { 211 | macro_rules! env { 212 | ($key: expr) => { 213 | match std::env::var($key).ok() { 214 | None => return None, 215 | Some(val) if val.is_empty() => return None, 216 | Some(val) => val, 217 | } 218 | }; 219 | ($key: expr, $fail_fn: expr) => { 220 | match std::env::var($key).ok() { 221 | None => return None, 222 | Some(val) if val.is_empty() => { 223 | $fail_fn; 224 | return None; 225 | } 226 | Some(val) => val, 227 | } 228 | }; 229 | (optional $key: expr) => { 230 | match std::env::var($key).ok() { 231 | None => "".to_string(), 232 | Some(val) if val.is_empty() => "".to_string(), 233 | Some(val) => val, 234 | } 235 | }; 236 | } 237 | let config_file = env!(optional "CONFIG_FILE"); 238 | if !config_file.is_empty() { 239 | return Some(read_config_file(config_file)); 240 | } 241 | let ss_remote_host = env!("SS_REMOTE_HOST"); 242 | let ss_remote_port = env!("SS_REMOTE_PORT"); 243 | let ss_local_host = env!("SS_LOCAL_HOST"); 244 | let ss_local_port = env!("SS_LOCAL_PORT"); 245 | #[allow(unreachable_code)] 246 | let ss_plugin_options = env!("SS_PLUGIN_OPTIONS", { 247 | tracing::error!("need SS_PLUGIN_OPTIONS when as SIP003 plugin"); 248 | exit(-1); 249 | }); 250 | 251 | let opts = parse_sip003_options(&ss_plugin_options).unwrap(); 252 | let opts: HashMap<_, _> = opts.into_iter().collect(); 253 | 254 | let threads = opts.get("threads").map(|s| s.parse::().unwrap()); 255 | let v3 = opts.contains_key("v3"); 256 | let passwd = opts 257 | .get("passwd") 258 | .expect("need passwd param(like passwd=123456)"); 259 | 260 | let args_opts = crate::Opts { 261 | threads, 262 | v3, 263 | ..Default::default() 264 | }; 265 | let args = if opts.contains_key("server") { 266 | let tls_addr = opts 267 | .get("tls") 268 | .expect("tls param must be specified(like tls=xxx.com:443)"); 269 | let tls_addrs = parse_server_addrs(tls_addr) 270 | .expect("tls param parse failed(like tls=xxx.com:443 or tls=yyy.com:1.2.3.4:443;zzz.com:443;xxx.com)"); 271 | let wildcard_sni = WildcardSNI::from_str( 272 | opts.get("wildcard-sni").map(AsRef::as_ref).unwrap_or("off"), 273 | true, 274 | ) 275 | .expect("wildcard_sni format error"); 276 | Args { 277 | cmd: crate::Commands::Server { 278 | listen: format!("{ss_remote_host}:{ss_remote_port}"), 279 | server_addr: format!("{ss_local_host}:{ss_local_port}"), 280 | tls_addr: tls_addrs, 281 | password: passwd.to_owned(), 282 | wildcard_sni, 283 | }, 284 | opts: args_opts, 285 | } 286 | } else { 287 | let host = opts 288 | .get("host") 289 | .expect("need host param(like host=www.baidu.com)"); 290 | let hosts = parse_client_names(host).expect("tls names parse failed"); 291 | Args { 292 | cmd: crate::Commands::Client { 293 | listen: format!("{ss_local_host}:{ss_local_port}"), 294 | server_addr: format!("{ss_remote_host}:{ss_remote_port}"), 295 | tls_names: hosts, 296 | password: passwd.to_owned(), 297 | alpn: Default::default(), 298 | }, 299 | opts: args_opts, 300 | } 301 | }; 302 | Some(args) 303 | } 304 | 305 | fn main() { 306 | tracing_subscriber::registry() 307 | .with(fmt::layer()) 308 | .with( 309 | EnvFilter::builder() 310 | .with_default_directive(LevelFilter::INFO.into()) 311 | .from_env_lossy() 312 | .add_directive("rustls=off".parse().unwrap()), 313 | ) 314 | .init(); 315 | let mut args = get_sip003_arg().unwrap_or_else(Args::parse); 316 | if let Commands::Config { config } = args.cmd { 317 | args = read_config_file(config.to_str().unwrap().to_string()); 318 | } 319 | let parallelism = get_parallelism(&args); 320 | let running_args = RunningArgs::from(args); 321 | tracing::info!("Start {parallelism}-thread {running_args}"); 322 | if let Err(e) = ctrlc::set_handler(|| std::process::exit(0)) { 323 | tracing::error!("Unable to register signal handler: {e}"); 324 | } 325 | let runnable = running_args.build().expect("unable to build runnable"); 326 | runnable.start(parallelism).into_iter().for_each(|t| { 327 | if let Err(e) = t.join().expect("couldn't join on the associated thread") { 328 | tracing::error!("Thread exit: {e}"); 329 | } 330 | }); 331 | } 332 | 333 | fn get_parallelism(args: &Args) -> usize { 334 | if let Some(n) = args.opts.threads { 335 | return n as usize; 336 | } 337 | std::thread::available_parallelism() 338 | .map(|n| n.get()) 339 | .unwrap_or(1) 340 | } 341 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | borrow::Cow, 3 | collections::VecDeque, 4 | ptr::{copy, copy_nonoverlapping}, 5 | rc::Rc, 6 | sync::Arc, 7 | }; 8 | 9 | use anyhow::bail; 10 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 11 | use local_sync::oneshot::Sender; 12 | use monoio::{ 13 | buf::{IoBuf, IoBufMut, Slice, SliceMut}, 14 | io::{ 15 | AsyncReadRent, AsyncReadRentExt, AsyncWriteRent, AsyncWriteRentExt, PrefixedReadIo, 16 | Splitable, 17 | }, 18 | net::TcpStream, 19 | }; 20 | use serde::Deserialize; 21 | 22 | use crate::{ 23 | helper_v2::{ 24 | copy_with_application_data, copy_without_application_data, ErrGroup, FirstRetGroup, 25 | FutureOrOutput, HashedWriteStream, HmacHandler, HMAC_SIZE_V2, 26 | }, 27 | util::{ 28 | bind_with_pretty_error, copy_bidirectional, copy_until_eof, kdf, mod_tcp_conn, prelude::*, 29 | resolve, support_tls13, verified_relay, xor_slice, CursorExt, Hmac, V3Mode, 30 | }, 31 | WildcardSNI, 32 | }; 33 | 34 | /// ShadowTlsServer. 35 | #[derive(Clone)] 36 | pub struct ShadowTlsServer { 37 | listen_addr: Arc, 38 | target_addr: Arc, 39 | tls_addr: Arc, 40 | password: Arc, 41 | nodelay: bool, 42 | fastopen: bool, 43 | v3: V3Mode, 44 | } 45 | 46 | #[derive(Clone, Debug, PartialEq, Deserialize)] 47 | pub struct TlsAddrs { 48 | dispatch: rustc_hash::FxHashMap, 49 | fallback: String, 50 | wildcard_sni: WildcardSNI, 51 | } 52 | 53 | impl TlsAddrs { 54 | fn find<'a>(&'a self, key: Option<&str>, auth: bool) -> Cow<'a, str> { 55 | match key { 56 | Some(k) => match self.dispatch.get(k) { 57 | Some(v) => Cow::Borrowed(v), 58 | None => match self.wildcard_sni { 59 | WildcardSNI::Authed if auth => Cow::Owned(format!("{k}:443")), 60 | WildcardSNI::All => Cow::Owned(format!("{k}:443")), 61 | _ => Cow::Borrowed(&self.fallback), 62 | }, 63 | }, 64 | None => Cow::Borrowed(&self.fallback), 65 | } 66 | } 67 | 68 | fn is_empty(&self) -> bool { 69 | self.dispatch.is_empty() 70 | } 71 | 72 | pub fn set_wildcard_sni(&mut self, wildcard_sni: WildcardSNI) { 73 | self.wildcard_sni = wildcard_sni; 74 | } 75 | } 76 | 77 | impl TryFrom<&str> for TlsAddrs { 78 | type Error = anyhow::Error; 79 | 80 | fn try_from(arg: &str) -> Result { 81 | let mut rev_parts = arg.split(';').rev(); 82 | let fallback = rev_parts 83 | .next() 84 | .and_then(|x| if x.trim().is_empty() { None } else { Some(x) }) 85 | .ok_or_else(|| anyhow::anyhow!("empty server addrs"))?; 86 | let fallback = if !fallback.contains(':') { 87 | format!("{fallback}:443") 88 | } else { 89 | fallback.to_string() 90 | }; 91 | 92 | let mut dispatch = rustc_hash::FxHashMap::default(); 93 | for p in rev_parts { 94 | let mut p = p.trim().split(':').rev(); 95 | let mut port = Cow::<'static, str>::Borrowed("443"); 96 | let maybe_port = p 97 | .next() 98 | .ok_or_else(|| anyhow::anyhow!("empty part found in server addrs"))?; 99 | let host = if maybe_port.parse::().is_ok() { 100 | // there is a port at the end 101 | port = maybe_port.into(); 102 | p.next() 103 | .ok_or_else(|| anyhow::anyhow!("no host found in server addrs part"))? 104 | } else { 105 | maybe_port 106 | }; 107 | let key = match p.next() { 108 | Some(key) => key, 109 | None => host, 110 | }; 111 | if p.next().is_some() { 112 | bail!("unrecognized server addrs part"); 113 | } 114 | if dispatch 115 | .insert(key.to_string(), format!("{host}:{port}")) 116 | .is_some() 117 | { 118 | bail!("duplicate server addrs part found"); 119 | } 120 | } 121 | Ok(TlsAddrs { 122 | dispatch, 123 | fallback, 124 | wildcard_sni: Default::default(), 125 | }) 126 | } 127 | } 128 | 129 | impl std::fmt::Display for TlsAddrs { 130 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 131 | write!(f, "(wildcard-sni:{})", self.wildcard_sni)?; 132 | for (k, v) in self.dispatch.iter() { 133 | write!(f, "{k}->{v};")?; 134 | } 135 | write!(f, "fallback->{}", self.fallback) 136 | } 137 | } 138 | 139 | impl ShadowTlsServer { 140 | pub fn new( 141 | listen_addr: String, 142 | target_addr: String, 143 | tls_addr: TlsAddrs, 144 | password: String, 145 | nodelay: bool, 146 | fastopen: bool, 147 | v3: V3Mode, 148 | ) -> Self { 149 | Self { 150 | listen_addr: Arc::new(listen_addr), 151 | target_addr: Arc::new(target_addr), 152 | tls_addr: Arc::new(tls_addr), 153 | password: Arc::new(password), 154 | nodelay, 155 | fastopen, 156 | v3, 157 | } 158 | } 159 | } 160 | 161 | impl ShadowTlsServer { 162 | /// Serve a raw connection. 163 | pub async fn serve(self) -> anyhow::Result<()> { 164 | let listener = bind_with_pretty_error(self.listen_addr.as_ref(), self.fastopen)?; 165 | let shared = Rc::new(self); 166 | loop { 167 | match listener.accept().await { 168 | Ok((mut conn, addr)) => { 169 | tracing::info!("Accepted a connection from {addr}"); 170 | let server = shared.clone(); 171 | mod_tcp_conn(&mut conn, true, shared.nodelay); 172 | monoio::spawn(async move { 173 | let _ = match server.v3.enabled() { 174 | false => server.relay_v2(conn).await, 175 | true => server.relay_v3(conn).await, 176 | }; 177 | tracing::info!("Relay for {addr} finished"); 178 | }); 179 | } 180 | Err(e) => { 181 | tracing::error!("Accept failed: {e}"); 182 | } 183 | } 184 | } 185 | } 186 | 187 | /// Main relay for V2 protocol. 188 | async fn relay_v2(&self, in_stream: TcpStream) -> anyhow::Result<()> { 189 | // wrap in_stream with hash layer 190 | let mut in_stream = HashedWriteStream::new(in_stream, self.password.as_bytes())?; 191 | let mut hmac = in_stream.hmac_handler(); 192 | 193 | // read and extract server name 194 | // if there is only one fallback server, skip it 195 | let (prefix, server_name) = match self.tls_addr.is_empty() { 196 | true => (Vec::new(), None), 197 | false => extract_sni_v2(&mut in_stream).await?, 198 | }; 199 | let prefixed_io = PrefixedReadIo::new(&mut in_stream, std::io::Cursor::new(prefix)); 200 | tracing::debug!("server name extracted from SNI extention: {server_name:?}"); 201 | 202 | // choose handshake server addr and connect 203 | let server_name = server_name.and_then(|s| String::from_utf8(s).ok()); 204 | let addr = resolve( 205 | &self 206 | .tls_addr 207 | .find(server_name.as_ref().map(AsRef::as_ref), true), 208 | ) 209 | .await?; 210 | let mut out_stream = TcpStream::connect_addr(addr).await?; 211 | mod_tcp_conn(&mut out_stream, true, self.nodelay); 212 | tracing::debug!("handshake server connected: {addr}"); 213 | 214 | // copy stage 1 215 | let (mut out_r, mut out_w) = out_stream.into_split(); 216 | let (mut in_r, mut in_w) = prefixed_io.into_split(); 217 | let (switch, cp) = FirstRetGroup::new( 218 | copy_until_handshake_finished(&mut in_r, &mut out_w, &hmac), 219 | Box::pin(copy_until_eof(&mut out_r, &mut in_w)), 220 | ) 221 | .await?; 222 | hmac.disable(); 223 | tracing::debug!("handshake finished, switch: {switch:?}"); 224 | 225 | // copy stage 2 226 | match switch { 227 | SwitchResult::Switch(data_left) => { 228 | drop(cp); 229 | let in_stream = unsafe { in_r.reunite(in_w).unwrap_unchecked() }; 230 | let in_stream = in_stream.into_inner(); 231 | let (mut in_r, mut in_w) = in_stream.into_split(); 232 | 233 | // connect our data server 234 | let _ = out_r.reunite(out_w).unwrap().shutdown().await; 235 | let mut data_stream = 236 | TcpStream::connect_addr(resolve(&self.target_addr).await?).await?; 237 | mod_tcp_conn(&mut data_stream, true, self.nodelay); 238 | tracing::debug!("data server connected, start relay"); 239 | let (mut data_r, mut data_w) = data_stream.into_split(); 240 | let (result, _) = data_w.write(data_left).await; 241 | result?; 242 | ErrGroup::new( 243 | copy_with_application_data::<0, _, _>(&mut data_r, &mut in_w, None), 244 | copy_without_application_data(&mut in_r, &mut data_w), 245 | ) 246 | .await?; 247 | } 248 | SwitchResult::DirectProxy => match cp { 249 | FutureOrOutput::Future(cp) => { 250 | ErrGroup::new(cp, copy_until_eof(in_r, out_w)).await?; 251 | } 252 | FutureOrOutput::Output(_) => { 253 | copy_until_eof(in_r, out_w).await?; 254 | } 255 | }, 256 | } 257 | Ok(()) 258 | } 259 | 260 | /// Main relay for V3 protocol. 261 | async fn relay_v3(&self, mut in_stream: TcpStream) -> anyhow::Result<()> { 262 | // stage 1.1: read and validate client hello 263 | let first_client_frame = read_exact_frame(&mut in_stream).await?; 264 | let (client_hello_pass, sni) = verified_extract_sni(&first_client_frame, &self.password); 265 | 266 | // connect handshake server 267 | let server_name = sni.and_then(|s| String::from_utf8(s).ok()); 268 | let addr = resolve( 269 | &self 270 | .tls_addr 271 | .find(server_name.as_ref().map(AsRef::as_ref), client_hello_pass), 272 | ) 273 | .await?; 274 | let mut handshake_stream = TcpStream::connect_addr(addr).await?; 275 | mod_tcp_conn(&mut handshake_stream, true, self.nodelay); 276 | tracing::debug!("handshake server connected: {addr}"); 277 | tracing::trace!("ClientHello frame {first_client_frame:?}"); 278 | let (res, _) = handshake_stream.write_all(first_client_frame).await; 279 | res?; 280 | if !client_hello_pass { 281 | // if client verify failed, bidirectional copy and return 282 | tracing::warn!("ClientHello verify failed, will work as a SNI proxy"); 283 | copy_bidirectional(in_stream, handshake_stream).await; 284 | return Ok(()); 285 | } 286 | tracing::debug!("ClientHello verify success"); 287 | 288 | // stage 1.2: read server hello and extract server random from it 289 | let first_server_frame = read_exact_frame(&mut handshake_stream).await?; 290 | let (res, first_server_frame) = in_stream.write_all(first_server_frame).await; 291 | res?; 292 | let server_random = match extract_server_random(&first_server_frame) { 293 | Some(sr) => sr, 294 | None => { 295 | // we cannot extract server random, bidirectional copy and return 296 | tracing::warn!("ServerRandom extract failed, will copy bidirectional"); 297 | copy_bidirectional(in_stream, handshake_stream).await; 298 | return Ok(()); 299 | } 300 | }; 301 | tracing::debug!("Client authenticated. ServerRandom extracted: {server_random:?}"); 302 | 303 | let use_tls13 = support_tls13(&first_server_frame); 304 | if self.v3.strict() && !use_tls13 { 305 | tracing::error!( 306 | "V3 strict enabled and TLS 1.3 is not supported, will copy bidirectional" 307 | ); 308 | copy_bidirectional(in_stream, handshake_stream).await; 309 | return Ok(()); 310 | } 311 | 312 | // stage 1.3.1: create HMAC_ServerRandomC and HMAC_ServerRandom 313 | let mut hmac_sr_c = Hmac::new(&self.password, (&server_random, b"C")); 314 | let hmac_sr_s = Hmac::new(&self.password, (&server_random, b"S")); 315 | let mut hmac_sr = Hmac::new(&self.password, (&server_random, &[])); 316 | 317 | // stage 1.3.2: copy ShadowTLS Client -> Handshake Server until hamc matches 318 | // stage 1.3.3: copy and modify Handshake Server -> ShadowTLS Client until 1.3.2 stops 319 | let (mut c_read, mut c_write) = in_stream.into_split(); 320 | let pure_data = { 321 | let (mut h_read, mut h_write) = handshake_stream.into_split(); 322 | let (mut sender, mut recevier) = local_sync::oneshot::channel::<()>(); 323 | let key = kdf(&self.password, &server_random); 324 | let (maybe_pure, _) = monoio::join!( 325 | async { 326 | let r = 327 | copy_by_frame_until_hmac_matches(&mut c_read, &mut h_write, &mut hmac_sr_c) 328 | .await; 329 | recevier.close(); 330 | if r.is_err() { 331 | let _ = h_write.shutdown().await; 332 | } 333 | r 334 | }, 335 | async { 336 | let r = copy_by_frame_with_modification( 337 | &mut h_read, 338 | &mut c_write, 339 | &mut hmac_sr, 340 | &key, 341 | &mut sender, 342 | ) 343 | .await; 344 | if r.is_err() { 345 | let _ = c_write.shutdown().await; 346 | } 347 | } 348 | ); 349 | maybe_pure? 350 | }; 351 | tracing::debug!("handshake relay finished"); 352 | 353 | // early drop useless resources 354 | drop(first_server_frame); 355 | 356 | // stage 2.2: copy ShadowTLS Client -> Data Server 357 | // stage 2.3: copy Data Server -> ShadowTLS Client 358 | let mut data_stream = TcpStream::connect_addr(resolve(&self.target_addr).await?).await?; 359 | mod_tcp_conn(&mut data_stream, true, self.nodelay); 360 | let (res, _) = data_stream.write_all(pure_data).await; 361 | res?; 362 | verified_relay( 363 | data_stream, 364 | unsafe { c_read.reunite(c_write).unwrap_unchecked() }, 365 | hmac_sr_s, 366 | hmac_sr_c, 367 | None, 368 | !use_tls13, 369 | ) 370 | .await; 371 | Ok(()) 372 | } 373 | } 374 | 375 | /// A helper struct for doing source switching. 376 | /// 377 | /// Only used by V2 protocol. 378 | enum SwitchResult { 379 | Switch(Vec), 380 | DirectProxy, 381 | } 382 | 383 | impl std::fmt::Debug for SwitchResult { 384 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 385 | match self { 386 | Self::Switch(_) => write!(f, "Switch"), 387 | Self::DirectProxy => write!(f, "DirectProxy"), 388 | } 389 | } 390 | } 391 | 392 | /// Copy until handshake finished. 393 | /// We use HMAC to check if handshake finished. 394 | /// 395 | /// Only used by V2 protocol. 396 | async fn copy_until_handshake_finished( 397 | mut read_half: R, 398 | mut write_half: W, 399 | hmac: &HmacHandler, 400 | ) -> std::io::Result 401 | where 402 | R: AsyncReadRent, 403 | W: AsyncWriteRent, 404 | { 405 | // We maintain 2 state to make sure current session is in an tls session. 406 | // This is essential for preventing active detection. 407 | let mut has_seen_change_cipher_spec = false; 408 | let mut has_seen_handshake = false; 409 | 410 | // header_buf is used to read handshake frame header, will be a fixed size buffer. 411 | let mut header_buf = vec![0_u8; TLS_HEADER_SIZE].into_boxed_slice(); 412 | let mut header_read_len = 0; 413 | let mut header_write_len = 0; 414 | // data_buf is used to read and write data, and can be expanded. 415 | let mut data_hmac_buf = vec![0_u8; HMAC_SIZE_V2].into_boxed_slice(); 416 | let mut data_buf = vec![0_u8; 2048]; 417 | let mut application_data_count: usize = 0; 418 | 419 | let mut hashes = VecDeque::with_capacity(10); 420 | loop { 421 | let header_buf_slice = SliceMut::new(header_buf, header_read_len, TLS_HEADER_SIZE); 422 | let (res, header_buf_slice_) = read_half.read(header_buf_slice).await; 423 | header_buf = header_buf_slice_.into_inner(); 424 | let read_len = res?; 425 | header_read_len += read_len; 426 | 427 | // If EOF, close write half. 428 | if read_len == 0 { 429 | let _ = write_half.shutdown().await; 430 | return Err(std::io::ErrorKind::UnexpectedEof.into()); 431 | } 432 | 433 | // We have to relay data now no matter header is enough or not. 434 | let header_buf_slice_w = Slice::new(header_buf, header_write_len, header_read_len); 435 | let (res, header_buf_slice_w_) = write_half.write_all(header_buf_slice_w).await; 436 | header_buf = header_buf_slice_w_.into_inner(); 437 | header_write_len += res?; 438 | 439 | if header_read_len != TLS_HEADER_SIZE { 440 | // Here we have not got enough data to parse header. 441 | // continue to read. 442 | continue; 443 | } 444 | 445 | // Now header has been read and redirected successfully. 446 | // We should clear header status. 447 | header_read_len = 0; 448 | header_write_len = 0; 449 | 450 | // Parse length. 451 | let mut size: [u8; 2] = Default::default(); 452 | size.copy_from_slice(&header_buf[3..5]); 453 | let data_size = u16::from_be_bytes(size) as usize; 454 | tracing::debug!( 455 | "read header with type {} and length {}", 456 | header_buf[0], 457 | data_size 458 | ); 459 | 460 | // Check data type, if not app data we want, we can forward it directly(in streaming way). 461 | if header_buf[0] != APPLICATION_DATA 462 | || !has_seen_handshake 463 | || !has_seen_change_cipher_spec 464 | || data_size < HMAC_SIZE_V2 465 | { 466 | // The first packet must be handshake. 467 | // Also, every packet's version must be valid. 468 | let valid = (has_seen_handshake || header_buf[0] == HANDSHAKE) 469 | && header_buf[1] == TLS_MAJOR 470 | && (header_buf[2] == TLS_MINOR.0 || header_buf[2] == TLS_MINOR.1); 471 | if header_buf[0] == CHANGE_CIPHER_SPEC { 472 | has_seen_change_cipher_spec = true; 473 | } 474 | if header_buf[0] == HANDSHAKE { 475 | has_seen_handshake = true; 476 | } 477 | // Copy data. 478 | let mut to_copy = data_size; 479 | while to_copy != 0 { 480 | let max_read = data_buf.capacity().min(to_copy); 481 | let buf = SliceMut::new(data_buf, 0, max_read); 482 | let (read_res, buf) = read_half.read(buf).await; 483 | 484 | // if EOF, close write half. 485 | let read_len = read_res?; 486 | if read_len == 0 { 487 | let _ = write_half.shutdown().await; 488 | return Err(std::io::ErrorKind::UnexpectedEof.into()); 489 | } 490 | 491 | let buf = buf.into_inner().slice(0..read_len); 492 | let (write_res, buf) = write_half.write_all(buf).await; 493 | to_copy -= write_res?; 494 | data_buf = buf.into_inner(); 495 | } 496 | tracing::debug!("copied data with length {:?}", data_size); 497 | if !valid { 498 | tracing::debug!("early invalid tls: header {:?}", &header_buf[..3]); 499 | return Ok(SwitchResult::DirectProxy); 500 | } 501 | continue; 502 | } 503 | 504 | // Here we need to check hmac. 505 | // We have to read and copy the maybe_hmac. 506 | // Note: Send this 8 byte to remote does not matters: 507 | // If the data is sent by our authorized client, the handshake server must within 508 | // a tls session. So it must read exact that length data and then process it. 509 | // For this reason, sending 8 byte hmac will not cause the handshake server 510 | // shuting down the connection. 511 | // If the data in sent by an attacker, we must behaves like a tcp proxy so it seems 512 | // we are the handshake server. 513 | let mut hmac_read_len = 0; 514 | while hmac_read_len < HMAC_SIZE_V2 { 515 | let buf = SliceMut::new(data_hmac_buf, hmac_read_len, HMAC_SIZE_V2); 516 | let (res, buf_) = read_half.read(buf).await; 517 | // if EOF, close write half. 518 | let read_len = res?; 519 | if read_len == 0 { 520 | let _ = write_half.shutdown().await; 521 | return Err(std::io::ErrorKind::UnexpectedEof.into()); 522 | } 523 | 524 | let buf = Slice::new(buf_.into_inner(), hmac_read_len, hmac_read_len + read_len); 525 | let (write_res, buf_) = write_half.write_all(buf).await; 526 | write_res?; 527 | hmac_read_len += read_len; 528 | data_hmac_buf = buf_.into_inner(); 529 | } 530 | 531 | // Now hmac has been read and copied. 532 | // If hmac matches, we need to read current data and return. 533 | let hash = hmac.hash(); 534 | let mut hash_trim = [0; HMAC_SIZE_V2]; 535 | unsafe { copy_nonoverlapping(hash.as_ptr(), hash_trim.as_mut_ptr(), HMAC_SIZE_V2) }; 536 | tracing::debug!("hmac calculated: {hash_trim:?}"); 537 | if hashes.len() + 1 > hashes.capacity() { 538 | hashes.pop_front(); 539 | } 540 | hashes.push_back(hash_trim); 541 | unsafe { 542 | copy_nonoverlapping(data_hmac_buf.as_ptr(), hash_trim.as_mut_ptr(), HMAC_SIZE_V2) 543 | }; 544 | if hashes.contains(&hash_trim) { 545 | tracing::debug!("hmac matches"); 546 | let pure_data = vec![0; data_size - HMAC_SIZE_V2]; 547 | let (read_res, pure_data) = read_half.read_exact(pure_data).await; 548 | read_res?; 549 | return Ok(SwitchResult::Switch(pure_data)); 550 | } 551 | 552 | // Now hmac does not match. We have to acc the counter and do copy. 553 | application_data_count += 1; 554 | let mut to_copy = data_size - HMAC_SIZE_V2; 555 | while to_copy != 0 { 556 | let max_read = data_buf.capacity().min(to_copy); 557 | let buf = SliceMut::new(data_buf, 0, max_read); 558 | let (read_res, buf) = read_half.read(buf).await; 559 | 560 | // if EOF, close write half. 561 | let read_len = read_res?; 562 | if read_len == 0 { 563 | let _ = write_half.shutdown().await; 564 | return Err(std::io::ErrorKind::UnexpectedEof.into()); 565 | } 566 | 567 | let buf = buf.into_inner().slice(0..read_len); 568 | let (write_res, buf) = write_half.write_all(buf).await; 569 | to_copy -= write_res?; 570 | data_buf = buf.into_inner(); 571 | } 572 | 573 | if application_data_count > 3 { 574 | tracing::debug!("hmac not matches after 3 times, fallback to direct"); 575 | return Ok(SwitchResult::DirectProxy); 576 | } 577 | } 578 | } 579 | 580 | /// Read from connection and parse the frame. 581 | /// Return consumed data and SNI. 582 | /// 583 | /// Only used by V2 protocol. 584 | async fn extract_sni_v2(mut r: R) -> std::io::Result<(Vec, Option>)> { 585 | macro_rules! read_ok { 586 | ($res: expr, $data: expr) => { 587 | match $res { 588 | Ok(r) => r, 589 | Err(_) => { 590 | return Ok(($data, None)); 591 | } 592 | } 593 | }; 594 | } 595 | 596 | let header = vec![0; TLS_HEADER_SIZE]; 597 | let (res, header) = r.read_exact(header).await; 598 | res?; 599 | 600 | // validate header and fail fast 601 | if header[0] != HANDSHAKE 602 | || header[1] != TLS_MAJOR 603 | || (header[2] != TLS_MINOR.0 && header[2] != TLS_MINOR.1) 604 | { 605 | return Ok((header, None)); 606 | } 607 | 608 | // read tls frame length 609 | let mut size: [u8; 2] = Default::default(); 610 | size.copy_from_slice(&header[3..5]); 611 | let data_size = u16::from_be_bytes(size); 612 | tracing::debug!("read handshake length {}", data_size); 613 | 614 | // read tls frame 615 | let mut data = vec![0; data_size as usize + TLS_HEADER_SIZE]; 616 | unsafe { copy_nonoverlapping(header.as_ptr(), data.as_mut_ptr(), TLS_HEADER_SIZE) }; 617 | let (res, data_slice) = r.read_exact(data.slice_mut(TLS_HEADER_SIZE..)).await; 618 | res?; 619 | 620 | // validate client hello 621 | let data_slice: SliceMut> = data_slice; 622 | let data = data_slice.into_inner(); 623 | let mut cursor = std::io::Cursor::new(&data[TLS_HEADER_SIZE..]); 624 | if read_ok!(cursor.read_u8(), data) != CLIENT_HELLO { 625 | tracing::debug!("first packet is not client hello"); 626 | return Ok((data, None)); 627 | } 628 | // length[0] must be 0 629 | if read_ok!(cursor.read_u8(), data) != 0 { 630 | tracing::debug!("client hello length first byte is not zero"); 631 | return Ok((data, None)); 632 | } 633 | // client hello length[1..=2] 634 | let prot_size = read_ok!(cursor.read_u16::(), data); 635 | if prot_size + 4 > data_size { 636 | tracing::debug!("invalid client hello length"); 637 | return Ok((data, None)); 638 | } 639 | // reset cursor with new smaller length limit 640 | let mut cursor = std::io::Cursor::new( 641 | &data[TLS_HMAC_HEADER_SIZE..TLS_HMAC_HEADER_SIZE + prot_size as usize], 642 | ); 643 | // skip 2 byte version 644 | read_ok!(cursor.read_u16::(), data); 645 | // skip 32 byte random 646 | read_ok!(cursor.skip(TLS_RANDOM_SIZE), data); 647 | // skip session id 648 | read_ok!(cursor.skip_by_u8(), data); 649 | // skip cipher suites 650 | read_ok!(cursor.skip_by_u16(), data); 651 | // skip compression method 652 | read_ok!(cursor.skip_by_u8(), data); 653 | // skip ext length 654 | read_ok!(cursor.read_u16::(), data); 655 | 656 | loop { 657 | let ext_type = read_ok!(cursor.read_u16::(), data); 658 | if ext_type != SNI_EXT_TYPE { 659 | read_ok!(cursor.skip_by_u16(), data); 660 | continue; 661 | } 662 | tracing::debug!("found server_name extension"); 663 | let _ext_len = read_ok!(cursor.read_u16::(), data); 664 | let _sni_len = read_ok!(cursor.read_u16::(), data); 665 | // must be host_name 666 | if read_ok!(cursor.read_u8(), data) != 0 { 667 | return Ok((data, None)); 668 | } 669 | let sni = Some(read_ok!(cursor.read_by_u16(), data)); 670 | return Ok((data, sni)); 671 | } 672 | } 673 | 674 | /// Read a single frame and return Vec. 675 | /// 676 | /// Only used by V3 protocol. 677 | async fn read_exact_frame(r: impl AsyncReadRent) -> std::io::Result> { 678 | read_exact_frame_into(r, Vec::new()).await 679 | } 680 | 681 | /// Read a single frame into given Vec. 682 | /// 683 | /// Only used by V3 protocol. 684 | async fn read_exact_frame_into( 685 | mut r: impl AsyncReadRent, 686 | mut buffer: Vec, 687 | ) -> std::io::Result> { 688 | unsafe { buffer.set_len(0) }; 689 | buffer.reserve(TLS_HEADER_SIZE); 690 | let (res, header) = r.read_exact(buffer.slice_mut(..TLS_HEADER_SIZE)).await; 691 | res?; 692 | let mut buffer = header.into_inner(); 693 | 694 | // read tls frame length 695 | let mut size: [u8; 2] = Default::default(); 696 | size.copy_from_slice(&buffer[3..5]); 697 | let data_size = u16::from_be_bytes(size) as usize; 698 | 699 | // read tls frame body 700 | buffer.reserve(data_size); 701 | let (res, data_slice) = r 702 | .read_exact(buffer.slice_mut(TLS_HEADER_SIZE..TLS_HEADER_SIZE + data_size)) 703 | .await; 704 | res?; 705 | 706 | Ok(data_slice.into_inner()) 707 | } 708 | 709 | /// Parse frame, verify it and extract SNI. 710 | /// Return is_pass and Option. 711 | /// It requires &mut but it is meant for doing operation inplace. 712 | /// It does not modify the data. 713 | /// 714 | /// Only used by V3 protocol. 715 | fn verified_extract_sni(frame: &[u8], password: &str) -> (bool, Option>) { 716 | // 5 frame header + 1 handshake type + 3 length + 2 version + 32 random + 1 session id len + 32 session id 717 | const MIN_LEN: usize = TLS_HEADER_SIZE + 1 + 3 + 2 + TLS_RANDOM_SIZE + 1 + TLS_SESSION_ID_SIZE; 718 | const HMAC_IDX: usize = SESSION_ID_LEN_IDX + 1 + TLS_SESSION_ID_SIZE - HMAC_SIZE; 719 | const ZERO4B: [u8; HMAC_SIZE] = [0; HMAC_SIZE]; 720 | 721 | if frame.len() < SESSION_ID_LEN_IDX || frame[0] != HANDSHAKE || frame[5] != CLIENT_HELLO { 722 | return (false, None); 723 | } 724 | 725 | let pass = if frame.len() < MIN_LEN || frame[SESSION_ID_LEN_IDX] != TLS_SESSION_ID_SIZE as u8 { 726 | false 727 | } else { 728 | let mut hmac = Hmac::new(password, (&[], &[])); 729 | hmac.update(&frame[TLS_HEADER_SIZE..HMAC_IDX]); 730 | hmac.update(&ZERO4B); 731 | hmac.update(&frame[HMAC_IDX + HMAC_SIZE..]); 732 | hmac.finalize() == frame[HMAC_IDX..HMAC_IDX + HMAC_SIZE] 733 | }; 734 | 735 | let mut cursor = std::io::Cursor::new(&frame[SESSION_ID_LEN_IDX..]); 736 | macro_rules! read_ok { 737 | ($res: expr) => { 738 | match $res { 739 | Ok(r) => r, 740 | Err(_) => { 741 | return (pass, None); 742 | } 743 | } 744 | }; 745 | } 746 | 747 | // skip session id 748 | read_ok!(cursor.skip_by_u8()); 749 | // skip cipher suites 750 | read_ok!(cursor.skip_by_u16()); 751 | // skip compression method 752 | read_ok!(cursor.skip_by_u8()); 753 | // skip ext length 754 | read_ok!(cursor.read_u16::()); 755 | 756 | loop { 757 | let ext_type = read_ok!(cursor.read_u16::()); 758 | if ext_type != SNI_EXT_TYPE { 759 | read_ok!(cursor.skip_by_u16()); 760 | continue; 761 | } 762 | tracing::debug!("found server_name extension"); 763 | let _ext_len = read_ok!(cursor.read_u16::()); 764 | let _sni_len = read_ok!(cursor.read_u16::()); 765 | // must be host_name 766 | if read_ok!(cursor.read_u8()) != 0 { 767 | return (pass, None); 768 | } 769 | let sni = Some(read_ok!(cursor.read_by_u16())); 770 | return (pass, sni); 771 | } 772 | } 773 | 774 | /// Parse given frame and extract ServerRandom. 775 | /// Return Option. 776 | /// 777 | /// Only used by V3 protocol. 778 | fn extract_server_random(frame: &[u8]) -> Option<[u8; TLS_RANDOM_SIZE]> { 779 | // 5 frame header + 1 handshake type + 3 length + 2 version + 32 random 780 | const MIN_LEN: usize = TLS_HEADER_SIZE + 1 + 3 + 2 + TLS_RANDOM_SIZE; 781 | 782 | if frame.len() < MIN_LEN || frame[0] != HANDSHAKE || frame[5] != SERVER_HELLO { 783 | return None; 784 | } 785 | 786 | let mut server_random = [0; TLS_RANDOM_SIZE]; 787 | unsafe { 788 | copy_nonoverlapping( 789 | frame.as_ptr().add(SERVER_RANDOM_IDX), 790 | server_random.as_mut_ptr(), 791 | TLS_RANDOM_SIZE, 792 | ) 793 | }; 794 | 795 | Some(server_random) 796 | } 797 | 798 | /// Copy frame by frame until a appdata frame matches hmac. 799 | /// Return the matched pure data(without header). 800 | /// 801 | /// Only used by V3 protocol. 802 | async fn copy_by_frame_until_hmac_matches( 803 | mut read: impl AsyncReadRent, 804 | mut write: impl AsyncWriteRent, 805 | hmac: &mut Hmac, 806 | ) -> std::io::Result> { 807 | let mut g_buffer = Vec::new(); 808 | 809 | loop { 810 | let buffer = read_exact_frame_into(&mut read, g_buffer).await?; 811 | if buffer.len() > 9 && buffer[0] == APPLICATION_DATA { 812 | // check hmac 813 | let mut tmp_hmac = hmac.to_owned(); 814 | tmp_hmac.update(&buffer[TLS_HMAC_HEADER_SIZE..]); 815 | let h = tmp_hmac.finalize(); 816 | 817 | if buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE] == h { 818 | hmac.update(&buffer[TLS_HMAC_HEADER_SIZE..]); 819 | hmac.update(&buffer[TLS_HEADER_SIZE..TLS_HMAC_HEADER_SIZE]); 820 | return Ok(buffer[TLS_HMAC_HEADER_SIZE..].to_vec()); 821 | } 822 | } 823 | 824 | let (res, buffer) = write.write_all(buffer).await; 825 | res?; 826 | g_buffer = buffer; 827 | } 828 | } 829 | 830 | /// Copy frame by frame. 831 | /// Modify appdata frame: 832 | /// 1. Cycle XOR xor data. 833 | /// 2. Calculate HMAC and insert before the frame data. 834 | /// 835 | /// Only used by V3 protocol. 836 | async fn copy_by_frame_with_modification( 837 | mut read: impl AsyncReadRent, 838 | mut write: impl AsyncWriteRent, 839 | hmac: &mut Hmac, 840 | xor: &[u8], 841 | stop: &mut Sender<()>, 842 | ) -> std::io::Result<()> { 843 | let mut g_buffer = Vec::new(); 844 | let stop = stop.closed(); 845 | let mut stop = std::pin::pin!(stop); 846 | 847 | loop { 848 | monoio::select! { 849 | // this function can be stopped by a channel when reading. 850 | _ = &mut stop => { 851 | return Ok(()); 852 | }, 853 | buffer_res = read_exact_frame_into(&mut read, g_buffer) => { 854 | let mut buffer = buffer_res?; 855 | // Note: if we get frame, it is guaranteed valid. 856 | if buffer[0] == APPLICATION_DATA { 857 | // do modification: xor data, add 4-byte hmac, update tls frame length 858 | xor_slice(&mut buffer[TLS_HEADER_SIZE..], xor); 859 | hmac.update(&buffer[TLS_HEADER_SIZE..]); 860 | let hash = hmac.finalize(); 861 | buffer.extend_from_slice(&hash); 862 | unsafe { 863 | copy(buffer.as_ptr().add(TLS_HEADER_SIZE), buffer.as_mut_ptr().add(TLS_HMAC_HEADER_SIZE), buffer.len() - TLS_HMAC_HEADER_SIZE); 864 | copy_nonoverlapping(hash.as_ptr(), buffer.as_mut_ptr().add(TLS_HEADER_SIZE), HMAC_SIZE); 865 | } 866 | 867 | let mut size: [u8; 2] = Default::default(); 868 | size.copy_from_slice(&buffer[3..5]); 869 | let data_size = u16::from_be_bytes(size); 870 | // Normally it does not overflow. 871 | let data_size = data_size.wrapping_add(HMAC_SIZE as u16); 872 | (&mut buffer[3..5]).write_u16::(data_size).unwrap(); 873 | } 874 | 875 | // writing is not cancelable 876 | let (res, buffer) = write.write_all(buffer).await; 877 | res?; 878 | g_buffer = buffer; 879 | } 880 | } 881 | } 882 | } 883 | 884 | #[cfg(test)] 885 | mod tests { 886 | use super::*; 887 | 888 | fn to_map, V: Into>( 889 | kvs: Vec<(K, V)>, 890 | ) -> rustc_hash::FxHashMap { 891 | kvs.into_iter().map(|(k, v)| (k.into(), v.into())).collect() 892 | } 893 | 894 | macro_rules! map { 895 | [] => {rustc_hash::FxHashMap::::default()}; 896 | [$($k:expr => $v:expr),*] => {to_map(vec![$(($k.to_owned(), $v.to_owned())), *])}; 897 | [$($k:expr => $v:expr,)*] => {to_map(vec![$(($k.to_owned(), $v.to_owned())), *])}; 898 | } 899 | 900 | macro_rules! s { 901 | ($v:expr) => { 902 | $v.to_string() 903 | }; 904 | } 905 | 906 | #[test] 907 | fn parse_tls_addrs() { 908 | assert_eq!( 909 | TlsAddrs::try_from("google.com").unwrap(), 910 | TlsAddrs { 911 | dispatch: map![], 912 | fallback: s!("google.com:443"), 913 | wildcard_sni: Default::default(), 914 | } 915 | ); 916 | assert_eq!( 917 | TlsAddrs::try_from("feishu.cn;cloudflare.com:1.1.1.1:80;google.com").unwrap(), 918 | TlsAddrs { 919 | dispatch: map![ 920 | "feishu.cn" => "feishu.cn:443", 921 | "cloudflare.com" => "1.1.1.1:80", 922 | ], 923 | fallback: s!("google.com:443"), 924 | wildcard_sni: Default::default(), 925 | } 926 | ); 927 | assert_eq!( 928 | TlsAddrs::try_from("captive.apple.com;feishu.cn:80").unwrap(), 929 | TlsAddrs { 930 | dispatch: map![ 931 | "captive.apple.com" => "captive.apple.com:443", 932 | ], 933 | fallback: s!("feishu.cn:80"), 934 | wildcard_sni: Default::default(), 935 | } 936 | ); 937 | } 938 | } 939 | -------------------------------------------------------------------------------- /src/sip003.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context}; 2 | 3 | // Parse SIP003 optinos from env 4 | pub fn parse_sip003_options(s: &str) -> Result, anyhow::Error> { 5 | let mut opts = vec![]; 6 | let mut i = 0; 7 | while i < s.len() { 8 | // read key 9 | let (offset, key) = index_unescaped(&s[i..], b"=;").context("read key")?; 10 | if key.is_empty() { 11 | bail!("empty key in {}", &s[i..]); 12 | } 13 | i += offset; 14 | // end of string or no equals sign 15 | if i >= s.len() || s.as_bytes()[i] != b'=' { 16 | opts.push((key, "1".to_string())); 17 | i += 1; 18 | continue; 19 | } 20 | 21 | // skip equals 22 | i += 1; 23 | // read value 24 | let (offset, value) = index_unescaped(&s[i..], b"=;").context("read value")?; 25 | i += offset; 26 | opts.push((key, value)); 27 | // Skip the semicolon. 28 | i += 1; 29 | } 30 | Ok(opts) 31 | } 32 | 33 | fn index_unescaped(s: &str, term: &[u8]) -> Result<(usize, String), anyhow::Error> { 34 | let mut i = 0; 35 | let mut unesc = vec![]; 36 | 37 | while i < s.len() { 38 | let mut b: u8 = s.as_bytes()[i]; 39 | if term.contains(&b) { 40 | break; 41 | } 42 | if b == b'\\' { 43 | i += 1; 44 | if i >= s.len() { 45 | bail!("nothing following final escape in {s}",); 46 | } 47 | b = s.as_bytes()[i]; 48 | } 49 | unesc.push(b); 50 | i += 1; 51 | } 52 | Ok((i, String::from_utf8(unesc).unwrap())) 53 | } 54 | 55 | #[cfg(test)] 56 | #[test] 57 | fn test_parse_sip003_options() { 58 | let ret = parse_sip003_options("server;secret=\\=nou;cache=/tmp/cache;secret=yes").unwrap(); 59 | assert!(ret.len() == 4); 60 | assert_eq!( 61 | ret, 62 | vec![ 63 | ("server".to_string(), "1".to_string()), 64 | ("secret".to_string(), "=nou".to_string()), 65 | ("cache".to_string(), "/tmp/cache".to_string()), 66 | ("secret".to_string(), "yes".to_string()), 67 | ] 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{ErrorKind, Read}, 3 | net::ToSocketAddrs, 4 | ptr::copy_nonoverlapping, 5 | time::Duration, 6 | }; 7 | 8 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 9 | use local_sync::oneshot::{Receiver, Sender}; 10 | use monoio::{ 11 | buf::IoBufMut, 12 | io::{AsyncReadRent, AsyncWriteRent, AsyncWriteRentExt, Splitable}, 13 | net::{ListenerOpts, TcpListener, TcpStream}, 14 | }; 15 | 16 | use hmac::Mac; 17 | use rand::Rng; 18 | use serde::Deserialize; 19 | use sha2::{Digest, Sha256}; 20 | 21 | use prelude::*; 22 | 23 | pub(crate) mod prelude { 24 | pub(crate) const TLS_MAJOR: u8 = 0x03; 25 | pub(crate) const TLS_MINOR: (u8, u8) = (0x03, 0x01); 26 | pub(crate) const SNI_EXT_TYPE: u16 = 0; 27 | pub(crate) const SUPPORTED_VERSIONS_TYPE: u16 = 43; 28 | pub(crate) const TLS_RANDOM_SIZE: usize = 32; 29 | pub(crate) const TLS_HEADER_SIZE: usize = 5; 30 | pub(crate) const TLS_SESSION_ID_SIZE: usize = 32; 31 | pub(crate) const TLS_13: u16 = 0x0304; 32 | 33 | pub(crate) const CLIENT_HELLO: u8 = 0x01; 34 | pub(crate) const SERVER_HELLO: u8 = 0x02; 35 | pub(crate) const ALERT: u8 = 0x15; 36 | pub(crate) const HANDSHAKE: u8 = 0x16; 37 | pub(crate) const APPLICATION_DATA: u8 = 0x17; 38 | pub(crate) const CHANGE_CIPHER_SPEC: u8 = 0x14; 39 | 40 | pub(crate) const SERVER_RANDOM_IDX: usize = TLS_HEADER_SIZE + 1 + 3 + 2; 41 | pub(crate) const SESSION_ID_LEN_IDX: usize = TLS_HEADER_SIZE + 1 + 3 + 2 + TLS_RANDOM_SIZE; 42 | pub(crate) const TLS_HMAC_HEADER_SIZE: usize = TLS_HEADER_SIZE + HMAC_SIZE; 43 | 44 | pub(crate) const COPY_BUF_SIZE: usize = 4096; 45 | pub(crate) const HMAC_SIZE: usize = 4; 46 | } 47 | 48 | #[derive(Copy, Clone, Debug)] 49 | pub enum V3Mode { 50 | Disabled, 51 | Lossy, 52 | Strict, 53 | } 54 | 55 | impl std::fmt::Display for V3Mode { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | match self { 58 | V3Mode::Disabled => write!(f, "disabled"), 59 | V3Mode::Lossy => write!(f, "enabled(lossy)"), 60 | V3Mode::Strict => write!(f, "enabled(strict)"), 61 | } 62 | } 63 | } 64 | 65 | impl V3Mode { 66 | #[inline] 67 | pub fn enabled(&self) -> bool { 68 | !matches!(self, V3Mode::Disabled) 69 | } 70 | 71 | #[inline] 72 | pub fn strict(&self) -> bool { 73 | matches!(self, V3Mode::Strict) 74 | } 75 | } 76 | 77 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, clap::ValueEnum, Deserialize)] 78 | pub enum WildcardSNI { 79 | /// Disabled 80 | #[serde(rename = "off")] 81 | Off, 82 | /// For authenticated client only(may be differentiable); in v2 protocol it is eq to all. 83 | #[serde(rename = "authed")] 84 | Authed, 85 | /// For all request(may cause service abused but not differentiable) 86 | #[serde(rename = "all")] 87 | All, 88 | } 89 | 90 | impl Default for WildcardSNI { 91 | fn default() -> Self { 92 | Self::Off 93 | } 94 | } 95 | 96 | impl std::fmt::Display for WildcardSNI { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | match self { 99 | WildcardSNI::Off => write!(f, "off"), 100 | WildcardSNI::Authed => write!(f, "authed"), 101 | WildcardSNI::All => write!(f, "all"), 102 | } 103 | } 104 | } 105 | 106 | pub(crate) async fn copy_until_eof(mut read_half: R, mut write_half: W) -> std::io::Result<()> 107 | where 108 | R: monoio::io::AsyncReadRent, 109 | W: monoio::io::AsyncWriteRent, 110 | { 111 | let copy_result = monoio::io::copy(&mut read_half, &mut write_half).await; 112 | let _ = write_half.shutdown().await; 113 | copy_result?; 114 | Ok(()) 115 | } 116 | 117 | pub(crate) async fn copy_bidirectional(l: TcpStream, r: TcpStream) { 118 | let (lr, lw) = l.into_split(); 119 | let (rr, rw) = r.into_split(); 120 | let _ = monoio::join!(copy_until_eof(lr, rw), copy_until_eof(rr, lw)); 121 | } 122 | 123 | pub(crate) fn mod_tcp_conn(conn: &mut TcpStream, keepalive: bool, nodelay: bool) { 124 | if keepalive { 125 | let _ = conn.set_tcp_keepalive( 126 | Some(Duration::from_secs(90)), 127 | Some(Duration::from_secs(90)), 128 | Some(2), 129 | ); 130 | } 131 | let _ = conn.set_nodelay(nodelay); 132 | } 133 | 134 | #[derive(Clone)] 135 | pub(crate) struct Hmac(hmac::Hmac); 136 | 137 | impl Hmac { 138 | #[inline] 139 | pub(crate) fn new(password: &str, init_data: (&[u8], &[u8])) -> Self { 140 | // Note: infact new_from_slice never returns Err. 141 | let mut hmac: hmac::Hmac = 142 | hmac::Hmac::new_from_slice(password.as_bytes()).expect("unable to build hmac instance"); 143 | hmac.update(init_data.0); 144 | hmac.update(init_data.1); 145 | Self(hmac) 146 | } 147 | 148 | #[inline] 149 | pub(crate) fn update(&mut self, data: &[u8]) { 150 | self.0.update(data); 151 | } 152 | 153 | #[inline] 154 | pub(crate) fn finalize(&self) -> [u8; HMAC_SIZE] { 155 | let hmac = self.0.clone(); 156 | let hash = hmac.finalize().into_bytes(); 157 | let mut res = [0; HMAC_SIZE]; 158 | unsafe { copy_nonoverlapping(hash.as_slice().as_ptr(), res.as_mut_ptr(), HMAC_SIZE) }; 159 | res 160 | } 161 | 162 | #[inline] 163 | pub(crate) fn to_owned(&self) -> Self { 164 | Self(self.0.clone()) 165 | } 166 | } 167 | 168 | #[inline] 169 | pub(crate) fn xor_slice(data: &mut [u8], key: &[u8]) { 170 | data.iter_mut() 171 | .zip(key.iter().cycle()) 172 | .for_each(|(d, k)| *d ^= k); 173 | } 174 | 175 | #[inline] 176 | pub(crate) fn kdf(password: &str, server_random: &[u8]) -> Vec { 177 | let mut hasher = Sha256::new(); 178 | hasher.update(password.as_bytes()); 179 | hasher.update(server_random); 180 | let hash = hasher.finalize(); 181 | hash.to_vec() 182 | } 183 | 184 | pub(crate) async fn verified_relay( 185 | raw: TcpStream, 186 | tls: TcpStream, 187 | mut hmac_add: Hmac, 188 | mut hmac_verify: Hmac, 189 | mut hmac_ignore: Option, 190 | alert_enabled: bool, 191 | ) { 192 | tracing::debug!("verified relay started"); 193 | let (mut tls_read, mut tls_write) = tls.into_split(); 194 | let (mut raw_read, mut raw_write) = raw.into_split(); 195 | let (mut notfied, mut notifier) = local_sync::oneshot::channel::<()>(); 196 | let _ = monoio::join!( 197 | async { 198 | copy_remove_appdata_and_verify( 199 | &mut tls_read, 200 | &mut raw_write, 201 | &mut hmac_verify, 202 | &mut hmac_ignore, 203 | &mut notifier, 204 | ) 205 | .await; 206 | let _ = raw_write.shutdown().await; 207 | }, 208 | async { 209 | copy_add_appdata( 210 | &mut raw_read, 211 | &mut tls_write, 212 | &mut hmac_add, 213 | &mut notfied, 214 | alert_enabled, 215 | ) 216 | .await; 217 | let _ = tls_write.shutdown().await; 218 | } 219 | ); 220 | } 221 | 222 | /// Bind with pretty error. 223 | pub(crate) fn bind_with_pretty_error( 224 | addr: A, 225 | fastopen: bool, 226 | ) -> anyhow::Result { 227 | let cfg = ListenerOpts::default().tcp_fast_open(fastopen); 228 | TcpListener::bind_with_config(addr, &cfg).map_err(|e| match e.kind() { 229 | ErrorKind::AddrInUse => { 230 | anyhow::anyhow!("bind failed, check if the port is used: {e}") 231 | } 232 | ErrorKind::PermissionDenied => { 233 | anyhow::anyhow!("bind failed, check if permission configured correct: {e}") 234 | } 235 | _ => anyhow::anyhow!("bind failed: {e}"), 236 | }) 237 | } 238 | 239 | /// Remove application data header, verify hmac, remove the 240 | /// hmac header and copy. 241 | async fn copy_remove_appdata_and_verify( 242 | read: impl AsyncReadRent, 243 | mut write: impl AsyncWriteRent, 244 | hmac_verify: &mut Hmac, 245 | hmac_ignore: &mut Option, 246 | alert_notifier: &mut Receiver<()>, 247 | ) { 248 | const INIT_BUFFER_SIZE: usize = 2048; 249 | 250 | let mut decoder = BufferFrameDecoder::new(read, INIT_BUFFER_SIZE); 251 | loop { 252 | let maybe_frame = match decoder.next().await { 253 | Ok(f) => f, 254 | Err(e) => { 255 | tracing::error!("io error {e}"); 256 | alert_notifier.close(); 257 | return; 258 | } 259 | }; 260 | let frame = match maybe_frame { 261 | Some(frame) => frame, 262 | None => { 263 | // EOF 264 | return; 265 | } 266 | }; 267 | // validate frame 268 | match frame[0] { 269 | ALERT => { 270 | return; 271 | } 272 | APPLICATION_DATA => { 273 | if let Some(hi) = hmac_ignore.as_mut() { 274 | if verify_appdata(frame, hi, false) { 275 | // we can ignore the data 276 | tracing::debug!("useless data skipped"); 277 | continue; 278 | } else { 279 | tracing::debug!("useless data detector disabled"); 280 | hmac_ignore.take(); 281 | } 282 | } 283 | 284 | if verify_appdata(frame, hmac_verify, true) { 285 | let (res, _) = write 286 | .write_all(unsafe { 287 | monoio::buf::RawBuf::new( 288 | frame.as_ptr().add(TLS_HMAC_HEADER_SIZE), 289 | frame.len() - TLS_HMAC_HEADER_SIZE, 290 | ) 291 | }) 292 | .await; 293 | if let Err(e) = res { 294 | tracing::error!("write data server failed: {e}"); 295 | alert_notifier.close(); 296 | return; 297 | } 298 | } else { 299 | tracing::debug!("buffer hmac validate failed"); 300 | alert_notifier.close(); 301 | return; 302 | } 303 | } 304 | _ => { 305 | alert_notifier.close(); 306 | return; 307 | } 308 | } 309 | } 310 | } 311 | 312 | async fn copy_add_appdata( 313 | mut read: impl AsyncReadRent, 314 | mut write: impl AsyncWriteRent, 315 | hmac: &mut Hmac, 316 | alert_notified: &mut Sender<()>, 317 | alert_enabled: bool, 318 | ) { 319 | const DEFAULT_DATA: [u8; TLS_HMAC_HEADER_SIZE] = 320 | [APPLICATION_DATA, TLS_MAJOR, TLS_MINOR.0, 0, 0, 0, 0, 0, 0]; 321 | 322 | let mut buffer = Vec::with_capacity(COPY_BUF_SIZE); 323 | buffer.extend_from_slice(&DEFAULT_DATA); 324 | 325 | let alert_notified = alert_notified.closed(); 326 | let mut alert_notified = std::pin::pin!(alert_notified); 327 | 328 | loop { 329 | monoio::select! { 330 | _ = &mut alert_notified => { 331 | send_alert(&mut write, alert_enabled).await; 332 | return; 333 | }, 334 | (res, buf) = read.read(buffer.slice_mut(TLS_HMAC_HEADER_SIZE..)) => { 335 | if matches!(res, Ok(0) | Err(_)) { 336 | send_alert(&mut write, alert_enabled).await; 337 | return; 338 | } 339 | buffer = buf.into_inner(); 340 | let frame_len = buffer.len() - TLS_HEADER_SIZE; 341 | (&mut buffer[3..5]) 342 | .write_u16::(frame_len as u16) 343 | .unwrap(); 344 | 345 | hmac.update(&buffer[TLS_HMAC_HEADER_SIZE..]); 346 | let hmac_val = hmac.finalize(); 347 | hmac.update(&hmac_val); 348 | unsafe { copy_nonoverlapping(hmac_val.as_ptr(), buffer.as_mut_ptr().add(TLS_HEADER_SIZE), HMAC_SIZE) }; 349 | 350 | let (res, buf) = write.write_all(buffer).await; 351 | buffer = buf; 352 | 353 | if res.is_err() { 354 | return; 355 | } 356 | } 357 | } 358 | } 359 | } 360 | 361 | fn verify_appdata(frame: &[u8], hmac: &mut Hmac, sep: bool) -> bool { 362 | if frame[1] != TLS_MAJOR || frame[2] != TLS_MINOR.0 || frame.len() < TLS_HMAC_HEADER_SIZE { 363 | return false; 364 | } 365 | hmac.update(&frame[TLS_HMAC_HEADER_SIZE..]); 366 | let hmac_real = hmac.finalize(); 367 | if sep { 368 | hmac.update(&hmac_real); 369 | } 370 | frame[TLS_HEADER_SIZE..TLS_HEADER_SIZE + HMAC_SIZE] == hmac_real 371 | } 372 | 373 | async fn send_alert(mut w: impl AsyncWriteRent, alert_enabled: bool) { 374 | if !alert_enabled { 375 | return; 376 | } 377 | const FULL_SIZE: u8 = 31; 378 | const HEADER: [u8; TLS_HEADER_SIZE] = [ 379 | ALERT, 380 | TLS_MAJOR, 381 | TLS_MINOR.0, 382 | 0x00, 383 | FULL_SIZE - TLS_HEADER_SIZE as u8, 384 | ]; 385 | 386 | let mut buf = vec![0; FULL_SIZE as usize]; 387 | unsafe { copy_nonoverlapping(HEADER.as_ptr(), buf.as_mut_ptr(), HEADER.len()) }; 388 | rand::thread_rng().fill(&mut buf[HEADER.len()..]); 389 | 390 | let _ = w.write_all(buf).await; 391 | } 392 | 393 | /// Parse ServerHello and return if tls1.3 is supported. 394 | pub(crate) fn support_tls13(frame: &[u8]) -> bool { 395 | if frame.len() < SESSION_ID_LEN_IDX { 396 | return false; 397 | } 398 | let mut cursor = std::io::Cursor::new(&frame[SESSION_ID_LEN_IDX..]); 399 | macro_rules! read_ok { 400 | ($res: expr) => { 401 | match $res { 402 | Ok(r) => r, 403 | Err(_) => { 404 | return false; 405 | } 406 | } 407 | }; 408 | } 409 | 410 | // skip session id 411 | read_ok!(cursor.skip_by_u8()); 412 | // skip cipher suites 413 | read_ok!(cursor.skip(3)); 414 | // skip ext length 415 | let cnt = read_ok!(cursor.read_u16::()); 416 | 417 | for _ in 0..cnt { 418 | let ext_type = read_ok!(cursor.read_u16::()); 419 | if ext_type != SUPPORTED_VERSIONS_TYPE { 420 | read_ok!(cursor.skip_by_u16()); 421 | continue; 422 | } 423 | let ext_len = read_ok!(cursor.read_u16::()); 424 | let ext_val = read_ok!(cursor.read_u16::()); 425 | let use_tls13 = ext_len == 2 && ext_val == TLS_13; 426 | tracing::debug!("found supported_versions extension, tls1.3: {use_tls13}"); 427 | return use_tls13; 428 | } 429 | false 430 | } 431 | 432 | /// A helper trait for fast read and skip. 433 | pub(crate) trait CursorExt { 434 | fn read_by_u16(&mut self) -> std::io::Result>; 435 | fn skip(&mut self, n: usize) -> std::io::Result<()>; 436 | fn skip_by_u8(&mut self) -> std::io::Result; 437 | fn skip_by_u16(&mut self) -> std::io::Result; 438 | } 439 | 440 | impl CursorExt for std::io::Cursor 441 | where 442 | std::io::Cursor: std::io::Read, 443 | { 444 | #[inline] 445 | fn read_by_u16(&mut self) -> std::io::Result> { 446 | let len = self.read_u16::()?; 447 | let mut buf = vec![0; len as usize]; 448 | self.read_exact(&mut buf)?; 449 | Ok(buf) 450 | } 451 | 452 | #[inline] 453 | fn skip(&mut self, n: usize) -> std::io::Result<()> { 454 | for _ in 0..n { 455 | self.read_u8()?; 456 | } 457 | Ok(()) 458 | } 459 | 460 | #[inline] 461 | fn skip_by_u8(&mut self) -> std::io::Result { 462 | let len = self.read_u8()?; 463 | self.skip(len as usize)?; 464 | Ok(len) 465 | } 466 | 467 | #[inline] 468 | fn skip_by_u16(&mut self) -> std::io::Result { 469 | let len = self.read_u16::()?; 470 | self.skip(len as usize)?; 471 | Ok(len) 472 | } 473 | } 474 | 475 | trait ReadExt { 476 | fn unexpected_eof(self) -> Self; 477 | } 478 | 479 | impl ReadExt for std::io::Result { 480 | #[inline] 481 | fn unexpected_eof(self) -> Self { 482 | self.and_then(|n| match n { 483 | 0 => Err(std::io::Error::new( 484 | std::io::ErrorKind::UnexpectedEof, 485 | "failed to fill whole buffer", 486 | )), 487 | _ => Ok(n), 488 | }) 489 | } 490 | } 491 | 492 | struct BufferFrameDecoder { 493 | reader: T, 494 | buffer: Option>, 495 | read_pos: usize, 496 | } 497 | 498 | impl BufferFrameDecoder { 499 | #[inline] 500 | fn new(reader: T, capacity: usize) -> Self { 501 | Self { 502 | reader, 503 | buffer: Some(Vec::with_capacity(capacity)), 504 | read_pos: 0, 505 | } 506 | } 507 | 508 | // note: uncancelable 509 | async fn next(&mut self) -> std::io::Result> { 510 | loop { 511 | let l = self.get_buffer().len(); 512 | match l { 513 | 0 => { 514 | // empty buffer 515 | if self.feed_data().await? == 0 { 516 | // eof 517 | return Ok(None); 518 | } 519 | continue; 520 | } 521 | 1..=4 => { 522 | // has header but not enough to parse length 523 | self.feed_data().await.unexpected_eof()?; 524 | continue; 525 | } 526 | _ => { 527 | // buffer is enough to parse length 528 | let buffer = self.get_buffer(); 529 | let mut size: [u8; 2] = Default::default(); 530 | size.copy_from_slice(&buffer[3..5]); 531 | let data_size = u16::from_be_bytes(size) as usize; 532 | if buffer.len() < TLS_HEADER_SIZE + data_size { 533 | // we will do compact and read more data 534 | self.reserve(TLS_HEADER_SIZE + data_size); 535 | self.feed_data().await.unexpected_eof()?; 536 | continue; 537 | } 538 | // buffer is enough to parse data 539 | let slice = &self.buffer.as_ref().unwrap() 540 | [self.read_pos..self.read_pos + TLS_HEADER_SIZE + data_size]; 541 | self.read_pos += TLS_HEADER_SIZE + data_size; 542 | return Ok(Some(slice)); 543 | } 544 | } 545 | } 546 | } 547 | 548 | // note: uncancelable 549 | async fn feed_data(&mut self) -> std::io::Result { 550 | self.compact(); 551 | let buffer = self.buffer.take().unwrap(); 552 | let idx = buffer.len(); 553 | let read_buffer = buffer.slice_mut(idx..); 554 | let (res, read_buffer) = self.reader.read(read_buffer).await; 555 | self.buffer = Some(read_buffer.into_inner()); 556 | res 557 | } 558 | 559 | #[inline] 560 | fn get_buffer(&self) -> &[u8] { 561 | &self.buffer.as_ref().unwrap()[self.read_pos..] 562 | } 563 | 564 | /// Make sure the Vec has at least that capacity. 565 | #[inline] 566 | fn reserve(&mut self, n: usize) { 567 | let buf = self.buffer.as_mut().unwrap(); 568 | if n > buf.len() { 569 | buf.reserve(n - buf.len()); 570 | } 571 | } 572 | 573 | #[inline] 574 | fn compact(&mut self) { 575 | if self.read_pos == 0 { 576 | return; 577 | } 578 | let buffer = self.buffer.as_mut().unwrap(); 579 | let ptr = buffer.as_mut_ptr(); 580 | let readable_len = buffer.len() - self.read_pos; 581 | unsafe { 582 | std::ptr::copy(ptr.add(self.read_pos), ptr, readable_len); 583 | buffer.set_init(readable_len); 584 | } 585 | self.read_pos = 0; 586 | } 587 | } 588 | 589 | pub(crate) async fn resolve(addr: &str) -> std::io::Result { 590 | // Try parse as SocketAddr 591 | if let Ok(sockaddr) = addr.parse() { 592 | return Ok(sockaddr); 593 | } 594 | // Spawn blocking 595 | let addr_clone = addr.to_string(); 596 | let mut addr_iter = monoio::spawn_blocking(move || addr_clone.to_socket_addrs()) 597 | .await 598 | .unwrap()?; 599 | addr_iter.next().ok_or_else(|| { 600 | std::io::Error::new( 601 | std::io::ErrorKind::InvalidInput, 602 | format!("unable to resolve addr: {addr}"), 603 | ) 604 | }) 605 | } 606 | -------------------------------------------------------------------------------- /tests/sni.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use monoio::{ 4 | io::{AsyncReadRentExt, AsyncWriteRentExt}, 5 | net::TcpStream, 6 | }; 7 | use monoio_rustls_fork_shadow_tls::TlsConnector; 8 | use rustls_fork_shadow_tls::{OwnedTrustAnchor, RootCertStore, ServerName}; 9 | use shadow_tls::{RunningArgs, TlsAddrs, V3Mode}; 10 | 11 | #[allow(unused)] 12 | mod utils; 13 | use utils::{CAPTIVE_HTTP_REQUEST, CAPTIVE_HTTP_RESP}; 14 | 15 | #[monoio::test(enable_timer = true)] 16 | async fn sni() { 17 | // construct tls connector 18 | let mut root_store = RootCertStore::empty(); 19 | root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| { 20 | OwnedTrustAnchor::from_subject_spki_name_constraints( 21 | ta.subject.as_ref(), 22 | ta.subject_public_key_info.as_ref(), 23 | ta.name_constraints.as_ref().map(|n| n.as_ref()), 24 | ) 25 | })); 26 | let tls_config = rustls_fork_shadow_tls::ClientConfig::builder() 27 | .with_safe_defaults() 28 | .with_root_certificates(root_store) 29 | .with_no_client_auth(); 30 | let tls_connector = TlsConnector::from(tls_config); 31 | 32 | // run server 33 | let server = RunningArgs::Server { 34 | listen_addr: "127.0.0.1:32000".to_string(), 35 | target_addr: "bing.com:80".to_string(), 36 | tls_addr: TlsAddrs::try_from("captive.apple.com").unwrap(), 37 | password: "test".to_string(), 38 | nodelay: true, 39 | fastopen: true, 40 | v3: V3Mode::Strict, 41 | }; 42 | server.build().expect("build server failed").start(1); 43 | monoio::time::sleep(Duration::from_secs(1)).await; 44 | 45 | // connect and handshake 46 | let mut captive_conn = tls_connector 47 | .connect( 48 | ServerName::try_from("captive.apple.com").unwrap(), 49 | TcpStream::connect("127.0.0.1:32000").await.unwrap(), 50 | ) 51 | .await 52 | .expect("unable to connect captive.apple.com"); 53 | captive_conn 54 | .write_all(CAPTIVE_HTTP_REQUEST) 55 | .await 56 | .0 57 | .unwrap(); 58 | 59 | let (res, buf) = captive_conn 60 | .read_exact(vec![0; CAPTIVE_HTTP_RESP.len()]) 61 | .await; 62 | assert!(res.is_ok()); 63 | assert_eq!(&buf, CAPTIVE_HTTP_RESP); 64 | } 65 | -------------------------------------------------------------------------------- /tests/tls12.rs: -------------------------------------------------------------------------------- 1 | use shadow_tls::{RunningArgs, TlsAddrs, TlsExtConfig, TlsNames, V3Mode}; 2 | 3 | #[allow(unused)] 4 | mod utils; 5 | use utils::*; 6 | 7 | // handshake: badssl.com(tls1.2 only) 8 | // data: captive.apple.com:80 9 | // protocol: v2 10 | #[test] 11 | fn tls12_v2() { 12 | let client = RunningArgs::Client { 13 | listen_addr: "127.0.0.1:30000".to_string(), 14 | target_addr: "127.0.0.1:30001".to_string(), 15 | tls_names: TlsNames::try_from("badssl.com").unwrap(), 16 | tls_ext: TlsExtConfig::new(None), 17 | password: "test".to_string(), 18 | nodelay: true, 19 | fastopen: true, 20 | v3: V3Mode::Disabled, 21 | }; 22 | let server = RunningArgs::Server { 23 | listen_addr: "127.0.0.1:30001".to_string(), 24 | target_addr: "captive.apple.com:80".to_string(), 25 | tls_addr: TlsAddrs::try_from("badssl.com").unwrap(), 26 | password: "test".to_string(), 27 | nodelay: true, 28 | fastopen: true, 29 | v3: V3Mode::Disabled, 30 | }; 31 | test_ok(client, server, CAPTIVE_HTTP_REQUEST, CAPTIVE_HTTP_RESP); 32 | } 33 | 34 | // handshake: badssl.com(tls1.2 only) 35 | // data: captive.apple.com:80 36 | // protocol: v3 lossy 37 | #[test] 38 | fn tls12_v3_lossy() { 39 | let client = RunningArgs::Client { 40 | listen_addr: "127.0.0.1:30002".to_string(), 41 | target_addr: "127.0.0.1:30003".to_string(), 42 | tls_names: TlsNames::try_from("badssl.com").unwrap(), 43 | tls_ext: TlsExtConfig::new(None), 44 | password: "test".to_string(), 45 | nodelay: true, 46 | fastopen: true, 47 | v3: V3Mode::Lossy, 48 | }; 49 | let server = RunningArgs::Server { 50 | listen_addr: "127.0.0.1:30003".to_string(), 51 | target_addr: "captive.apple.com:80".to_string(), 52 | tls_addr: TlsAddrs::try_from("badssl.com").unwrap(), 53 | password: "test".to_string(), 54 | nodelay: true, 55 | fastopen: true, 56 | v3: V3Mode::Lossy, 57 | }; 58 | utils::test_ok(client, server, CAPTIVE_HTTP_REQUEST, CAPTIVE_HTTP_RESP); 59 | } 60 | 61 | // handshake: badssl.com(tls1.2 only) 62 | // data: captive.apple.com:80 63 | // protocol: v3 strict 64 | // v3 strict cannot work with tls1.2, so it must fail 65 | #[test] 66 | #[should_panic] 67 | fn tls12_v3_strict() { 68 | let client = RunningArgs::Client { 69 | listen_addr: "127.0.0.1:30004".to_string(), 70 | target_addr: "127.0.0.1:30005".to_string(), 71 | tls_names: TlsNames::try_from("badssl.com").unwrap(), 72 | tls_ext: TlsExtConfig::new(None), 73 | password: "test".to_string(), 74 | nodelay: true, 75 | fastopen: true, 76 | v3: V3Mode::Strict, 77 | }; 78 | let server = RunningArgs::Server { 79 | listen_addr: "127.0.0.1:30005".to_string(), 80 | target_addr: "captive.apple.com:80".to_string(), 81 | tls_addr: TlsAddrs::try_from("badssl.com").unwrap(), 82 | password: "test".to_string(), 83 | nodelay: true, 84 | fastopen: true, 85 | v3: V3Mode::Strict, 86 | }; 87 | utils::test_ok(client, server, CAPTIVE_HTTP_REQUEST, CAPTIVE_HTTP_RESP); 88 | } 89 | 90 | // handshake: badssl.com(tls1.2 only) 91 | // data: badssl.com:443 92 | // protocol: v2 93 | // Note: v2 can not defend against hijack attack. 94 | // Here hijack means directly connect to the handshake server. 95 | // The interceptor will see TLS Alert. 96 | // But it will not cause data error since the connection will be closed. 97 | #[test] 98 | fn tls12_v2_hijack() { 99 | let client = RunningArgs::Client { 100 | listen_addr: "127.0.0.1:30006".to_string(), 101 | target_addr: "badssl.com:443".to_string(), 102 | tls_names: TlsNames::try_from("badssl.com").unwrap(), 103 | tls_ext: TlsExtConfig::new(None), 104 | password: "test".to_string(), 105 | nodelay: true, 106 | fastopen: true, 107 | v3: V3Mode::Disabled, 108 | }; 109 | test_hijack(client); 110 | } 111 | 112 | // handshake: badssl.com(tls1.2 only) 113 | // data: captive.apple.com:80 114 | // protocol: v3 lossy 115 | // (v3 strict can not work with tls1.2) 116 | // Note: tls1.2 with v3 lossy can not defend against hijack attack. 117 | // Here hijack means directly connect to the handshake server. 118 | // The interceptor will see TLS Alert. 119 | // But it will not cause data error since the connection will be closed. 120 | #[test] 121 | fn tls12_v3_lossy_hijack() { 122 | let client = RunningArgs::Client { 123 | listen_addr: "127.0.0.1:30007".to_string(), 124 | target_addr: "badssl.com:443".to_string(), 125 | tls_names: TlsNames::try_from("badssl.com").unwrap(), 126 | tls_ext: TlsExtConfig::new(None), 127 | password: "test".to_string(), 128 | nodelay: true, 129 | fastopen: true, 130 | v3: V3Mode::Lossy, 131 | }; 132 | test_hijack(client); 133 | } 134 | -------------------------------------------------------------------------------- /tests/tls13.rs: -------------------------------------------------------------------------------- 1 | use shadow_tls::{RunningArgs, TlsAddrs, TlsExtConfig, TlsNames, V3Mode}; 2 | 3 | #[allow(unused)] 4 | mod utils; 5 | use utils::*; 6 | 7 | // handshake: captive.apple.com(tls1.3) 8 | // data: bing.com:80 9 | // protocol: v2 10 | #[test] 11 | fn tls13_v2() { 12 | let client = RunningArgs::Client { 13 | listen_addr: "127.0.0.1:31000".to_string(), 14 | target_addr: "127.0.0.1:31001".to_string(), 15 | tls_names: TlsNames::try_from("captive.apple.com").unwrap(), 16 | tls_ext: TlsExtConfig::new(None), 17 | password: "test".to_string(), 18 | nodelay: true, 19 | fastopen: true, 20 | v3: V3Mode::Disabled, 21 | }; 22 | let server = RunningArgs::Server { 23 | listen_addr: "127.0.0.1:31001".to_string(), 24 | target_addr: "bing.com:80".to_string(), 25 | tls_addr: TlsAddrs::try_from("captive.apple.com").unwrap(), 26 | password: "test".to_string(), 27 | nodelay: true, 28 | fastopen: true, 29 | v3: V3Mode::Disabled, 30 | }; 31 | test_ok(client, server, BING_HTTP_REQUEST, BING_HTTP_RESP); 32 | } 33 | 34 | // handshake: captive.apple.com(tls1.3) 35 | // data: bing.com:80 36 | // protocol: v3 lossy 37 | #[test] 38 | fn tls13_v3_lossy() { 39 | let client = RunningArgs::Client { 40 | listen_addr: "127.0.0.1:31002".to_string(), 41 | target_addr: "127.0.0.1:31003".to_string(), 42 | tls_names: TlsNames::try_from("captive.apple.com").unwrap(), 43 | tls_ext: TlsExtConfig::new(None), 44 | password: "test".to_string(), 45 | nodelay: true, 46 | fastopen: true, 47 | v3: V3Mode::Lossy, 48 | }; 49 | let server = RunningArgs::Server { 50 | listen_addr: "127.0.0.1:31003".to_string(), 51 | target_addr: "bing.com:80".to_string(), 52 | tls_addr: TlsAddrs::try_from("captive.apple.com").unwrap(), 53 | password: "test".to_string(), 54 | nodelay: true, 55 | fastopen: true, 56 | v3: V3Mode::Lossy, 57 | }; 58 | utils::test_ok(client, server, BING_HTTP_REQUEST, BING_HTTP_RESP); 59 | } 60 | 61 | // handshake: captive.apple.com(tls1.3) 62 | // data: bing.com:80 63 | // protocol: v3 strict 64 | #[test] 65 | fn tls13_v3_strict() { 66 | let client = RunningArgs::Client { 67 | listen_addr: "127.0.0.1:31004".to_string(), 68 | target_addr: "127.0.0.1:31005".to_string(), 69 | tls_names: TlsNames::try_from("captive.apple.com").unwrap(), 70 | tls_ext: TlsExtConfig::new(None), 71 | password: "test".to_string(), 72 | nodelay: true, 73 | fastopen: true, 74 | v3: V3Mode::Strict, 75 | }; 76 | let server = RunningArgs::Server { 77 | listen_addr: "127.0.0.1:31005".to_string(), 78 | target_addr: "bing.com:80".to_string(), 79 | tls_addr: TlsAddrs::try_from("captive.apple.com").unwrap(), 80 | password: "test".to_string(), 81 | nodelay: true, 82 | fastopen: true, 83 | v3: V3Mode::Strict, 84 | }; 85 | utils::test_ok(client, server, BING_HTTP_REQUEST, BING_HTTP_RESP); 86 | } 87 | 88 | // handshake: captive.apple.com(tls1.3) 89 | // protocol: v3 lossy 90 | // tls1.3 with v3 protocol defends against hijack attack. 91 | #[test] 92 | fn tls13_v3_lossy_hijack() { 93 | let client = RunningArgs::Client { 94 | listen_addr: "127.0.0.1:31007".to_string(), 95 | target_addr: "captive.apple.com:443".to_string(), 96 | tls_names: TlsNames::try_from("captive.apple.com").unwrap(), 97 | tls_ext: TlsExtConfig::new(None), 98 | password: "test".to_string(), 99 | nodelay: true, 100 | fastopen: true, 101 | v3: V3Mode::Lossy, 102 | }; 103 | test_hijack(client); 104 | } 105 | 106 | // handshake: captive.apple.com(tls1.3) 107 | // protocol: v3 strict 108 | // tls1.3 with v3 protocol defends against hijack attack. 109 | #[test] 110 | fn tls13_v3_strict_hijack() { 111 | let client = RunningArgs::Client { 112 | listen_addr: "127.0.0.1:31008".to_string(), 113 | target_addr: "captive.apple.com:443".to_string(), 114 | tls_names: TlsNames::try_from("captive.apple.com").unwrap(), 115 | tls_ext: TlsExtConfig::new(None), 116 | password: "test".to_string(), 117 | nodelay: true, 118 | fastopen: true, 119 | v3: V3Mode::Strict, 120 | }; 121 | test_hijack(client); 122 | } 123 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | net::{Shutdown, TcpStream}, 4 | time::Duration, 5 | }; 6 | 7 | use shadow_tls::RunningArgs; 8 | 9 | pub const BING_HTTP_REQUEST: &[u8; 47] = b"GET / HTTP/1.1\r\nHost: bing.com\r\nAccept: */*\r\n\r\n"; 10 | pub const BING_HTTP_RESP: &[u8; 12] = b"HTTP/1.1 301"; 11 | 12 | pub const CAPTIVE_HTTP_REQUEST: &[u8; 56] = 13 | b"GET / HTTP/1.1\r\nHost: captive.apple.com\r\nAccept: */*\r\n\r\n"; 14 | pub const CAPTIVE_HTTP_RESP: &[u8; 15] = b"HTTP/1.1 200 OK"; 15 | 16 | pub fn test_ok( 17 | client: RunningArgs, 18 | server: RunningArgs, 19 | http_request: &[u8], 20 | http_response: &[u8], 21 | ) { 22 | let client_listen = match &client { 23 | RunningArgs::Client { listen_addr, .. } => listen_addr.clone(), 24 | RunningArgs::Server { .. } => panic!("not valid client args"), 25 | }; 26 | client.build().expect("build client failed").start(1); 27 | server.build().expect("build server failed").start(1); 28 | 29 | // sleep 1s to make sure client and server have started 30 | std::thread::sleep(Duration::from_secs(3)); 31 | let mut conn = TcpStream::connect(client_listen).unwrap(); 32 | conn.write_all(http_request) 33 | .expect("unable to send http request"); 34 | conn.shutdown(Shutdown::Write).unwrap(); 35 | 36 | let mut buf = vec![0; http_response.len()]; 37 | conn.read_exact(&mut buf).unwrap(); 38 | assert_eq!(&buf, http_response); 39 | } 40 | 41 | pub fn test_hijack(client: RunningArgs) { 42 | let client_listen = match &client { 43 | RunningArgs::Client { listen_addr, .. } => listen_addr.clone(), 44 | RunningArgs::Server { .. } => panic!("not valid client args"), 45 | }; 46 | client.build().expect("build client failed").start(1); 47 | 48 | // sleep 1s to make sure client and server have started 49 | std::thread::sleep(Duration::from_secs(3)); 50 | let mut conn = TcpStream::connect(client_listen).unwrap(); 51 | conn.write_all(b"dummy").unwrap(); 52 | conn.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); 53 | let mut dummy_buf = [0; 1]; 54 | assert!(!matches!(conn.read(&mut dummy_buf), Ok(1))); 55 | } 56 | --------------------------------------------------------------------------------