├── .cargo └── config.toml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── need-help.md └── workflows │ ├── auto-close.yaml │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── .goreleaser_hook.sh ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── certs └── mTLS │ ├── certs │ ├── ca.cert.pem │ ├── wstunnel-client-1.cert.pem │ └── wstunnel-server.cert.pem │ ├── csr │ ├── wstunnel-client-1.csr.pem │ └── wstunnel-server.csr.pem │ ├── index.txt │ ├── index.txt.attr │ ├── newcerts │ ├── 1000.pem │ └── 1001.pem │ ├── openssl.cnf │ ├── private │ ├── ca.key.pem │ ├── wstunnel-client-1.pem │ └── wstunnel-server.pem │ └── serial ├── docs ├── logo_serviceplanet.png ├── logo_wstunnel.png └── using_mtls.md ├── goreleaser.go ├── justfile ├── restrictions.yaml ├── rustfmt.toml ├── wstunnel-cli ├── Cargo.toml └── src │ └── main.rs └── wstunnel ├── Cargo.toml └── src ├── config.rs ├── embedded_certificate.rs ├── executor.rs ├── lib.rs ├── protocols ├── dns │ ├── mod.rs │ └── resolver.rs ├── http_proxy │ ├── mod.rs │ └── server.rs ├── mod.rs ├── socks5 │ ├── mod.rs │ ├── tcp_server.rs │ └── udp_server.rs ├── stdio │ ├── mod.rs │ ├── server_unix.rs │ └── server_windows.rs ├── tcp │ ├── mod.rs │ └── server.rs ├── tls │ ├── mod.rs │ ├── server.rs │ └── utils.rs ├── udp │ ├── mod.rs │ └── server.rs └── unix_sock │ ├── mod.rs │ └── server.rs ├── restrictions ├── config_reloader.rs ├── mod.rs └── types.rs ├── somark.rs ├── test_integrations.rs └── tunnel ├── client ├── client.rs ├── cnx_pool.rs ├── config.rs ├── l4_transport_stream.rs └── mod.rs ├── connectors ├── mod.rs ├── sock5.rs ├── tcp.rs └── udp.rs ├── listeners ├── http_proxy.rs ├── mod.rs ├── socks5.rs ├── stdio.rs ├── tcp.rs ├── tproxy.rs ├── udp.rs └── unix_sock.rs ├── mod.rs ├── server ├── handler_http2.rs ├── handler_websocket.rs ├── mod.rs ├── reverse_tunnel.rs ├── server.rs └── utils.rs ├── tls_reloader.rs └── transport ├── http2.rs ├── io.rs ├── jwt.rs ├── mod.rs ├── types.rs └── websocket.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["--cfg", "uuid_unstable"] 3 | 4 | #rustflags = ["-C", "linker=ld.lld", "-C", "relocation-model=static", "-C", "strip=symbols", "--cfg", "uuid_unstable"] 5 | 6 | #[build] 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .github 3 | .git 4 | dist/ 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: Short description of the issue 5 | labels: bug 6 | --- 7 | 8 | ## Describe the bug 9 | A clear and concise description of what the bug is. 10 | 11 | ## To Reproduce 12 | Steps to reproduce the behavior: 13 | 14 | ## Expected behavior 15 | A clear and concise description of what you expected to happen. 16 | 17 | ## Your wstunnel setup 18 | Paste your logs of wstunnel, started with `--log-lvl=DEBUG`, and with the `command line used` 19 | - client 20 | - server 21 | 22 | ## Desktop (please complete the following information): 23 | - OS: [e.g. iOS] 24 | - Version [e.g. 22] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | --- 7 | 8 | ## Describe the feature 9 | A clear and concise description of `what` you want to happen. 10 | 11 | ## Describe the reason for such feature 12 | A clear and concise description of `why` you want it to happen. 13 | 14 | ## Describe alternatives you've considered 15 | A clear and concise description of any alternative solutions or features you've considered. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/need-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Need help 3 | about: You encounter an issue you can't solve 4 | title: '' 5 | labels: 'help wanted' 6 | --- 7 | 8 | 9 | (ノಠ益ಠ)ノ彡┻━┻ 10 | 11 | ==> Read and try the examples first before requesting help <== 12 | 13 | https://github.com/erebe/wstunnel?#examples 14 | 15 | If you have trouble with wireguard https://github.com/erebe/wstunnel?#wireguard-and-wstunnel- 16 | ``` 17 | If wstunnel cannot connect to server while wireguard is on, 18 | be sure you have added a static route via your main gateway for the ip of wstunnel server. 19 | Else if you forward all the traffic without putting a static route, 20 | you will endup looping your traffic wireguard interface -> wstunnel client -> wireguard interface 21 | If you have trouble making it works on windows, please check this issue #252 22 | ``` 23 | 24 | (ノಠ益ಠ)ノ彡┻━┻ 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Describe the goal 32 | A clear and concise description of `what` you try to achieve. 33 | Describe if you are behind a proxy, if you use some kind relay, what protocol/app you want to forward 34 | 35 | ## Describe what does not work 36 | A clear and concise description of why you can't reach your goal. 37 | 38 | 39 | ## Describe your wstunnel setup 40 | Paste your logs of wstunnel, started with `--log-lvl=DEBUG`, and with the `command line used` 41 | - client 42 | - server 43 | 44 | ## Desktop (please complete the following information): 45 | - OS: [e.g. iOS] 46 | - Version [e.g. 22] 47 | -------------------------------------------------------------------------------- /.github/workflows/auto-close.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: [opened, edited] 4 | 5 | jobs: 6 | auto_close_issues: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v1 11 | - name: Automatically close issues that don't follow the issue template 12 | uses: roots/issue-closer@v1.1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-pattern: ".*Describe the (bug|feature|goal).*" 16 | issue-close-message: |- 17 | Hello @${issue.user.login} :wave: 18 | 19 | This issue is being automatically closed because it does not follow the issue template. 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | # Indicates I want to run this workflow on all branches, PR, and tags 3 | push: 4 | branches: [ "main", "draft" ] 5 | tags: [ "*" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | RUST_VERSION: 1.86.0 11 | BIN_NAME: "wstunnel" 12 | 13 | jobs: 14 | build: 15 | name: Build - ${{ matrix.platform.name }} 16 | # By default, runs on Ubuntu, otherwise, override with the desired os 17 | runs-on: ${{ matrix.platform.os || 'ubuntu-24.04' }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | # Set platforms you want to build your binaries on 22 | platform: 23 | # Linux 24 | - name: Linux x86_64 25 | target: x86_64-unknown-linux-musl 26 | build-args: "--release --features=jemalloc" 27 | 28 | - name: Linux x86 29 | target: i686-unknown-linux-musl 30 | build-args: "--release --features aws-lc-rs-bindgen" 31 | 32 | - name: Linux aarch64 33 | target: aarch64-unknown-linux-musl 34 | build-args: "--release --features=jemalloc" 35 | 36 | - name: Linux armv7hf 37 | target: armv7-unknown-linux-musleabihf 38 | build-args: "--release --no-default-features --features ring" 39 | 40 | - name: Freebsd x86_64 41 | target: x86_64-unknown-freebsd 42 | build-args: "--release --no-default-features --features ring --features=jemalloc" 43 | 44 | - name: Freebsd x86 45 | target: i686-unknown-freebsd 46 | build-args: "--release --no-default-features --features ring" 47 | 48 | - name: Android aarch64 49 | target: aarch64-linux-android 50 | build-args: "--release --features=jemalloc" 51 | 52 | - name: Android armv7 53 | target: armv7-linux-androideabi 54 | build-args: "--release --no-default-features --features ring" 55 | 56 | #- name: Linux mips 57 | # target: mips-unknown-linux-musl 58 | 59 | #- name: Linux mips64 60 | # target: mips64-unknown-linux-muslabi64 61 | 62 | # Mac OS 63 | - name: MacOS x86_64 64 | os: macos-latest 65 | target: x86_64-apple-darwin 66 | build-args: "--release --features=jemalloc" 67 | 68 | - name: MacOS aarch64 69 | os: macos-latest 70 | target: aarch64-apple-darwin 71 | build-args: "--release --features=jemalloc" 72 | 73 | # - name: iOS x86_64 74 | # target: x86_64-apple-ios 75 | 76 | 77 | #- name: iOS aarch64 78 | # target: aarch64-apple-ios 79 | 80 | 81 | # Windows 82 | - name: Windows x86_64 83 | os: windows-latest 84 | target: x86_64-pc-windows-msvc 85 | build-args: "--profile release-with-symbols" 86 | 87 | - name: Windows x86 88 | os: windows-latest 89 | target: i686-pc-windows-msvc 90 | build-args: "--profile release-with-symbols --no-default-features --features ring" 91 | 92 | steps: 93 | 94 | - name: Install package for linux 95 | if: contains(matrix.platform.target, 'linux') 96 | run: sudo apt install musl-tools 97 | 98 | - name: Install package for Android 99 | if: contains(matrix.platform.target, 'android') 100 | run: sudo apt install android-libunwind android-libunwind-dev libunwind-dev 101 | 102 | - name: Set up JDK 17 103 | if: contains(matrix.platform.target, 'android') 104 | uses: actions/setup-java@v3 105 | with: 106 | java-version: '17' 107 | distribution: 'temurin' 108 | 109 | - name: Setup Android SDK 110 | if: contains(matrix.platform.target, 'android') 111 | uses: android-actions/setup-android@v3 112 | 113 | - name: Checkout Git repo 114 | uses: actions/checkout@v3 115 | 116 | # Linux & Windows 117 | - name: Install rust toolchain for Linux 118 | uses: actions-rs/toolchain@v1 119 | with: 120 | # We setup Rust toolchain and the desired target 121 | profile: minimal 122 | toolchain: "${{ env.RUST_VERSION }}" 123 | override: true 124 | target: ${{ matrix.platform.target }} 125 | components: rustfmt, clippy 126 | 127 | - name: Install package for Android 128 | if: contains(matrix.platform.target, 'android') 129 | run: cargo install cross --git https://github.com/cross-rs/cross 130 | 131 | - name: Install bindgen-cli 132 | run: cargo install bindgen-cli 133 | 134 | - name: Show command used for Cargo 135 | run: | 136 | echo "cargo command is: ${{ env.CARGO }}" 137 | echo "target flag is: ${{ env.BUILD_ARGS }}" 138 | 139 | - name: Build ${{ matrix.platform.name }} binary 140 | uses: actions-rs/cargo@v1 141 | # We use cross-rs if not running on x86_64 architecture on Linux 142 | with: 143 | command: build 144 | use-cross: ${{ !contains(matrix.platform.target, 'x86_64') || contains(matrix.platform.target, 'freebsd') }} 145 | args: ${{ matrix.platform.build-args }} --bin wstunnel --target ${{ matrix.platform.target }} 146 | 147 | - name: Store artifact 148 | uses: actions/upload-artifact@v4 149 | with: 150 | # Finally, we store the binary as GitHub artifact for later usage 151 | name: ${{ env.BIN_NAME }}-${{ matrix.platform.target }} 152 | path: target/${{ matrix.platform.target }}/release${{ contains(matrix.platform.target, 'windows') && '-with-symbols' || '' }}/${{ env.BIN_NAME }}${{ contains(matrix.platform.target, 'windows') && '.exe' || '' }} 153 | retention-days: 1 154 | 155 | release: 156 | name: Release 157 | needs: [ build ] 158 | # We run the release job only if a tag starts with 'v' letter 159 | if: startsWith( github.ref, 'refs/tags/v' ) 160 | runs-on: ubuntu-22.04 161 | steps: 162 | - name: Checkout Git repo 163 | uses: actions/checkout@v3 164 | with: 165 | fetch-depth: 0 166 | 167 | # Download all artifacts 168 | - uses: actions/download-artifact@v4.1.7 169 | with: 170 | path: artifacts 171 | 172 | - name: list artifacts 173 | run: find artifacts/ 174 | 175 | - name: Set up Go 176 | uses: actions/setup-go@v4 177 | 178 | - name: Run GoReleaser 179 | uses: goreleaser/goreleaser-action@v5 180 | with: 181 | distribution: goreleaser 182 | version: '~> v1' 183 | args: release --clean --skip=validate 184 | env: 185 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 186 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | artifacts/ 6 | dist/ 7 | .idea/ 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # MSVC Windows builds of rustc generate these, which store debugging information 13 | *.pdb 14 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - goos: 3 | - linux 4 | - darwin 5 | - windows 6 | - freebsd 7 | - android 8 | goarch: 9 | - "386" 10 | - amd64 11 | - arm64 12 | - arm 13 | goarm: 14 | - "7" 15 | binary: wstunnel 16 | ignore: 17 | - goos: windows 18 | goarch: arm64 19 | - goos: windows 20 | goarch: arm 21 | - goos: darwin 22 | goarch: arm 23 | - goos: android 24 | goarch: "386" 25 | - goos: android 26 | goarch: amd64 27 | - goos: android 28 | goarch: arm 29 | - goos: darwin 30 | goarch: "386" 31 | - goos: freebsd 32 | goarch: "arm" 33 | - goos: freebsd 34 | goarch: "arm64" 35 | main: goreleaser.go 36 | hooks: 37 | #pre: 38 | # - /bin/sh -c "if [ ! -e ./goreleaser.go ]; then echo -e 'package main\\\nfunc main() { }' > ./goreleaser.go ; fi" 39 | post: 40 | - ./.goreleaser_hook.sh "{{ .Arch }}" "{{ .Os }}" "{{ .Arm }}" "{{ .ProjectName }}" 41 | checksum: 42 | name_template: "checksums.txt" 43 | changelog: 44 | use: github 45 | sort: asc 46 | filters: 47 | exclude: 48 | - "^docs:" 49 | - "^test:" 50 | release: 51 | replace_existing_artifacts: true 52 | prerelease: auto 53 | -------------------------------------------------------------------------------- /.goreleaser_hook.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | go_arch=$1 4 | go_os=$2 5 | go_arm=$3 6 | project_name=$4 7 | 8 | # Make Go -> Rust arch/os mapping 9 | case $go_arch in 10 | amd64) rust_arch='x86_64' ;; 11 | arm64) rust_arch='aarch64' ;; 12 | arm) rust_arch='armv7' ;; 13 | 386) rust_arch='i686' ;; 14 | *) echo "unknown arch: $go_arch" && exit 1 ;; 15 | esac 16 | case $go_os in 17 | linux) rust_os='linux-musl' ;; 18 | darwin) rust_os='apple-darwin' ;; 19 | windows) rust_os='windows' ;; 20 | freebsd) rust_os='freebsd' ;; 21 | android) rust_os='android' ;; 22 | *) echo "unknown os: $go_os" && exit 1 ;; 23 | esac 24 | 25 | # Find artifacts and uncompress in the corresponding directory 26 | if [ -z "$go_arm" ] 27 | then 28 | DIST_DIR=$(find dist -type d -name "*${go_os}_${go_arch}*") 29 | else 30 | DIST_DIR=$(find dist -type d -name "*${go_os}_${go_arch}_${go_arm}*") 31 | fi 32 | 33 | echo "DIST_DIR: $DIST_DIR" 34 | rm -f ${DIST_DIR}/${project_name}* 35 | 36 | find artifacts -type f -wholename "*${rust_arch}*${rust_os}*/${project_name}*" -exec cp {} ${DIST_DIR}/ \; 37 | 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | 4 | members = [ 5 | "wstunnel", 6 | "wstunnel-cli", 7 | ] 8 | 9 | [workspace.dependencies] 10 | 11 | [profile.release] 12 | lto = "fat" 13 | panic = "abort" 14 | codegen-units = 1 15 | opt-level = 3 16 | debug = 0 17 | strip = "symbols" 18 | 19 | [profile.release-with-symbols] 20 | inherits = "release" 21 | strip = false 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILDER_IMAGE=builder_cache 2 | 3 | ############################################################ 4 | # Cache image with all the deps 5 | FROM rust:1.86-bookworm AS builder_cache 6 | 7 | RUN rustup component add rustfmt clippy && apt-get update && apt-get install cmake libclang-dev -y 8 | 9 | WORKDIR /build 10 | COPY . ./ 11 | 12 | 13 | RUN cargo fmt --all -- --check --color=always || (echo "Use cargo fmt to format your code"; exit 1) 14 | RUN cargo clippy --all --all-features -- -D warnings || (echo "Solve your clippy warnings to succeed"; exit 1) 15 | 16 | #RUN cargo test --all --all-features 17 | #RUN just test "tcp://localhost:2375" || (echo "Test are failing"; exit 1) 18 | 19 | #ENV RUSTFLAGS="-C link-arg=-Wl,--compress-debug-sections=zlib -C force-frame-pointers=yes" 20 | RUN cargo build --tests --all-features 21 | #RUN cargo build --release --all-features 22 | 23 | 24 | ############################################################ 25 | # Builder for production image 26 | FROM ${BUILDER_IMAGE} AS builder_release 27 | 28 | WORKDIR /build 29 | COPY . ./ 30 | 31 | ARG BIN_TARGET=--bins 32 | ARG PROFILE=release 33 | 34 | #ENV RUSTFLAGS="-C link-arg=-Wl,--compress-debug-sections=zlib -C force-frame-pointers=yes" 35 | RUN cargo build --features=jemalloc --profile=${PROFILE} ${BIN_TARGET} 36 | 37 | 38 | ############################################################ 39 | # Final image 40 | FROM debian:bookworm-slim AS final-image 41 | 42 | RUN useradd -ms /bin/bash app && \ 43 | apt-get update && \ 44 | apt-get -y upgrade && \ 45 | apt install -y --no-install-recommends ca-certificates dumb-init && \ 46 | apt-get clean && \ 47 | rm -rf /var/lib/apt/lists 48 | 49 | WORKDIR /home/app 50 | 51 | ARG PROFILE=release 52 | COPY --from=builder_release /build/target/${PROFILE}/wstunnel wstunnel 53 | 54 | ENV RUST_LOG="INFO" 55 | ENV SERVER_PROTOCOL="wss" 56 | ENV SERVER_LISTEN="[::]" 57 | ENV SERVER_PORT="8080" 58 | EXPOSE 8080 59 | 60 | USER app 61 | 62 | ENTRYPOINT ["/usr/bin/dumb-init", "-v", "--"] 63 | CMD ["/bin/sh", "-c", "exec /home/app/wstunnel server ${SERVER_PROTOCOL}://${SERVER_LISTEN}:${SERVER_PORT}"] 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016-2024, Erèbe - Romain Gerard 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /certs/mTLS/certs/ca.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFuzCCA6OgAwIBAgIUTThGq9gehDDkacteS/vHEnRFEdswDQYJKoZIhvcNAQEL 3 | BQAwZTELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdz 4 | dHVubmVsIGRldmVsb3BtZW50MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVu 5 | dCBSb290IENBMB4XDTI0MDQxOTA2MzgxM1oXDTQ0MDQxNDA2MzgxM1owZTELMAkG 6 | A1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRl 7 | dmVsb3BtZW50MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVudCBSb290IENB 8 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvWTTaZrNEIz9rNTv89Qi 9 | jG0HetlAO5Rao+44donIl09Uf4uax//J6kMwVgTxeW5576lk9+Z+OVVSdwdPhEMN 10 | Et1Yam4EtAK11OVxkMIydDGQDh9GkBwoogHRt/jDs2y6wnKFdLloIxEzRNtvEaDd 11 | 7WrMOa+F7ixIqdzDIPUyYxWdtmvZnghmNCMijGbxBRHSelRAZbUZJjaxD9dey29Q 12 | ClCw8S8Uv20Jti3d7J10etsgnJoN4MMeRI+kVfAHOMZN3wYv3sCXEZ0pgErOrkqs 13 | YcQSOQlKDvHB6cbctic08CKvfRs6XamsWcfX7p0FXrV4HzoRT6LOZ0eHMgq95R1b 14 | YgGtCqvzrtTemN/tTIH7dYXu+qdZijrqMvLLn/wplY/e5sbFAfHMResMrO+sQj3A 15 | 5gdNC+d5HnTBRX+tMxu37473jLCtbWNIkmB7DM/vpd8oMKiNFKSyK5apQjjDc4CR 16 | quefRtZaJk9naEQrJUGgOIipLhXUHM9m8ZrIkhqBK0Bk1QbG3Tha800doWxDvf70 17 | Ob+b6oxyD920U4P4QXWDct9BRuvkwNc7Q6sJu3dpdL36E7mOMExPE3x4kIGTQe41 18 | Fwh+tpKUYLYSKqyUP3qDmfP0cHceJU4AWjQ5Yk58VLQ6oe2rv7AdUeA9i+RE0RDS 19 | pJn0o8uXFP2RxNTF5qPtMBcCAwEAAaNjMGEwHQYDVR0OBBYEFO1t8Q8+5cLY7YA4 20 | fFE9aAKijTvDMB8GA1UdIwQYMBaAFO1t8Q8+5cLY7YA4fFE9aAKijTvDMA8GA1Ud 21 | EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQAF 22 | +5F44/80+1pJ6qBA3ACIp4NMfhOdYcqNhODh5qmWqP9uolUuvyimPAqiw2a9j+zC 23 | dhhSOR8rQGN+9K6xvuFCFmnYikeBi9NsK5pCYzr1No/TdT3RvVXrxe/fd6L4kIGG 24 | usQW6SUUR4zmVTctLfvI+sQcPmwaes492kQz4k2/PiYuYNwxAiGx5Az3oqyGH8a5 25 | 6tqm5z0w9YS9T+QXYWn/CzyW5IYTTgTdnE40luoIzOII2JiLAr7HjlxEtTAs26Ez 26 | hVQ2HwG1Y9XQg9hjtgQnolPKYg8E7q2CFYBTTTOQnlpI3YW8dVG+Yyt2N5UPFlc9 27 | G5tf6LnZTBRM51Kd81fEXCpoJwK7nb9Y+4Tx+PK9yQFbEdvoCjx0+I3+qAd0ykC5 28 | iwvkdEaKvMDXFy+WDaWrCpZjgyPHLMklNqrSM9lmHq220ugxfTftGUc1M/GpJPy0 29 | zlB7a6VusvZ4kyQM3w0JQ6rQY5aFPVoj6Y2YRg3gWm0mQcazvZ/oRmASX5N1ubFx 30 | /r3yxeztYeyMCyjr9pNnbx8q56NoG5zBzY66gc3V+4NpiFFuejUEgFMCvNuu0eTP 31 | 9dMjN4yCFCLvQyysIhHiQCiq/JODucwlUuJY7FwsmqAYQ65OZaeMqD5O9KFgEg4+ 32 | 5vUrsdSZp8uMF1uGOsH/Ft/YGxM4EzJ9s/gddId2og== 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /certs/mTLS/certs/wstunnel-client-1.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFDTCCAvWgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCR0Ix 3 | EDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50 4 | MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVudCBSb290IENBMB4XDTI0MDQx 5 | OTA2NDAwMVoXDTI1MDQyOTA2NDAwMVowZjELMAkGA1UEBhMCR0IxEDAOBgNVBAgM 6 | B0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSYwJAYDVQQD 7 | DB13c3R1bm5lbCBEZXZlbG9wbWVudCBDbGllbnQgMTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAMI87ijKk6jMWPcJtkcK0AKQ5rbrkLMACkCliB4v11gi 9 | kMbqMIT8eQHEfKmTu5uuisutMNrPqZQXCXgoYbUk0IuRuzXtmuZSMAzA5X/xWAU4 10 | fpbXwS5vd21impE6UZkXGQunngD5WNEn4sm7t4g/ioCTqMdv+svBMTaWXUIwpgEO 11 | KtMsaC9FVQxYe9zyJPN0QaSU6NMOM2LwQNzh51vtJyVT9lCixTQWM1Q5bTbQrVrz 12 | MdRZHEX6ogEg/WPUTaO1y+InBAVn6DzcLwslLPV7s4/6l6hmNEkZWBqz77KFc5H1 13 | Ol7PRInYLt8BlACb1MIlE81RYLIW1THMN7G4eVRrgq8CAwEAAaOBxTCBwjAJBgNV 14 | HRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNT 15 | TCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0GA1UdDgQWBBS3UAwJmyci 16 | WunqYPI4DSNTMjpBIjAfBgNVHSMEGDAWgBTtbfEPPuXC2O2AOHxRPWgCoo07wzAO 17 | BgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0G 18 | CSqGSIb3DQEBCwUAA4ICAQCyUuIIIylwAEJ0x4cvwuuV65MnsDiNktpqllsih/wo 19 | bfutcvGy//FOMk/04YYJZbLAOfvNLakp3A+oEGOuYaMJst9e2zGXRI2AqPZzxxkY 20 | P1GAfnxiLtjHltUMlpMrx+thGWqKYYAXim8TY3O3hjXkrpE8vOTG5vpQmMMmo2/y 21 | Po++eiVbd/+cXDdMwbum31rexpN0JC9SBF9GmrHoKekPpQ+gGJe6HAVAEWV2UMId 22 | MEtKzk8RrwySfn6oErNkOD+4DF9eJogl2cjlfl5y+DHeadAtYDcXS8lEvgJ6LwsE 23 | ftDMhtY1FBrq5nCxip2Fk6uB08wypcHXC/+sXP71tCBuQiaHCW/GTaFlWQijfepq 24 | yh/zx9narIxpTFCVfX1hiy6uYkehvWiBsds0LNwVzSUEC+wfxuwGlVWRtalf/K9e 25 | +1CHraMP0VjsSO+U5CWKuNdPffq/V/v2nyKzkbakssZex3Xoy0sWZ/i0tl2AD+K8 26 | p9SRRuWShv2q3qZUfbKRgGnIlsCJNK3xUlak6HTPpL/woV3QfakO81E06x/Mlgvm 27 | 9PXA9t0yp+hyk0S9mgwAiL5/s6pm+A0WsSjJ6ohl2jsktvAYgFy7S/rFL7ZC7kN1 28 | TH+dlY/0Cj5yBlA0G0B2VEoW0nrnUGfA6xxnl1Tv096bhxCUKg9YC+/VDRgIVyX4 29 | uw== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/mTLS/certs/wstunnel-server.cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFhzCCA2+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCR0Ix 3 | EDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50 4 | MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVudCBSb290IENBMB4XDTI0MDQx 5 | OTA2MzkwMloXDTI1MDQyOTA2MzkwMlowZDELMAkGA1UEBhMCR0IxEDAOBgNVBAgM 6 | B0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSQwIgYDVQQD 7 | DBt3c3R1bm5lbCBEZXZlbG9wbWVudCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUA 8 | A4IBDwAwggEKAoIBAQCukQ8xc1LHj+NxSGK714s3jMBJdzuaPsUh5tVj5I+2SMNf 9 | t6SmLxRm14meUChoiyIaZ5K3Nd8F8+PzF2CmdH6dTY+XzZv2b0Ap544qNJ2trHwi 10 | J9AR8zYOoCnHnlc4p7NatQsSJ8HozhVuPSBA46xtTUsHUFSVZt0kfGBDDXDtsnU1 11 | e3BWqvjyndvqSF/TF1YX1CakuY+rNKKyVUOYHE/dncMdlBA+G09NiDYhrSybiadW 12 | OU3D++oJmOFC32Mz2r/sMTqmHgb/NoNA83br5I8AUmhFcZTNE5pbjLgbukQrlA1s 13 | noTCNCHK39YvuC/DGFONWiwQdMCITn3S7z9k4/ObAgMBAAGjggFAMIIBPDAJBgNV 14 | HRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIGQDAzBglghkgBhvhCAQ0EJhYkT3BlblNT 15 | TCBHZW5lcmF0ZWQgU2VydmVyIENlcnRpZmljYXRlMB0GA1UdDgQWBBTevpkP7cSW 16 | XnmhdtxQej4tBVMd1DCBogYDVR0jBIGaMIGXgBTtbfEPPuXC2O2AOHxRPWgCoo07 17 | w6FppGcwZTELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoM 18 | FHdzdHVubmVsIGRldmVsb3BtZW50MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9w 19 | bWVudCBSb290IENBghRNOEar2B6EMORpy15L+8cSdEUR2zAOBgNVHQ8BAf8EBAMC 20 | BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggIBABztA4Vm 21 | ROMZmtMmcjxV5hsNXs1iB50MBjjMvsLS2bVSfqRiENDnSrwHrGtvvcaX2sLrKtld 22 | Xv+zgIMU7n4UxAIPiJa+X47+glyQZXpK7MNpzQgsCtNCUyI6flqIADm/RPCwVbbM 23 | tQOW3rsoPj01/lQ1oiAndVjDe83DXcA25fbXTFY7qtDqSbNGo/WIyR9npPSGrOdb 24 | vgAxvapJ41XdmJNXcJ9Q/Pdi2EV8FFRUPxjq48NQMuoDwcyPmGZ/OfuHN2ln/+8Q 25 | PBuzi/nEP1iA/kF3XWwEHPbrJDCmWXRBt5JDy41StlK3IP+1BJHfSq4+Z1lffhS+ 26 | HmXDaGbejUc6AjC6P26GehwAwY9q2e/43Dgz9lsgatn+BujvnJAB5UfWDCXWIMqo 27 | +TndKEPEdnNiNqdm015Tgx55bRtedzGAk9ezjgiopCp/CmyUfI4P88cKEcOUilFi 28 | 3GKjppaloUd66sgWHSNTKUqN17xkbfZMjEn90Bo+5fj1KlkeJBcesQadS9SHv9hA 29 | isquaduRsrRikDwqX3pil1/zWS7cYIw0VomBk0QQ78u0UMwofs5Ra0VOTLJPRl+p 30 | u1PUhwi76IKFkVsLaxaaAwE5wYm+VPQP+dScOSkJz6swEkDhe5A7Q61Lu3GxHM/Y 31 | La4WMAlE3IDOA7Piao6jI/TU5Jg65SmS7gX2 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /certs/mTLS/csr/wstunnel-client-1.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICqzCCAZMCAQAwZjELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAb 3 | BgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSYwJAYDVQQDDB13c3R1bm5lbCBE 4 | ZXZlbG9wbWVudCBDbGllbnQgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMI87ijKk6jMWPcJtkcK0AKQ5rbrkLMACkCliB4v11gikMbqMIT8eQHEfKmT 6 | u5uuisutMNrPqZQXCXgoYbUk0IuRuzXtmuZSMAzA5X/xWAU4fpbXwS5vd21impE6 7 | UZkXGQunngD5WNEn4sm7t4g/ioCTqMdv+svBMTaWXUIwpgEOKtMsaC9FVQxYe9zy 8 | JPN0QaSU6NMOM2LwQNzh51vtJyVT9lCixTQWM1Q5bTbQrVrzMdRZHEX6ogEg/WPU 9 | TaO1y+InBAVn6DzcLwslLPV7s4/6l6hmNEkZWBqz77KFc5H1Ol7PRInYLt8BlACb 10 | 1MIlE81RYLIW1THMN7G4eVRrgq8CAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQAW 11 | OnKLj8cAGS568iF30bcVZHXdM99pwtXXwu/KCNCEH2XzVQAq1LmT1mpxOui+HO4e 12 | k8PcOqSfw14V4sDxgGuDugPysb68Dpml4zwa99MyZOr4PqJWsRLpWhXk4sRFTlQo 13 | yk6EE8faRg3vN/ZCR4um869Vn7pgrCky/q6e/RSsFgjRX2tNEQkHYHaSM6adA3f4 14 | wzKj9gHKZcE1mVmTfzfGLnPi0Y4YzqA0Zbd9rtSHVg4SX6wvJrO+bPw8CQaBh4TK 15 | 26c11vB6S1snboi9AVN8rliYLSblZESX5txvTmjHiGV6vHnpBVceMJklgeLH2oP3 16 | 6L9oLWpe8YewnB+d0ufb 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /certs/mTLS/csr/wstunnel-server.csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICqTCCAZECAQAwZDELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAb 3 | BgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSQwIgYDVQQDDBt3c3R1bm5lbCBE 4 | ZXZlbG9wbWVudCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 5 | AQCukQ8xc1LHj+NxSGK714s3jMBJdzuaPsUh5tVj5I+2SMNft6SmLxRm14meUCho 6 | iyIaZ5K3Nd8F8+PzF2CmdH6dTY+XzZv2b0Ap544qNJ2trHwiJ9AR8zYOoCnHnlc4 7 | p7NatQsSJ8HozhVuPSBA46xtTUsHUFSVZt0kfGBDDXDtsnU1e3BWqvjyndvqSF/T 8 | F1YX1CakuY+rNKKyVUOYHE/dncMdlBA+G09NiDYhrSybiadWOU3D++oJmOFC32Mz 9 | 2r/sMTqmHgb/NoNA83br5I8AUmhFcZTNE5pbjLgbukQrlA1snoTCNCHK39YvuC/D 10 | GFONWiwQdMCITn3S7z9k4/ObAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAdSKi 11 | gdQSKSl9rXQH5FqpQgjHc9iOjQCf4JQ2cm/w+JyhS1aMtGFPaCcRc/D5rAc/3lkO 12 | jL+i4fS3TSo+2bfO8geU+WUNnHU18QYf9CwENsSi9lCKV1d+mTSn2H1oA+ThJKjR 13 | DnqzPA8AGUd9dM0dAAxPZL5A8w/IzzcK4LVEGEmiIzcVEPWjBPvSY4DQOyA2y4h/ 14 | WL2nsG/2AD7CIFLI03bvSaTmnbQ2KSWTaSXaj5uFobJwajRLMcuCjshgdhD0C4aT 15 | +vlzm1gc+VbSwsyKxbmWjCLagM+BBxySLJfq9GsoXRl9QCuyd+h94rrWMeXdleZU 16 | fPs6uUYg2nqbyr7Vuw== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /certs/mTLS/index.txt: -------------------------------------------------------------------------------- 1 | V 250429063902Z 1000 unknown /C=GB/ST=England/O=wstunnel development/CN=wstunnel Development Server 2 | V 250429064001Z 1001 unknown /C=GB/ST=England/O=wstunnel development/CN=wstunnel Development Client 1 3 | -------------------------------------------------------------------------------- /certs/mTLS/index.txt.attr: -------------------------------------------------------------------------------- 1 | unique_subject = yes 2 | -------------------------------------------------------------------------------- /certs/mTLS/newcerts/1000.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFhzCCA2+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCR0Ix 3 | EDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50 4 | MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVudCBSb290IENBMB4XDTI0MDQx 5 | OTA2MzkwMloXDTI1MDQyOTA2MzkwMlowZDELMAkGA1UEBhMCR0IxEDAOBgNVBAgM 6 | B0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSQwIgYDVQQD 7 | DBt3c3R1bm5lbCBEZXZlbG9wbWVudCBTZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUA 8 | A4IBDwAwggEKAoIBAQCukQ8xc1LHj+NxSGK714s3jMBJdzuaPsUh5tVj5I+2SMNf 9 | t6SmLxRm14meUChoiyIaZ5K3Nd8F8+PzF2CmdH6dTY+XzZv2b0Ap544qNJ2trHwi 10 | J9AR8zYOoCnHnlc4p7NatQsSJ8HozhVuPSBA46xtTUsHUFSVZt0kfGBDDXDtsnU1 11 | e3BWqvjyndvqSF/TF1YX1CakuY+rNKKyVUOYHE/dncMdlBA+G09NiDYhrSybiadW 12 | OU3D++oJmOFC32Mz2r/sMTqmHgb/NoNA83br5I8AUmhFcZTNE5pbjLgbukQrlA1s 13 | noTCNCHK39YvuC/DGFONWiwQdMCITn3S7z9k4/ObAgMBAAGjggFAMIIBPDAJBgNV 14 | HRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIGQDAzBglghkgBhvhCAQ0EJhYkT3BlblNT 15 | TCBHZW5lcmF0ZWQgU2VydmVyIENlcnRpZmljYXRlMB0GA1UdDgQWBBTevpkP7cSW 16 | XnmhdtxQej4tBVMd1DCBogYDVR0jBIGaMIGXgBTtbfEPPuXC2O2AOHxRPWgCoo07 17 | w6FppGcwZTELMAkGA1UEBhMCR0IxEDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoM 18 | FHdzdHVubmVsIGRldmVsb3BtZW50MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9w 19 | bWVudCBSb290IENBghRNOEar2B6EMORpy15L+8cSdEUR2zAOBgNVHQ8BAf8EBAMC 20 | BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggIBABztA4Vm 21 | ROMZmtMmcjxV5hsNXs1iB50MBjjMvsLS2bVSfqRiENDnSrwHrGtvvcaX2sLrKtld 22 | Xv+zgIMU7n4UxAIPiJa+X47+glyQZXpK7MNpzQgsCtNCUyI6flqIADm/RPCwVbbM 23 | tQOW3rsoPj01/lQ1oiAndVjDe83DXcA25fbXTFY7qtDqSbNGo/WIyR9npPSGrOdb 24 | vgAxvapJ41XdmJNXcJ9Q/Pdi2EV8FFRUPxjq48NQMuoDwcyPmGZ/OfuHN2ln/+8Q 25 | PBuzi/nEP1iA/kF3XWwEHPbrJDCmWXRBt5JDy41StlK3IP+1BJHfSq4+Z1lffhS+ 26 | HmXDaGbejUc6AjC6P26GehwAwY9q2e/43Dgz9lsgatn+BujvnJAB5UfWDCXWIMqo 27 | +TndKEPEdnNiNqdm015Tgx55bRtedzGAk9ezjgiopCp/CmyUfI4P88cKEcOUilFi 28 | 3GKjppaloUd66sgWHSNTKUqN17xkbfZMjEn90Bo+5fj1KlkeJBcesQadS9SHv9hA 29 | isquaduRsrRikDwqX3pil1/zWS7cYIw0VomBk0QQ78u0UMwofs5Ra0VOTLJPRl+p 30 | u1PUhwi76IKFkVsLaxaaAwE5wYm+VPQP+dScOSkJz6swEkDhe5A7Q61Lu3GxHM/Y 31 | La4WMAlE3IDOA7Piao6jI/TU5Jg65SmS7gX2 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /certs/mTLS/newcerts/1001.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFDTCCAvWgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCR0Ix 3 | EDAOBgNVBAgMB0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50 4 | MSUwIwYDVQQDDBx3c3R1bm5lbCBEZXZlbG9wbWVudCBSb290IENBMB4XDTI0MDQx 5 | OTA2NDAwMVoXDTI1MDQyOTA2NDAwMVowZjELMAkGA1UEBhMCR0IxEDAOBgNVBAgM 6 | B0VuZ2xhbmQxHTAbBgNVBAoMFHdzdHVubmVsIGRldmVsb3BtZW50MSYwJAYDVQQD 7 | DB13c3R1bm5lbCBEZXZlbG9wbWVudCBDbGllbnQgMTCCASIwDQYJKoZIhvcNAQEB 8 | BQADggEPADCCAQoCggEBAMI87ijKk6jMWPcJtkcK0AKQ5rbrkLMACkCliB4v11gi 9 | kMbqMIT8eQHEfKmTu5uuisutMNrPqZQXCXgoYbUk0IuRuzXtmuZSMAzA5X/xWAU4 10 | fpbXwS5vd21impE6UZkXGQunngD5WNEn4sm7t4g/ioCTqMdv+svBMTaWXUIwpgEO 11 | KtMsaC9FVQxYe9zyJPN0QaSU6NMOM2LwQNzh51vtJyVT9lCixTQWM1Q5bTbQrVrz 12 | MdRZHEX6ogEg/WPUTaO1y+InBAVn6DzcLwslLPV7s4/6l6hmNEkZWBqz77KFc5H1 13 | Ol7PRInYLt8BlACb1MIlE81RYLIW1THMN7G4eVRrgq8CAwEAAaOBxTCBwjAJBgNV 14 | HRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIFoDAzBglghkgBhvhCAQ0EJhYkT3BlblNT 15 | TCBHZW5lcmF0ZWQgQ2xpZW50IENlcnRpZmljYXRlMB0GA1UdDgQWBBS3UAwJmyci 16 | WunqYPI4DSNTMjpBIjAfBgNVHSMEGDAWgBTtbfEPPuXC2O2AOHxRPWgCoo07wzAO 17 | BgNVHQ8BAf8EBAMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMA0G 18 | CSqGSIb3DQEBCwUAA4ICAQCyUuIIIylwAEJ0x4cvwuuV65MnsDiNktpqllsih/wo 19 | bfutcvGy//FOMk/04YYJZbLAOfvNLakp3A+oEGOuYaMJst9e2zGXRI2AqPZzxxkY 20 | P1GAfnxiLtjHltUMlpMrx+thGWqKYYAXim8TY3O3hjXkrpE8vOTG5vpQmMMmo2/y 21 | Po++eiVbd/+cXDdMwbum31rexpN0JC9SBF9GmrHoKekPpQ+gGJe6HAVAEWV2UMId 22 | MEtKzk8RrwySfn6oErNkOD+4DF9eJogl2cjlfl5y+DHeadAtYDcXS8lEvgJ6LwsE 23 | ftDMhtY1FBrq5nCxip2Fk6uB08wypcHXC/+sXP71tCBuQiaHCW/GTaFlWQijfepq 24 | yh/zx9narIxpTFCVfX1hiy6uYkehvWiBsds0LNwVzSUEC+wfxuwGlVWRtalf/K9e 25 | +1CHraMP0VjsSO+U5CWKuNdPffq/V/v2nyKzkbakssZex3Xoy0sWZ/i0tl2AD+K8 26 | p9SRRuWShv2q3qZUfbKRgGnIlsCJNK3xUlak6HTPpL/woV3QfakO81E06x/Mlgvm 27 | 9PXA9t0yp+hyk0S9mgwAiL5/s6pm+A0WsSjJ6ohl2jsktvAYgFy7S/rFL7ZC7kN1 28 | TH+dlY/0Cj5yBlA0G0B2VEoW0nrnUGfA6xxnl1Tv096bhxCUKg9YC+/VDRgIVyX4 29 | uw== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /certs/mTLS/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | 4 | [ CA_default ] 5 | # Directory and file locations. 6 | dir = /home/erebe/progs/wstunnel/certs/mTLS 7 | certs = $dir/certs 8 | crl_dir = $dir/crl 9 | new_certs_dir = $dir/newcerts 10 | database = $dir/index.txt 11 | serial = $dir/serial 12 | RANDFILE = $dir/private/.rand 13 | 14 | # The root key and root certificate. 15 | private_key = $dir/private/ca.key.pem 16 | certificate = $dir/certs/ca.cert.pem 17 | 18 | # For certificate revocation lists. 19 | crlnumber = $dir/crlnumber 20 | crl = $dir/crl/ca.crl.pem 21 | crl_extensions = crl_ext 22 | default_crl_days = 30 23 | 24 | # SHA-1 is deprecated, so use SHA-2 instead. 25 | default_md = sha256 26 | 27 | name_opt = ca_default 28 | cert_opt = ca_default 29 | default_days = 375 30 | preserve = no 31 | policy = policy_loose 32 | 33 | [ policy_loose ] 34 | countryName = optional 35 | stateOrProvinceName = optional 36 | localityName = optional 37 | organizationName = optional 38 | organizationalUnitName = optional 39 | commonName = supplied 40 | emailAddress = optional 41 | 42 | [ req ] 43 | # Configuration for a certificate signing request. 44 | default_bits = 2048 45 | distinguished_name = req_distinguished_name 46 | string_mask = utf8only 47 | # SHA-1 is deprecated, so use SHA-2 instead. 48 | default_md = sha256 49 | # Extension to add when the -x509 option is used. 50 | x509_extensions = v3_ca 51 | 52 | [ req_distinguished_name ] 53 | # See . 54 | countryName = Country Name (2 letter code) 55 | stateOrProvinceName = State or Province Name 56 | localityName = Locality Name 57 | 0.organizationName = Organization Name 58 | organizationalUnitName = Organizational Unit Name 59 | commonName = Common Name 60 | emailAddress = Email Address 61 | 62 | # Optionally, specify some defaults. 63 | countryName_default = GB 64 | stateOrProvinceName_default = England 65 | localityName_default = 66 | 0.organizationName_default = wstunnel development 67 | #organizationalUnitName_default = 68 | #emailAddress_default = 69 | 70 | [ v3_ca ] 71 | # Configuration for a certificate authority. 72 | subjectKeyIdentifier = hash 73 | authorityKeyIdentifier = keyid:always,issuer 74 | basicConstraints = critical, CA:true 75 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 76 | 77 | [ client_cert ] 78 | # Configuration for client certificates. 79 | basicConstraints = CA:FALSE 80 | nsCertType = client, email 81 | nsComment = "OpenSSL Generated Client Certificate" 82 | subjectKeyIdentifier = hash 83 | authorityKeyIdentifier = keyid,issuer 84 | keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 85 | extendedKeyUsage = clientAuth, emailProtection 86 | 87 | [ server_cert ] 88 | # Configuration for server certificates. 89 | basicConstraints = CA:FALSE 90 | nsCertType = server 91 | nsComment = "OpenSSL Generated Server Certificate" 92 | subjectKeyIdentifier = hash 93 | authorityKeyIdentifier = keyid,issuer:always 94 | keyUsage = critical, digitalSignature, keyEncipherment 95 | extendedKeyUsage = serverAuth 96 | 97 | [ crl_ext ] 98 | # Configuration for CRLs. 99 | authorityKeyIdentifier=keyid:always 100 | -------------------------------------------------------------------------------- /certs/mTLS/private/ca.key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC9ZNNpms0QjP2s 3 | 1O/z1CKMbQd62UA7lFqj7jh2iciXT1R/i5rH/8nqQzBWBPF5bnnvqWT35n45VVJ3 4 | B0+EQw0S3VhqbgS0ArXU5XGQwjJ0MZAOH0aQHCiiAdG3+MOzbLrCcoV0uWgjETNE 5 | 228RoN3tasw5r4XuLEip3MMg9TJjFZ22a9meCGY0IyKMZvEFEdJ6VEBltRkmNrEP 6 | 117Lb1AKULDxLxS/bQm2Ld3snXR62yCcmg3gwx5Ej6RV8Ac4xk3fBi/ewJcRnSmA 7 | Ss6uSqxhxBI5CUoO8cHpxty2JzTwIq99GzpdqaxZx9funQVetXgfOhFPos5nR4cy 8 | Cr3lHVtiAa0Kq/Ou1N6Y3+1Mgft1he76p1mKOuoy8suf/CmVj97mxsUB8cxF6wys 9 | 76xCPcDmB00L53kedMFFf60zG7fvjveMsK1tY0iSYHsMz++l3ygwqI0UpLIrlqlC 10 | OMNzgJGq559G1lomT2doRCslQaA4iKkuFdQcz2bxmsiSGoErQGTVBsbdOFrzTR2h 11 | bEO9/vQ5v5vqjHIP3bRTg/hBdYNy30FG6+TA1ztDqwm7d2l0vfoTuY4wTE8TfHiQ 12 | gZNB7jUXCH62kpRgthIqrJQ/eoOZ8/Rwdx4lTgBaNDliTnxUtDqh7au/sB1R4D2L 13 | 5ETRENKkmfSjy5cU/ZHE1MXmo+0wFwIDAQABAoICAEnLAjCQdzvuo1x27zNiwT9T 14 | r+lmwoc0S4i55dgR4U1LRJIZk+o/OK4FFc0+SdPVfr8pkkSg0yeFngbwm0PeWDa0 15 | daGqUjzNHYnhCDmt4LizIvzNpNG7lv1glhUHYUEEqVPgCS2sm+2l4wL+OK12r2G1 16 | DfOf9yAQsxM0B/dMciB3KKcOKJFRlnjUA78O0PP3uLmfICRAxpbEEoMomC/NpDMQ 17 | s5CVlpDrbDBGeMSbqOnBfVhnEec0PxPZn984EahGY8r0/yvcgEAFq0joXNU+FSJW 18 | of8FJoziF3r917tFVXQHH7cwJ7KczKGCoxi+p3v6Wt5X4qzTs3Y8QWn3E3w0zwia 19 | HnD1KqfDmDs+u+kLl6yEUgTmEiRdEIAfrgaNEZ2otGTbECYBkl/Vc86IgvYOV2tp 20 | YzSiPHPcgfE3a9UM70ki8VUHVjbUyG0t9sygHAcEP66otPLou7zLE1xWLzvnhsNH 21 | eb4UPmCu3K4xp6Uo7LDMvyAkg4ZKnR9FZ1k4hEfXWRPX41I0P4LOvnUIuedNnbNs 22 | 7Az+TPWljnMmbgZ66nnGYCRg74/I2TAZziBiVYqEjLT7YN7l15t++7tk/OU35eLE 23 | flgr368j4Qx0pSr1h8SWG/d/TM41iin/y9eEQIrlxtkPIcBu/cLayMydwwmrQksU 24 | Hf1/6d7XLpCLQC6VTk9xAoIBAQDgRbG5nuToTYCRhNMg6xHZFRKTjX1+EqkRtsO3 25 | QzGyhdYiyFOSBIGL6JwMOk0557XIa/lDMO+eb9sTIOegCCy9fgeCZQq8OTd4ocjD 26 | fPq54t75TvidQIK2IQDJc8nMndr5nNVCnYzaiPcriKL7PWwZtM8vjNGX+CxqQYcs 27 | nu61WAazWQl83bKCZgMopFHkm/hAXFF1GiADwlq4/AtN5t9gkex70N3x3mxq5SvC 28 | FNJpFbH/NBg2l/2pzOUiZNW9uCE8bItOeRMMQwO+hY1PIgm4Ejl6izhsXArJzCC8 29 | SOkEK6vPmI0me36eEiY6PxX3ngJ/R0h38Fscgz4zyaHevi15AoIBAQDYL/dQCHIU 30 | G3KlYJs65mGwshK+qRYt+HXWJvlfQVVvTtfsB6iIOB5IGyIovHNXvyURtVTgH3+p 31 | rTi5ZITmuZhjY/c+ZCd465WrxC+3yVPKgNnlgRp6cxP4iqroO4VXz9ARVDnLrJbq 32 | 7M7FTglIM7jHdcU2gWnQFg5hULA9hVtqTsWDjK876H6tK2oZkQukjj4VCLMQSTro 33 | IVhWMmj5b1B3Vk9Ex0EpQ+2WXAUxJ983HUA2RJ3lv5LLWYbunNS09BNwSWwy39UN 34 | K2m4+fUP9EZWZ8xDUg42bGIA2a2lAg3Gra0P68L6GIITr1kgb77nuGeICL4bySVc 35 | i9Y5CMMTeTYPAoIBAHD4KlIKC9xITd7/PSpzvoXO65CP0QrUc32MxoFlw37dk8Pp 36 | jM8cnfTPsusl4wisAxF18hU6bTkttvintoUSGRdKiJXSN9ogKCUHuY1fQxynfxGu 37 | CeWMPUtozHCtdpUvXsIlkfcATZc3Luoq5Y4QnodEYKjfEiSuyhCr+V8sn6mRMa7d 38 | xr2zHtw4bpbmTqoNNruUxSNriXzbRb+wljEjfpmyZ6Dm0SWomIwv7B7TRjnQx9x5 39 | bUjyvr/tie4NRO1P9s3tDy70JfgjOZuawld+Gc8yvulPf5h1tKl5vXOadmW3adAk 40 | U9Vyl5EgK0ljxbj5SuC6E3L3C64NHiQQCQ2eVmECggEBAK9x7eEzmXEb/WSdDB02 41 | zl0ZhwDYNDnGg8ryAjr9yJn2gGD6rhkugdS+wHAS0ACMDUdbw6/HoFFRVNGP9BNS 42 | 14sBm6s0mJwXhHXLV3ZtmuSiwTLyHUz2i2SPFLg3ZbWn3xHRKr5SKIArAns5I2tH 43 | HlQxDYV4bSkEXVM4qm6jBVc26jAiQiv6OKPMseRhw/MdxOBJGRjEdVvhg9EgQ/T6 44 | E3FlyBrnIcidafk2YLhNxWbzBCOGeCX13OnOlCSdfjoEQqpDy91VrY9shfYqVGlI 45 | MrT4s9qGgyZDux05iyR4kDmGxQZArRFORnI3QbuDNIjVLKBHiBEAoqOCkK3koHvz 46 | SJ0CggEAAovWqQ1J6qbMBcUyJODLuyZ/Ju4muEeLMHF0G4nwCsBsF+XBJxrBQpfK 47 | yMn//dmJsgiiUOnpAofX9geZti4XMbd8V76x8Ph6Ifn41uJx30VHF4jcoKdaoGFp 48 | tqNuRRRGIBAQdS/wRQj23hJv99plONTWQPoPom+N1TnmQ4OvZ9pH2RkAvlky7Elb 49 | 9rg2Yb2e8RDYWC8SOkxFzq0Iqb2BbpvnARZQRqJtl32pxCiXSEUY1K8KnAQ29gZ3 50 | a0In4mAvTXDplViup2p6kH1PmbUc7ucJTOmirkK9yMA4eB4wxjbxJlZpL2y7UKWh 51 | d9HqBI2AwjW8MYtrV4UaM2bM6n7aow== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /certs/mTLS/private/wstunnel-client-1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCPO4oypOozFj3 3 | CbZHCtACkOa265CzAApApYgeL9dYIpDG6jCE/HkBxHypk7ubrorLrTDaz6mUFwl4 4 | KGG1JNCLkbs17ZrmUjAMwOV/8VgFOH6W18Eub3dtYpqROlGZFxkLp54A+VjRJ+LJ 5 | u7eIP4qAk6jHb/rLwTE2ll1CMKYBDirTLGgvRVUMWHvc8iTzdEGklOjTDjNi8EDc 6 | 4edb7SclU/ZQosU0FjNUOW020K1a8zHUWRxF+qIBIP1j1E2jtcviJwQFZ+g83C8L 7 | JSz1e7OP+peoZjRJGVgas++yhXOR9Tpez0SJ2C7fAZQAm9TCJRPNUWCyFtUxzDex 8 | uHlUa4KvAgMBAAECggEAPWP3nAHm6IdpqO6zY0HKG72DhhXu/nxJQURwQKY7SDpo 9 | is4S9r07W7En+4rbVWm1qYk8MzRGMy2SyxzsQM35cdtmEbXe6uPYFvfSsXzspn3E 10 | GNXpU01csEBlfPgzOREhU9su57zncvfJyJvhdpkqo9fHlP1SBZsyfD/LCvQIS7WL 11 | IsjQeRYGeGI+T+HiJyRGqkkk0YWZ09OL8QCAwTyhT58TH+7HzoWRBCCHGQaIS/Wm 12 | kMcPJsYLSNtwrj2qvG7dC4nZcl3ZHoRHVGnD4B+6y0esS8cHvu67FX09FviIrz6G 13 | l7EnU/uWnJ8iKDwl7Jwqeu/mk06OecI6KlYokbbeZQKBgQDgC0yqdPyIU7TZtihq 14 | cnet9yqm20yATSODIeOc2SPchMvF/EqLgBikSOBMTbl7OsZqfeTbPLbcKXbuJKa7 15 | QGu+5NJM6OKWW3h+aCwaB0YSQQnNtiqEOJoMPErm9Bj4jLEdVMP1J66i9ra+gxka 16 | plL/cUsNxCZaUhieTHq70zlQWwKBgQDd8UvMtUdqLvAQ1oT8oX+C7hlGAdYjHXIH 17 | PeBz+pWZWfNdH/lijWUEKBtynzmTJpvE52M6849MamUSFk7ypTe0NZJ/oPgxC0Nl 18 | yAr49yZmByJ6RiKOJTexmykWVhSi6x++HmWE0oz6+2vRIQ+Wdj0VWmUP+LVsvMD0 19 | QjrkhIinPQKBgQCf7NDr+BfvRDkbEyEkYtM1NfKXKoEgMHACAeXUp1cm6RAAIogf 20 | re9pDbA2J2EYKqtJhtYe/ObWny6K7VSq42BF4laPmclsZJzNNpUMe1a0XwKdecQ9 21 | n52u0DbzRxiwCtW+xywdyhapswxdT31S/ZjPSFK33+U0odd638LYYf1OcwKBgQDR 22 | bsh7dLjeP0q0aOn3RyJ/V9UrlcIPQtL+eGpcpyMSIaqfvvNjB1BCmuJDyHLZI/6r 23 | 0Tl3QKyBjIixh7GaEUQ+XqtOmoR6K0m/OwT3qKlob+UeAx7Kid5DT8p21GYG0t8S 24 | VbawnssAb85u+sat0geUJcfmSWhSIs/l7rWKPHKDdQKBgDqVTFHDncc3p4luCRgi 25 | 63cjz1fLSn+wiiICMsYTCJ9lHDwbXOKWAqyHDP0qTI8s3b13/LITOdj/Q0R33i0Y 26 | 38qJWBag5K2u7iyMQr/oxipuBhyhHU1mRXGXHl4/N3Oa5tkVhapz/e6PN0mMgTJQ 27 | /x19K09p5hc4sLnrk6BCOr3q 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /certs/mTLS/private/wstunnel-server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCukQ8xc1LHj+Nx 3 | SGK714s3jMBJdzuaPsUh5tVj5I+2SMNft6SmLxRm14meUChoiyIaZ5K3Nd8F8+Pz 4 | F2CmdH6dTY+XzZv2b0Ap544qNJ2trHwiJ9AR8zYOoCnHnlc4p7NatQsSJ8HozhVu 5 | PSBA46xtTUsHUFSVZt0kfGBDDXDtsnU1e3BWqvjyndvqSF/TF1YX1CakuY+rNKKy 6 | VUOYHE/dncMdlBA+G09NiDYhrSybiadWOU3D++oJmOFC32Mz2r/sMTqmHgb/NoNA 7 | 83br5I8AUmhFcZTNE5pbjLgbukQrlA1snoTCNCHK39YvuC/DGFONWiwQdMCITn3S 8 | 7z9k4/ObAgMBAAECggEAQgKcaiifrtLcQKQModdZz4Gr3Jv3r9X4mV8+Ze2x4k4V 9 | gwZgfm7jGhh686CABzhFhxKPSjRWx1t4YR3/8DGxBy6jE9YuGbvr2Wy0N4V58oh8 10 | 0DWZ2o/LazBpXBCmDshra+t16kGac7wqImt+3Mq7EwHdU0CvG2ewS/G0PObCQz9O 11 | UCIitSoO/TCLBmVOVND9u9tR6VX+nmqv7lbs5s311g2OXWbFQQDaWj9ftx6nZ0w+ 12 | k1MhXVYpKw1iM6fiaq5HB8AFF+H92bZyBGNxE3L9/peynzBnrwGb/n+w5cDSlmWI 13 | NsIDMslAgAJktttNXanwwWPiZTUj9/RJ0aE7Md3QEQKBgQDbsYumFBVuV3khr4pL 14 | fHRnAcDHRIGyWG5H/VZbNzeEZ1VVDGP1Xym0K3QtNKfM00gBIPIqnFf/gUuReY67 15 | H9HKg8i8kjSFH0G1HtR/dPVPqSc/DVeoEmy9hwgRH01ZYebL/fT9kRhn0bBLtoVh 16 | bYUSFDYxkJIT9uhZDysDbKR8JQKBgQDLalrSWG9G+PIGdKuPeMDdZHMNWSdGYECe 17 | Fv+oyhDQoH9228UJihIcbwT+ocsl/w0CK8Gvri8jxOnHW0yAzCaxCwi5VQWRCt1G 18 | rTs8O6jtDi3wBc3IdLl3ktfA8OEbOShiQh3YYMRCPBg/21A5SCyHdMEEwaF/842A 19 | 6OxeUPPEvwKBgBwVtF6EzsCOWiPeRvWjcVYBuV0/+ryL5X06e6Gpi2VXuGbo8JZb 20 | lf88Vtu4kYLzt469YXflCLLXGov8WCy/wpf7BNxmbGRgPIwk5tFsaDfIzgWXdQ89 21 | W71W18cok0DL7S9CxeDsfYw4GCt1p9NupsZK4yqu6p22wLkx4TPM3bIpAoGAO+nE 22 | fGYNyIK0jpA4o9Z2P/9BH/Jdbg4Vmjq97JIvp7NON8z9WRTwxq0wdGtlMXjQ9Q28 23 | S6lrOwbZsJ1EiD8ZOlY8qJHRROpFSHbnlpMf60qc3zBmbx9qLTz0DWElfGY2bdJ5 24 | hezigXu/zLclBuoqK2+JFoSNs+khiZGRZSpE0nMCgYEAzs0oC0doQhulvZz1nPzb 25 | AgthFZS4gIq8GsTW0GfXRYPRHEtHtDoYROEYM2aX7FcFRAKO/9iinW19rtMj6cpS 26 | 37tBMiEAGqInmPYGVfUalZpZKq14f85lVOg9HjTf/MxPs7RHgKj4WD1r4aeAhxPz 27 | d1+I0rsmh2lceuhHer/sS4M= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /certs/mTLS/serial: -------------------------------------------------------------------------------- 1 | 1002 2 | -------------------------------------------------------------------------------- /docs/logo_serviceplanet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erebe/wstunnel/0daa3fba68c3610599cd95b60e35365c77f5df16/docs/logo_serviceplanet.png -------------------------------------------------------------------------------- /docs/logo_wstunnel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erebe/wstunnel/0daa3fba68c3610599cd95b60e35365c77f5df16/docs/logo_wstunnel.png -------------------------------------------------------------------------------- /docs/using_mtls.md: -------------------------------------------------------------------------------- 1 | # Using mTLS with wstunnel 2 | 3 | ## Generating keys and certificates 4 | 5 | WARNING: The following instructions are intended for using in a development / testing environment. They are **not** 6 | intended for setting up a production environment. In a production environment you could use a solution such 7 | as [OpenBao](https://openbao.org/) (opensource fork of Hashicorp Vault), [EJBCA](https://www.ejbca.org/) 8 | or [Dogtag PKI](https://www.dogtagpki.org/) for example. 9 | 10 | These steps are based on: https://jamielinux.com/docs/openssl-certificate-authority/ 11 | 12 | In order to setup wstunnel to authenticate clients with certificates (mTLS) one must have a certificate authority for 13 | signing client certificates. In this example we will create a certificate authority using OpenSSL. 14 | 15 | Run these commands from a directory which we will use to store the CA's files. For example under `~/wstunnel/client_ca` 16 | 17 | ```shell 18 | $ mkdir -p $HOME/wstunnel/ca/{certs,csr,crl,newcerts,private} 19 | $ cd $HOME/wstunnel/ca/ 20 | $ echo 1000 > serial 21 | $ touch index.txt 22 | ``` 23 | 24 | Create the OpenSSL CA configuration. Beware some entries are escaped so they can be easily written out with `cat`: 25 | 26 | ```shell 27 | $ cat > ./openssl.cnf << END_OF_FILE 28 | [ ca ] 29 | default_ca = CA_default 30 | 31 | [ CA_default ] 32 | # Directory and file locations. 33 | dir = $HOME/wstunnel/ca 34 | certs = \$dir/certs 35 | crl_dir = \$dir/crl 36 | new_certs_dir = \$dir/newcerts 37 | database = \$dir/index.txt 38 | serial = \$dir/serial 39 | RANDFILE = \$dir/private/.rand 40 | 41 | # The root key and root certificate. 42 | private_key = \$dir/private/ca.key.pem 43 | certificate = \$dir/certs/ca.cert.pem 44 | 45 | # For certificate revocation lists. 46 | crlnumber = \$dir/crlnumber 47 | crl = \$dir/crl/ca.crl.pem 48 | crl_extensions = crl_ext 49 | default_crl_days = 30 50 | 51 | # SHA-1 is deprecated, so use SHA-2 instead. 52 | default_md = sha256 53 | 54 | name_opt = ca_default 55 | cert_opt = ca_default 56 | default_days = 375 57 | preserve = no 58 | policy = policy_loose 59 | 60 | [ policy_loose ] 61 | countryName = optional 62 | stateOrProvinceName = optional 63 | localityName = optional 64 | organizationName = optional 65 | organizationalUnitName = optional 66 | commonName = optional 67 | emailAddress = optional 68 | 69 | [ req ] 70 | # Configuration for a certificate signing request. 71 | default_bits = 2048 72 | distinguished_name = req_distinguished_name 73 | string_mask = utf8only 74 | # SHA-1 is deprecated, so use SHA-2 instead. 75 | default_md = sha256 76 | # Extension to add when the -x509 option is used. 77 | x509_extensions = v3_ca 78 | 79 | [ req_distinguished_name ] 80 | # See . 81 | countryName = Country Name (2 letter code) 82 | stateOrProvinceName = State or Province Name 83 | localityName = Locality Name 84 | 0.organizationName = Organization Name 85 | organizationalUnitName = Organizational Unit Name 86 | commonName = Common Name 87 | emailAddress = Email Address 88 | 89 | # Optionally, specify some defaults. 90 | countryName_default = GB 91 | stateOrProvinceName_default = England 92 | localityName_default = 93 | 0.organizationName_default = wstunnel development 94 | #organizationalUnitName_default = 95 | #emailAddress_default = 96 | 97 | [ v3_ca ] 98 | # Configuration for a certificate authority. 99 | subjectKeyIdentifier = hash 100 | authorityKeyIdentifier = keyid:always,issuer 101 | basicConstraints = critical, CA:true 102 | keyUsage = critical, digitalSignature, cRLSign, keyCertSign 103 | 104 | [ client_cert ] 105 | # Configuration for client certificates. 106 | basicConstraints = CA:FALSE 107 | nsCertType = client, email 108 | nsComment = "OpenSSL Generated Client Certificate" 109 | subjectKeyIdentifier = hash 110 | authorityKeyIdentifier = keyid,issuer 111 | keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment 112 | extendedKeyUsage = clientAuth, emailProtection 113 | 114 | [ server_cert ] 115 | # Configuration for server certificates. 116 | basicConstraints = CA:FALSE 117 | nsCertType = server 118 | nsComment = "OpenSSL Generated Server Certificate" 119 | subjectKeyIdentifier = hash 120 | authorityKeyIdentifier = keyid,issuer:always 121 | keyUsage = critical, digitalSignature, keyEncipherment 122 | extendedKeyUsage = serverAuth 123 | 124 | [ crl_ext ] 125 | # Configuration for CRLs. 126 | authorityKeyIdentifier=keyid:always 127 | END_OF_FILE 128 | ``` 129 | 130 | Generate the private key of the certificate authority. Normally you would encrypt it and set a passphrase on it but for 131 | development purposes we will leave it unencrypted. 132 | 133 | ```shell 134 | $ cd $HOME/wstunnel/ca/ 135 | $ openssl genrsa -out private/ca.key.pem 4096 136 | ``` 137 | 138 | The certificate of the root certificate authority is self-signed (since it is the root of trust): 139 | 140 | ```shell 141 | $ openssl req -config openssl.cnf \ 142 | -key private/ca.key.pem \ 143 | -new -x509 -days 7300 -sha256 -extensions v3_ca \ 144 | -out certs/ca.cert.pem 145 | ---8<------ 146 | Country Name (2 letter code) [GB]: 147 | State or Province Name [England]: 148 | Locality Name []: 149 | Organization Name [Alice Ltd]: 150 | Organizational Unit Name []: 151 | Common Name []:wstunnel Development Root CA 152 | Email Address []: 153 | ``` 154 | 155 | Generate a key for the wstunnel server, generate a certificate signing request (CSR) and create a certificate with our 156 | CA for the CSR: 157 | 158 | ```shell 159 | $ openssl genrsa -out private/wstunnel-server.pem 2048 160 | $ openssl req -config openssl.cnf \ 161 | -key private/wstunnel-server.pem \ 162 | -new -sha256 -out csr/wstunnel-server.csr.pem 163 | ---8<------ 164 | Country Name (2 letter code) [GB]: 165 | State or Province Name [England]: 166 | Locality Name []: 167 | Organization Name [Alice Ltd]: 168 | Organizational Unit Name []: 169 | Common Name []:wstunnel Development Server 170 | Email Address []: 171 | 172 | $ openssl ca -config openssl.cnf \ 173 | -extensions server_cert -days 375 -notext -md sha256 \ 174 | -in csr/wstunnel-server.csr.pem \ 175 | -out certs/wstunnel-server.cert.pem 176 | ---8<------ 177 | Sign the certificate? [y/n]:y 178 | 1 out of 1 certificate requests certified, commit? [y/n]y 179 | ``` 180 | 181 | Next we do the same thing (generate key, create request, sign request) but then for a wstunnel client: 182 | 183 | ```shell 184 | $ openssl genrsa -out private/wstunnel-client-1.pem 2048 185 | $ openssl req -config openssl.cnf \ 186 | -key private/wstunnel-client-1.pem \ 187 | -new -sha256 -out csr/wstunnel-client-1.csr.pem 188 | ---8<------ 189 | Country Name (2 letter code) [GB]: 190 | State or Province Name [England]: 191 | Locality Name []: 192 | Organization Name [Alice Ltd]: 193 | Organizational Unit Name []: 194 | Common Name []:wstunnel_client_1 # must contains only url valid characters 195 | Email Address []: 196 | 197 | $ openssl ca -config openssl.cnf \ 198 | -extensions client_cert -days 375 -notext -md sha256 \ 199 | -in csr/wstunnel-client-1.csr.pem \ 200 | -out certs/wstunnel-client-1.cert.pem 201 | ---8<------ 202 | Sign the certificate? [y/n]:y 203 | 1 out of 1 certificate requests certified, commit? [y/n]y 204 | ``` 205 | 206 | ## Using mTLS on the wstunnel server side 207 | 208 | This section assumes you have generated the certificate authority, keys, certificates, etc. as outlined in the " 209 | Generating keys and certificates" section. 210 | 211 | Start a `wstunnel` server and make it use the server key pair certificate (`--tls-certificate` and `--tls-private-key`) 212 | and configure it to authenticate clients via mTLS (`--tls-client-ca-certs`): 213 | 214 | ```shell 215 | $ wstunnel server \ 216 | --tls-certificate ./certs/wstunnel-server.cert.pem \ 217 | --tls-private-key ./private/wstunnel-server.pem \ 218 | --tls-client-ca-certs ./certs/ca.cert.pem \ 219 | wss://0.0.0.0:8443 220 | ``` 221 | 222 | ### Testing 223 | 224 | You can use `openssl` to test connecting with the client certificate to the wstunnel server: 225 | 226 | ```shell 227 | $ openssl s_client -connect 127.0.0.1:8443 \ 228 | -key ./private/wstunnel-client-1.pem \ 229 | -cert ./certs/wstunnel-client-1.cert.pem \ 230 | -cert_chain ./certs/ca.cert.pem \ 231 | -state -debug 232 | ---8<----- 233 | Acceptable client certificate CA names 234 | C = GB, ST = England, O = Alice Ltd, CN = wstunnel Development Root CA 235 | ---8<----- 236 | ``` 237 | 238 | Similarly, you can use `openssl` to test what happens if you try to connect with a client certificate which is not 239 | signed by our CA by generating a self-signed certificate: 240 | 241 | ```shell 242 | $ openssl req -nodes -x509 -sha256 -newkey rsa:4096 \ 243 | -keyout faux.key.pem \ 244 | -out faux.crt.pem \ 245 | -days 356 \ 246 | -subj "/C=GB/ST=England/L=London/O=ACME Corp/OU=IT Dept/CN=Development Faux Client" 247 | $ openssl s_client -connect 127.0.0.1:8443 \ 248 | -key faux.key.pem \ 249 | -cert faux.crt.pem \ 250 | -cert_chain ./certs/ca.cert.pem \ 251 | -state -debug 252 | ----8<---- 253 | SSL3 alert read:fatal:certificate unknown 254 | ---8<----- 255 | ``` 256 | 257 | Trying to connect without the client presenting any certificate at all will also fail (the `--cacert` flag only 258 | tells `curl` which CA certificate to use to verify the certificate of the **server** with): 259 | 260 | ```shell 261 | $ curl -vvv --cacert ./certs/ca.cert.pem https://127.0.0.1:8443 262 | ``` 263 | 264 | ## Using mTLS on the wstunnel client side 265 | 266 | This section assumes you have generated the certificate authority, keys, certificates, etc. as outlined in the " 267 | Generating keys and certificates" section. It also assumes you have a running wstunnel server with mTLS configured. For 268 | example as setup in the `Using mTLS on the wstunnel server side` section. 269 | 270 | ```shell 271 | $ wstunnel client \ 272 | --tls-certificate ./certs/wstunnel-client-1.cert.pem \ 273 | --tls-private-key ./private/wstunnel-client-1.pem \ 274 | -L tcp://1212:localhost:1313 \ 275 | wss://127.0.0.1:8443 276 | 277 | $ nc 127.0.0.1 1212 278 | ``` 279 | 280 | -------------------------------------------------------------------------------- /goreleaser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() {} 4 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := false 2 | 3 | _default: 4 | @just --list 5 | 6 | make_release $VERSION $FORCE="": 7 | sed -i 's/^version = .*/version = "'$VERSION'"/g' wstunnel-cli/Cargo.toml wstunnel/Cargo.toml 8 | cargo fmt --all -- --check --color=always || (echo "Use cargo fmt to format your code"; exit 1) 9 | #cargo clippy --all --all-features -- -D warnings || (echo "Solve your clippy warnings to succeed"; exit 1) 10 | git add wstunnel/Cargo.* wstunnel-cli/Cargo.* Cargo.* 11 | git commit -m 'Bump version v'$VERSION 12 | git tag $FORCE v$VERSION -m 'version v'$VERSION 13 | git push $FORCE 14 | git push $FORCE origin v$VERSION 15 | @just docker_release v$VERSION 16 | 17 | docker_release $TAG: 18 | #docker login -u erebe ghcr.io 19 | #~/.depot/bin/depot build --project v4z5w7md33 --platform linux/arm/v7,linux/arm64,linux/amd64 -t ghcr.io/erebe/wstunnel:$TAG -t ghcr.io/erebe/wstunnel:latest --push . 20 | docker buildx create --name builder --driver=kubernetes --platform=linux/arm64 '--driver-opt="nodeselector=kubernetes.io/arch=arm64","tolerations=key=kubernetes.io/hostname,value=server","requests.cpu=16","requests.memory=16G"' --node=build-arm64 21 | docker buildx create --append --name builder --driver=kubernetes --platform=linux/amd64 '--driver-opt="nodeselector=kubernetes.io/arch=amd64","tolerations=key=kubernetes.io/hostname,value=toybox","requests.cpu=16","requests.memory=16G"' --node=build-amd64 22 | docker buildx use builder 23 | docker buildx build \ 24 | --platform linux/arm/v7,linux/arm64,linux/amd64 \ 25 | --cache-from type=registry,ref=ghcr.io/erebe/wstunnel:cache \ 26 | --cache-to type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=ghcr.io/erebe/wstunnel:cache \ 27 | -t ghcr.io/erebe/wstunnel:$TAG \ 28 | -t ghcr.io/erebe/wstunnel:latest \ 29 | --push . 30 | docker buildx rm builder 31 | 32 | -------------------------------------------------------------------------------- /restrictions.yaml: -------------------------------------------------------------------------------- 1 | # Restrictions are whitelist rules for the tunnels 2 | # By default, all requests are denied and only if a restriction match, the request is allowed 3 | restrictions: 4 | - name: "Allow all" 5 | description: "This restriction allows all requests" 6 | # This restriction apply only and only if all matchers match/are evaluated to true 7 | # It is a logical AND 8 | match: 9 | # This match apply only if it succeeds to match the path prefix with the given regex 10 | # The regex does a match, so if you want to match exactly you need to bound the pattern with ^ $ 11 | # I.e: "tesotron" is going to match "XXXtesotronXXX", but "^tesotron$" is going to match only "tesotron" 12 | - !PathPrefix "^.*$" 13 | # This match applies only if it succeeds to match the Authentication Header with the given regex. 14 | # If present, Authentication Header must exists and must match the regex. 15 | # - !Authorization "^[Bb]earer +actual_bearer_token_to_match$" 16 | # The only other possible match type for now is !Any, that match everything/any request 17 | # - !Any 18 | 19 | # This is the list of tunnels your restriction is going to allow 20 | # The list is checked in order, the first match is going to allow the request 21 | allow: 22 | # !Tunnel allows forward tunnels 23 | - !Tunnel 24 | # Protocol that are allowed. Empty list means all protocols are allowed 25 | # Logical OR 26 | protocol: 27 | - Tcp 28 | - Udp 29 | # Port that are allowed. Can be a single port or an inclusive range (i.e. 80..90) 30 | # Logical OR 31 | port: 32 | - 80 33 | - 443 34 | - 8080..8089 35 | 36 | # if the tunnel wants to connect to a specific host, this regex must match 37 | host: ^.*$ 38 | # if the tunnel wants to connect to a specific IP, it must be included in one of the network cidr 39 | # Logical OR 40 | cidr: 41 | - 0.0.0.0/0 42 | - ::/0 43 | 44 | # !ReverseTunnel allows reverse tunnels 45 | # Not specifying anything means all reverse tunnels are allowed 46 | - !ReverseTunnel 47 | protocol: 48 | - Tcp 49 | - Udp 50 | - Socks5 51 | - Unix 52 | port: 53 | - 1..65535 54 | # Maps ports on the server side from X to Y (X:Y). For example with 10001:8080 configured and a client 55 | # which connects using '-R tcp://10001:localhost:80' the server will listen on port 8080 instead of 10001. 56 | # The originally requested ports (NOT the mapped ports) need to be allowed via the 'ports' directive. 57 | port_mapping: 58 | - 10001:8080 59 | cidr: 60 | - 0.0.0.0/0 61 | - ::/0 62 | 63 | --- 64 | # Examples 65 | restrictions: 66 | - name: "example 1" 67 | description: "Only allow forward tunnels to port 443 and forbid reverse tunnels" 68 | match: 69 | - !PathPrefix "^.*$" 70 | allow: 71 | - !Tunnel 72 | port: 73 | - 443 74 | --- 75 | restrictions: 76 | - name: "example 2" 77 | description: "Only allow forward tunnels to local ssh and forbid reverse tunnels" 78 | match: 79 | - !PathPrefix "^.*$" 80 | allow: 81 | - !Tunnel 82 | protocol: 83 | - Tcp 84 | port: 85 | - 22 86 | host: ^localhost$ 87 | cidr: 88 | - 127.0.0.1/32 89 | --- 90 | restrictions: 91 | - name: "example 3" 92 | description: "Only allow socks5 reverse tunnels listening on port between 1080..1443 on lan network" 93 | match: 94 | - !PathPrefix "^.*$" 95 | allow: 96 | - !ReverseTunnel 97 | protocol: 98 | - Socks5 99 | port: 100 | - 1080..1443 101 | cidr: 102 | - 192.168.0.0/16 103 | --- 104 | restrictions: 105 | - name: "example 4" 106 | description: "Allow everything for client using path prefix my-super-secret-path" 107 | match: 108 | - !PathPrefix "^my-super-secret-path$" 109 | allow: 110 | - !Tunnel 111 | - !ReverseTunnel 112 | --- 113 | restrictions: 114 | - name: "example 5" 115 | description: "Forbid everything ..." 116 | match: 117 | - !Any 118 | allow: [] 119 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" 2 | max_width = 120 3 | fn_call_width = 80 4 | -------------------------------------------------------------------------------- /wstunnel-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstunnel-cli" 3 | version = "10.4.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1.0.98" 8 | clap = { version = "4.5.39", features = ["derive", "env"] } 9 | fdlimit = "0.3.0" 10 | tokio = { version = "1.45.1", features = ["full"] } 11 | tracing = { version = "0.1.41", features = ["log"] } 12 | tracing-subscriber = { version = "0.3.19", features = ["env-filter", "fmt", "local-time"] } 13 | wstunnel = { path = "../wstunnel" , default-features = false, features = ["clap"] } 14 | 15 | tikv-jemallocator = { version = "0.6", optional = true } 16 | 17 | [features] 18 | default = ["aws-lc-rs"] 19 | jemalloc = ["dep:tikv-jemallocator"] 20 | aws-lc-rs = ["wstunnel/aws-lc-rs"] 21 | ring = ["wstunnel/ring"] 22 | aws-lc-rs-bindgen = ["wstunnel/aws-lc-rs-bindgen"] 23 | 24 | [[bin]] 25 | name = "wstunnel" 26 | path = "src/main.rs" 27 | -------------------------------------------------------------------------------- /wstunnel-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io; 3 | use std::str::FromStr; 4 | use tracing::warn; 5 | use tracing_subscriber::EnvFilter; 6 | use tracing_subscriber::filter::Directive; 7 | use wstunnel::LocalProtocol; 8 | use wstunnel::config::{Client, Server}; 9 | use wstunnel::executor::DefaultTokioExecutor; 10 | use wstunnel::{run_client, run_server}; 11 | 12 | #[cfg(feature = "jemalloc")] 13 | use tikv_jemallocator::Jemalloc; 14 | 15 | #[cfg(feature = "jemalloc")] 16 | #[global_allocator] 17 | static GLOBAL: Jemalloc = Jemalloc; 18 | 19 | /// Use Websocket or HTTP2 protocol to tunnel {TCP,UDP} traffic 20 | /// wsTunnelClient <---> wsTunnelServer <---> RemoteHost 21 | #[derive(clap::Parser, Debug)] 22 | #[command(author, version, about, verbatim_doc_comment, long_about = None)] 23 | pub struct Wstunnel { 24 | #[command(subcommand)] 25 | commands: Commands, 26 | 27 | /// Disable color output in logs 28 | #[arg(long, global = true, verbatim_doc_comment, env = "NO_COLOR")] 29 | no_color: Option, 30 | 31 | /// *WARNING* The flag does nothing, you need to set the env variable *WARNING* 32 | /// Control the number of threads that will be used. 33 | /// By default, it is equal the number of cpus 34 | #[arg( 35 | long, 36 | global = true, 37 | value_name = "INT", 38 | verbatim_doc_comment, 39 | env = "TOKIO_WORKER_THREADS" 40 | )] 41 | nb_worker_threads: Option, 42 | 43 | /// Control the log verbosity. i.e: TRACE, DEBUG, INFO, WARN, ERROR, OFF 44 | /// for more details: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#example-syntax 45 | #[arg( 46 | long, 47 | global = true, 48 | value_name = "LOG_LEVEL", 49 | verbatim_doc_comment, 50 | env = "RUST_LOG", 51 | default_value = "INFO" 52 | )] 53 | log_lvl: String, 54 | } 55 | 56 | #[derive(clap::Subcommand, Debug)] 57 | pub enum Commands { 58 | Client(Box), 59 | Server(Box), 60 | } 61 | 62 | #[tokio::main] 63 | async fn main() -> anyhow::Result<()> { 64 | let args = Wstunnel::parse(); 65 | 66 | // Setup logging 67 | let mut env_filter = EnvFilter::builder().parse(&args.log_lvl).expect("Invalid log level"); 68 | if !(args.log_lvl.contains("h2::") || args.log_lvl.contains("h2=")) { 69 | env_filter = env_filter.add_directive(Directive::from_str("h2::codec=off").expect("Invalid log directive")); 70 | } 71 | let logger = tracing_subscriber::fmt() 72 | .with_ansi(args.no_color.is_none()) 73 | .with_env_filter(env_filter); 74 | 75 | // stdio tunnel capture stdio, so need to log into stderr 76 | if let Commands::Client(args) = &args.commands { 77 | if args 78 | .local_to_remote 79 | .iter() 80 | .filter(|x| matches!(x.local_protocol, LocalProtocol::Stdio { .. })) 81 | .count() 82 | > 0 83 | { 84 | logger.with_writer(io::stderr).init(); 85 | } else { 86 | logger.init() 87 | } 88 | } else { 89 | logger.init(); 90 | }; 91 | if let Err(err) = fdlimit::raise_fd_limit() { 92 | warn!("Failed to set soft filelimit to hard file limit: {}", err) 93 | } 94 | 95 | match args.commands { 96 | Commands::Client(args) => { 97 | run_client(*args, DefaultTokioExecutor::default()) 98 | .await 99 | .unwrap_or_else(|err| { 100 | panic!("Cannot start wstunnel client: {:?}", err); 101 | }); 102 | } 103 | Commands::Server(args) => { 104 | run_server(*args, DefaultTokioExecutor::default()) 105 | .await 106 | .unwrap_or_else(|err| { 107 | panic!("Cannot start wstunnel server: {:?}", err); 108 | }); 109 | } 110 | } 111 | 112 | Ok(()) 113 | } 114 | -------------------------------------------------------------------------------- /wstunnel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstunnel" 3 | version = "10.4.0" 4 | edition = "2024" 5 | repository = "https://github.com/erebe/wstunnel.git" 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ahash = { version = "0.8.12", features = [] } 10 | anyhow = "1.0.98" 11 | base64 = "0.22.1" 12 | scopeguard = "1.2.0" 13 | 14 | bb8 = { version = "0.9.0", features = [] } 15 | bytes = { version = "1.10.1", features = [] } 16 | clap = { version = "4.5.39", features = ["derive", "env"], optional = true } 17 | fast-socks5 = { version = "0.10.0", features = [] } 18 | fastwebsockets = { version = "0.10.0", features = ["upgrade", "simd", "unstable-split"] } 19 | futures-util = { version = "0.3.31" } 20 | ppp = { version = "2.3.0", features = [] } 21 | async-channel = { version = "2.3.1", features = [] } 22 | arc-swap = { version = "1.7.1", features = [] } 23 | 24 | # For config file parsing 25 | regex = { version = "1.11.1", default-features = false, features = ["std", "perf"] } 26 | serde_regex = "1.1.0" 27 | serde_yaml = { version = "0.9.34", features = [] } 28 | ipnet = { version = "2.11.0", features = ["serde"] } 29 | 30 | hyper = { version = "1.6.0", features = ["client", "http1", "http2"] } 31 | hyper-util = { version = "0.1.13", features = ["tokio", "server", "server-auto"] } 32 | http-body-util = { version = "0.1.3" } 33 | jsonwebtoken = { version = "9.3.1", default-features = false } 34 | log = "0.4.27" 35 | nix = { version = "0.30.1", features = ["socket", "net", "uio"] } 36 | parking_lot = "0.12.4" 37 | pin-project = "1" 38 | notify = { version = "8.0.0", features = [] } 39 | 40 | rustls-native-certs = { version = "0.8.1", features = [] } 41 | rustls-pemfile = { version = "2.2.0", features = [] } 42 | x509-parser = "0.17.0" 43 | serde = { version = "1.0.219", features = ["derive"] } 44 | socket2 = { version = "0.5.10", features = [] } 45 | tokio = { version = "1.45.1", features = ["io-std", "net", "signal", "sync", "time"] } 46 | tokio-stream = { version = "0.1.17", features = ["net"] } 47 | 48 | tracing = { version = "0.1.41", features = ["log"] } 49 | url = "2.5.4" 50 | urlencoding = "2.1.3" 51 | uuid = { version = "1.17.0", features = ["v7", "serde"] } 52 | derive_more = { version = "2.0.1", features = ["display", "error"] } 53 | 54 | tokio-rustls = { version = "0.26.2", default-features = false, features = ["logging", "tls12"] } 55 | rcgen = { version = "0.13.2", default-features = false, features = [] } 56 | hickory-resolver = { version = "0.25.2", default-features = false, features = ["system-config", "tokio", "rustls-platform-verifier"] } 57 | aws-lc-rs = { version = "*", optional = true } 58 | 59 | [target.'cfg(not(target_family = "unix"))'.dependencies] 60 | crossterm = { version = "0.29.0" } 61 | tokio-util = { version = "0.7.15", features = ["io"] } 62 | 63 | [target.'cfg(target_family = "unix")'.dependencies] 64 | tokio-fd = "0.3.0" 65 | 66 | [dev-dependencies] 67 | testcontainers = "0.24.0" 68 | test-case = "3.3.1" 69 | collection_macros = "0.2.0" 70 | rstest = "0.25.0" 71 | serial_test = "3.2.0" 72 | derive_more = { version = "2.0.1", features = ["from"] } 73 | get_if_addrs = "0.5.3" 74 | 75 | [features] 76 | default = ["aws-lc-rs"] 77 | clap = ["dep:clap"] 78 | aws-lc-rs = ["tokio-rustls/aws-lc-rs", "rcgen/aws_lc_rs", "hickory-resolver/tls-aws-lc-rs", "hickory-resolver/https-aws-lc-rs"] 79 | aws-lc-rs-bindgen = ["dep:aws-lc-rs", "aws-lc-rs/bindgen"] 80 | ring = ["tokio-rustls/ring", "rcgen/ring", "hickory-resolver/tls-ring", "hickory-resolver/https-ring"] 81 | 82 | -------------------------------------------------------------------------------- /wstunnel/src/embedded_certificate.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use rcgen::{CertificateParams, DnType, KeyPair, date_time_ymd}; 3 | use std::sync::LazyLock; 4 | use std::time::Instant; 5 | use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; 6 | 7 | pub static TLS_CERTIFICATE: LazyLock<(Vec>, PrivateKeyDer<'static>)> = LazyLock::new(|| { 8 | info!("Generating self-signed tls certificate"); 9 | 10 | let now = Instant::now(); 11 | let key_pair = KeyPair::generate().unwrap(); 12 | let mut cert = CertificateParams::new(vec![]).unwrap(); 13 | cert.distinguished_name = rcgen::DistinguishedName::new(); 14 | cert.distinguished_name.push(DnType::CountryName, "FR".to_string()); 15 | let el = now.elapsed(); 16 | let year = 2024 - (el.as_nanos() % 2) as i32; 17 | let month = 1 + (el.as_nanos() % 12) as u8; 18 | let day = 1 + (el.as_nanos() % 28) as u8; 19 | cert.not_before = date_time_ymd(year, month, day); 20 | 21 | let el = now.elapsed(); 22 | let year = 2025 + (el.as_nanos() % 50) as i32; 23 | let month = 1 + (el.as_nanos() % 12) as u8; 24 | let day = 1 + (el.as_nanos() % 28) as u8; 25 | cert.not_after = date_time_ymd(year, month, day); 26 | 27 | let cert = cert.self_signed(&key_pair).unwrap().der().clone(); 28 | let private_key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialized_der().to_vec())); 29 | 30 | (vec![cert], private_key) 31 | }); 32 | -------------------------------------------------------------------------------- /wstunnel/src/executor.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use std::sync::{Arc, Weak}; 3 | use tokio::runtime::Handle; 4 | use tokio::task::{AbortHandle, JoinSet}; 5 | 6 | pub trait TokioExecutorRef: Clone + Send + Sync + 'static { 7 | fn spawn(&self, f: F) -> AbortHandle 8 | where 9 | F: Future + Send + 'static, 10 | F::Output: Send + 'static; 11 | } 12 | 13 | pub trait TokioExecutor: TokioExecutorRef { 14 | type Ref: TokioExecutorRef; 15 | fn ref_clone(&self) -> Self::Ref; 16 | } 17 | 18 | // /////////////////////////////// 19 | // Default TokioExecutor 20 | // /////////////////////////////// 21 | #[derive(Clone)] 22 | pub struct DefaultTokioExecutor { 23 | handle: Handle, 24 | } 25 | impl DefaultTokioExecutor { 26 | pub fn new(handle: Handle) -> Self { 27 | Self { handle } 28 | } 29 | } 30 | 31 | impl Default for DefaultTokioExecutor { 32 | fn default() -> Self { 33 | Self::new(Handle::current()) 34 | } 35 | } 36 | 37 | impl TokioExecutorRef for DefaultTokioExecutor { 38 | fn spawn(&self, f: F) -> AbortHandle 39 | where 40 | F: Future + Send + 'static, 41 | F::Output: Send + 'static, 42 | { 43 | self.handle.spawn(f).abort_handle() 44 | } 45 | } 46 | 47 | impl TokioExecutor for DefaultTokioExecutor { 48 | type Ref = DefaultTokioExecutor; 49 | 50 | fn ref_clone(&self) -> DefaultTokioExecutor { 51 | self.clone() 52 | } 53 | } 54 | 55 | // /////////////////////////////// 56 | // JoinSetTokioExecutor 57 | // /////////////////////////////// 58 | 59 | #[derive(Clone)] 60 | pub struct JoinSetTokioExecutor { 61 | join_set: Arc>>, 62 | } 63 | 64 | impl JoinSetTokioExecutor { 65 | pub fn new(join_set: JoinSet<()>) -> Self { 66 | Self { 67 | join_set: Arc::new(Mutex::new(join_set)), 68 | } 69 | } 70 | pub fn abort_all(&self) { 71 | self.join_set.lock().abort_all(); 72 | } 73 | } 74 | 75 | impl Drop for JoinSetTokioExecutor { 76 | fn drop(&mut self) { 77 | self.abort_all(); 78 | } 79 | } 80 | impl Default for JoinSetTokioExecutor { 81 | fn default() -> Self { 82 | Self::new(JoinSet::new()) 83 | } 84 | } 85 | 86 | impl TokioExecutorRef for JoinSetTokioExecutor { 87 | fn spawn(&self, f: F) -> AbortHandle 88 | where 89 | F: Future + Send + 'static, 90 | F::Output: Send + 'static, 91 | { 92 | self.join_set.lock().spawn(async { 93 | f.await; 94 | }) 95 | } 96 | } 97 | 98 | impl TokioExecutor for JoinSetTokioExecutor { 99 | type Ref = JoinSetTokioExecutorRef; 100 | 101 | fn ref_clone(&self) -> Self::Ref { 102 | JoinSetTokioExecutorRef::new(self) 103 | } 104 | } 105 | 106 | #[derive(Clone)] 107 | pub struct JoinSetTokioExecutorRef { 108 | join_set: Weak>>, 109 | default_abort_handle: AbortHandle, 110 | } 111 | impl JoinSetTokioExecutorRef { 112 | fn new(exec: &JoinSetTokioExecutor) -> Self { 113 | let default_abort_handle = exec.join_set.lock().spawn(futures_util::future::pending()); 114 | let join_set = Arc::downgrade(&exec.join_set); 115 | Self { 116 | join_set, 117 | default_abort_handle, 118 | } 119 | } 120 | } 121 | 122 | impl TokioExecutorRef for JoinSetTokioExecutorRef { 123 | fn spawn(&self, f: F) -> AbortHandle 124 | where 125 | F: Future + Send + 'static, 126 | F::Output: Send + 'static, 127 | { 128 | self.join_set 129 | .upgrade() 130 | .map(|l| { 131 | l.lock().spawn(async { 132 | f.await; 133 | }) 134 | }) 135 | .unwrap_or_else(|| self.default_abort_handle.clone()) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/dns/mod.rs: -------------------------------------------------------------------------------- 1 | mod resolver; 2 | 3 | pub use resolver::DnsResolver; 4 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/http_proxy/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | pub use server::HttpProxyListener; 4 | pub use server::run_server; 5 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/http_proxy/server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use std::future::Future; 3 | 4 | use bytes::Bytes; 5 | use log::{debug, error}; 6 | use std::net::SocketAddr; 7 | use std::pin::Pin; 8 | use std::sync::Arc; 9 | 10 | use base64::Engine; 11 | use futures_util::{Stream, future, stream}; 12 | use http_body_util::Empty; 13 | use hyper::body::Incoming; 14 | use hyper::server::conn::http1; 15 | use hyper::service::service_fn; 16 | use hyper::{Request, Response}; 17 | use hyper_util::rt::TokioTimer; 18 | use parking_lot::Mutex; 19 | use std::time::Duration; 20 | use tokio::net::{TcpListener, TcpStream}; 21 | use tokio::select; 22 | use tokio::task::JoinSet; 23 | use tracing::log::info; 24 | use url::Host; 25 | 26 | #[allow(clippy::type_complexity)] 27 | pub struct HttpProxyListener { 28 | listener: Pin> + Send>>, 29 | } 30 | 31 | impl Stream for HttpProxyListener { 32 | type Item = anyhow::Result<(TcpStream, (Host, u16))>; 33 | 34 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll> { 35 | unsafe { self.map_unchecked_mut(|x| &mut x.listener) }.poll_next(cx) 36 | } 37 | } 38 | 39 | fn handle_request( 40 | credentials: &Option, 41 | dest: &Mutex>, 42 | req: Request, 43 | ) -> impl Future>, &'static str>> { 44 | const PROXY_AUTHORIZATION_PREFIX: &str = "Basic "; 45 | let ok_response = |forward_to: Option<(Host, u16)>| -> Result>, _> { 46 | *dest.lock() = forward_to; 47 | Ok(Response::builder().status(200).body(Empty::new()).unwrap()) 48 | }; 49 | fn err_response() -> Result>, &'static str> { 50 | info!("Un-authorized connection to http proxy"); 51 | Err("Un-authorized") 52 | } 53 | 54 | if req.method() != hyper::Method::CONNECT { 55 | return future::ready(err_response()); 56 | } 57 | 58 | debug!("HTTP Proxy CONNECT request to {}", req.uri()); 59 | let forward_to = Host::parse(req.uri().host().unwrap_or_default()) 60 | .ok() 61 | .map(|h| (h, req.uri().port_u16().unwrap_or(443))); 62 | 63 | let Some(token) = credentials else { 64 | return future::ready(ok_response(forward_to)); 65 | }; 66 | 67 | let Some(auth) = req.headers().get(hyper::header::PROXY_AUTHORIZATION) else { 68 | return future::ready(err_response()); 69 | }; 70 | 71 | let auth = auth.to_str().unwrap_or_default().trim(); 72 | if auth.starts_with(PROXY_AUTHORIZATION_PREFIX) && &auth[PROXY_AUTHORIZATION_PREFIX.len()..] == token { 73 | return future::ready(ok_response(forward_to)); 74 | } 75 | 76 | future::ready(err_response()) 77 | } 78 | 79 | pub async fn run_server( 80 | bind: SocketAddr, 81 | timeout: Option, 82 | credentials: Option<(String, String)>, 83 | ) -> Result { 84 | info!( 85 | "Starting http proxy server listening cnx on {} with credentials {:?}", 86 | bind, credentials 87 | ); 88 | 89 | let listener = TcpListener::bind(bind) 90 | .await 91 | .with_context(|| format!("Cannot create TCP server {:?}", bind))?; 92 | 93 | let http1 = { 94 | let mut builder = http1::Builder::new(); 95 | builder 96 | .timer(TokioTimer::new()) 97 | .header_read_timeout(timeout) 98 | .keep_alive(false); 99 | builder 100 | }; 101 | let auth_header = 102 | credentials.map(|(user, pass)| base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass))); 103 | let tasks = JoinSet::)>>::new(); 104 | 105 | let proxy_cfg = Arc::new((auth_header, http1)); 106 | let listener = stream::unfold((listener, tasks, proxy_cfg), |(listener, mut tasks, proxy_cfg)| async { 107 | loop { 108 | let (mut stream, forward_to) = select! { 109 | biased; 110 | 111 | cnx = tasks.join_next(), if !tasks.is_empty() => { 112 | match cnx { 113 | Some(Ok(Some((stream, Some(f))))) => (stream, Some(f)), 114 | Some(Ok(Some((_, None)))) =>{ 115 | error!("Error while trying to parse connect request"); 116 | continue 117 | }, 118 | None | Some(Ok(None)) => continue, 119 | Some(Err(err)) => { 120 | error!("Error while joinning tasks {:?}", err); 121 | continue 122 | }, 123 | } 124 | }, 125 | 126 | stream = listener.accept() => { 127 | match stream { 128 | Ok((stream, _)) => (stream, None), 129 | Err(err) => { 130 | error!("Error while accepting connection {:?}", err); 131 | continue; 132 | } 133 | } 134 | } 135 | }; 136 | 137 | if let Some(forward_to) = forward_to { 138 | return Some((Ok((stream, forward_to)), (listener, tasks, proxy_cfg))); 139 | } 140 | 141 | let handle_new_cnx = { 142 | let proxy_cfg = proxy_cfg.clone(); 143 | async move { 144 | let http1 = &proxy_cfg.1; 145 | let auth_header = &proxy_cfg.0; 146 | let forward_to = Mutex::new(None); 147 | let conn_fut = http1.serve_connection( 148 | hyper_util::rt::TokioIo::new(&mut stream), 149 | service_fn(|req| handle_request(auth_header, &forward_to, req)), 150 | ); 151 | 152 | match conn_fut.await { 153 | Ok(_) => Some((stream, forward_to.into_inner())), 154 | Err(err) => { 155 | info!("Error while serving connection: {}", err); 156 | None 157 | } 158 | } 159 | } 160 | }; 161 | tasks.spawn(handle_new_cnx); 162 | } 163 | }); 164 | 165 | Ok(HttpProxyListener { 166 | listener: Box::pin(listener), 167 | }) 168 | } 169 | 170 | //#[cfg(test)] 171 | //mod tests { 172 | // use super::*; 173 | // use tracing::level_filters::LevelFilter; 174 | // 175 | // #[tokio::test] 176 | // async fn test_run_server() { 177 | // tracing_subscriber::fmt() 178 | // .with_ansi(true) 179 | // .with_max_level(LevelFilter::TRACE) 180 | // .init(); 181 | // let x = run_server("127.0.0.1:1212".parse().unwrap(), None, None).await; 182 | // } 183 | //} 184 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dns; 2 | pub mod http_proxy; 3 | pub mod socks5; 4 | pub mod stdio; 5 | pub mod tcp; 6 | pub mod tls; 7 | pub mod udp; 8 | #[cfg(unix)] 9 | pub mod unix_sock; 10 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/socks5/mod.rs: -------------------------------------------------------------------------------- 1 | mod tcp_server; 2 | mod udp_server; 3 | 4 | pub use tcp_server::Socks5Listener; 5 | pub use tcp_server::Socks5ReadHalf; 6 | pub use tcp_server::Socks5WriteHalf; 7 | pub use tcp_server::run_server; 8 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/stdio/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | mod server_unix; 3 | #[cfg(not(unix))] 4 | mod server_windows; 5 | 6 | #[cfg(unix)] 7 | pub use server_unix::run_server; 8 | #[cfg(not(unix))] 9 | pub use server_windows::run_server; 10 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/stdio/server_unix.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | use tokio::io::{AsyncRead, ReadBuf}; 4 | use tokio::sync::oneshot; 5 | use tokio_fd::AsyncFd; 6 | use tracing::info; 7 | 8 | pub struct WsStdin { 9 | stdin: AsyncFd, 10 | _receiver: oneshot::Receiver<()>, 11 | } 12 | 13 | impl AsyncRead for WsStdin { 14 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 15 | unsafe { self.map_unchecked_mut(|s| &mut s.stdin) }.poll_read(cx, buf) 16 | } 17 | } 18 | 19 | pub async fn run_server() -> Result<((WsStdin, AsyncFd), oneshot::Sender<()>), anyhow::Error> { 20 | info!("Starting STDIO server"); 21 | 22 | let stdin = AsyncFd::try_from(nix::libc::STDIN_FILENO)?; 23 | let stdout = AsyncFd::try_from(nix::libc::STDOUT_FILENO)?; 24 | let (tx, rx) = oneshot::channel::<()>(); 25 | 26 | Ok(((WsStdin { stdin, _receiver: rx }, stdout), tx)) 27 | } 28 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/stdio/server_windows.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | use log::error; 3 | use parking_lot::Mutex; 4 | use scopeguard::guard; 5 | use std::io::{Read, Write}; 6 | use std::sync::Arc; 7 | use std::{io, thread}; 8 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; 9 | use tokio::sync::oneshot; 10 | use tokio::task::LocalSet; 11 | use tokio_stream::wrappers::UnboundedReceiverStream; 12 | use tokio_util::io::StreamReader; 13 | use tracing::info; 14 | 15 | pub async fn run_server() -> Result<((impl AsyncRead, impl AsyncWrite), oneshot::Sender<()>), anyhow::Error> { 16 | info!("Starting STDIO server. Press ctrl+c twice to exit"); 17 | 18 | crossterm::terminal::enable_raw_mode()?; 19 | 20 | let stdin = io::stdin(); 21 | let (send, recv) = tokio::sync::mpsc::unbounded_channel(); 22 | let (abort_tx, abort_rx) = oneshot::channel::<()>(); 23 | let abort_rx = Arc::new(Mutex::new(abort_rx)); 24 | let abort_rx2 = abort_rx.clone(); 25 | thread::spawn(move || { 26 | let _restore_terminal = guard((), move |_| { 27 | let _ = crossterm::terminal::disable_raw_mode(); 28 | abort_rx.lock().close(); 29 | }); 30 | let stdin = stdin; 31 | let mut stdin = stdin.lock(); 32 | let mut buf = [0u8; 65536]; 33 | 34 | loop { 35 | let n = stdin.read(&mut buf).unwrap_or(0); 36 | if n == 0 || (n == 1 && buf[0] == 3) { 37 | // ctrl+c send char 3 38 | break; 39 | } 40 | if let Err(err) = send.send(Result::<_, io::Error>::Ok(BytesMut::from(&buf[..n]))) { 41 | error!("Failed send inout: {:?}", err); 42 | break; 43 | } 44 | } 45 | }); 46 | let stdin = StreamReader::new(UnboundedReceiverStream::new(recv)); 47 | 48 | let (stdout, mut recv) = tokio::io::duplex(65536); 49 | let rt = tokio::runtime::Handle::current(); 50 | thread::spawn(move || { 51 | let task = async move { 52 | let _restore_terminal = guard((), move |_| { 53 | let _ = crossterm::terminal::disable_raw_mode(); 54 | abort_rx2.lock().close(); 55 | }); 56 | let mut stdout = io::stdout().lock(); 57 | let mut buf = [0u8; 65536]; 58 | loop { 59 | let Ok(n) = recv.read(&mut buf).await else { 60 | break; 61 | }; 62 | 63 | if n == 0 { 64 | break; 65 | } 66 | 67 | if let Err(err) = stdout.write_all(&buf[..n]) { 68 | error!("Failed to write to stdout: {:?}", err); 69 | break; 70 | }; 71 | let _ = stdout.flush(); 72 | } 73 | }; 74 | 75 | let local = LocalSet::new(); 76 | local.spawn_local(task); 77 | 78 | rt.block_on(local); 79 | }); 80 | 81 | Ok(((stdin, stdout), abort_tx)) 82 | } 83 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/tcp/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | pub use server::configure_socket; 4 | pub use server::connect; 5 | pub use server::connect_with_http_proxy; 6 | pub use server::run_server; 7 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/tls/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | mod utils; 3 | 4 | pub use server::connect; 5 | pub use server::load_certificates_from_pem; 6 | pub use server::load_private_key_from_file; 7 | pub use server::tls_acceptor; 8 | pub use server::tls_connector; 9 | pub use utils::cn_from_certificate; 10 | pub use utils::find_leaf_certificate; 11 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/tls/server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, anyhow}; 2 | use std::fs::File; 3 | use tokio_rustls::rustls::client::{EchConfig, EchMode}; 4 | 5 | use log::warn; 6 | use std::io::BufReader; 7 | use std::path::Path; 8 | use std::sync::Arc; 9 | use tokio::net::TcpStream; 10 | use tokio_rustls::client::TlsStream; 11 | 12 | use crate::tunnel::client::WsClientConfig; 13 | use crate::tunnel::server::TlsServerConfig; 14 | use crate::tunnel::transport::TransportAddr; 15 | use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; 16 | use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}; 17 | use tokio_rustls::rustls::server::WebPkiClientVerifier; 18 | use tokio_rustls::rustls::{ClientConfig, DigitallySignedStruct, Error, KeyLogFile, RootCertStore, SignatureScheme}; 19 | use tokio_rustls::{TlsAcceptor, TlsConnector, rustls}; 20 | use tracing::info; 21 | 22 | #[derive(Debug)] 23 | struct NullVerifier; 24 | 25 | impl ServerCertVerifier for NullVerifier { 26 | fn verify_server_cert( 27 | &self, 28 | _end_entity: &CertificateDer<'_>, 29 | _intermediates: &[CertificateDer<'_>], 30 | _server_name: &ServerName<'_>, 31 | _ocsp_response: &[u8], 32 | _now: UnixTime, 33 | ) -> Result { 34 | Ok(ServerCertVerified::assertion()) 35 | } 36 | 37 | fn verify_tls12_signature( 38 | &self, 39 | _message: &[u8], 40 | _cert: &CertificateDer<'_>, 41 | _dss: &DigitallySignedStruct, 42 | ) -> Result { 43 | Ok(HandshakeSignatureValid::assertion()) 44 | } 45 | 46 | fn verify_tls13_signature( 47 | &self, 48 | _message: &[u8], 49 | _cert: &CertificateDer<'_>, 50 | _dss: &DigitallySignedStruct, 51 | ) -> Result { 52 | Ok(HandshakeSignatureValid::assertion()) 53 | } 54 | 55 | fn supported_verify_schemes(&self) -> Vec { 56 | vec![ 57 | SignatureScheme::RSA_PKCS1_SHA1, 58 | SignatureScheme::ECDSA_SHA1_Legacy, 59 | SignatureScheme::RSA_PKCS1_SHA256, 60 | SignatureScheme::ECDSA_NISTP256_SHA256, 61 | SignatureScheme::RSA_PKCS1_SHA384, 62 | SignatureScheme::ECDSA_NISTP384_SHA384, 63 | SignatureScheme::RSA_PKCS1_SHA512, 64 | SignatureScheme::ECDSA_NISTP521_SHA512, 65 | SignatureScheme::RSA_PSS_SHA256, 66 | SignatureScheme::RSA_PSS_SHA384, 67 | SignatureScheme::RSA_PSS_SHA512, 68 | SignatureScheme::ED25519, 69 | SignatureScheme::ED448, 70 | ] 71 | } 72 | } 73 | 74 | pub fn load_certificates_from_pem(path: &Path) -> anyhow::Result>> { 75 | info!("Loading tls certificate from {:?}", path); 76 | 77 | let file = File::open(path)?; 78 | let mut reader = BufReader::new(file); 79 | let certs = rustls_pemfile::certs(&mut reader); 80 | 81 | Ok(certs 82 | .into_iter() 83 | .filter_map(|cert| match cert { 84 | Ok(cert) => Some(cert), 85 | Err(err) => { 86 | warn!("Error while parsing tls certificate: {:?}", err); 87 | None 88 | } 89 | }) 90 | .collect()) 91 | } 92 | 93 | pub fn load_private_key_from_file(path: &Path) -> anyhow::Result> { 94 | info!("Loading tls private key from {:?}", path); 95 | 96 | let file = File::open(path)?; 97 | let mut reader = BufReader::new(file); 98 | 99 | let Some(private_key) = rustls_pemfile::private_key(&mut reader)? else { 100 | return Err(anyhow!("No private key found in {path:?}")); 101 | }; 102 | 103 | Ok(private_key) 104 | } 105 | 106 | pub fn tls_connector( 107 | tls_verify_certificate: bool, 108 | alpn_protocols: Vec>, 109 | enable_sni: bool, 110 | ech_config: Option, 111 | tls_client_certificate: Option>>, 112 | tls_client_key: Option>, 113 | ) -> anyhow::Result { 114 | let mut root_store = RootCertStore::empty(); 115 | 116 | // Load system certificates and add them to the root store 117 | let certs = rustls_native_certs::load_native_certs(); 118 | certs.errors.iter().for_each(|err| { 119 | warn!("cannot load system some system certificates: {}", err); 120 | }); 121 | for cert in certs.certs { 122 | if let Err(err) = root_store.add(cert) { 123 | warn!("cannot load a system certificate: {:?}", err); 124 | continue; 125 | } 126 | } 127 | 128 | let crypto_provider = ClientConfig::builder().crypto_provider().clone(); 129 | let config_builder = ClientConfig::builder_with_provider(crypto_provider); 130 | let config_builder = if let Some(ech_config) = ech_config { 131 | info!("Using TLS ECH (encrypted sni) with config: {:?}", ech_config); 132 | config_builder.with_ech(EchMode::Enable(ech_config))? 133 | } else { 134 | config_builder.with_safe_default_protocol_versions()? 135 | }; 136 | let config_builder = config_builder.with_root_certificates(root_store); 137 | 138 | let mut config = match (tls_client_certificate, tls_client_key) { 139 | (Some(tls_client_certificate), Some(tls_client_key)) => config_builder 140 | .with_client_auth_cert(tls_client_certificate, tls_client_key) 141 | .with_context(|| "Error setting up mTLS")?, 142 | _ => config_builder.with_no_client_auth(), 143 | }; 144 | 145 | config.enable_sni = enable_sni; 146 | config.key_log = Arc::new(KeyLogFile::new()); 147 | 148 | // To bypass certificate verification 149 | if !tls_verify_certificate { 150 | config.dangerous().set_certificate_verifier(Arc::new(NullVerifier)); 151 | } 152 | 153 | config.alpn_protocols = alpn_protocols; 154 | let tls_connector = TlsConnector::from(Arc::new(config)); 155 | Ok(tls_connector) 156 | } 157 | 158 | pub fn tls_acceptor(tls_cfg: &TlsServerConfig, alpn_protocols: Option>>) -> anyhow::Result { 159 | let client_cert_verifier = if let Some(tls_client_ca_certificates) = &tls_cfg.tls_client_ca_certificates { 160 | let mut root_store = RootCertStore::empty(); 161 | for tls_client_ca_certificate in tls_client_ca_certificates.lock().iter() { 162 | root_store 163 | .add(tls_client_ca_certificate.clone()) 164 | .with_context(|| "Failed to add mTLS client CA certificate")?; 165 | } 166 | 167 | WebPkiClientVerifier::builder(Arc::new(root_store)) 168 | .build() 169 | .map_err(|err| anyhow!("Failed to build mTLS client verifier: {:?}", err))? 170 | } else { 171 | WebPkiClientVerifier::no_client_auth() 172 | }; 173 | 174 | let mut config = rustls::ServerConfig::builder() 175 | .with_client_cert_verifier(client_cert_verifier) 176 | .with_single_cert(tls_cfg.tls_certificate.lock().clone(), tls_cfg.tls_key.lock().clone_key()) 177 | .with_context(|| "invalid tls certificate or private key")?; 178 | 179 | config.key_log = Arc::new(KeyLogFile::new()); 180 | if let Some(alpn_protocols) = alpn_protocols { 181 | config.alpn_protocols = alpn_protocols; 182 | } 183 | Ok(TlsAcceptor::from(Arc::new(config))) 184 | } 185 | 186 | pub async fn connect(client_cfg: &WsClientConfig, tcp_stream: TcpStream) -> anyhow::Result> { 187 | let sni = client_cfg.tls_server_name(); 188 | let tls_config = match &client_cfg.remote_addr { 189 | TransportAddr::Wss { tls, .. } => tls, 190 | TransportAddr::Https { tls, .. } => tls, 191 | TransportAddr::Http { .. } | TransportAddr::Ws { .. } => { 192 | return Err(anyhow!("Transport does not support TLS: {}", client_cfg.remote_addr.scheme())); 193 | } 194 | }; 195 | 196 | if tls_config.tls_sni_disabled { 197 | info!( 198 | "Doing TLS handshake without SNI with the server {}:{}", 199 | client_cfg.remote_addr.host(), 200 | client_cfg.remote_addr.port() 201 | ); 202 | } else { 203 | info!( 204 | "Doing TLS handshake using SNI {sni:?} with the server {}:{}", 205 | client_cfg.remote_addr.host(), 206 | client_cfg.remote_addr.port() 207 | ); 208 | } 209 | 210 | let tls_connector = tls_config.tls_connector(); 211 | let tls_stream = tls_connector.connect(sni, tcp_stream).await?; 212 | 213 | Ok(tls_stream) 214 | } 215 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/tls/utils.rs: -------------------------------------------------------------------------------- 1 | use tokio_rustls::rustls::pki_types::CertificateDer; 2 | use x509_parser::parse_x509_certificate; 3 | use x509_parser::prelude::X509Certificate; 4 | 5 | /// Find a leaf certificate in a vector of certificates. It is assumed only a single leaf certificate 6 | /// is present in the vector. The other certificates should be (intermediate) CA certificates. 7 | pub fn find_leaf_certificate<'a>(tls_certificates: &'a [CertificateDer<'static>]) -> Option> { 8 | for tls_certificate in tls_certificates { 9 | if let Ok((_, tls_certificate_x509)) = parse_x509_certificate(tls_certificate) { 10 | if !tls_certificate_x509.is_ca() { 11 | return Some(tls_certificate_x509); 12 | } 13 | } 14 | } 15 | None 16 | } 17 | 18 | /// Returns the common name (CN) as specified in the supplied certificate. 19 | pub fn cn_from_certificate(tls_certificate_x509: &X509Certificate) -> Option { 20 | tls_certificate_x509 21 | .tbs_certificate 22 | .subject 23 | .iter_common_name() 24 | .flat_map(|cn| cn.as_str().ok()) 25 | .next() 26 | .map(|cn| cn.to_string()) 27 | } 28 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/udp/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | pub use server::UdpStream; 4 | pub use server::UdpStreamWriter; 5 | pub use server::WsUdpSocket; 6 | #[cfg(target_os = "linux")] 7 | pub use server::configure_tproxy; 8 | pub use server::connect; 9 | #[cfg(target_os = "linux")] 10 | pub use server::mk_send_socket_tproxy; 11 | pub use server::run_server; 12 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/unix_sock/mod.rs: -------------------------------------------------------------------------------- 1 | mod server; 2 | 3 | pub use server::UnixListenerStream; 4 | pub use server::run_server; 5 | -------------------------------------------------------------------------------- /wstunnel/src/protocols/unix_sock/server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use futures_util::Stream; 3 | use std::io; 4 | use std::path::Path; 5 | use std::pin::Pin; 6 | use std::task::Poll; 7 | use tokio::net::{UnixListener, UnixStream}; 8 | use tracing::log::info; 9 | 10 | pub struct UnixListenerStream { 11 | inner: UnixListener, 12 | path_to_delete: bool, 13 | } 14 | 15 | impl UnixListenerStream { 16 | pub const fn new(listener: UnixListener, path_to_delete: bool) -> Self { 17 | Self { 18 | inner: listener, 19 | path_to_delete, 20 | } 21 | } 22 | } 23 | 24 | impl Drop for UnixListenerStream { 25 | fn drop(&mut self) { 26 | if self.path_to_delete { 27 | let Ok(addr) = &self.inner.local_addr() else { 28 | return; 29 | }; 30 | let Some(path) = addr.as_pathname() else { 31 | return; 32 | }; 33 | let _ = std::fs::remove_file(path); 34 | } 35 | } 36 | } 37 | 38 | impl Stream for UnixListenerStream { 39 | type Item = io::Result; 40 | 41 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll>> { 42 | match self.inner.poll_accept(cx) { 43 | Poll::Ready(Ok((stream, _))) => Poll::Ready(Some(Ok(stream))), 44 | Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))), 45 | Poll::Pending => Poll::Pending, 46 | } 47 | } 48 | } 49 | 50 | pub async fn run_server(socket_path: &Path) -> Result { 51 | info!("Starting Unix socket server listening cnx on {:?}", socket_path); 52 | 53 | let path_to_delete = !socket_path.exists(); 54 | let listener = UnixListener::bind(socket_path) 55 | .with_context(|| format!("Cannot create Unix socket server {:?}", socket_path))?; 56 | 57 | Ok(UnixListenerStream::new(listener, path_to_delete)) 58 | } 59 | -------------------------------------------------------------------------------- /wstunnel/src/restrictions/config_reloader.rs: -------------------------------------------------------------------------------- 1 | use super::types::RestrictionsRules; 2 | use crate::restrictions::config_reloader::RestrictionsRulesReloaderState::{Config, Static}; 3 | use anyhow::Context; 4 | use arc_swap::ArcSwap; 5 | use log::trace; 6 | use notify::{EventKind, RecommendedWatcher, Watcher}; 7 | use parking_lot::Mutex; 8 | use std::path::PathBuf; 9 | use std::sync::Arc; 10 | use std::thread; 11 | use std::time::Duration; 12 | use tracing::{error, info, warn}; 13 | 14 | struct ConfigReloaderState { 15 | fs_watcher: Mutex, 16 | config_path: PathBuf, 17 | } 18 | 19 | #[derive(Clone)] 20 | enum RestrictionsRulesReloaderState { 21 | Static, 22 | Config(Arc), 23 | } 24 | 25 | impl RestrictionsRulesReloaderState { 26 | fn fs_watcher(&self) -> &Mutex { 27 | match self { 28 | Static => unreachable!(), 29 | Config(this) => &this.fs_watcher, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Clone)] 35 | pub struct RestrictionsRulesReloader { 36 | state: RestrictionsRulesReloaderState, 37 | restrictions: Arc>, 38 | } 39 | 40 | impl RestrictionsRulesReloader { 41 | pub fn new(restrictions_rules: RestrictionsRules, config_path: Option) -> anyhow::Result { 42 | // If there is no custom certificate and private key, there is nothing to watch 43 | let config_path = if let Some(config_path) = config_path { 44 | config_path 45 | } else { 46 | return Ok(Self { 47 | state: Static, 48 | restrictions: Arc::new(ArcSwap::from_pointee(restrictions_rules)), 49 | }); 50 | }; 51 | let reloader = Self { 52 | state: Config(Arc::new(ConfigReloaderState { 53 | fs_watcher: Mutex::new(notify::recommended_watcher(|_| {})?), 54 | config_path, 55 | })), 56 | restrictions: Arc::new(ArcSwap::from_pointee(restrictions_rules)), 57 | }; 58 | 59 | info!("Starting to watch restriction config file for changes to reload them"); 60 | let mut watcher = notify::recommended_watcher({ 61 | let reloader = reloader.clone(); 62 | 63 | move |event: notify::Result| Self::handle_config_fs_event(&reloader, event) 64 | }) 65 | .with_context(|| "Cannot create restriction config watcher")?; 66 | 67 | match &reloader.state { 68 | Static => {} 69 | Config(cfg) => { 70 | watcher.watch(&cfg.config_path, notify::RecursiveMode::NonRecursive)?; 71 | *cfg.fs_watcher.lock() = watcher 72 | } 73 | } 74 | 75 | Ok(reloader) 76 | } 77 | 78 | pub fn reload_restrictions_config(&self) { 79 | let restrictions = match &self.state { 80 | Static => return, 81 | Config(st) => match RestrictionsRules::from_config_file(&st.config_path) { 82 | Ok(restrictions) => { 83 | info!("Restrictions config file has been reloaded"); 84 | restrictions 85 | } 86 | Err(err) => { 87 | error!("Cannot reload restrictions config file, keeping the old one. Error: {:?}", err); 88 | return; 89 | } 90 | }, 91 | }; 92 | 93 | self.restrictions.store(Arc::new(restrictions)); 94 | } 95 | 96 | pub const fn restrictions_rules(&self) -> &Arc> { 97 | &self.restrictions 98 | } 99 | 100 | fn try_rewatch_config(this: RestrictionsRulesReloader, path: PathBuf) { 101 | thread::spawn(move || { 102 | while !path.exists() { 103 | warn!( 104 | "Restrictions config file {:?} does not exist anymore, waiting for it to be created", 105 | path 106 | ); 107 | thread::sleep(Duration::from_secs(10)); 108 | } 109 | let mut watcher = this.state.fs_watcher().lock(); 110 | let _ = watcher.unwatch(&path); 111 | let Ok(_) = watcher 112 | .watch(&path, notify::RecursiveMode::NonRecursive) 113 | .map_err(|err| { 114 | error!("Cannot re-set a watch for Restriction config file {:?}: {:?}", path, err); 115 | error!("Restriction config file will not be auto-reloaded anymore"); 116 | }) 117 | else { 118 | return; 119 | }; 120 | drop(watcher); 121 | 122 | // Generate a fake event to force-reload the config 123 | let event = notify::Event { 124 | kind: EventKind::Create(notify::event::CreateKind::Any), 125 | paths: vec![path], 126 | attrs: Default::default(), 127 | }; 128 | 129 | Self::handle_config_fs_event(&this, Ok(event)) 130 | }); 131 | } 132 | 133 | fn handle_config_fs_event(reloader: &RestrictionsRulesReloader, event: notify::Result) { 134 | let this = match &reloader.state { 135 | Static => return, 136 | Config(st) => st, 137 | }; 138 | 139 | let event = match event { 140 | Ok(event) => event, 141 | Err(err) => { 142 | error!("Error while watching restrictions config file for changes {:?}", err); 143 | return; 144 | } 145 | }; 146 | 147 | if event.kind.is_access() { 148 | return; 149 | } 150 | 151 | trace!("Received event: {:#?}", event); 152 | if let Some(path) = event.paths.iter().find(|p| p.ends_with(&this.config_path)) { 153 | match event.kind { 154 | EventKind::Create(_) | EventKind::Modify(_) => { 155 | reloader.reload_restrictions_config(); 156 | } 157 | EventKind::Remove(_) => { 158 | warn!("Restriction config file has been removed, trying to re-set a watch for it"); 159 | Self::try_rewatch_config(reloader.clone(), path.to_path_buf()); 160 | } 161 | EventKind::Access(_) | EventKind::Other | EventKind::Any => { 162 | trace!("Ignoring event {:?}", event); 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /wstunnel/src/restrictions/mod.rs: -------------------------------------------------------------------------------- 1 | use ipnet::IpNet; 2 | use std::fs::File; 3 | use std::io::BufReader; 4 | use std::net::IpAddr; 5 | use std::ops::RangeInclusive; 6 | use std::path::Path; 7 | use std::str::FromStr; 8 | use std::vec; 9 | 10 | use regex::Regex; 11 | 12 | use types::RestrictionsRules; 13 | 14 | use crate::restrictions::types::{default_cidr, default_host}; 15 | 16 | pub mod config_reloader; 17 | pub mod types; 18 | 19 | impl RestrictionsRules { 20 | pub fn from_config_file(config_path: &Path) -> anyhow::Result { 21 | let restrictions: Self = serde_yaml::from_reader(BufReader::new(File::open(config_path)?))?; 22 | Ok(restrictions) 23 | } 24 | 25 | pub fn from_path_prefix(path_prefixes: &[String], restrict_to: &[(String, u16)]) -> anyhow::Result { 26 | let tunnels_restrictions = if restrict_to.is_empty() { 27 | let r = types::AllowConfig::Tunnel(types::AllowTunnelConfig { 28 | protocol: vec![], 29 | port: vec![], 30 | host: default_host(), 31 | cidr: default_cidr(), 32 | }); 33 | let reverse_tunnel = types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { 34 | protocol: vec![], 35 | port: vec![], 36 | port_mapping: Default::default(), 37 | cidr: default_cidr(), 38 | }); 39 | 40 | vec![r, reverse_tunnel] 41 | } else { 42 | restrict_to 43 | .iter() 44 | .map(|(host, port)| { 45 | let reg = Regex::new(&format!("^{}$", regex::escape(host)))?; 46 | let tunnels = if let Ok(ip) = IpAddr::from_str(host) { 47 | vec![ 48 | types::AllowConfig::Tunnel(types::AllowTunnelConfig { 49 | protocol: vec![], 50 | port: vec![RangeInclusive::new(*port, *port)], 51 | host: reg, 52 | cidr: default_cidr(), 53 | }), 54 | types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { 55 | protocol: vec![], 56 | port: vec![RangeInclusive::new(*port, *port)], 57 | port_mapping: Default::default(), 58 | cidr: vec![IpNet::new(ip, if ip.is_ipv4() { 32 } else { 128 })?], 59 | }), 60 | ] 61 | } else { 62 | vec![ 63 | types::AllowConfig::Tunnel(types::AllowTunnelConfig { 64 | protocol: vec![], 65 | port: vec![RangeInclusive::new(*port, *port)], 66 | host: reg, 67 | cidr: default_cidr(), 68 | }), 69 | types::AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { 70 | protocol: vec![], 71 | port: vec![], 72 | port_mapping: Default::default(), 73 | cidr: default_cidr(), 74 | }), 75 | ] 76 | }; 77 | 78 | Ok(tunnels) 79 | }) 80 | .collect::, anyhow::Error>>()? 81 | .into_iter() 82 | .flatten() 83 | .collect() 84 | }; 85 | 86 | let restrictions = if path_prefixes.is_empty() { 87 | // if no path prefixes are provided, we allow all 88 | let r = types::RestrictionConfig { 89 | name: "Allow All".to_string(), 90 | r#match: vec![types::MatchConfig::Any], 91 | allow: tunnels_restrictions, 92 | }; 93 | vec![r] 94 | } else { 95 | path_prefixes 96 | .iter() 97 | .map(|path_prefix| { 98 | let reg = Regex::new(&format!("^{}$", regex::escape(path_prefix)))?; 99 | Ok(types::RestrictionConfig { 100 | name: format!("Allow path prefix {}", path_prefix), 101 | r#match: vec![types::MatchConfig::PathPrefix(reg)], 102 | allow: tunnels_restrictions.clone(), 103 | }) 104 | }) 105 | .collect::, anyhow::Error>>()? 106 | }; 107 | 108 | Ok(Self { restrictions }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /wstunnel/src/restrictions/types.rs: -------------------------------------------------------------------------------- 1 | use crate::tunnel::LocalProtocol; 2 | use ipnet::{IpNet, Ipv4Net, Ipv6Net}; 3 | use regex::Regex; 4 | use serde::{Deserialize, Deserializer}; 5 | use std::collections::HashMap; 6 | use std::ops::RangeInclusive; 7 | 8 | #[derive(Debug, Clone, Deserialize)] 9 | pub struct RestrictionsRules { 10 | pub restrictions: Vec, 11 | } 12 | 13 | #[derive(Debug, Clone, Deserialize)] 14 | pub struct RestrictionConfig { 15 | pub name: String, 16 | #[serde(deserialize_with = "deserialize_non_empty_vec")] 17 | pub r#match: Vec, 18 | pub allow: Vec, 19 | } 20 | 21 | #[derive(Debug, Clone, Deserialize)] 22 | pub enum MatchConfig { 23 | Any, 24 | #[serde(with = "serde_regex")] 25 | PathPrefix(Regex), 26 | #[serde(with = "serde_regex")] 27 | Authorization(Regex), 28 | } 29 | 30 | #[derive(Debug, Clone, Deserialize)] 31 | #[cfg_attr(test, derive(derive_more::From))] 32 | pub enum AllowConfig { 33 | ReverseTunnel(AllowReverseTunnelConfig), 34 | Tunnel(AllowTunnelConfig), 35 | } 36 | 37 | #[derive(Debug, Clone, Deserialize)] 38 | pub struct AllowTunnelConfig { 39 | #[serde(default)] 40 | pub protocol: Vec, 41 | 42 | #[serde(deserialize_with = "deserialize_port_range")] 43 | #[serde(default)] 44 | pub port: Vec>, 45 | 46 | #[serde(with = "serde_regex")] 47 | #[serde(default = "default_host")] 48 | pub host: Regex, 49 | 50 | #[serde(default = "default_cidr")] 51 | pub cidr: Vec, 52 | } 53 | 54 | #[derive(Debug, Clone, Deserialize)] 55 | pub struct AllowReverseTunnelConfig { 56 | #[serde(default)] 57 | pub protocol: Vec, 58 | 59 | #[serde(deserialize_with = "deserialize_port_range")] 60 | #[serde(default)] 61 | pub port: Vec>, 62 | 63 | #[serde(deserialize_with = "deserialize_port_mapping")] 64 | #[serde(default)] 65 | pub port_mapping: HashMap, 66 | 67 | #[serde(default = "default_cidr")] 68 | pub cidr: Vec, 69 | } 70 | 71 | #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] 72 | pub enum TunnelConfigProtocol { 73 | Tcp, 74 | Udp, 75 | Unknown, 76 | } 77 | 78 | #[derive(Debug, Clone, Deserialize, Eq, PartialEq)] 79 | pub enum ReverseTunnelConfigProtocol { 80 | Tcp, 81 | Udp, 82 | Socks5, 83 | Unix, 84 | HttpProxy, 85 | Unknown, 86 | } 87 | 88 | pub fn default_host() -> Regex { 89 | Regex::new("^.*$").unwrap() 90 | } 91 | 92 | pub fn default_cidr() -> Vec { 93 | vec![IpNet::V4(Ipv4Net::default()), IpNet::V6(Ipv6Net::default())] 94 | } 95 | 96 | fn deserialize_port_range<'de, D>(deserializer: D) -> Result>, D::Error> 97 | where 98 | D: Deserializer<'de>, 99 | { 100 | let s = Vec::::deserialize(deserializer)?; 101 | let ranges = s 102 | .into_iter() 103 | .map(|s| { 104 | let range: Result, D::Error> = if let Some((l, r)) = s.split_once("..") { 105 | Ok(RangeInclusive::new( 106 | l.parse().map_err(::custom)?, 107 | r.parse().map_err(::custom)?, 108 | )) 109 | } else { 110 | let port = s.parse::().map_err(serde::de::Error::custom)?; 111 | Ok(RangeInclusive::new(port, port)) 112 | }; 113 | range 114 | }) 115 | .collect::>() 116 | .into_iter() 117 | .collect::>, D::Error>>()?; 118 | 119 | Ok(ranges) 120 | } 121 | 122 | fn deserialize_port_mapping<'de, D>(deserializer: D) -> Result, D::Error> 123 | where 124 | D: Deserializer<'de>, 125 | { 126 | let mappings: Vec = Deserialize::deserialize(deserializer)?; 127 | mappings 128 | .into_iter() 129 | .map(|port_mapping| { 130 | let port_mapping_parts: Vec<&str> = port_mapping.split(':').collect(); 131 | if port_mapping_parts.len() != 2 { 132 | Err(serde::de::Error::custom(format!( 133 | "Invalid port_mapping entry: {}", 134 | port_mapping 135 | ))) 136 | } else { 137 | let orig_port = port_mapping_parts[0].parse::().map_err(serde::de::Error::custom)?; 138 | let target_port = port_mapping_parts[1].parse::().map_err(serde::de::Error::custom)?; 139 | Ok((orig_port, target_port)) 140 | } 141 | }) 142 | .collect() 143 | } 144 | 145 | fn deserialize_non_empty_vec<'de, D, T>(d: D) -> Result, D::Error> 146 | where 147 | D: Deserializer<'de>, 148 | T: Deserialize<'de>, 149 | { 150 | let vec = >::deserialize(d)?; 151 | if vec.is_empty() { 152 | Err(serde::de::Error::custom("List must not be empty")) 153 | } else { 154 | Ok(vec) 155 | } 156 | } 157 | 158 | impl From<&LocalProtocol> for ReverseTunnelConfigProtocol { 159 | fn from(value: &LocalProtocol) -> Self { 160 | match value { 161 | LocalProtocol::Tcp { .. } 162 | | LocalProtocol::Udp { .. } 163 | | LocalProtocol::Stdio { .. } 164 | | LocalProtocol::Socks5 { .. } 165 | | LocalProtocol::TProxyTcp 166 | | LocalProtocol::TProxyUdp { .. } 167 | | LocalProtocol::HttpProxy { .. } 168 | | LocalProtocol::Unix { .. } => Self::Unknown, 169 | LocalProtocol::ReverseTcp => Self::Tcp, 170 | LocalProtocol::ReverseUdp { .. } => Self::Udp, 171 | LocalProtocol::ReverseSocks5 { .. } => Self::Socks5, 172 | LocalProtocol::ReverseUnix { .. } => Self::Unix, 173 | LocalProtocol::ReverseHttpProxy { .. } => Self::HttpProxy, 174 | } 175 | } 176 | } 177 | impl From<&LocalProtocol> for TunnelConfigProtocol { 178 | fn from(value: &LocalProtocol) -> Self { 179 | match value { 180 | LocalProtocol::ReverseTcp 181 | | LocalProtocol::ReverseUdp { .. } 182 | | LocalProtocol::ReverseSocks5 { .. } 183 | | LocalProtocol::ReverseUnix { .. } 184 | | LocalProtocol::Stdio { .. } 185 | | LocalProtocol::Socks5 { .. } 186 | | LocalProtocol::TProxyTcp 187 | | LocalProtocol::TProxyUdp { .. } 188 | | LocalProtocol::HttpProxy { .. } 189 | | LocalProtocol::ReverseHttpProxy { .. } 190 | | LocalProtocol::Unix { .. } => Self::Unknown, 191 | LocalProtocol::Tcp { .. } => Self::Tcp, 192 | LocalProtocol::Udp { .. } => Self::Udp, 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /wstunnel/src/somark.rs: -------------------------------------------------------------------------------- 1 | //! somark - abstraction to be used only on linux 2 | //! 3 | //! on other platforms it's noop without memory footprint 4 | 5 | use socket2::SockRef; 6 | 7 | #[derive(Copy, Clone, Debug)] 8 | #[repr(transparent)] 9 | pub struct SoMark { 10 | #[cfg(target_os = "linux")] 11 | inner: Option, 12 | } 13 | 14 | impl SoMark { 15 | #[cfg_attr(not(target_os = "linux"), expect(unused_variables))] 16 | pub fn new(so_mark: Option) -> Self { 17 | SoMark { 18 | #[cfg(target_os = "linux")] 19 | inner: so_mark, 20 | } 21 | } 22 | 23 | #[cfg(not(target_os = "linux"))] 24 | #[inline] 25 | pub fn set_mark(self, _: SockRef) -> Result<(), std::convert::Infallible> { 26 | Ok(()) 27 | } 28 | 29 | #[cfg(target_os = "linux")] 30 | #[inline] 31 | pub fn set_mark(self, socket: SockRef) -> std::io::Result<()> { 32 | let Some(so_mark) = self.inner else { return Ok(()) }; 33 | 34 | socket.set_mark(so_mark).map_err(|_| std::io::Error::last_os_error()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /wstunnel/src/test_integrations.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::DefaultTokioExecutor; 2 | use crate::protocols; 3 | use crate::protocols::dns::DnsResolver; 4 | use crate::restrictions::types; 5 | use crate::restrictions::types::{AllowConfig, MatchConfig, RestrictionConfig, RestrictionsRules}; 6 | use crate::somark::SoMark; 7 | use crate::tunnel::client::{WsClient, WsClientConfig}; 8 | use crate::tunnel::listeners::{TcpTunnelListener, UdpTunnelListener}; 9 | use crate::tunnel::server::{WsServer, WsServerConfig}; 10 | use crate::tunnel::transport::{TransportAddr, TransportScheme}; 11 | use bytes::BytesMut; 12 | use futures_util::StreamExt; 13 | use hyper::http::HeaderValue; 14 | use ipnet::{IpNet, Ipv4Net, Ipv6Net}; 15 | use regex::Regex; 16 | use rstest::{fixture, rstest}; 17 | use scopeguard::defer; 18 | use serial_test::serial; 19 | use std::collections::HashMap; 20 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 21 | use std::time::Duration; 22 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 23 | use tokio::pin; 24 | use url::Host; 25 | 26 | #[fixture] 27 | fn dns_resolver() -> DnsResolver { 28 | DnsResolver::new_from_urls(&[], None, SoMark::new(None), true).expect("Cannot create DNS resolver") 29 | } 30 | 31 | #[fixture] 32 | fn server_no_tls(dns_resolver: DnsResolver) -> WsServer { 33 | let server_config = WsServerConfig { 34 | socket_so_mark: SoMark::new(None), 35 | bind: "127.0.0.1:8080".parse().unwrap(), 36 | websocket_ping_frequency: Some(Duration::from_secs(10)), 37 | timeout_connect: Duration::from_secs(10), 38 | websocket_mask_frame: false, 39 | tls: None, 40 | dns_resolver, 41 | restriction_config: None, 42 | http_proxy: None, 43 | remote_server_idle_timeout: Duration::from_secs(30), 44 | }; 45 | WsServer::new(server_config, DefaultTokioExecutor::default()) 46 | } 47 | 48 | #[fixture] 49 | async fn client_ws(dns_resolver: DnsResolver) -> WsClient { 50 | let client_config = WsClientConfig { 51 | remote_addr: TransportAddr::new(TransportScheme::Ws, Host::Ipv4("127.0.0.1".parse().unwrap()), 8080, None) 52 | .unwrap(), 53 | socket_so_mark: SoMark::new(None), 54 | http_upgrade_path_prefix: "wstunnel".to_string(), 55 | http_upgrade_credentials: None, 56 | http_headers: HashMap::new(), 57 | http_headers_file: None, 58 | http_header_host: HeaderValue::from_static("127.0.0.1:8080"), 59 | timeout_connect: Duration::from_secs(10), 60 | websocket_ping_frequency: Some(Duration::from_secs(10)), 61 | websocket_mask_frame: false, 62 | dns_resolver, 63 | http_proxy: None, 64 | }; 65 | 66 | WsClient::new( 67 | client_config, 68 | 1, 69 | Duration::from_secs(1), 70 | Duration::from_secs(1), 71 | DefaultTokioExecutor::default(), 72 | ) 73 | .await 74 | .unwrap() 75 | } 76 | 77 | #[fixture] 78 | fn no_restrictions() -> RestrictionsRules { 79 | pub fn default_host() -> Regex { 80 | Regex::new("^.*$").unwrap() 81 | } 82 | 83 | pub fn default_cidr() -> Vec { 84 | vec![IpNet::V4(Ipv4Net::default()), IpNet::V6(Ipv6Net::default())] 85 | } 86 | 87 | let tunnels = types::AllowConfig::Tunnel(types::AllowTunnelConfig { 88 | protocol: vec![], 89 | port: vec![], 90 | host: default_host(), 91 | cidr: default_cidr(), 92 | }); 93 | let reverse_tunnel = AllowConfig::ReverseTunnel(types::AllowReverseTunnelConfig { 94 | protocol: vec![], 95 | port: vec![], 96 | port_mapping: Default::default(), 97 | cidr: default_cidr(), 98 | }); 99 | 100 | RestrictionsRules { 101 | restrictions: vec![RestrictionConfig { 102 | name: "".to_string(), 103 | r#match: vec![MatchConfig::Any], 104 | allow: vec![tunnels, reverse_tunnel], 105 | }], 106 | } 107 | } 108 | 109 | const TUNNEL_LISTEN: (SocketAddr, Host) = ( 110 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9998)), 111 | Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)), 112 | ); 113 | const ENDPOINT_LISTEN: (SocketAddr, Host) = ( 114 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 9999)), 115 | Host::Ipv4(Ipv4Addr::new(127, 0, 0, 1)), 116 | ); 117 | 118 | #[rstest] 119 | #[timeout(Duration::from_secs(10))] 120 | #[tokio::test] 121 | #[serial] 122 | async fn test_tcp_tunnel( 123 | #[future] client_ws: WsClient, 124 | server_no_tls: WsServer, 125 | no_restrictions: RestrictionsRules, 126 | dns_resolver: DnsResolver, 127 | ) { 128 | let server_h = tokio::spawn(server_no_tls.serve(no_restrictions)); 129 | defer! { server_h.abort(); }; 130 | 131 | let client_ws = client_ws.await; 132 | 133 | let server = TcpTunnelListener::new(TUNNEL_LISTEN.0, (ENDPOINT_LISTEN.1, ENDPOINT_LISTEN.0.port()), false) 134 | .await 135 | .unwrap(); 136 | tokio::spawn(async move { 137 | client_ws.run_tunnel(server).await.unwrap(); 138 | }); 139 | 140 | let mut tcp_listener = protocols::tcp::run_server(ENDPOINT_LISTEN.0, false).await.unwrap(); 141 | let mut client = protocols::tcp::connect( 142 | &TUNNEL_LISTEN.1, 143 | TUNNEL_LISTEN.0.port(), 144 | SoMark::new(None), 145 | Duration::from_secs(10), 146 | &dns_resolver, 147 | ) 148 | .await 149 | .unwrap(); 150 | 151 | client.write_all(b"Hello").await.unwrap(); 152 | let mut dd = tcp_listener.next().await.unwrap().unwrap(); 153 | let mut buf = BytesMut::new(); 154 | dd.read_buf(&mut buf).await.unwrap(); 155 | assert_eq!(&buf[..5], b"Hello"); 156 | buf.clear(); 157 | 158 | dd.write_all(b"world!").await.unwrap(); 159 | client.read_buf(&mut buf).await.unwrap(); 160 | assert_eq!(&buf[..6], b"world!"); 161 | } 162 | 163 | #[rstest] 164 | #[timeout(Duration::from_secs(10))] 165 | #[tokio::test] 166 | #[serial] 167 | async fn test_udp_tunnel( 168 | #[future] client_ws: WsClient, 169 | server_no_tls: WsServer, 170 | no_restrictions: RestrictionsRules, 171 | dns_resolver: DnsResolver, 172 | ) { 173 | let server_h = tokio::spawn(server_no_tls.serve(no_restrictions)); 174 | defer! { server_h.abort(); }; 175 | 176 | let client_ws = client_ws.await; 177 | 178 | let server = UdpTunnelListener::new(TUNNEL_LISTEN.0, (ENDPOINT_LISTEN.1, ENDPOINT_LISTEN.0.port()), None) 179 | .await 180 | .unwrap(); 181 | tokio::spawn(async move { 182 | client_ws.run_tunnel(server).await.unwrap(); 183 | }); 184 | 185 | let udp_listener = protocols::udp::run_server(ENDPOINT_LISTEN.0, None, |_| Ok(()), |s| Ok(s.clone())) 186 | .await 187 | .unwrap(); 188 | let mut client = protocols::udp::connect( 189 | &TUNNEL_LISTEN.1, 190 | TUNNEL_LISTEN.0.port(), 191 | Duration::from_secs(10), 192 | SoMark::new(None), 193 | &dns_resolver, 194 | ) 195 | .await 196 | .unwrap(); 197 | 198 | client.write_all(b"Hello").await.unwrap(); 199 | pin!(udp_listener); 200 | let dd = udp_listener.next().await.unwrap().unwrap(); 201 | pin!(dd); 202 | let mut buf = BytesMut::new(); 203 | dd.read_buf(&mut buf).await.unwrap(); 204 | assert_eq!(&buf[..5], b"Hello"); 205 | buf.clear(); 206 | 207 | dd.writer().write_all(b"world!").await.unwrap(); 208 | client.read_buf(&mut buf).await.unwrap(); 209 | assert_eq!(&buf[..6], b"world!"); 210 | } 211 | 212 | //#[rstest] 213 | //#[timeout(Duration::from_secs(10))] 214 | //#[tokio::test] 215 | //async fn test_socks5_tunnel( 216 | // #[future] client_ws: WsClient, 217 | // server_no_tls: WsServer, 218 | // no_restrictions: RestrictionsRules, 219 | // dns_resolver: DnsResolver, 220 | //) { 221 | // let server_h = tokio::spawn(server_no_tls.serve(no_restrictions)); 222 | // defer! { server_h.abort(); }; 223 | // 224 | // let client_ws = client_ws.await; 225 | // 226 | // let server = Socks5TunnelListener::new(TUNNEL_LISTEN.0, None, None).await.unwrap(); 227 | // tokio::spawn(async move { client_ws.run_tunnel(server).await.unwrap(); }); 228 | // 229 | // let socks5_listener = protocols::socks5::run_server(ENDPOINT_LISTEN.0, None, None).await.unwrap(); 230 | // let mut client = protocols::tcp::connect(&TUNNEL_LISTEN.1, TUNNEL_LISTEN.0.port(), None, Duration::from_secs(10), &dns_resolver).await.unwrap(); 231 | // 232 | // client.write_all(b"Hello").await.unwrap(); 233 | // pin!(socks5_listener); 234 | // let (dd, _) = socks5_listener.next().await.unwrap().unwrap(); 235 | // let (mut read, mut write) = dd.into_split(); 236 | // let mut buf = BytesMut::new(); 237 | // read.read_buf(&mut buf).await.unwrap(); 238 | // assert_eq!(&buf[..5], b"Hello"); 239 | // buf.clear(); 240 | // 241 | // write.write_all(b"world!").await.unwrap(); 242 | // client.read_buf(&mut buf).await.unwrap(); 243 | // assert_eq!(&buf[..6], b"world!"); 244 | //} 245 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/client/client.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::{DefaultTokioExecutor, TokioExecutorRef}; 2 | use crate::tunnel; 3 | use crate::tunnel::RemoteAddr; 4 | use crate::tunnel::client::WsClientConfig; 5 | use crate::tunnel::client::cnx_pool::WsConnection; 6 | use crate::tunnel::connectors::TunnelConnector; 7 | use crate::tunnel::listeners::TunnelListener; 8 | use crate::tunnel::tls_reloader::TlsReloader; 9 | use crate::tunnel::transport::io::{TunnelReader, TunnelWriter}; 10 | use crate::tunnel::transport::{TransportScheme, jwt_token_to_tunnel}; 11 | use anyhow::Context; 12 | use futures_util::pin_mut; 13 | use hyper::header::COOKIE; 14 | use log::debug; 15 | use std::cmp::min; 16 | use std::sync::Arc; 17 | use std::time::Duration; 18 | use tokio::io::{AsyncRead, AsyncWrite}; 19 | use tokio::sync::oneshot; 20 | use tokio_stream::StreamExt; 21 | use tracing::{Instrument, Level, Span, error, event, span}; 22 | use url::Host; 23 | use uuid::Uuid; 24 | 25 | #[derive(Clone)] 26 | pub struct WsClient { 27 | pub config: Arc, 28 | pub cnx_pool: bb8::Pool, 29 | reverse_tunnel_connection_retry_max_backoff: Duration, 30 | _tls_reloader: Arc, 31 | pub(crate) executor: E, 32 | } 33 | 34 | impl WsClient { 35 | pub async fn new( 36 | config: WsClientConfig, 37 | connection_min_idle: u32, 38 | connection_retry_max_backoff: Duration, 39 | reverse_tunnel_connection_retry_max_backoff: Duration, 40 | executor: E, 41 | ) -> anyhow::Result { 42 | let config = Arc::new(config); 43 | let cnx = WsConnection::new(config.clone()); 44 | let tls_reloader = TlsReloader::new_for_client(config.clone()).with_context(|| "Cannot create tls reloader")?; 45 | let cnx_pool = bb8::Pool::builder() 46 | .max_size(1000) 47 | .min_idle(Some(connection_min_idle)) 48 | .max_lifetime(Some(Duration::from_secs(30))) 49 | .connection_timeout(connection_retry_max_backoff) 50 | .retry_connection(true) 51 | .build(cnx) 52 | .await?; 53 | 54 | Ok(Self { 55 | config, 56 | cnx_pool, 57 | reverse_tunnel_connection_retry_max_backoff, 58 | _tls_reloader: Arc::new(tls_reloader), 59 | executor, 60 | }) 61 | } 62 | 63 | async fn connect_to_server( 64 | &self, 65 | request_id: Uuid, 66 | remote_cfg: &RemoteAddr, 67 | duplex_stream: (R, W), 68 | ) -> anyhow::Result<()> 69 | where 70 | R: AsyncRead + Send + 'static, 71 | W: AsyncWrite + Send + 'static, 72 | { 73 | // Connect to server with the correct protocol 74 | let (ws_rx, ws_tx, response) = match self.config.remote_addr.scheme() { 75 | TransportScheme::Ws | TransportScheme::Wss => { 76 | tunnel::transport::websocket::connect(request_id, self, remote_cfg) 77 | .await 78 | .map(|(r, w, response)| (TunnelReader::Websocket(r), TunnelWriter::Websocket(w), response))? 79 | } 80 | TransportScheme::Http | TransportScheme::Https => { 81 | tunnel::transport::http2::connect(request_id, self, remote_cfg) 82 | .await 83 | .map(|(r, w, response)| (TunnelReader::Http2(r), TunnelWriter::Http2(w), response))? 84 | } 85 | }; 86 | 87 | debug!("Server response: {:?}", response); 88 | let (local_rx, local_tx) = duplex_stream; 89 | let (close_tx, close_rx) = oneshot::channel::<()>(); 90 | 91 | // Forward local tx to websocket tx 92 | let ping_frequency = self.config.websocket_ping_frequency; 93 | self.executor.spawn( 94 | super::super::transport::io::propagate_local_to_remote(local_rx, ws_tx, close_tx, ping_frequency) 95 | .instrument(Span::current()), 96 | ); 97 | 98 | // Forward websocket rx to local rx 99 | let _ = super::super::transport::io::propagate_remote_to_local(local_tx, ws_rx, close_rx).await; 100 | 101 | Ok(()) 102 | } 103 | 104 | pub async fn run_tunnel(self, tunnel_listener: impl TunnelListener) -> anyhow::Result<()> { 105 | pin_mut!(tunnel_listener); 106 | while let Some(cnx) = tunnel_listener.next().await { 107 | let (cnx_stream, remote_addr) = match cnx { 108 | Ok((cnx_stream, remote_addr)) => (cnx_stream, remote_addr), 109 | Err(err) => { 110 | error!("Error accepting connection: {:?}", err); 111 | continue; 112 | } 113 | }; 114 | 115 | let request_id = Uuid::now_v7(); 116 | let span = span!( 117 | Level::INFO, 118 | "tunnel", 119 | id = request_id.to_string(), 120 | remote = format!("{}:{}", remote_addr.host, remote_addr.port) 121 | ); 122 | let client = self.clone(); 123 | let tunnel = async move { 124 | let _ = client 125 | .connect_to_server(request_id, &remote_addr, cnx_stream) 126 | .await 127 | .map_err(|err| error!("{:?}", err)); 128 | } 129 | .instrument(span); 130 | 131 | self.executor.spawn(tunnel); 132 | } 133 | 134 | Ok(()) 135 | } 136 | 137 | pub async fn run_reverse_tunnel( 138 | self, 139 | remote_addr: RemoteAddr, 140 | connector: impl TunnelConnector, 141 | ) -> anyhow::Result<()> { 142 | fn new_reconnect_delay(max_delay: Duration) -> impl FnMut() -> Duration { 143 | let mut reconnect_delay = Duration::from_secs(1); 144 | 145 | move || -> Duration { 146 | let delay = reconnect_delay; 147 | reconnect_delay = min(reconnect_delay * 2, max_delay); 148 | delay 149 | } 150 | } 151 | 152 | let mut reconnect_delay = new_reconnect_delay(self.reverse_tunnel_connection_retry_max_backoff); 153 | loop { 154 | let client = self.clone(); 155 | let request_id = Uuid::now_v7(); 156 | let span = span!( 157 | Level::INFO, 158 | "tunnel", 159 | id = request_id.to_string(), 160 | remote = format!("{}:{}", remote_addr.host, remote_addr.port) 161 | ); 162 | // Correctly configure tunnel cfg 163 | let (ws_rx, ws_tx, response) = match client.config.remote_addr.scheme() { 164 | TransportScheme::Ws | TransportScheme::Wss => { 165 | match tunnel::transport::websocket::connect(request_id, &client, &remote_addr) 166 | .instrument(span.clone()) 167 | .await 168 | { 169 | Ok((r, w, response)) => (TunnelReader::Websocket(r), TunnelWriter::Websocket(w), response), 170 | Err(err) => { 171 | let reconnect_delay = reconnect_delay(); 172 | event!(parent: &span, Level::ERROR, "Retrying in {:?}, cannot connect to remote server: {:?}", reconnect_delay, err); 173 | tokio::time::sleep(reconnect_delay).await; 174 | continue; 175 | } 176 | } 177 | } 178 | TransportScheme::Http | TransportScheme::Https => { 179 | match tunnel::transport::http2::connect(request_id, &client, &remote_addr) 180 | .instrument(span.clone()) 181 | .await 182 | { 183 | Ok((r, w, response)) => (TunnelReader::Http2(r), TunnelWriter::Http2(w), response), 184 | Err(err) => { 185 | let reconnect_delay = reconnect_delay(); 186 | event!(parent: &span, Level::ERROR, "Retrying in {:?}, cannot connect to remote server: {:?}", reconnect_delay, err); 187 | tokio::time::sleep(reconnect_delay).await; 188 | continue; 189 | } 190 | } 191 | } 192 | }; 193 | reconnect_delay = new_reconnect_delay(self.reverse_tunnel_connection_retry_max_backoff); 194 | 195 | // Connect to endpoint 196 | event!(parent: &span, Level::DEBUG, "Server response: {:?}", response); 197 | let remote = response 198 | .headers 199 | .get(COOKIE) 200 | .and_then(|h| h.to_str().ok()) 201 | .and_then(|h| jwt_token_to_tunnel(h).ok()) 202 | .map(|jwt| RemoteAddr { 203 | protocol: jwt.claims.p, 204 | host: Host::parse(&jwt.claims.r).unwrap_or_else(|_| Host::Domain(String::new())), 205 | port: jwt.claims.rp, 206 | }); 207 | 208 | let (local_rx, local_tx) = match connector.connect(&remote).instrument(span.clone()).await { 209 | Ok(s) => s, 210 | Err(err) => { 211 | event!(parent: &span, Level::ERROR, "Cannot connect to {remote:?}: {err:?}"); 212 | continue; 213 | } 214 | }; 215 | 216 | let (close_tx, close_rx) = oneshot::channel::<()>(); 217 | self.executor.spawn({ 218 | let ping_frequency = client.config.websocket_ping_frequency; 219 | super::super::transport::io::propagate_local_to_remote(local_rx, ws_tx, close_tx, ping_frequency) 220 | .instrument(span.clone()) 221 | }); 222 | 223 | // Forward websocket rx to local rx 224 | self.executor.spawn( 225 | super::super::transport::io::propagate_remote_to_local(local_tx, ws_rx, close_rx) 226 | .instrument(span.clone()), 227 | ); 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/client/cnx_pool.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols; 2 | use crate::protocols::tls; 3 | use crate::tunnel::client::WsClientConfig; 4 | use crate::tunnel::client::l4_transport_stream::TransportStream; 5 | use bb8::ManageConnection; 6 | use bytes::Bytes; 7 | use std::ops::Deref; 8 | use std::sync::Arc; 9 | use tracing::instrument; 10 | 11 | #[derive(Clone)] 12 | pub struct WsConnection(Arc); 13 | 14 | impl WsConnection { 15 | pub fn new(config: Arc) -> Self { 16 | Self(config) 17 | } 18 | } 19 | 20 | impl Deref for WsConnection { 21 | type Target = WsClientConfig; 22 | 23 | fn deref(&self) -> &Self::Target { 24 | &self.0 25 | } 26 | } 27 | 28 | impl ManageConnection for WsConnection { 29 | type Connection = Option; 30 | type Error = anyhow::Error; 31 | 32 | #[instrument(level = "trace", name = "cnx_server", skip_all)] 33 | async fn connect(&self) -> Result { 34 | let timeout = self.timeout_connect; 35 | 36 | let tcp_stream = if let Some(http_proxy) = &self.http_proxy { 37 | protocols::tcp::connect_with_http_proxy( 38 | http_proxy, 39 | self.remote_addr.host(), 40 | self.remote_addr.port(), 41 | self.socket_so_mark, 42 | timeout, 43 | &self.dns_resolver, 44 | ) 45 | .await? 46 | } else { 47 | protocols::tcp::connect( 48 | self.remote_addr.host(), 49 | self.remote_addr.port(), 50 | self.socket_so_mark, 51 | timeout, 52 | &self.dns_resolver, 53 | ) 54 | .await? 55 | }; 56 | 57 | if self.remote_addr.tls().is_some() { 58 | let tls_stream = tls::connect(self, tcp_stream).await?; 59 | Ok(Some(TransportStream::from_client_tls(tls_stream, Bytes::default()))) 60 | } else { 61 | Ok(Some(TransportStream::from_tcp(tcp_stream, Bytes::default()))) 62 | } 63 | } 64 | 65 | async fn is_valid(&self, _conn: &mut Self::Connection) -> Result<(), Self::Error> { 66 | Ok(()) 67 | } 68 | 69 | fn has_broken(&self, conn: &mut Self::Connection) -> bool { 70 | conn.is_none() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/client/config.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::dns::DnsResolver; 2 | use crate::somark::SoMark; 3 | use crate::tunnel::transport::TransportAddr; 4 | use hyper::header::{HeaderName, HeaderValue}; 5 | use parking_lot::RwLock; 6 | use std::collections::HashMap; 7 | use std::net::IpAddr; 8 | use std::path::PathBuf; 9 | use std::sync::{Arc, LazyLock}; 10 | use std::time::Duration; 11 | use tokio_rustls::TlsConnector; 12 | use tokio_rustls::rustls::pki_types::{DnsName, ServerName}; 13 | use url::{Host, Url}; 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct WsClientConfig { 17 | pub remote_addr: TransportAddr, 18 | pub socket_so_mark: SoMark, 19 | pub http_upgrade_path_prefix: String, 20 | pub http_upgrade_credentials: Option, 21 | pub http_headers: HashMap, 22 | pub http_headers_file: Option, 23 | pub http_header_host: HeaderValue, 24 | pub timeout_connect: Duration, 25 | pub websocket_ping_frequency: Option, 26 | pub websocket_mask_frame: bool, 27 | pub http_proxy: Option, 28 | pub dns_resolver: DnsResolver, 29 | } 30 | 31 | impl WsClientConfig { 32 | pub fn tls_server_name(&self) -> ServerName<'static> { 33 | static INVALID_DNS_NAME: LazyLock = 34 | LazyLock::new(|| DnsName::try_from("dns-name-invalid.com").unwrap()); 35 | 36 | self.remote_addr 37 | .tls() 38 | .and_then(|tls| tls.tls_sni_override.as_ref()) 39 | .map_or_else( 40 | || match &self.remote_addr.host() { 41 | Host::Domain(domain) => ServerName::DnsName( 42 | DnsName::try_from(domain.clone()).unwrap_or_else(|_| INVALID_DNS_NAME.clone()), 43 | ), 44 | Host::Ipv4(ip) => ServerName::IpAddress(IpAddr::V4(*ip).into()), 45 | Host::Ipv6(ip) => ServerName::IpAddress(IpAddr::V6(*ip).into()), 46 | }, 47 | |sni_override| ServerName::DnsName(sni_override.clone()), 48 | ) 49 | } 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct TlsClientConfig { 54 | pub tls_sni_disabled: bool, 55 | pub tls_sni_override: Option>, 56 | pub tls_verify_certificate: bool, 57 | pub tls_connector: Arc>, 58 | pub tls_certificate_path: Option, 59 | pub tls_key_path: Option, 60 | } 61 | 62 | impl TlsClientConfig { 63 | pub fn tls_connector(&self) -> TlsConnector { 64 | self.tls_connector.read().clone() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/client/l4_transport_stream.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | use std::cmp; 3 | use std::io::{Error, IoSlice}; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf, ReadHalf, WriteHalf}; 7 | use tokio::net::TcpStream; 8 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 9 | 10 | pub struct TransportStream { 11 | read: TransportReadHalf, 12 | write: TransportWriteHalf, 13 | } 14 | 15 | impl TransportStream { 16 | pub fn from_tcp(tcp: TcpStream, read_buf: Bytes) -> Self { 17 | let (read, write) = tcp.into_split(); 18 | Self { 19 | read: TransportReadHalf::Plain(read, read_buf), 20 | write: TransportWriteHalf::Plain(write), 21 | } 22 | } 23 | 24 | pub fn from_client_tls(tls: tokio_rustls::client::TlsStream, read_buf: Bytes) -> Self { 25 | let (read, write) = tokio::io::split(tls); 26 | Self { 27 | read: TransportReadHalf::Tls(read, read_buf), 28 | write: TransportWriteHalf::Tls(write), 29 | } 30 | } 31 | 32 | pub fn from_server_tls(tls: tokio_rustls::server::TlsStream, read_buf: Bytes) -> Self { 33 | let (read, write) = tokio::io::split(tls); 34 | Self { 35 | read: TransportReadHalf::TlsSrv(read, read_buf), 36 | write: TransportWriteHalf::TlsSrv(write), 37 | } 38 | } 39 | 40 | pub fn from(self, read_buf: Bytes) -> Self { 41 | let mut read = self.read; 42 | *read.read_buf_mut() = read_buf; 43 | Self { 44 | read, 45 | write: self.write, 46 | } 47 | } 48 | 49 | pub fn into_split(self) -> (TransportReadHalf, TransportWriteHalf) { 50 | (self.read, self.write) 51 | } 52 | } 53 | 54 | pub enum TransportReadHalf { 55 | Plain(OwnedReadHalf, Bytes), 56 | Tls(ReadHalf>, Bytes), 57 | TlsSrv(ReadHalf>, Bytes), 58 | } 59 | 60 | impl TransportReadHalf { 61 | fn read_buf_mut(&mut self) -> &mut Bytes { 62 | match self { 63 | Self::Plain(_, buf) => buf, 64 | Self::Tls(_, buf) => buf, 65 | Self::TlsSrv(_, buf) => buf, 66 | } 67 | } 68 | } 69 | 70 | pub enum TransportWriteHalf { 71 | Plain(OwnedWriteHalf), 72 | Tls(WriteHalf>), 73 | TlsSrv(WriteHalf>), 74 | } 75 | 76 | impl AsyncRead for TransportStream { 77 | #[inline] 78 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 79 | unsafe { self.map_unchecked_mut(|s| &mut s.read).poll_read(cx, buf) } 80 | } 81 | } 82 | 83 | impl AsyncWrite for TransportStream { 84 | #[inline] 85 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 86 | unsafe { self.map_unchecked_mut(|s| &mut s.write).poll_write(cx, buf) } 87 | } 88 | 89 | #[inline] 90 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 91 | unsafe { self.map_unchecked_mut(|s| &mut s.write).poll_flush(cx) } 92 | } 93 | 94 | #[inline] 95 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 96 | unsafe { self.map_unchecked_mut(|s| &mut s.write).poll_shutdown(cx) } 97 | } 98 | 99 | #[inline] 100 | fn poll_write_vectored( 101 | self: Pin<&mut Self>, 102 | cx: &mut Context<'_>, 103 | bufs: &[IoSlice<'_>], 104 | ) -> Poll> { 105 | unsafe { self.map_unchecked_mut(|s| &mut s.write).poll_write_vectored(cx, bufs) } 106 | } 107 | 108 | #[inline] 109 | fn is_write_vectored(&self) -> bool { 110 | self.write.is_write_vectored() 111 | } 112 | } 113 | 114 | impl AsyncRead for TransportReadHalf { 115 | #[inline] 116 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 117 | let this = self.get_mut(); 118 | 119 | let read_buf = this.read_buf_mut(); 120 | if !read_buf.is_empty() { 121 | let copy_len = cmp::min(read_buf.len(), buf.remaining()); 122 | buf.put_slice(&read_buf[..copy_len]); 123 | read_buf.advance(copy_len); 124 | if read_buf.is_empty() { 125 | read_buf.clear(); 126 | } 127 | return Poll::Ready(Ok(())); 128 | } 129 | 130 | match this { 131 | Self::Plain(cnx, _) => Pin::new(cnx).poll_read(cx, buf), 132 | Self::Tls(cnx, _) => Pin::new(cnx).poll_read(cx, buf), 133 | Self::TlsSrv(cnx, _) => Pin::new(cnx).poll_read(cx, buf), 134 | } 135 | } 136 | } 137 | 138 | impl AsyncWrite for TransportWriteHalf { 139 | #[inline] 140 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 141 | match self.get_mut() { 142 | Self::Plain(cnx) => Pin::new(cnx).poll_write(cx, buf), 143 | Self::Tls(cnx) => Pin::new(cnx).poll_write(cx, buf), 144 | Self::TlsSrv(cnx) => Pin::new(cnx).poll_write(cx, buf), 145 | } 146 | } 147 | 148 | #[inline] 149 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 150 | match self.get_mut() { 151 | Self::Plain(cnx) => Pin::new(cnx).poll_flush(cx), 152 | Self::Tls(cnx) => Pin::new(cnx).poll_flush(cx), 153 | Self::TlsSrv(cnx) => Pin::new(cnx).poll_flush(cx), 154 | } 155 | } 156 | 157 | #[inline] 158 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 159 | match self.get_mut() { 160 | Self::Plain(cnx) => Pin::new(cnx).poll_shutdown(cx), 161 | Self::Tls(cnx) => Pin::new(cnx).poll_shutdown(cx), 162 | Self::TlsSrv(cnx) => Pin::new(cnx).poll_shutdown(cx), 163 | } 164 | } 165 | 166 | #[inline] 167 | fn poll_write_vectored( 168 | self: Pin<&mut Self>, 169 | cx: &mut Context<'_>, 170 | bufs: &[IoSlice<'_>], 171 | ) -> Poll> { 172 | match self.get_mut() { 173 | Self::Plain(cnx) => Pin::new(cnx).poll_write_vectored(cx, bufs), 174 | Self::Tls(cnx) => Pin::new(cnx).poll_write_vectored(cx, bufs), 175 | Self::TlsSrv(cnx) => Pin::new(cnx).poll_write_vectored(cx, bufs), 176 | } 177 | } 178 | 179 | #[inline] 180 | fn is_write_vectored(&self) -> bool { 181 | match &self { 182 | Self::Plain(cnx) => cnx.is_write_vectored(), 183 | Self::Tls(cnx) => cnx.is_write_vectored(), 184 | Self::TlsSrv(cnx) => cnx.is_write_vectored(), 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/client/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | mod client; 3 | mod cnx_pool; 4 | mod config; 5 | pub mod l4_transport_stream; 6 | 7 | pub use client::WsClient; 8 | pub use config::TlsClientConfig; 9 | pub use config::WsClientConfig; 10 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/connectors/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use tokio::io::{AsyncRead, AsyncWrite}; 3 | use url::Url; 4 | 5 | pub use sock5::Socks5TunnelConnector; 6 | pub use tcp::TcpTunnelConnector; 7 | pub use udp::UdpTunnelConnector; 8 | 9 | use crate::tunnel::RemoteAddr; 10 | 11 | mod sock5; 12 | mod tcp; 13 | mod udp; 14 | 15 | pub trait TunnelConnector { 16 | type Reader: AsyncRead + Send + 'static; 17 | type Writer: AsyncWrite + Send + 'static; 18 | 19 | async fn connect(&self, remote: &Option) -> anyhow::Result<(Self::Reader, Self::Writer)>; 20 | async fn connect_with_http_proxy( 21 | &self, 22 | _proxy: &Url, 23 | _remote: &Option, 24 | ) -> anyhow::Result<(Self::Reader, Self::Writer)> { 25 | Err(anyhow!( 26 | "Requested to use HTTP Proxy to connect but it is not supported with this connector" 27 | )) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/connectors/sock5.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, IoSlice}; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | use std::time::Duration; 5 | 6 | use anyhow::anyhow; 7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 8 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 9 | use url::Url; 10 | 11 | use crate::protocols; 12 | use crate::protocols::dns::DnsResolver; 13 | use crate::protocols::udp; 14 | use crate::protocols::udp::WsUdpSocket; 15 | use crate::somark::SoMark; 16 | use crate::tunnel::connectors::TunnelConnector; 17 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 18 | 19 | pub struct Socks5TunnelConnector<'a> { 20 | so_mark: SoMark, 21 | connect_timeout: Duration, 22 | dns_resolver: &'a DnsResolver, 23 | } 24 | 25 | impl Socks5TunnelConnector<'_> { 26 | pub fn new(so_mark: SoMark, connect_timeout: Duration, dns_resolver: &DnsResolver) -> Socks5TunnelConnector { 27 | Socks5TunnelConnector { 28 | so_mark, 29 | connect_timeout, 30 | dns_resolver, 31 | } 32 | } 33 | } 34 | 35 | impl TunnelConnector for Socks5TunnelConnector<'_> { 36 | type Reader = Socks5Reader; 37 | type Writer = Socks5Writer; 38 | 39 | async fn connect(&self, remote: &Option) -> anyhow::Result<(Self::Reader, Self::Writer)> { 40 | let Some(remote) = remote else { 41 | return Err(anyhow!("Missing remote destination for reverse socks5")); 42 | }; 43 | 44 | match remote.protocol { 45 | LocalProtocol::Tcp { proxy_protocol: _ } => { 46 | let stream = protocols::tcp::connect( 47 | &remote.host, 48 | remote.port, 49 | self.so_mark, 50 | self.connect_timeout, 51 | self.dns_resolver, 52 | ) 53 | .await?; 54 | let (reader, writer) = stream.into_split(); 55 | Ok((Socks5Reader::Tcp(reader), Socks5Writer::Tcp(writer))) 56 | } 57 | LocalProtocol::Udp { .. } => { 58 | let stream = 59 | udp::connect(&remote.host, remote.port, self.connect_timeout, self.so_mark, self.dns_resolver) 60 | .await?; 61 | Ok((Socks5Reader::Udp(stream.clone()), Socks5Writer::Udp(stream))) 62 | } 63 | _ => Err(anyhow!("Invalid protocol for reverse socks5 {:?}", remote.protocol)), 64 | } 65 | } 66 | 67 | async fn connect_with_http_proxy( 68 | &self, 69 | proxy: &Url, 70 | remote: &Option, 71 | ) -> anyhow::Result<(Self::Reader, Self::Writer)> { 72 | let Some(remote) = remote else { 73 | return Err(anyhow!("Missing remote destination for reverse socks5")); 74 | }; 75 | 76 | match remote.protocol { 77 | LocalProtocol::Tcp { proxy_protocol: _ } => { 78 | let stream = protocols::tcp::connect_with_http_proxy( 79 | proxy, 80 | &remote.host, 81 | remote.port, 82 | self.so_mark, 83 | self.connect_timeout, 84 | self.dns_resolver, 85 | ) 86 | .await?; 87 | let (reader, writer) = stream.into_split(); 88 | Ok((Socks5Reader::Tcp(reader), Socks5Writer::Tcp(writer))) 89 | } 90 | _ => Err(anyhow!("Socks5 UDP cannot use http proxy to connect to destination")), 91 | } 92 | } 93 | } 94 | 95 | pub enum Socks5Reader { 96 | Tcp(OwnedReadHalf), 97 | Udp(WsUdpSocket), 98 | } 99 | 100 | impl AsyncRead for Socks5Reader { 101 | fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>) -> Poll> { 102 | match self.get_mut() { 103 | Socks5Reader::Tcp(reader) => Pin::new(reader).poll_read(cx, buf), 104 | Socks5Reader::Udp(reader) => Pin::new(reader).poll_read(cx, buf), 105 | } 106 | } 107 | } 108 | 109 | pub enum Socks5Writer { 110 | Tcp(OwnedWriteHalf), 111 | Udp(WsUdpSocket), 112 | } 113 | 114 | impl AsyncWrite for Socks5Writer { 115 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 116 | match self.get_mut() { 117 | Socks5Writer::Tcp(writer) => Pin::new(writer).poll_write(cx, buf), 118 | Socks5Writer::Udp(wrtier) => Pin::new(wrtier).poll_write(cx, buf), 119 | } 120 | } 121 | 122 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 123 | match self.get_mut() { 124 | Socks5Writer::Tcp(writer) => Pin::new(writer).poll_flush(cx), 125 | Socks5Writer::Udp(wrtier) => Pin::new(wrtier).poll_flush(cx), 126 | } 127 | } 128 | 129 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 130 | match self.get_mut() { 131 | Socks5Writer::Tcp(writer) => Pin::new(writer).poll_shutdown(cx), 132 | Socks5Writer::Udp(wrtier) => Pin::new(wrtier).poll_shutdown(cx), 133 | } 134 | } 135 | 136 | fn poll_write_vectored( 137 | self: Pin<&mut Self>, 138 | cx: &mut Context<'_>, 139 | bufs: &[IoSlice<'_>], 140 | ) -> Poll> { 141 | match self.get_mut() { 142 | Socks5Writer::Tcp(writer) => Pin::new(writer).poll_write_vectored(cx, bufs), 143 | Socks5Writer::Udp(wrtier) => Pin::new(wrtier).poll_write_vectored(cx, bufs), 144 | } 145 | } 146 | 147 | fn is_write_vectored(&self) -> bool { 148 | match self { 149 | Socks5Writer::Tcp(v) => v.is_write_vectored(), 150 | Socks5Writer::Udp(v) => v.is_write_vectored(), 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/connectors/tcp.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 4 | use url::{Host, Url}; 5 | 6 | use crate::protocols; 7 | use crate::protocols::dns::DnsResolver; 8 | use crate::somark::SoMark; 9 | use crate::tunnel::RemoteAddr; 10 | use crate::tunnel::connectors::TunnelConnector; 11 | 12 | pub struct TcpTunnelConnector<'a> { 13 | host: &'a Host, 14 | port: u16, 15 | so_mark: SoMark, 16 | connect_timeout: Duration, 17 | dns_resolver: &'a DnsResolver, 18 | } 19 | 20 | impl<'a> TcpTunnelConnector<'a> { 21 | pub fn new( 22 | host: &'a Host, 23 | port: u16, 24 | so_mark: SoMark, 25 | connect_timeout: Duration, 26 | dns_resolver: &'a DnsResolver, 27 | ) -> TcpTunnelConnector<'a> { 28 | TcpTunnelConnector { 29 | host, 30 | port, 31 | so_mark, 32 | connect_timeout, 33 | dns_resolver, 34 | } 35 | } 36 | } 37 | 38 | impl TunnelConnector for TcpTunnelConnector<'_> { 39 | type Reader = OwnedReadHalf; 40 | type Writer = OwnedWriteHalf; 41 | 42 | async fn connect(&self, remote: &Option) -> anyhow::Result<(Self::Reader, Self::Writer)> { 43 | let (host, port) = match remote { 44 | Some(remote) => (&remote.host, remote.port), 45 | None => (self.host, self.port), 46 | }; 47 | 48 | let stream = protocols::tcp::connect(host, port, self.so_mark, self.connect_timeout, self.dns_resolver).await?; 49 | Ok(stream.into_split()) 50 | } 51 | 52 | async fn connect_with_http_proxy( 53 | &self, 54 | proxy: &Url, 55 | remote: &Option, 56 | ) -> anyhow::Result<(Self::Reader, Self::Writer)> { 57 | let (host, port) = match remote { 58 | Some(remote) => (&remote.host, remote.port), 59 | None => (self.host, self.port), 60 | }; 61 | 62 | let stream = protocols::tcp::connect_with_http_proxy( 63 | proxy, 64 | host, 65 | port, 66 | self.so_mark, 67 | self.connect_timeout, 68 | self.dns_resolver, 69 | ) 70 | .await?; 71 | Ok(stream.into_split()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/connectors/udp.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use url::Host; 4 | 5 | use crate::protocols; 6 | use crate::protocols::dns::DnsResolver; 7 | use crate::protocols::udp::WsUdpSocket; 8 | use crate::somark::SoMark; 9 | use crate::tunnel::RemoteAddr; 10 | use crate::tunnel::connectors::TunnelConnector; 11 | 12 | pub struct UdpTunnelConnector<'a> { 13 | host: &'a Host, 14 | port: u16, 15 | so_mark: SoMark, 16 | connect_timeout: Duration, 17 | dns_resolver: &'a DnsResolver, 18 | } 19 | 20 | impl<'a> UdpTunnelConnector<'a> { 21 | pub fn new( 22 | host: &'a Host, 23 | port: u16, 24 | so_mark: SoMark, 25 | connect_timeout: Duration, 26 | dns_resolver: &'a DnsResolver, 27 | ) -> UdpTunnelConnector<'a> { 28 | UdpTunnelConnector { 29 | host, 30 | port, 31 | so_mark, 32 | connect_timeout, 33 | dns_resolver, 34 | } 35 | } 36 | } 37 | 38 | impl TunnelConnector for UdpTunnelConnector<'_> { 39 | type Reader = WsUdpSocket; 40 | type Writer = WsUdpSocket; 41 | 42 | async fn connect(&self, _: &Option) -> anyhow::Result<(Self::Reader, Self::Writer)> { 43 | let stream = 44 | protocols::udp::connect(self.host, self.port, self.connect_timeout, self.so_mark, self.dns_resolver) 45 | .await?; 46 | 47 | Ok((stream.clone(), stream)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/http_proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::http_proxy; 2 | use crate::protocols::http_proxy::HttpProxyListener; 3 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 4 | use anyhow::{Context, anyhow}; 5 | use std::net::SocketAddr; 6 | use std::pin::Pin; 7 | use std::task::{Poll, ready}; 8 | use std::time::Duration; 9 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 10 | use tokio_stream::Stream; 11 | 12 | pub struct HttpProxyTunnelListener { 13 | listener: HttpProxyListener, 14 | proxy_protocol: bool, 15 | } 16 | 17 | impl HttpProxyTunnelListener { 18 | pub async fn new( 19 | bind_addr: SocketAddr, 20 | timeout: Option, 21 | credentials: Option<(String, String)>, 22 | proxy_protocol: bool, 23 | ) -> anyhow::Result { 24 | let listener = http_proxy::run_server(bind_addr, timeout, credentials) 25 | .await 26 | .with_context(|| anyhow!("Cannot start http proxy server on {}", bind_addr))?; 27 | 28 | Ok(Self { 29 | listener, 30 | proxy_protocol, 31 | }) 32 | } 33 | } 34 | 35 | impl Stream for HttpProxyTunnelListener { 36 | type Item = anyhow::Result<((OwnedReadHalf, OwnedWriteHalf), RemoteAddr)>; 37 | 38 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 39 | let this = self.get_mut(); 40 | let ret = ready!(Pin::new(&mut this.listener).poll_next(cx)); 41 | let ret = match ret { 42 | Some(Ok((stream, (host, port)))) => { 43 | let protocol = LocalProtocol::Tcp { 44 | proxy_protocol: this.proxy_protocol, 45 | }; 46 | Some(anyhow::Ok((stream.into_split(), RemoteAddr { protocol, host, port }))) 47 | } 48 | Some(Err(err)) => Some(Err(err)), 49 | None => None, 50 | }; 51 | Poll::Ready(ret) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/mod.rs: -------------------------------------------------------------------------------- 1 | mod tcp; 2 | #[cfg(target_os = "linux")] 3 | mod tproxy; 4 | 5 | mod http_proxy; 6 | mod socks5; 7 | mod stdio; 8 | mod udp; 9 | #[cfg(unix)] 10 | mod unix_sock; 11 | 12 | #[cfg(target_os = "linux")] 13 | pub use tproxy::TproxyTcpTunnelListener; 14 | #[cfg(target_os = "linux")] 15 | pub use tproxy::new_tproxy_udp; 16 | 17 | pub use http_proxy::HttpProxyTunnelListener; 18 | pub use socks5::Socks5TunnelListener; 19 | pub use stdio::new_stdio_listener; 20 | pub use tcp::TcpTunnelListener; 21 | pub use udp::UdpTunnelListener; 22 | 23 | #[cfg(unix)] 24 | pub use unix_sock::UnixTunnelListener; 25 | 26 | use crate::tunnel::RemoteAddr; 27 | use tokio::io::{AsyncRead, AsyncWrite}; 28 | use tokio_stream::Stream; 29 | 30 | pub trait TunnelListener: Stream> { 31 | type Reader: AsyncRead + Send + 'static; 32 | type Writer: AsyncWrite + Send + 'static; 33 | } 34 | 35 | impl TunnelListener for T 36 | where 37 | T: Stream>, 38 | R: AsyncRead + Send + 'static, 39 | W: AsyncWrite + Send + 'static, 40 | { 41 | type Reader = R; 42 | type Writer = W; 43 | } 44 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/socks5.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::socks5; 2 | use crate::protocols::socks5::{Socks5Listener, Socks5ReadHalf, Socks5WriteHalf}; 3 | use crate::tunnel::RemoteAddr; 4 | use anyhow::{Context, anyhow}; 5 | use std::net::SocketAddr; 6 | use std::pin::Pin; 7 | use std::task::{Poll, ready}; 8 | use std::time::Duration; 9 | use tokio_stream::Stream; 10 | 11 | pub struct Socks5TunnelListener { 12 | listener: Socks5Listener, 13 | } 14 | 15 | impl Socks5TunnelListener { 16 | pub async fn new( 17 | bind_addr: SocketAddr, 18 | timeout: Option, 19 | credentials: Option<(String, String)>, 20 | ) -> anyhow::Result { 21 | let listener = socks5::run_server(bind_addr, timeout, credentials) 22 | .await 23 | .with_context(|| anyhow!("Cannot start Socks5 server on {}", bind_addr))?; 24 | 25 | Ok(Self { listener }) 26 | } 27 | } 28 | 29 | impl Stream for Socks5TunnelListener { 30 | type Item = anyhow::Result<((Socks5ReadHalf, Socks5WriteHalf), RemoteAddr)>; 31 | 32 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 33 | let this = self.get_mut(); 34 | let ret = ready!(Pin::new(&mut this.listener).poll_next(cx)); 35 | let ret = match ret { 36 | Some(Ok((stream, (host, port)))) => { 37 | let protocol = stream.local_protocol(); 38 | Some(anyhow::Ok((stream.into_split(), RemoteAddr { protocol, host, port }))) 39 | } 40 | Some(Err(err)) => Some(Err(err)), 41 | None => None, 42 | }; 43 | Poll::Ready(ret) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/stdio.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::stdio; 2 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 3 | use anyhow::{Context, anyhow}; 4 | use std::pin::Pin; 5 | use std::task::Poll; 6 | use tokio::io::{AsyncRead, AsyncWrite}; 7 | use tokio::sync::oneshot; 8 | use tokio_stream::Stream; 9 | use url::Host; 10 | 11 | pub struct StdioTunnelListener 12 | where 13 | R: AsyncRead + Send + 'static, 14 | W: AsyncWrite + Send + 'static, 15 | { 16 | listener: Option<(R, W)>, 17 | dest: (Host, u16), 18 | proxy_protocol: bool, 19 | } 20 | 21 | pub async fn new_stdio_listener( 22 | dest: (Host, u16), 23 | proxy_protocol: bool, 24 | ) -> anyhow::Result<( 25 | StdioTunnelListener, 26 | oneshot::Sender<()>, 27 | )> { 28 | let (listener, handle) = stdio::run_server() 29 | .await 30 | .with_context(|| anyhow!("Cannot start STDIO server"))?; 31 | Ok(( 32 | StdioTunnelListener { 33 | listener: Some(listener), 34 | proxy_protocol, 35 | dest, 36 | }, 37 | handle, 38 | )) 39 | } 40 | 41 | impl Stream for StdioTunnelListener 42 | where 43 | R: AsyncRead + Send + 'static, 44 | W: AsyncWrite + Send + 'static, 45 | { 46 | type Item = anyhow::Result<((R, W), RemoteAddr)>; 47 | 48 | fn poll_next(self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll> { 49 | let this = unsafe { self.get_unchecked_mut() }; 50 | let ret = match this.listener.take() { 51 | None => None, 52 | Some(stream) => { 53 | let (host, port) = this.dest.clone(); 54 | Some(Ok(( 55 | stream, 56 | RemoteAddr { 57 | protocol: LocalProtocol::Tcp { 58 | proxy_protocol: this.proxy_protocol, 59 | }, 60 | host, 61 | port, 62 | }, 63 | ))) 64 | } 65 | }; 66 | 67 | Poll::Ready(ret) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/tcp.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols; 2 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 3 | use anyhow::{Context, anyhow}; 4 | use std::net::SocketAddr; 5 | use std::pin::Pin; 6 | use std::task::{Poll, ready}; 7 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 8 | use tokio_stream::Stream; 9 | use tokio_stream::wrappers::TcpListenerStream; 10 | use url::Host; 11 | 12 | pub struct TcpTunnelListener { 13 | listener: TcpListenerStream, 14 | dest: (Host, u16), 15 | proxy_protocol: bool, 16 | } 17 | 18 | impl TcpTunnelListener { 19 | pub async fn new(bind_addr: SocketAddr, dest: (Host, u16), proxy_protocol: bool) -> anyhow::Result { 20 | let listener = protocols::tcp::run_server(bind_addr, false) 21 | .await 22 | .with_context(|| anyhow!("Cannot start TCP server on {}", bind_addr))?; 23 | 24 | Ok(Self { 25 | listener, 26 | dest, 27 | proxy_protocol, 28 | }) 29 | } 30 | } 31 | 32 | impl Stream for TcpTunnelListener { 33 | type Item = anyhow::Result<((OwnedReadHalf, OwnedWriteHalf), RemoteAddr)>; 34 | 35 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 36 | let this = self.get_mut(); 37 | let ret = ready!(Pin::new(&mut this.listener).poll_next(cx)); 38 | let ret = match ret { 39 | Some(Ok(strean)) => { 40 | let (host, port) = this.dest.clone(); 41 | Some(anyhow::Ok(( 42 | strean.into_split(), 43 | RemoteAddr { 44 | protocol: LocalProtocol::Tcp { 45 | proxy_protocol: this.proxy_protocol, 46 | }, 47 | host, 48 | port, 49 | }, 50 | ))) 51 | } 52 | Some(Err(err)) => Some(Err(anyhow::Error::new(err))), 53 | None => None, 54 | }; 55 | Poll::Ready(ret) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/tproxy.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols; 2 | use crate::protocols::udp; 3 | use crate::protocols::udp::{UdpStream, UdpStreamWriter}; 4 | use crate::tunnel::{LocalProtocol, RemoteAddr, to_host_port}; 5 | use anyhow::{Context, anyhow}; 6 | use std::io; 7 | use std::net::SocketAddr; 8 | use std::pin::Pin; 9 | use std::task::{Poll, ready}; 10 | use std::time::Duration; 11 | use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf}; 12 | use tokio_stream::Stream; 13 | use tokio_stream::wrappers::TcpListenerStream; 14 | 15 | pub struct TproxyTcpTunnelListener { 16 | listener: TcpListenerStream, 17 | proxy_protocol: bool, 18 | } 19 | 20 | impl TproxyTcpTunnelListener { 21 | pub async fn new(bind_addr: SocketAddr, proxy_protocol: bool) -> anyhow::Result { 22 | let listener = protocols::tcp::run_server(bind_addr, true) 23 | .await 24 | .with_context(|| anyhow!("Cannot start TProxy TCP server on {}", bind_addr))?; 25 | 26 | Ok(Self { 27 | listener, 28 | proxy_protocol, 29 | }) 30 | } 31 | } 32 | 33 | impl Stream for TproxyTcpTunnelListener { 34 | type Item = anyhow::Result<((OwnedReadHalf, OwnedWriteHalf), RemoteAddr)>; 35 | 36 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 37 | let this = self.get_mut(); 38 | let ret = ready!(Pin::new(&mut this.listener).poll_next(cx)); 39 | let ret = match ret { 40 | Some(Ok(stream)) => { 41 | let (host, port) = to_host_port(stream.local_addr().unwrap()); 42 | Some(anyhow::Ok(( 43 | stream.into_split(), 44 | RemoteAddr { 45 | protocol: LocalProtocol::Tcp { 46 | proxy_protocol: this.proxy_protocol, 47 | }, 48 | host, 49 | port, 50 | }, 51 | ))) 52 | } 53 | Some(Err(err)) => Some(Err(anyhow::Error::new(err))), 54 | None => None, 55 | }; 56 | Poll::Ready(ret) 57 | } 58 | } 59 | 60 | // TPROXY UDP 61 | pub struct TProxyUdpTunnelListener 62 | where 63 | S: Stream>, 64 | { 65 | listener: S, 66 | timeout: Option, 67 | } 68 | 69 | pub async fn new_tproxy_udp( 70 | bind_addr: SocketAddr, 71 | timeout: Option, 72 | ) -> anyhow::Result>>> { 73 | let listener = udp::run_server(bind_addr, timeout, udp::configure_tproxy, udp::mk_send_socket_tproxy) 74 | .await 75 | .with_context(|| anyhow!("Cannot start TProxy UDP server on {}", bind_addr))?; 76 | 77 | Ok(TProxyUdpTunnelListener { listener, timeout }) 78 | } 79 | 80 | impl Stream for TProxyUdpTunnelListener 81 | where 82 | S: Stream>, 83 | { 84 | type Item = anyhow::Result<((UdpStream, UdpStreamWriter), RemoteAddr)>; 85 | 86 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 87 | let this = unsafe { self.get_unchecked_mut() }; 88 | let ret = ready!(unsafe { Pin::new_unchecked(&mut this.listener) }.poll_next(cx)); 89 | let ret = match ret { 90 | Some(Ok(stream)) => { 91 | let (host, port) = to_host_port(stream.local_addr().unwrap()); 92 | let stream_writer = stream.writer(); 93 | Some(anyhow::Ok(( 94 | (stream, stream_writer), 95 | RemoteAddr { 96 | protocol: LocalProtocol::Udp { timeout: this.timeout }, 97 | host, 98 | port, 99 | }, 100 | ))) 101 | } 102 | Some(Err(err)) => Some(Err(anyhow::Error::new(err))), 103 | None => None, 104 | }; 105 | Poll::Ready(ret) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/udp.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::udp; 2 | use crate::protocols::udp::{UdpStream, UdpStreamWriter}; 3 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 4 | use anyhow::{Context, anyhow}; 5 | use std::io; 6 | use std::net::SocketAddr; 7 | use std::pin::Pin; 8 | use std::task::{Poll, ready}; 9 | use std::time::Duration; 10 | use tokio_stream::Stream; 11 | use url::Host; 12 | 13 | pub struct UdpTunnelListener { 14 | listener: Pin> + Send>>, 15 | dest: (Host, u16), 16 | timeout: Option, 17 | } 18 | 19 | impl UdpTunnelListener { 20 | pub async fn new( 21 | bind_addr: SocketAddr, 22 | dest: (Host, u16), 23 | timeout: Option, 24 | ) -> anyhow::Result { 25 | let listener = udp::run_server(bind_addr, timeout, |_| Ok(()), |s| Ok(s.clone())) 26 | .await 27 | .with_context(|| anyhow!("Cannot start UDP server on {}", bind_addr))?; 28 | 29 | Ok(UdpTunnelListener { 30 | listener: Box::pin(listener), 31 | dest, 32 | timeout, 33 | }) 34 | } 35 | } 36 | 37 | impl Stream for UdpTunnelListener { 38 | type Item = anyhow::Result<((UdpStream, UdpStreamWriter), RemoteAddr)>; 39 | 40 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 41 | let this = unsafe { self.get_unchecked_mut() }; 42 | let ret = ready!(unsafe { Pin::new_unchecked(&mut this.listener) }.poll_next(cx)); 43 | let ret = match ret { 44 | Some(Ok(stream)) => { 45 | let (host, port) = this.dest.clone(); 46 | let stream_writer = stream.writer(); 47 | Some(anyhow::Ok(( 48 | (stream, stream_writer), 49 | RemoteAddr { 50 | protocol: LocalProtocol::Udp { timeout: this.timeout }, 51 | host, 52 | port, 53 | }, 54 | ))) 55 | } 56 | Some(Err(err)) => Some(Err(anyhow::Error::new(err))), 57 | None => None, 58 | }; 59 | Poll::Ready(ret) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/listeners/unix_sock.rs: -------------------------------------------------------------------------------- 1 | use crate::protocols::unix_sock; 2 | use crate::protocols::unix_sock::UnixListenerStream; 3 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 4 | use anyhow::{Context, anyhow}; 5 | use std::path::Path; 6 | use std::pin::Pin; 7 | use std::task::{Poll, ready}; 8 | use tokio::net::unix; 9 | use tokio_stream::Stream; 10 | use url::Host; 11 | 12 | pub struct UnixTunnelListener { 13 | listener: UnixListenerStream, 14 | dest: (Host, u16), 15 | proxy_protocol: bool, 16 | } 17 | 18 | impl UnixTunnelListener { 19 | pub async fn new(path: &Path, dest: (Host, u16), proxy_protocol: bool) -> anyhow::Result { 20 | let listener = unix_sock::run_server(path) 21 | .await 22 | .with_context(|| anyhow!("Cannot start Unix domain server on {}", path.display()))?; 23 | 24 | Ok(Self { 25 | listener, 26 | dest, 27 | proxy_protocol, 28 | }) 29 | } 30 | } 31 | impl Stream for UnixTunnelListener { 32 | type Item = anyhow::Result<((unix::OwnedReadHalf, unix::OwnedWriteHalf), RemoteAddr)>; 33 | 34 | fn poll_next(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll> { 35 | let this = self.get_mut(); 36 | let ret = ready!(Pin::new(&mut this.listener).poll_next(cx)); 37 | let ret = match ret { 38 | Some(Ok(stream)) => { 39 | let stream = stream.into_split(); 40 | let (host, port) = this.dest.clone(); 41 | Some(anyhow::Ok(( 42 | stream, 43 | RemoteAddr { 44 | protocol: LocalProtocol::Tcp { 45 | proxy_protocol: this.proxy_protocol, 46 | }, 47 | host, 48 | port, 49 | }, 50 | ))) 51 | } 52 | Some(Err(err)) => Some(Err(anyhow::Error::new(err))), 53 | None => None, 54 | }; 55 | Poll::Ready(ret) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod connectors; 3 | pub mod listeners; 4 | pub mod server; 5 | mod tls_reloader; 6 | pub mod transport; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | use std::fmt::Debug; 10 | use std::net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}; 11 | use std::path::PathBuf; 12 | use std::time::Duration; 13 | use url::Host; 14 | 15 | #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] 16 | pub enum LocalProtocol { 17 | Tcp { 18 | proxy_protocol: bool, 19 | }, 20 | Udp { 21 | timeout: Option, 22 | }, 23 | Stdio { 24 | proxy_protocol: bool, 25 | }, 26 | Socks5 { 27 | timeout: Option, 28 | credentials: Option<(String, String)>, 29 | }, 30 | TProxyTcp, 31 | TProxyUdp { 32 | timeout: Option, 33 | }, 34 | HttpProxy { 35 | timeout: Option, 36 | credentials: Option<(String, String)>, 37 | proxy_protocol: bool, 38 | }, 39 | ReverseTcp, 40 | ReverseUdp { 41 | timeout: Option, 42 | }, 43 | ReverseSocks5 { 44 | timeout: Option, 45 | credentials: Option<(String, String)>, 46 | }, 47 | ReverseHttpProxy { 48 | timeout: Option, 49 | credentials: Option<(String, String)>, 50 | }, 51 | ReverseUnix { 52 | path: PathBuf, 53 | }, 54 | Unix { 55 | path: PathBuf, 56 | proxy_protocol: bool, 57 | }, 58 | } 59 | 60 | impl LocalProtocol { 61 | pub const fn is_reverse_tunnel(&self) -> bool { 62 | matches!( 63 | self, 64 | Self::ReverseTcp 65 | | Self::ReverseUdp { .. } 66 | | Self::ReverseSocks5 { .. } 67 | | Self::ReverseUnix { .. } 68 | | Self::ReverseHttpProxy { .. } 69 | ) 70 | } 71 | 72 | pub const fn is_dynamic_reverse_tunnel(&self) -> bool { 73 | matches!(self, Self::ReverseSocks5 { .. } | Self::ReverseHttpProxy { .. }) 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | pub struct RemoteAddr { 79 | pub protocol: LocalProtocol, 80 | pub host: Host, 81 | pub port: u16, 82 | } 83 | 84 | pub fn to_host_port(addr: SocketAddr) -> (Host, u16) { 85 | match addr.ip() { 86 | IpAddr::V4(ip) => (Host::Ipv4(ip), addr.port()), 87 | IpAddr::V6(ip) => (Host::Ipv6(ip), addr.port()), 88 | } 89 | } 90 | 91 | pub fn try_to_sock_addr((host, port): (Host, u16)) -> anyhow::Result { 92 | match host { 93 | Host::Domain(_) => Err(anyhow::anyhow!("Cannot convert domain to socket address")), 94 | Host::Ipv4(ip) => Ok(SocketAddr::V4(SocketAddrV4::new(ip, port))), 95 | Host::Ipv6(ip) => Ok(SocketAddr::V6(SocketAddrV6::new(ip, port, 0, 0))), 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/server/handler_http2.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::TokioExecutorRef; 2 | use crate::restrictions::types::RestrictionsRules; 3 | use crate::tunnel::server::WsServer; 4 | use crate::tunnel::server::utils::{HttpResponse, bad_request, inject_cookie}; 5 | use crate::tunnel::transport; 6 | use crate::tunnel::transport::http2::{Http2TunnelRead, Http2TunnelWrite}; 7 | use bytes::Bytes; 8 | use futures_util::StreamExt; 9 | use http_body_util::combinators::BoxBody; 10 | use http_body_util::{BodyStream, Either, StreamBody}; 11 | use hyper::body::{Frame, Incoming}; 12 | use hyper::header::CONTENT_TYPE; 13 | use hyper::{Request, Response, StatusCode}; 14 | use std::net::SocketAddr; 15 | use std::sync::Arc; 16 | use tokio::sync::{mpsc, oneshot}; 17 | use tokio_stream::wrappers::ReceiverStream; 18 | use tracing::{Instrument, Span}; 19 | 20 | pub(super) async fn http_server_upgrade( 21 | server: WsServer, 22 | restrictions: Arc, 23 | restrict_path_prefix: Option, 24 | client_addr: SocketAddr, 25 | mut req: Request, 26 | ) -> HttpResponse { 27 | let (remote_addr, local_rx, local_tx, need_cookie) = match server 28 | .handle_tunnel_request(restrictions, restrict_path_prefix, client_addr, &req) 29 | .await 30 | { 31 | Ok(ret) => ret, 32 | Err(err) => return err, 33 | }; 34 | 35 | let req_content_type = req.headers_mut().remove(CONTENT_TYPE); 36 | let ws_rx = BodyStream::new(req.into_body()); 37 | let (ws_tx, rx) = mpsc::channel::(1024); 38 | let body = BoxBody::new(StreamBody::new( 39 | ReceiverStream::new(rx).map(|s| -> anyhow::Result> { Ok(Frame::data(s)) }), 40 | )); 41 | 42 | let mut response = Response::builder() 43 | .status(StatusCode::OK) 44 | .body(Either::Right(body)) 45 | .expect("bug: failed to build response"); 46 | 47 | let (close_tx, close_rx) = oneshot::channel::<()>(); 48 | server.executor.spawn( 49 | transport::io::propagate_remote_to_local(local_tx, Http2TunnelRead::new(ws_rx, None), close_rx) 50 | .instrument(Span::current()), 51 | ); 52 | 53 | server.executor.spawn( 54 | transport::io::propagate_local_to_remote(local_rx, Http2TunnelWrite::new(ws_tx), close_tx, None) 55 | .instrument(Span::current()), 56 | ); 57 | 58 | if need_cookie && inject_cookie(&mut response, &remote_addr).is_err() { 59 | return bad_request(); 60 | } 61 | 62 | if let Some(content_type) = req_content_type { 63 | response.headers_mut().insert(CONTENT_TYPE, content_type); 64 | } 65 | 66 | response 67 | } 68 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/server/handler_websocket.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::TokioExecutorRef; 2 | use crate::restrictions::types::RestrictionsRules; 3 | use crate::tunnel::server::WsServer; 4 | use crate::tunnel::server::utils::{HttpResponse, bad_request, inject_cookie}; 5 | use crate::tunnel::transport; 6 | use crate::tunnel::transport::websocket::mk_websocket_tunnel; 7 | use fastwebsockets::Role; 8 | use http_body_util::Either; 9 | use http_body_util::combinators::BoxBody; 10 | use hyper::body::Incoming; 11 | use hyper::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL}; 12 | use hyper::{Request, Response}; 13 | use std::net::SocketAddr; 14 | use std::sync::Arc; 15 | use tokio::sync::oneshot; 16 | use tracing::{Instrument, Span, error, warn}; 17 | 18 | pub(super) async fn ws_server_upgrade( 19 | server: WsServer, 20 | restrictions: Arc, 21 | restrict_path_prefix: Option, 22 | client_addr: SocketAddr, 23 | mut req: Request, 24 | ) -> HttpResponse { 25 | if !fastwebsockets::upgrade::is_upgrade_request(&req) { 26 | warn!("Rejecting connection with bad upgrade request: {}", req.uri()); 27 | return bad_request(); 28 | } 29 | 30 | let mask_frame = server.config.websocket_mask_frame; 31 | let (remote_addr, local_rx, local_tx, need_cookie) = match server 32 | .handle_tunnel_request(restrictions, restrict_path_prefix, client_addr, &req) 33 | .await 34 | { 35 | Ok(ret) => ret, 36 | Err(err) => return err, 37 | }; 38 | 39 | let (response, fut) = match fastwebsockets::upgrade::upgrade(&mut req) { 40 | Ok(ret) => ret, 41 | Err(err) => { 42 | warn!("Rejecting connection with bad upgrade request: {} {}", err, req.uri()); 43 | return bad_request(); 44 | } 45 | }; 46 | 47 | let executor = server.executor.clone(); 48 | server.executor.spawn( 49 | async move { 50 | let (ws_rx, ws_tx) = match fut.await { 51 | Ok(ws) => match mk_websocket_tunnel(ws, Role::Server, mask_frame) { 52 | Ok(ws) => ws, 53 | Err(err) => { 54 | error!("Error during http upgrade request: {:?}", err); 55 | return Err(err); 56 | } 57 | }, 58 | Err(err) => { 59 | error!("Error during http upgrade request: {:?}", err); 60 | return Err(anyhow::Error::from(err)); 61 | } 62 | }; 63 | let (close_tx, close_rx) = oneshot::channel::<()>(); 64 | 65 | executor 66 | .spawn(transport::io::propagate_remote_to_local(local_tx, ws_rx, close_rx).instrument(Span::current())); 67 | 68 | let _ = transport::io::propagate_local_to_remote( 69 | local_rx, 70 | ws_tx, 71 | close_tx, 72 | server.config.websocket_ping_frequency, 73 | ) 74 | .await; 75 | Ok(()) 76 | } 77 | .instrument(Span::current()), 78 | ); 79 | 80 | let mut response = Response::from_parts(response.into_parts().0, Either::Right(BoxBody::default())); 81 | if need_cookie && inject_cookie(&mut response, &remote_addr).is_err() { 82 | return bad_request(); 83 | } 84 | 85 | response 86 | .headers_mut() 87 | .insert(SEC_WEBSOCKET_PROTOCOL, HeaderValue::from_static("v1")); 88 | 89 | response 90 | } 91 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/server/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | mod handler_http2; 3 | mod handler_websocket; 4 | mod reverse_tunnel; 5 | mod server; 6 | mod utils; 7 | 8 | pub use server::TlsServerConfig; 9 | pub use server::WsServer; 10 | pub use server::WsServerConfig; 11 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/server/reverse_tunnel.rs: -------------------------------------------------------------------------------- 1 | use crate::executor::TokioExecutorRef; 2 | use crate::tunnel::RemoteAddr; 3 | use crate::tunnel::listeners::TunnelListener; 4 | use ahash::AHashMap; 5 | use anyhow::anyhow; 6 | use futures_util::{StreamExt, pin_mut}; 7 | use log::warn; 8 | use parking_lot::Mutex; 9 | use std::future::Future; 10 | use std::net::SocketAddr; 11 | use std::sync::Arc; 12 | use std::sync::atomic::{AtomicUsize, Ordering}; 13 | use std::time::Duration; 14 | use tokio::task::AbortHandle; 15 | use tokio::{select, time}; 16 | use tracing::{Instrument, Span, info}; 17 | 18 | struct ReverseTunnelItem { 19 | #[allow(clippy::type_complexity)] 20 | receiver: async_channel::Receiver<((::Reader, ::Writer), RemoteAddr)>, 21 | nb_seen_clients: Arc, 22 | server_task: AbortHandle, 23 | } 24 | 25 | impl ReverseTunnelItem { 26 | #[allow(clippy::type_complexity)] 27 | pub fn get_cnx_awaiter( 28 | &self, 29 | ) -> async_channel::Receiver<((::Reader, ::Writer), RemoteAddr)> { 30 | self.nb_seen_clients.fetch_add(1, Ordering::Relaxed); 31 | self.receiver.clone() 32 | } 33 | } 34 | 35 | impl Drop for ReverseTunnelItem { 36 | fn drop(&mut self) { 37 | self.server_task.abort(); 38 | } 39 | } 40 | 41 | pub struct ReverseTunnelServer { 42 | servers: Arc>>>, 43 | } 44 | 45 | impl ReverseTunnelServer { 46 | pub fn new() -> Self { 47 | Self { 48 | servers: Arc::new(Mutex::new(AHashMap::with_capacity(1))), 49 | } 50 | } 51 | 52 | pub async fn run_listening_server( 53 | &self, 54 | executor: &impl TokioExecutorRef, 55 | bind_addr: SocketAddr, 56 | idle_timeout: Duration, 57 | gen_listening_server: impl Future>, 58 | ) -> anyhow::Result<((::Reader, ::Writer), RemoteAddr)> 59 | where 60 | T: TunnelListener + Send + 'static, 61 | { 62 | let listening_server = self 63 | .servers 64 | .lock() 65 | .get(&bind_addr) 66 | .map(|server| server.get_cnx_awaiter()); 67 | let cnx = if let Some(listening_server) = listening_server { 68 | listening_server 69 | } else { 70 | let listening_server = gen_listening_server.await?; 71 | let (tx, rx) = async_channel::bounded(10); 72 | let nb_seen_clients = Arc::new(AtomicUsize::new(0)); 73 | let seen_clients = nb_seen_clients.clone(); 74 | let server = self.servers.clone(); 75 | let local_srv2 = bind_addr; 76 | 77 | let fut = async move { 78 | scopeguard::defer!({ 79 | server.lock().remove(&local_srv2); 80 | }); 81 | 82 | let mut timer = time::interval(idle_timeout); 83 | pin_mut!(listening_server); 84 | loop { 85 | select! { 86 | biased; 87 | cnx = listening_server.next() => { 88 | match cnx { 89 | None => break, 90 | Some(Err(err)) => { 91 | warn!("Error while listening for incoming connections {err:?}"); 92 | continue; 93 | } 94 | Some(Ok(cnx)) => { 95 | if time::timeout(idle_timeout, tx.send(cnx)).await.is_err() { 96 | info!("New reverse connection failed to be picked by client after {}s. Closing reverse tunnel server", idle_timeout.as_secs()); 97 | break; 98 | } 99 | } 100 | } 101 | }, 102 | _ = timer.tick() => { 103 | 104 | // if no client connected to the reverse tunnel server, close it 105 | // <= 1 because the server itself has a receiver 106 | if seen_clients.swap(0, Ordering::Relaxed) == 0 && tx.receiver_count() <= 1 { 107 | info!("No client connected to reverse tunnel server for {}s. Closing reverse tunnel server", idle_timeout.as_secs()); 108 | break; 109 | } 110 | }, 111 | } 112 | } 113 | info!("Stopping listening reverse server"); 114 | }.instrument(Span::current()); 115 | 116 | let item = ReverseTunnelItem { 117 | receiver: rx, 118 | nb_seen_clients, 119 | server_task: executor.spawn(fut), 120 | }; 121 | let cnx_awaiter = item.get_cnx_awaiter(); 122 | self.servers.lock().insert(bind_addr, item); 123 | cnx_awaiter 124 | }; 125 | 126 | let cnx = cnx 127 | .recv() 128 | .await 129 | .map_err(|_| anyhow!("listening reverse server stopped"))?; 130 | Ok(cnx) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/transport/http2.rs: -------------------------------------------------------------------------------- 1 | use super::io::{MAX_PACKET_LENGTH, TunnelRead, TunnelWrite}; 2 | use crate::tunnel::RemoteAddr; 3 | use crate::tunnel::client::WsClient; 4 | use crate::tunnel::transport::jwt::tunnel_to_jwt_token; 5 | use crate::tunnel::transport::{TransportScheme, headers_from_file}; 6 | use anyhow::{Context, anyhow}; 7 | use bytes::{Bytes, BytesMut}; 8 | use http_body_util::{BodyExt, BodyStream, StreamBody}; 9 | use hyper::Request; 10 | use hyper::body::{Frame, Incoming}; 11 | use hyper::header::{AUTHORIZATION, CONTENT_TYPE, COOKIE}; 12 | use hyper::http::response::Parts; 13 | use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer}; 14 | use log::{debug, error, warn}; 15 | use std::future::Future; 16 | use std::io; 17 | use std::io::ErrorKind; 18 | use std::ops::DerefMut; 19 | use std::sync::Arc; 20 | use std::time::Duration; 21 | use tokio::io::{AsyncWrite, AsyncWriteExt}; 22 | use tokio::sync::{Notify, mpsc}; 23 | use tokio::task::AbortHandle; 24 | use tokio_stream::StreamExt; 25 | use tokio_stream::wrappers::ReceiverStream; 26 | use uuid::Uuid; 27 | 28 | pub struct Http2TunnelRead { 29 | inner: BodyStream, 30 | cnx_poller: Option, 31 | } 32 | 33 | impl Http2TunnelRead { 34 | pub const fn new(inner: BodyStream, cnx_poller: Option) -> Self { 35 | Self { inner, cnx_poller } 36 | } 37 | } 38 | 39 | impl Drop for Http2TunnelRead { 40 | fn drop(&mut self) { 41 | if let Some(t) = self.cnx_poller.as_ref() { 42 | t.abort() 43 | } 44 | } 45 | } 46 | 47 | impl TunnelRead for Http2TunnelRead { 48 | async fn copy(&mut self, mut writer: impl AsyncWrite + Unpin + Send) -> Result<(), io::Error> { 49 | loop { 50 | match self.inner.next().await { 51 | Some(Ok(frame)) => match frame.into_data() { 52 | Ok(data) => { 53 | return match writer.write_all(data.as_ref()).await { 54 | Ok(_) => Ok(()), 55 | Err(err) => Err(io::Error::new(ErrorKind::ConnectionAborted, err)), 56 | }; 57 | } 58 | Err(err) => { 59 | warn!("{:?}", err); 60 | continue; 61 | } 62 | }, 63 | Some(Err(err)) => { 64 | return Err(io::Error::new(ErrorKind::ConnectionAborted, err)); 65 | } 66 | None => return Err(io::Error::new(ErrorKind::BrokenPipe, "closed")), 67 | } 68 | } 69 | } 70 | } 71 | 72 | pub struct Http2TunnelWrite { 73 | inner: mpsc::Sender, 74 | buf: BytesMut, 75 | } 76 | 77 | impl Http2TunnelWrite { 78 | pub fn new(inner: mpsc::Sender) -> Self { 79 | Self { 80 | inner, 81 | buf: BytesMut::with_capacity(MAX_PACKET_LENGTH * 20), // ~ 1Mb 82 | } 83 | } 84 | } 85 | 86 | impl TunnelWrite for Http2TunnelWrite { 87 | fn buf_mut(&mut self) -> &mut BytesMut { 88 | &mut self.buf 89 | } 90 | 91 | async fn write(&mut self) -> Result<(), io::Error> { 92 | let data = self.buf.split().freeze(); 93 | let ret = match self.inner.send(data).await { 94 | Ok(_) => Ok(()), 95 | Err(err) => Err(io::Error::new(ErrorKind::ConnectionAborted, err)), 96 | }; 97 | 98 | if self.buf.capacity() < MAX_PACKET_LENGTH { 99 | //info!("read {} Kb {} Kb", self.buf.capacity() / 1024, old_capa / 1024); 100 | self.buf.reserve(MAX_PACKET_LENGTH) 101 | } 102 | 103 | ret 104 | } 105 | 106 | async fn ping(&mut self) -> Result<(), io::Error> { 107 | Ok(()) 108 | } 109 | 110 | async fn close(&mut self) -> Result<(), io::Error> { 111 | Ok(()) 112 | } 113 | 114 | fn pending_operations_notify(&mut self) -> Arc { 115 | Arc::new(Notify::new()) 116 | } 117 | 118 | fn handle_pending_operations(&mut self) -> impl Future> + Send { 119 | std::future::ready(Ok(())) 120 | } 121 | } 122 | 123 | pub async fn connect( 124 | request_id: Uuid, 125 | client: &WsClient, 126 | dest_addr: &RemoteAddr, 127 | ) -> anyhow::Result<(Http2TunnelRead, Http2TunnelWrite, Parts)> { 128 | let mut pooled_cnx = match client.cnx_pool.get().await { 129 | Ok(cnx) => Ok(cnx), 130 | Err(err) => Err(anyhow!("failed to get a connection to the server from the pool: {err:?}")), 131 | }?; 132 | 133 | // In http2 HOST header does not exist, it is explicitly set in the authority from the request uri 134 | let (headers_file, authority) = 135 | client 136 | .config 137 | .http_headers_file 138 | .as_ref() 139 | .map_or((None, None), |headers_file_path| { 140 | let (host, headers) = headers_from_file(headers_file_path); 141 | let host = if let Some((_, v)) = host { 142 | match (client.config.remote_addr.scheme(), client.config.remote_addr.port()) { 143 | (TransportScheme::Http, 80) | (TransportScheme::Https, 443) => { 144 | Some(v.to_str().unwrap_or("").to_string()) 145 | } 146 | (_, port) => Some(format!("{}:{}", v.to_str().unwrap_or(""), port)), 147 | } 148 | } else { 149 | None 150 | }; 151 | 152 | (Some(headers), host) 153 | }); 154 | 155 | let mut req = Request::builder() 156 | .method("POST") 157 | .uri(format!( 158 | "{}://{}/{}/events", 159 | client.config.remote_addr.scheme(), 160 | authority 161 | .as_deref() 162 | .unwrap_or_else(|| client.config.http_header_host.to_str().unwrap_or("")), 163 | &client.config.http_upgrade_path_prefix 164 | )) 165 | .header(COOKIE, tunnel_to_jwt_token(request_id, dest_addr)) 166 | .header(CONTENT_TYPE, "application/json") 167 | .version(hyper::Version::HTTP_2); 168 | 169 | let headers = match req.headers_mut() { 170 | Some(h) => h, 171 | None => { 172 | return Err(anyhow!( 173 | "failed to build HTTP request to contact the server {:?}. Most likely path_prefix `{}` or http headers is not valid", 174 | req, 175 | client.config.http_upgrade_path_prefix 176 | )); 177 | } 178 | }; 179 | for (k, v) in &client.config.http_headers { 180 | let _ = headers.remove(k); 181 | headers.append(k, v.clone()); 182 | } 183 | 184 | if let Some(auth) = &client.config.http_upgrade_credentials { 185 | let _ = headers.remove(AUTHORIZATION); 186 | headers.append(AUTHORIZATION, auth.clone()); 187 | } 188 | 189 | if let Some(headers_file) = headers_file { 190 | for (k, v) in headers_file { 191 | let _ = headers.remove(&k); 192 | headers.append(k, v); 193 | } 194 | } 195 | 196 | let (tx, rx) = mpsc::channel::(1024); 197 | let body = StreamBody::new(ReceiverStream::new(rx).map(|s| -> anyhow::Result> { Ok(Frame::data(s)) })); 198 | let req = req.body(body).with_context(|| { 199 | format!( 200 | "failed to build HTTP request to contact the server {:?}", 201 | client.config.remote_addr 202 | ) 203 | })?; 204 | debug!("with HTTP upgrade request {:?}", req); 205 | let transport = pooled_cnx.deref_mut().take().unwrap(); 206 | let (mut request_sender, cnx) = hyper::client::conn::http2::Builder::new(TokioExecutor::new()) 207 | .timer(TokioTimer::new()) 208 | .adaptive_window(true) 209 | .keep_alive_interval(client.config.websocket_ping_frequency) 210 | .keep_alive_timeout(Duration::from_secs(10)) 211 | .keep_alive_while_idle(false) 212 | .handshake(TokioIo::new(transport)) 213 | .await 214 | .with_context(|| format!("failed to do http2 handshake with the server {:?}", client.config.remote_addr))?; 215 | let cnx_poller = client.executor.spawn(async move { 216 | if let Err(err) = cnx.await { 217 | error!("{:?}", err) 218 | } 219 | }); 220 | 221 | let response = request_sender 222 | .send_request(req) 223 | .await 224 | .with_context(|| format!("failed to send http2 request with the server {:?}", client.config.remote_addr))?; 225 | 226 | if !response.status().is_success() { 227 | return Err(anyhow!( 228 | "Http2 server rejected the connection: {:?}: {:?}", 229 | response.status(), 230 | String::from_utf8(response.into_body().collect().await?.to_bytes().to_vec()).unwrap_or_default() 231 | )); 232 | } 233 | 234 | let (parts, body) = response.into_parts(); 235 | Ok(( 236 | Http2TunnelRead::new(BodyStream::new(body), Some(cnx_poller)), 237 | Http2TunnelWrite::new(tx), 238 | parts, 239 | )) 240 | } 241 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/transport/io.rs: -------------------------------------------------------------------------------- 1 | use crate::tunnel::transport::http2::{Http2TunnelRead, Http2TunnelWrite}; 2 | use crate::tunnel::transport::websocket::{WebsocketTunnelRead, WebsocketTunnelWrite}; 3 | use bytes::{BufMut, BytesMut}; 4 | use futures_util::{FutureExt, pin_mut}; 5 | use std::future::Future; 6 | use std::io::ErrorKind; 7 | use std::pin::Pin; 8 | use std::sync::Arc; 9 | use std::time::Duration; 10 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite}; 11 | use tokio::select; 12 | use tokio::sync::{Notify, oneshot}; 13 | use tokio::time::Instant; 14 | use tracing::log::debug; 15 | use tracing::{error, info, warn}; 16 | 17 | pub(super) static MAX_PACKET_LENGTH: usize = 64 * 1024; 18 | 19 | pub trait TunnelWrite: Send + 'static { 20 | fn buf_mut(&mut self) -> &mut BytesMut; 21 | fn write(&mut self) -> impl Future> + Send; 22 | fn ping(&mut self) -> impl Future> + Send; 23 | fn close(&mut self) -> impl Future> + Send; 24 | fn pending_operations_notify(&mut self) -> Arc; 25 | fn handle_pending_operations(&mut self) -> impl Future> + Send; 26 | } 27 | 28 | pub trait TunnelRead: Send + 'static { 29 | fn copy( 30 | &mut self, 31 | writer: impl AsyncWrite + Unpin + Send, 32 | ) -> impl Future> + Send; 33 | } 34 | 35 | pub enum TunnelReader { 36 | Websocket(WebsocketTunnelRead), 37 | Http2(Http2TunnelRead), 38 | } 39 | 40 | impl TunnelRead for TunnelReader { 41 | async fn copy(&mut self, writer: impl AsyncWrite + Unpin + Send) -> Result<(), std::io::Error> { 42 | match self { 43 | Self::Websocket(s) => s.copy(writer).await, 44 | Self::Http2(s) => s.copy(writer).await, 45 | } 46 | } 47 | } 48 | 49 | pub enum TunnelWriter { 50 | Websocket(WebsocketTunnelWrite), 51 | Http2(Http2TunnelWrite), 52 | } 53 | 54 | impl TunnelWrite for TunnelWriter { 55 | fn buf_mut(&mut self) -> &mut BytesMut { 56 | match self { 57 | Self::Websocket(s) => s.buf_mut(), 58 | Self::Http2(s) => s.buf_mut(), 59 | } 60 | } 61 | 62 | async fn write(&mut self) -> Result<(), std::io::Error> { 63 | match self { 64 | Self::Websocket(s) => s.write().await, 65 | Self::Http2(s) => s.write().await, 66 | } 67 | } 68 | 69 | async fn ping(&mut self) -> Result<(), std::io::Error> { 70 | match self { 71 | Self::Websocket(s) => s.ping().await, 72 | Self::Http2(s) => s.ping().await, 73 | } 74 | } 75 | 76 | async fn close(&mut self) -> Result<(), std::io::Error> { 77 | match self { 78 | Self::Websocket(s) => s.close().await, 79 | Self::Http2(s) => s.close().await, 80 | } 81 | } 82 | 83 | fn pending_operations_notify(&mut self) -> Arc { 84 | match self { 85 | Self::Websocket(s) => s.pending_operations_notify(), 86 | Self::Http2(s) => s.pending_operations_notify(), 87 | } 88 | } 89 | 90 | async fn handle_pending_operations(&mut self) -> Result<(), std::io::Error> { 91 | match self { 92 | Self::Websocket(s) => s.handle_pending_operations().await, 93 | Self::Http2(s) => s.handle_pending_operations().await, 94 | } 95 | } 96 | } 97 | 98 | pub async fn propagate_local_to_remote( 99 | local_rx: impl AsyncRead, 100 | mut ws_tx: impl TunnelWrite, 101 | mut close_tx: oneshot::Sender<()>, 102 | ping_frequency: Option, 103 | ) -> anyhow::Result<()> { 104 | let _guard = scopeguard::guard((), |_| { 105 | info!("Closing local => remote tunnel"); 106 | }); 107 | 108 | static MAX_PACKET_LENGTH: usize = 64 * 1024; 109 | 110 | // We do our own pin_mut! to avoid shadowing timeout and be able to reset it, on next loop iteration 111 | // We reuse the future to avoid creating a timer in the tight loop 112 | let frequency = ping_frequency.unwrap_or(Duration::from_secs(3600 * 24)); 113 | let start_at = Instant::now().checked_add(frequency).unwrap_or_else(Instant::now); 114 | let timeout = tokio::time::interval_at(start_at, frequency); 115 | let should_close = close_tx.closed().fuse(); 116 | let notify = ws_tx.pending_operations_notify(); 117 | let mut has_pending_operations = notify.notified(); 118 | let mut has_pending_operations_pin = unsafe { Pin::new_unchecked(&mut has_pending_operations) }; 119 | 120 | pin_mut!(timeout); 121 | pin_mut!(should_close); 122 | pin_mut!(local_rx); 123 | loop { 124 | debug_assert!( 125 | ws_tx.buf_mut().chunk_mut().len() >= MAX_PACKET_LENGTH, 126 | "buffer must be large enough to receive a whole packet length" 127 | ); 128 | 129 | let read_len = select! { 130 | biased; 131 | 132 | _ = &mut has_pending_operations_pin => { 133 | has_pending_operations = notify.notified(); 134 | has_pending_operations_pin = unsafe { Pin::new_unchecked(&mut has_pending_operations) }; 135 | match ws_tx.handle_pending_operations().await { 136 | Ok(_) => continue, 137 | Err(err) => { 138 | warn!("error while handling pending operations {}", err); 139 | break; 140 | } 141 | } 142 | }, 143 | 144 | read_len = local_rx.read_buf(ws_tx.buf_mut()) => read_len, 145 | 146 | _ = &mut should_close => break, 147 | 148 | _ = timeout.tick(), if ping_frequency.is_some() => { 149 | debug!("sending ping to keep connection alive"); 150 | ws_tx.ping().await?; 151 | continue; 152 | } 153 | }; 154 | 155 | let _read_len = match read_len { 156 | Ok(0) => break, 157 | Ok(read_len) => read_len, 158 | Err(err) => { 159 | warn!("error while reading incoming bytes from local tx tunnel: {}", err); 160 | break; 161 | } 162 | }; 163 | 164 | //debug!("read {} wasted {}% usable {} capa {}", read_len, 100 - (read_len * 100 / buffer.capacity()), buffer.as_slice().len(), buffer.capacity()); 165 | if let Err(err) = ws_tx.write().await { 166 | warn!("error while writing to tx tunnel {}", err); 167 | break; 168 | } 169 | } 170 | 171 | // Send normal close 172 | let _ = ws_tx.close().await; 173 | 174 | Ok(()) 175 | } 176 | 177 | pub async fn propagate_remote_to_local( 178 | local_tx: impl AsyncWrite + Send, 179 | mut ws_rx: impl TunnelRead, 180 | mut close_rx: oneshot::Receiver<()>, 181 | ) -> anyhow::Result<()> { 182 | let _guard = scopeguard::guard((), |_| { 183 | info!("Closing local <= remote tunnel"); 184 | }); 185 | 186 | pin_mut!(local_tx); 187 | loop { 188 | let msg = select! { 189 | biased; 190 | msg = ws_rx.copy(&mut local_tx) => msg, 191 | _ = &mut close_rx => break, 192 | }; 193 | 194 | if let Err(err) = msg { 195 | match err.kind() { 196 | ErrorKind::NotConnected => debug!("Connection closed frame received"), 197 | ErrorKind::BrokenPipe => debug!("Remote side closed connection"), 198 | _ => error!("error while reading from tunnel rx {err}"), 199 | } 200 | break; 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/transport/jwt.rs: -------------------------------------------------------------------------------- 1 | use crate::tunnel::{LocalProtocol, RemoteAddr}; 2 | use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashSet; 5 | use std::ops::Deref; 6 | use std::sync::LazyLock; 7 | use std::time::SystemTime; 8 | use url::Host; 9 | use uuid::Uuid; 10 | 11 | pub static JWT_HEADER_PREFIX: &str = "authorization.bearer."; 12 | static JWT_KEY: LazyLock<(Header, EncodingKey)> = LazyLock::new(|| { 13 | let now = SystemTime::now() 14 | .duration_since(SystemTime::UNIX_EPOCH) 15 | .unwrap() 16 | .as_nanos() 17 | .to_ne_bytes(); 18 | (Header::new(Algorithm::HS256), EncodingKey::from_secret(&now)) 19 | }); 20 | 21 | static JWT_DECODE: LazyLock<(Validation, DecodingKey)> = LazyLock::new(|| { 22 | let mut validation = Validation::new(Algorithm::HS256); 23 | validation.required_spec_claims = HashSet::with_capacity(0); 24 | validation.insecure_disable_signature_validation(); 25 | (validation, DecodingKey::from_secret(b"champignonfrais")) 26 | }); 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct JwtTunnelConfig { 30 | pub id: String, // tunnel id 31 | pub p: LocalProtocol, // protocol to use 32 | pub r: String, // remote host 33 | pub rp: u16, // remote port 34 | } 35 | 36 | impl JwtTunnelConfig { 37 | fn new(request_id: Uuid, dest: &RemoteAddr) -> Self { 38 | Self { 39 | id: request_id.to_string(), 40 | p: match dest.protocol { 41 | LocalProtocol::Tcp { .. } => dest.protocol.clone(), 42 | LocalProtocol::Udp { .. } => dest.protocol.clone(), 43 | LocalProtocol::ReverseTcp => dest.protocol.clone(), 44 | LocalProtocol::ReverseUdp { .. } => dest.protocol.clone(), 45 | LocalProtocol::ReverseSocks5 { .. } => dest.protocol.clone(), 46 | LocalProtocol::ReverseUnix { .. } => dest.protocol.clone(), 47 | LocalProtocol::ReverseHttpProxy { .. } => dest.protocol.clone(), 48 | LocalProtocol::TProxyTcp => unreachable!("cannot use tproxy tcp as destination protocol"), 49 | LocalProtocol::TProxyUdp { .. } => unreachable!("cannot use tproxy udp as destination protocol"), 50 | LocalProtocol::Stdio { .. } => unreachable!("cannot use stdio as destination protocol"), 51 | LocalProtocol::Unix { .. } => unreachable!("canont use unix as destination protocol"), 52 | LocalProtocol::Socks5 { .. } => unreachable!("cannot use socks5 as destination protocol"), 53 | LocalProtocol::HttpProxy { .. } => unreachable!("cannot use http proxy as destination protocol"), 54 | }, 55 | r: dest.host.to_string(), 56 | rp: dest.port, 57 | } 58 | } 59 | } 60 | 61 | pub fn tunnel_to_jwt_token(request_id: Uuid, tunnel: &RemoteAddr) -> String { 62 | let cfg = JwtTunnelConfig::new(request_id, tunnel); 63 | let (alg, secret) = JWT_KEY.deref(); 64 | jsonwebtoken::encode(alg, &cfg, secret).unwrap_or_default() 65 | } 66 | 67 | pub fn jwt_token_to_tunnel(token: &str) -> anyhow::Result> { 68 | let (validation, decode_key) = JWT_DECODE.deref(); 69 | let jwt: TokenData = jsonwebtoken::decode(token, decode_key, validation)?; 70 | Ok(jwt) 71 | } 72 | 73 | impl TryFrom for RemoteAddr { 74 | type Error = anyhow::Error; 75 | fn try_from(jwt: JwtTunnelConfig) -> anyhow::Result { 76 | Ok(Self { 77 | protocol: jwt.p, 78 | host: Host::parse(&jwt.r)?, 79 | port: jwt.rp, 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/transport/mod.rs: -------------------------------------------------------------------------------- 1 | use hyper::header::HOST; 2 | use hyper::http::{HeaderName, HeaderValue}; 3 | use std::io::{BufRead, BufReader}; 4 | use std::path::Path; 5 | use std::str::FromStr; 6 | 7 | use tracing::error; 8 | 9 | pub mod http2; 10 | pub mod io; 11 | mod jwt; 12 | mod types; 13 | pub mod websocket; 14 | 15 | pub use jwt::JWT_HEADER_PREFIX; 16 | pub use jwt::JwtTunnelConfig; 17 | pub use jwt::jwt_token_to_tunnel; 18 | pub use jwt::tunnel_to_jwt_token; 19 | pub use types::TransportAddr; 20 | pub use types::TransportScheme; 21 | 22 | #[allow(clippy::type_complexity)] 23 | #[inline] 24 | pub fn headers_from_file(path: &Path) -> (Option<(HeaderName, HeaderValue)>, Vec<(HeaderName, HeaderValue)>) { 25 | let file = match std::fs::File::open(path) { 26 | Ok(file) => file, 27 | Err(err) => { 28 | error!("Cannot read headers from file: {:?}: {:?}", path, err); 29 | return (None, vec![]); 30 | } 31 | }; 32 | 33 | let mut host_header = None; 34 | let headers = BufReader::new(file) 35 | .lines() 36 | .filter_map(|line| { 37 | let line = line.ok()?; 38 | let (header, value) = line.split_once(':')?; 39 | let header = HeaderName::from_str(header.trim()).ok()?; 40 | let value = HeaderValue::from_str(value.trim()).ok()?; 41 | if header == HOST { 42 | host_header = Some((header, value)); 43 | return None; 44 | } 45 | Some((header, value)) 46 | }) 47 | .collect(); 48 | 49 | (host_header, headers) 50 | } 51 | -------------------------------------------------------------------------------- /wstunnel/src/tunnel/transport/types.rs: -------------------------------------------------------------------------------- 1 | use crate::tunnel::client::TlsClientConfig; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | use std::str::FromStr; 4 | use url::Host; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | pub enum TransportScheme { 8 | Ws, 9 | Wss, 10 | Http, 11 | Https, 12 | } 13 | 14 | impl TransportScheme { 15 | #[cfg(feature = "clap")] // this is only used inside a clap value parser 16 | pub const fn values() -> &'static [Self] { 17 | &[Self::Ws, Self::Wss, Self::Http, Self::Https] 18 | } 19 | pub const fn to_str(self) -> &'static str { 20 | match self { 21 | Self::Ws => "ws", 22 | Self::Wss => "wss", 23 | Self::Http => "http", 24 | Self::Https => "https", 25 | } 26 | } 27 | 28 | pub fn alpn_protocols(&self) -> Vec> { 29 | match self { 30 | Self::Ws => vec![], 31 | Self::Wss => vec![b"http/1.1".to_vec()], 32 | Self::Http => vec![], 33 | Self::Https => vec![b"h2".to_vec()], 34 | } 35 | } 36 | } 37 | impl FromStr for TransportScheme { 38 | type Err = (); 39 | 40 | fn from_str(s: &str) -> Result { 41 | match s { 42 | "https" => Ok(Self::Https), 43 | "http" => Ok(Self::Http), 44 | "wss" => Ok(Self::Wss), 45 | "ws" => Ok(Self::Ws), 46 | _ => Err(()), 47 | } 48 | } 49 | } 50 | 51 | impl Display for TransportScheme { 52 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 53 | f.write_str(self.to_str()) 54 | } 55 | } 56 | 57 | #[derive(Clone)] 58 | pub enum TransportAddr { 59 | Wss { 60 | tls: TlsClientConfig, 61 | scheme: TransportScheme, 62 | host: Host, 63 | port: u16, 64 | }, 65 | Ws { 66 | scheme: TransportScheme, 67 | host: Host, 68 | port: u16, 69 | }, 70 | Https { 71 | scheme: TransportScheme, 72 | tls: TlsClientConfig, 73 | host: Host, 74 | port: u16, 75 | }, 76 | Http { 77 | scheme: TransportScheme, 78 | host: Host, 79 | port: u16, 80 | }, 81 | } 82 | 83 | impl Debug for TransportAddr { 84 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 85 | f.write_fmt(format_args!("{}://{}:{}", self.scheme(), self.host(), self.port())) 86 | } 87 | } 88 | 89 | impl TransportAddr { 90 | pub fn new(scheme: TransportScheme, host: Host, port: u16, tls: Option) -> Option { 91 | match scheme { 92 | TransportScheme::Https => Some(Self::Https { 93 | scheme: TransportScheme::Https, 94 | tls: tls?, 95 | host, 96 | port, 97 | }), 98 | TransportScheme::Http => Some(Self::Http { 99 | scheme: TransportScheme::Http, 100 | host, 101 | port, 102 | }), 103 | TransportScheme::Wss => Some(Self::Wss { 104 | scheme: TransportScheme::Wss, 105 | tls: tls?, 106 | host, 107 | port, 108 | }), 109 | TransportScheme::Ws => Some(Self::Ws { 110 | scheme: TransportScheme::Ws, 111 | host, 112 | port, 113 | }), 114 | } 115 | } 116 | 117 | pub const fn tls(&self) -> Option<&TlsClientConfig> { 118 | match self { 119 | Self::Wss { tls, .. } => Some(tls), 120 | Self::Https { tls, .. } => Some(tls), 121 | Self::Ws { .. } => None, 122 | Self::Http { .. } => None, 123 | } 124 | } 125 | 126 | pub const fn host(&self) -> &Host { 127 | match self { 128 | Self::Wss { host, .. } => host, 129 | Self::Ws { host, .. } => host, 130 | Self::Https { host, .. } => host, 131 | Self::Http { host, .. } => host, 132 | } 133 | } 134 | 135 | pub const fn port(&self) -> u16 { 136 | match self { 137 | Self::Wss { port, .. } => *port, 138 | Self::Ws { port, .. } => *port, 139 | Self::Https { port, .. } => *port, 140 | Self::Http { port, .. } => *port, 141 | } 142 | } 143 | 144 | pub const fn scheme(&self) -> &TransportScheme { 145 | match self { 146 | Self::Wss { scheme, .. } => scheme, 147 | Self::Ws { scheme, .. } => scheme, 148 | Self::Https { scheme, .. } => scheme, 149 | Self::Http { scheme, .. } => scheme, 150 | } 151 | } 152 | } 153 | --------------------------------------------------------------------------------