├── .github └── workflows │ ├── build-nightly.yml │ └── build-pr.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── README.md ├── build ├── README.md ├── build-host-release ├── build-host-release.ps1 └── build-release └── src ├── args └── mod.rs ├── client.rs ├── common └── mod.rs ├── lib.rs └── server.rs /.github/workflows/build-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Build Nightly Releases 2 | on: 3 | push: 4 | branches: [master] 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build-cross: 11 | runs-on: ubuntu-latest 12 | env: 13 | RUST_BACKTRACE: full 14 | strategy: 15 | matrix: 16 | target: 17 | - x86_64-unknown-linux-musl 18 | - aarch64-unknown-linux-musl 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Install cross 24 | run: cargo install cross 25 | 26 | - name: Build ${{ matrix.target }} 27 | timeout-minutes: 120 28 | run: | 29 | compile_target=${{ matrix.target }} 30 | 31 | cd build 32 | ./build-release -t ${{ matrix.target }} $compile_features $compile_compress 33 | 34 | - name: Upload Artifacts 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: ${{ matrix.target }} 38 | path: build/release/* 39 | 40 | build-unix: 41 | runs-on: ${{ matrix.os }} 42 | env: 43 | RUST_BACKTRACE: full 44 | strategy: 45 | matrix: 46 | os: [macos-latest] 47 | target: 48 | - x86_64-apple-darwin 49 | - aarch64-apple-darwin 50 | steps: 51 | - uses: actions/checkout@v2 52 | 53 | - name: Install GNU tar 54 | if: runner.os == 'macOS' 55 | run: | 56 | brew install gnu-tar 57 | # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" 58 | echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH 59 | 60 | - name: Install Rust stable 61 | uses: actions-rs/toolchain@v1 62 | with: 63 | profile: minimal 64 | toolchain: stable 65 | target: ${{ matrix.target }} 66 | default: true 67 | override: true 68 | 69 | - name: Build release 70 | shell: bash 71 | run: | 72 | ./build/build-host-release -t ${{ matrix.target }} 73 | 74 | - name: Upload Artifacts 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: ${{ matrix.target }} 78 | path: build/release/* 79 | 80 | build-windows: 81 | runs-on: windows-latest 82 | env: 83 | RUSTFLAGS: "-Ctarget-feature=+crt-static" 84 | RUST_BACKTRACE: full 85 | steps: 86 | - uses: actions/checkout@v2 87 | 88 | - name: Install Rust stable 89 | uses: actions-rs/toolchain@v1 90 | with: 91 | profile: minimal 92 | toolchain: stable 93 | default: true 94 | override: true 95 | 96 | - name: Build release 97 | run: | 98 | pwsh ./build/build-host-release.ps1 99 | 100 | - name: Upload Artifacts 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: windows-native 104 | path: build/release/* 105 | -------------------------------------------------------------------------------- /.github/workflows/build-pr.yml: -------------------------------------------------------------------------------- 1 | name: Build PR 2 | on: 3 | pull_request: 4 | types: [ opened, edited, reopened, review_requested ] 5 | branches: [ master ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build-cross: 12 | runs-on: ubuntu-latest 13 | env: 14 | RUST_BACKTRACE: full 15 | strategy: 16 | matrix: 17 | target: 18 | - x86_64-unknown-linux-musl 19 | - aarch64-unknown-linux-musl 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Install cross 25 | run: cargo install cross 26 | 27 | - name: Build ${{ matrix.target }} 28 | timeout-minutes: 120 29 | run: | 30 | compile_target=${{ matrix.target }} 31 | 32 | cd build 33 | ./build-release -t ${{ matrix.target }} $compile_features $compile_compress 34 | 35 | build-unix: 36 | runs-on: ${{ matrix.os }} 37 | env: 38 | RUST_BACKTRACE: full 39 | strategy: 40 | matrix: 41 | os: [macos-latest] 42 | target: 43 | - x86_64-apple-darwin 44 | - aarch64-apple-darwin 45 | steps: 46 | - uses: actions/checkout@v2 47 | 48 | - name: Install GNU tar 49 | if: runner.os == 'macOS' 50 | run: | 51 | brew install gnu-tar 52 | # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" 53 | echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH 54 | 55 | - name: Install Rust stable 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | target: ${{ matrix.target }} 61 | default: true 62 | override: true 63 | 64 | - name: Build release 65 | shell: bash 66 | run: | 67 | ./build/build-host-release -t ${{ matrix.target }} 68 | 69 | build-windows: 70 | runs-on: windows-latest 71 | env: 72 | RUSTFLAGS: "-Ctarget-feature=+crt-static" 73 | RUST_BACKTRACE: full 74 | steps: 75 | - uses: actions/checkout@v2 76 | 77 | - name: Install Rust stable 78 | uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: stable 82 | default: true 83 | override: true 84 | 85 | - name: Build release 86 | run: | 87 | pwsh ./build/build-host-release.ps1 88 | 89 | - name: Upload Artifacts 90 | uses: actions/upload-artifact@v2 91 | with: 92 | name: windows-native 93 | path: build/release/* 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.iml 3 | /.idea 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_try_shorthand = true 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qtun" 3 | version = "0.3.0" 4 | authors = ["Max Lv "] 5 | repository = "https://github.com/shadowsocks/qtun" 6 | license = "MIT" 7 | edition = "2021" 8 | 9 | [[bin]] 10 | name = "qtun-client" 11 | path = "src/client.rs" 12 | 13 | [[bin]] 14 | name = "qtun-server" 15 | path = "src/server.rs" 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | 19 | [dependencies] 20 | tokio = { version = "1", features = ["full"] } 21 | bytes = "1.2.1" 22 | futures = "0.3" 23 | rustls = { version = "0.23.5", default-features = false, features = ["std"] } 24 | rustls-pemfile = "2" 25 | rustls-platform-verifier = "0.5" 26 | rustls-pki-types = "1.7" 27 | webpki-roots = "0.26.7" 28 | quinn = "0.11.6" 29 | quinn-proto = "0.11.9" 30 | structopt = "0.3" 31 | anyhow = "1.0" 32 | tracing = "0.1" 33 | log = "0.4" 34 | env_logger = "0.10.0" 35 | dirs = "5.0.1" 36 | rustls-native-certs = "0.8.1" 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtun 2 | 3 | Yet another SIP003 plugin based on IETF-QUIC 4 | 5 | ## Major features: 6 | 7 | * IETF-QUIC 8 | * ACME compatible 9 | * BBR congestion 10 | * Low resource usage 11 | 12 | ## Install 13 | 14 | ```bash 15 | # Install rust 16 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 17 | 18 | cargo install --git https://github.com/shadowsocks/qtun 19 | ``` 20 | 21 | ## Examples 22 | 23 | ### Issue a cert for TLS and QUIC 24 | 25 | qtun will look for TLS certificates signed by acme.sh by default. Here's some sample commands for issuing a certificate using CloudFlare. You can find commands for issuing certificates for other DNS providers at acme.sh. 26 | 27 | ```bash 28 | curl https://get.acme.sh | sh 29 | ~/.acme.sh/acme.sh --issue --dns dns_cf -d mydomain.me 30 | ``` 31 | 32 | ### Server 33 | 34 | ```bash 35 | ./ssserver -s 0.0.0.0:443 -k example -m aes-256-gcm --plugin ./qtun-server --plugin-opts "acme_host=example.com" 36 | ``` 37 | 38 | ### Client 39 | 40 | ```bash 41 | ./sslocal -s example.com:443 -k example -m aes-256-gcm --plugin ./qtun-client --plugin-opts "host=example.com" 42 | ``` 43 | 44 | ## License 45 | 46 | The MIT License (MIT) 47 | 48 | Copyright (c) 2020 Max Lv 49 | 50 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 51 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 52 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 53 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 54 | 55 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 56 | Software. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 59 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 60 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 61 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 62 | -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | ## Build Standalone Binaries 2 | 3 | ### Build with `cross` 4 | 5 | - Install [`cross`](https://github.com/rust-embedded/cross) 6 | 7 | ```bash 8 | cargo install cross 9 | ``` 10 | 11 | - Build with cross 12 | 13 | ```bash 14 | cross build --target x86_64-unknown-linux-musl 15 | ``` 16 | 17 | ### Predefined build routines 18 | 19 | - `build-release`: Build binaries with `cross` and packages outputs into `release` folder 20 | - `build-host-release`: Build binaries with host's Rust toolchain. *NIX shell script 21 | - `build-host-release.ps1`: Build binaries with host's Rust toolchain. PowerShell script 22 | -------------------------------------------------------------------------------- /build/build-host-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_TARGET="" 4 | BUILD_FEATURES=() 5 | while getopts "t:f:" opt; do 6 | case $opt in 7 | t) 8 | BUILD_TARGET=$OPTARG 9 | ;; 10 | f) 11 | BUILD_FEATURES+=($OPTARG) 12 | ;; 13 | ?) 14 | echo "Usage: $(basename $0) [-t ] [-f ]" 15 | ;; 16 | esac 17 | done 18 | 19 | BUILD_FEATURES+=${BUILD_EXTRA_FEATURES} 20 | 21 | ROOT_DIR=$( cd $( dirname $0 ) && pwd ) 22 | VERSION=$(grep -E '^version' "${ROOT_DIR}/../Cargo.toml" | awk '{print $3}' | sed 's/"//g') 23 | HOST_TRIPLE=$(rustc -Vv | grep 'host:' | awk '{print $2}') 24 | 25 | echo "Started build release ${VERSION} for ${HOST_TRIPLE} (target: ${BUILD_TARGET}) with features \"${BUILD_FEATURES}\"..." 26 | 27 | if [[ "${BUILD_TARGET}" != "" ]]; then 28 | if [[ "${BUILD_FEATURES}" != "" ]]; then 29 | cargo build --release --features "${BUILD_FEATURES}" --target "${BUILD_TARGET}" 30 | else 31 | cargo build --release --target "${BUILD_TARGET}" 32 | fi 33 | else 34 | if [[ "${BUILD_FEATURES}" != "" ]]; then 35 | cargo build --release --features "${BUILD_FEATURES}" 36 | else 37 | cargo build --release 38 | fi 39 | fi 40 | 41 | if [[ "$?" != "0" ]]; then 42 | exit $?; 43 | fi 44 | 45 | if [[ "${BUILD_TARGET}" == "" ]]; then 46 | BUILD_TARGET=$HOST_TRIPLE 47 | fi 48 | 49 | TARGET_SUFFIX="" 50 | if [[ "${BUILD_TARGET}" == *"-windows-"* ]]; then 51 | TARGET_SUFFIX=".exe" 52 | fi 53 | 54 | TARGETS=("qtun-client${TARGET_SUFFIX}" "qtun-server${TARGET_SUFFIX}") 55 | 56 | RELEASE_FOLDER="${ROOT_DIR}/release" 57 | RELEASE_PACKAGE_NAME="qtun-v${VERSION}.${BUILD_TARGET}" 58 | 59 | mkdir -p "${RELEASE_FOLDER}" 60 | 61 | # Into release folder 62 | if [[ "${BUILD_TARGET}" != "" ]]; then 63 | cd "${ROOT_DIR}/../target/${BUILD_TARGET}/release" 64 | else 65 | cd "${ROOT_DIR}/../target/release" 66 | fi 67 | 68 | if [[ "${BUILD_TARGET}" == *"-windows-"* ]]; then 69 | # For Windows, use zip 70 | 71 | RELEASE_PACKAGE_FILE_NAME="${RELEASE_PACKAGE_NAME}.zip" 72 | RELEASE_PACKAGE_FILE_PATH="${RELEASE_FOLDER}/${RELEASE_PACKAGE_FILE_NAME}" 73 | zip "${RELEASE_PACKAGE_FILE_PATH}" "${TARGETS[@]}" 74 | 75 | # Checksum 76 | cd "${RELEASE_FOLDER}" 77 | shasum -a 256 "${RELEASE_PACKAGE_FILE_NAME}" > "${RELEASE_PACKAGE_FILE_NAME}.sha256" 78 | else 79 | # For others, Linux, OS X, uses tar.xz 80 | 81 | # For Darwin, .DS_Store and other related files should be ignored 82 | if [[ "$(uname -s)" == "Darwin" ]]; then 83 | export COPYFILE_DISABLE=1 84 | fi 85 | 86 | RELEASE_PACKAGE_FILE_NAME="${RELEASE_PACKAGE_NAME}.tar.xz" 87 | RELEASE_PACKAGE_FILE_PATH="${RELEASE_FOLDER}/${RELEASE_PACKAGE_FILE_NAME}" 88 | tar -cJf "${RELEASE_PACKAGE_FILE_PATH}" "${TARGETS[@]}" 89 | 90 | # Checksum 91 | cd "${RELEASE_FOLDER}" 92 | shasum -a 256 "${RELEASE_PACKAGE_FILE_NAME}" > "${RELEASE_PACKAGE_FILE_NAME}.sha256" 93 | fi 94 | 95 | echo "Finished build release ${RELEASE_PACKAGE_FILE_PATH}" 96 | -------------------------------------------------------------------------------- /build/build-host-release.ps1: -------------------------------------------------------------------------------- 1 | #!pwsh 2 | <# 3 | OpenSSL is already installed on windows-latest virtual environment. 4 | If you need OpenSSL, consider install it by: 5 | 6 | choco install openssl 7 | #> 8 | param( 9 | [Parameter(HelpMessage = "extra features")] 10 | [Alias('f')] 11 | [string]$Features 12 | ) 13 | 14 | $ErrorActionPreference = "Stop" 15 | 16 | $TargetTriple = (rustc -Vv | Select-String -Pattern "host: (.*)" | ForEach-Object { $_.Matches.Value }).split()[-1] 17 | 18 | Write-Host "Started building release for ${TargetTriple} ..." 19 | 20 | if ([string]::IsNullOrEmpty($Features)) { 21 | cargo build --release 22 | } 23 | else { 24 | cargo build --release --features "${Features}" 25 | } 26 | 27 | if (!$?) { 28 | exit $LASTEXITCODE 29 | } 30 | 31 | $Version = (Select-String -Pattern '^version *= *"([^"]*)"$' -Path "${PSScriptRoot}\..\Cargo.toml" | ForEach-Object { $_.Matches.Value }).split()[-1] 32 | $Version = $Version -replace '"' 33 | 34 | $PackageReleasePath = "${PSScriptRoot}\release" 35 | $PackageName = "qtun-v${Version}.${TargetTriple}.zip" 36 | $PackagePath = "${PackageReleasePath}\${PackageName}" 37 | 38 | Write-Host $Version 39 | Write-Host $PackageReleasePath 40 | Write-Host $PackageName 41 | Write-Host $PackagePath 42 | 43 | Push-Location "${PSScriptRoot}\..\target\release" 44 | 45 | $ProgressPreference = "SilentlyContinue" 46 | New-Item "${PackageReleasePath}" -ItemType Directory -ErrorAction SilentlyContinue 47 | $CompressParam = @{ 48 | LiteralPath = "qtun-client.exe", "qtun-server.exe" 49 | DestinationPath = "${PackagePath}" 50 | } 51 | Compress-Archive @CompressParam 52 | 53 | Write-Host "Created release packet ${PackagePath}" 54 | 55 | $PackageChecksumPath = "${PackagePath}.sha256" 56 | $PackageHash = (Get-FileHash -Path "${PackagePath}" -Algorithm SHA256).Hash 57 | "${PackageHash} ${PackageName}" | Out-File -FilePath "${PackageChecksumPath}" 58 | 59 | Write-Host "Created release packet checksum ${PackageChecksumPath}" 60 | -------------------------------------------------------------------------------- /build/build-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CUR_DIR=$( cd $( dirname $0 ) && pwd ) 4 | VERSION=$(grep -E '^version' ${CUR_DIR}/../Cargo.toml | awk '{print $3}' | sed 's/"//g') 5 | 6 | ## Disable macos ACL file 7 | if [[ "$(uname -s)" == "Darwin" ]]; then 8 | export COPYFILE_DISABLE=1 9 | fi 10 | 11 | targets=() 12 | features=() 13 | use_upx=false 14 | 15 | while getopts "t:f:u" opt; do 16 | case $opt in 17 | t) 18 | targets+=($OPTARG) 19 | ;; 20 | f) 21 | features+=($OPTARG) 22 | ;; 23 | u) 24 | use_upx=true 25 | ;; 26 | ?) 27 | echo "Usage: $(basename $0) [-t ] [-f features] [-u]" 28 | ;; 29 | esac 30 | done 31 | 32 | features+=${EXTRA_FEATURES} 33 | 34 | if [[ "${#targets[@]}" == "0" ]]; then 35 | echo "Specifying compile target with -t " 36 | exit 1 37 | fi 38 | 39 | if [[ "${use_upx}" = true ]]; then 40 | if [[ -z "$upx" ]] && command -v upx &> /dev/null; then 41 | upx="upx -9" 42 | fi 43 | 44 | if [[ "x$upx" == "x" ]]; then 45 | echo "Couldn't find upx in PATH, consider specifying it with variable \$upx" 46 | exit 1 47 | fi 48 | fi 49 | 50 | 51 | function build() { 52 | cd "$CUR_DIR/.." 53 | 54 | TARGET=$1 55 | 56 | RELEASE_DIR="target/${TARGET}/release" 57 | TARGET_FEATURES="${features[@]}" 58 | 59 | if [[ "${TARGET_FEATURES}" != "" ]]; then 60 | echo "* Building ${TARGET} package ${VERSION} with features \"${TARGET_FEATURES}\" ..." 61 | 62 | cross build --target "${TARGET}" \ 63 | --features "${TARGET_FEATURES}" \ 64 | --release 65 | else 66 | echo "* Building ${TARGET} package ${VERSION} ..." 67 | 68 | cross build --target "${TARGET}" \ 69 | --release 70 | fi 71 | 72 | if [[ $? != "0" ]]; then 73 | exit $? 74 | fi 75 | 76 | PKG_DIR="${CUR_DIR}/release" 77 | mkdir -p "${PKG_DIR}" 78 | 79 | if [[ "$TARGET" == *"-linux-"* ]]; then 80 | PKG_NAME="qtun-v${VERSION}.${TARGET}.tar.xz" 81 | PKG_PATH="${PKG_DIR}/${PKG_NAME}" 82 | 83 | cd ${RELEASE_DIR} 84 | 85 | if [[ "${use_upx}" = true ]]; then 86 | # Enable upx for MIPS. 87 | $upx sslocal ssserver ssurl ssmanager #>/dev/null 88 | fi 89 | 90 | echo "* Packaging XZ in ${PKG_PATH} ..." 91 | tar -cJf ${PKG_PATH} \ 92 | "qtun-client" \ 93 | "qtun-server" 94 | 95 | if [[ $? != "0" ]]; then 96 | exit $? 97 | fi 98 | 99 | cd "${PKG_DIR}" 100 | shasum -a 256 "${PKG_NAME}" > "${PKG_NAME}.sha256" 101 | elif [[ "$TARGET" == *"-windows-"* ]]; then 102 | PKG_NAME="qtun-v${VERSION}.${TARGET}.zip" 103 | PKG_PATH="${PKG_DIR}/${PKG_NAME}" 104 | 105 | echo "* Packaging ZIP in ${PKG_PATH} ..." 106 | cd ${RELEASE_DIR} 107 | zip ${PKG_PATH} \ 108 | "qtun-client.exe" \ 109 | "qtun-server.exe" 110 | 111 | if [[ $? != "0" ]]; then 112 | exit $? 113 | fi 114 | 115 | cd "${PKG_DIR}" 116 | shasum -a 256 "${PKG_NAME}" > "${PKG_NAME}.sha256" 117 | fi 118 | 119 | echo "* Done build package ${PKG_NAME}" 120 | } 121 | 122 | for target in "${targets[@]}"; do 123 | build "$target"; 124 | done 125 | -------------------------------------------------------------------------------- /src/args/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::collections::HashMap; 3 | use std::env::var; 4 | use std::net::{SocketAddr, ToSocketAddrs}; 5 | 6 | pub fn parse_env_addr() -> Result<(SocketAddr, SocketAddr)> { 7 | let ss_remote_host = var("SS_REMOTE_HOST")?; 8 | let ss_remote_port = var("SS_REMOTE_PORT")?; 9 | let ss_local_host = var("SS_LOCAL_HOST")?; 10 | let ss_local_port = var("SS_LOCAL_PORT")?; 11 | 12 | let ss_local_addr = format!("{}:{}", ss_local_host, ss_local_port) 13 | .to_socket_addrs()? 14 | .next() 15 | .unwrap(); 16 | let ss_remote_addr = format!("{}:{}", ss_remote_host, ss_remote_port) 17 | .to_socket_addrs()? 18 | .next() 19 | .unwrap(); 20 | 21 | Ok((ss_local_addr, ss_remote_addr)) 22 | } 23 | 24 | pub fn parse_env_opts() -> Result> { 25 | let ss_plugin_options = var("SS_PLUGIN_OPTIONS")?; 26 | 27 | let ss_plugin_options = parse_plugin_options(&ss_plugin_options); 28 | 29 | Ok(ss_plugin_options) 30 | } 31 | 32 | /// Parse a name–value mapping as from SS_PLUGIN_OPTIONS. 33 | /// 34 | /// " is a k=v string value with options that are to be passed to the 35 | /// transport. semicolons, equal signs and backslashes must be escaped 36 | /// with a backslash." 37 | /// Example: secret=nou;cache=/tmp/cache;secret=yes 38 | /// 39 | fn parse_plugin_options(options: &str) -> HashMap { 40 | let mut plugin_options = HashMap::::new(); 41 | 42 | let opts: Vec<&str> = options.split(';').collect(); 43 | 44 | // FIXME: backslash is not escaped in this plugin 45 | for opt in opts { 46 | let o: Vec<&str> = opt.splitn(2, '=').collect(); 47 | plugin_options.insert(o[0].to_string(), o[1].to_string()); 48 | } 49 | 50 | plugin_options 51 | } 52 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::Arc; 3 | 4 | use quinn::crypto::rustls::QuicClientConfig; 5 | use tokio::net::{TcpListener, TcpStream}; 6 | 7 | use anyhow::{anyhow, Result}; 8 | use futures::future::try_join; 9 | use log::{error, info}; 10 | use quinn::ConnectionError; 11 | use quinn::Endpoint; 12 | use structopt::{self, StructOpt}; 13 | 14 | use env_logger::Builder; 15 | use log::LevelFilter; 16 | 17 | mod args; 18 | mod common; 19 | 20 | #[derive(StructOpt, Debug)] 21 | #[structopt(name = "qtun-client")] 22 | struct Opt { 23 | /// Address to listen on 24 | #[structopt(long = "listen", default_value = "127.0.0.1:8138")] 25 | listen: SocketAddr, 26 | /// Address to listen on 27 | #[structopt(long = "relay", default_value = "127.0.0.1:4433")] 28 | relay: SocketAddr, 29 | /// Override hostname used for certificate verification 30 | #[structopt(long = "host", default_value = "bing.com")] 31 | host: String, 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() -> Result<()> { 36 | // setup log 37 | let mut log_builder = Builder::new(); 38 | log_builder.filter(None, LevelFilter::Info).default_format(); 39 | log_builder.filter(Some("qtun-client"), LevelFilter::Debug); 40 | log_builder.init(); 41 | 42 | // parse command line args 43 | let options = Opt::from_args(); 44 | 45 | // init all parameters 46 | let mut listen_addr = options.listen; 47 | let mut relay_addr = options.relay; 48 | let mut host = options.host; 49 | 50 | // parse environment variables 51 | if let Ok((ss_local_addr, ss_remote_addr)) = args::parse_env_addr() { 52 | // init all parameters 53 | listen_addr = ss_local_addr; 54 | relay_addr = ss_remote_addr; 55 | } 56 | if let Ok(ss_plugin_opts) = args::parse_env_opts() { 57 | if let Some(h) = ss_plugin_opts.get("host") { 58 | host = h.clone(); 59 | } 60 | } 61 | 62 | let mut roots = rustls::RootCertStore { 63 | roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), 64 | }; 65 | 66 | for certs in rustls_native_certs::load_native_certs().expect("could not load platform certs") { 67 | roots.add(certs).unwrap(); 68 | } 69 | 70 | let mut client_crypto = rustls::ClientConfig::builder() 71 | .with_root_certificates(roots) 72 | .with_no_client_auth(); 73 | 74 | client_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); 75 | 76 | // WAR for Windows endpoint 77 | let mut endpoint = if cfg!(target_os = "windows") { 78 | Endpoint::client("0.0.0.0:0".parse().unwrap()) 79 | } else { 80 | Endpoint::client("[::]:0".parse().unwrap()) 81 | }?; 82 | let client_config = 83 | quinn::ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?)); 84 | 85 | endpoint.set_default_client_config(client_config); 86 | 87 | let remote = Arc::::from(relay_addr); 88 | let host = Arc::::from(host); 89 | let endpoint = Arc::::from(endpoint); 90 | 91 | info!("listening on {}", listen_addr); 92 | 93 | let listener = TcpListener::bind(listen_addr).await?; 94 | 95 | while let Ok((inbound, _)) = listener.accept().await { 96 | info!("connection incoming"); 97 | 98 | let remote = Arc::clone(&remote); 99 | let host = Arc::clone(&host); 100 | let endpoint = Arc::clone(&endpoint); 101 | 102 | let transfer = transfer(remote, host, endpoint, inbound); 103 | tokio::spawn(transfer); 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | async fn transfer( 110 | remote: Arc, 111 | host: Arc, 112 | endpoint: Arc, 113 | mut inbound: TcpStream, 114 | ) -> Result<()> { 115 | let new_conn = endpoint 116 | .connect(*remote, &host)? 117 | .await 118 | .map_err(|e| { 119 | if e == ConnectionError::TimedOut { 120 | let socket = if cfg!(target_os = "windows") { 121 | std::net::UdpSocket::bind("0.0.0.0:0").unwrap() 122 | } else { 123 | std::net::UdpSocket::bind("[::]:0").unwrap() 124 | }; 125 | let addr = socket.local_addr().unwrap(); 126 | let ret = endpoint.rebind(socket); 127 | match ret { 128 | Ok(_) => { 129 | info!("rebinding to: {}", addr); 130 | } 131 | Err(e) => { 132 | error!("rebind fail: {:?}", e); 133 | } 134 | } 135 | } 136 | anyhow!("failed to connect: {:?}", e) 137 | }) 138 | .unwrap(); 139 | 140 | let (mut ri, mut wi) = inbound.split(); 141 | let (mut wo, mut ro) = new_conn 142 | .open_bi() 143 | .await 144 | .map_err(|e| anyhow!("failed to open stream: {:?}", e))?; 145 | 146 | let client_to_server = tokio::io::copy(&mut ri, &mut wo); 147 | let server_to_client = tokio::io::copy(&mut ro, &mut wi); 148 | 149 | try_join(client_to_server, server_to_client).await?; 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"]; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | pub mod common; 3 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::net::SocketAddr; 3 | use std::path::PathBuf; 4 | use std::sync::Arc; 5 | 6 | use anyhow::{Context, Result}; 7 | use dirs::home_dir; 8 | use env_logger::Builder; 9 | use futures::future::try_join; 10 | use futures::TryFutureExt; 11 | use log::LevelFilter; 12 | use log::{error, info}; 13 | use quinn_proto::crypto::rustls::QuicServerConfig; 14 | use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; 15 | use structopt::{self, StructOpt}; 16 | use tokio::net::TcpStream; 17 | 18 | mod args; 19 | mod common; 20 | 21 | #[derive(StructOpt, Debug)] 22 | #[structopt(name = "qtun-server")] 23 | struct Opt { 24 | /// TLS private key in PEM format 25 | #[structopt( 26 | parse(from_os_str), 27 | short = "k", 28 | long = "key", 29 | requires = "cert", 30 | default_value = "key.der" 31 | )] 32 | key: PathBuf, 33 | /// TLS certificate in PEM format 34 | #[structopt( 35 | parse(from_os_str), 36 | short = "c", 37 | long = "cert", 38 | requires = "key", 39 | default_value = "cert.der" 40 | )] 41 | cert: PathBuf, 42 | /// Enable stateless retries 43 | #[structopt(long = "stateless-retry")] 44 | stateless_retry: bool, 45 | /// Address to listen on 46 | #[structopt(long = "listen", default_value = "0.0.0.0:4433")] 47 | listen: SocketAddr, 48 | /// Address to listen on 49 | #[structopt(long = "relay", default_value = "127.0.0.1:8138")] 50 | relay: SocketAddr, 51 | /// Specify the host to load TLS certificates from ~/.acme.sh/host 52 | #[structopt(long = "acme-host")] 53 | acme_host: Option, 54 | /// Client address to block 55 | #[structopt(long = "block")] 56 | block: Option, 57 | /// Maximum number of concurrent connections to allow 58 | #[structopt(long = "connection-limit")] 59 | connection_limit: Option, 60 | } 61 | 62 | #[tokio::main] 63 | async fn main() -> Result<()> { 64 | let mut log_builder = Builder::new(); 65 | log_builder.filter(None, LevelFilter::Info).default_format(); 66 | log_builder.filter(Some("qtun-server"), LevelFilter::Debug); 67 | log_builder.init(); 68 | 69 | let options = Opt::from_args(); 70 | 71 | // init all parameters 72 | let mut cert_path = options.cert; 73 | let mut key_path = options.key; 74 | let mut acme_host = options.acme_host; 75 | let mut listen_addr = options.listen; 76 | let mut relay_addr = options.relay; 77 | 78 | // parse environment variables 79 | if let Ok((ss_local_addr, ss_remote_addr)) = args::parse_env_addr() { 80 | relay_addr = ss_local_addr; 81 | listen_addr = ss_remote_addr; 82 | } 83 | if let Ok(ss_plugin_opts) = args::parse_env_opts() { 84 | if let Some(cert) = ss_plugin_opts.get("cert") { 85 | cert_path = PathBuf::from(cert); 86 | } 87 | if let Some(key) = ss_plugin_opts.get("key") { 88 | key_path = PathBuf::from(key); 89 | } 90 | if let Some(host) = ss_plugin_opts.get("acme_host") { 91 | acme_host = Some(host.clone()); 92 | } 93 | } 94 | 95 | if let Some(host) = acme_host { 96 | key_path = PathBuf::new(); 97 | key_path.push(home_dir().unwrap_or_else(|| PathBuf::from("~"))); 98 | key_path.push(format!(".acme.sh/{a}/{a}.key", a = host)); 99 | 100 | cert_path = PathBuf::new(); 101 | cert_path.push(home_dir().unwrap_or_else(|| PathBuf::from("~"))); 102 | cert_path.push(format!(".acme.sh/{}/fullchain.cer", host)); 103 | } 104 | 105 | info!("loading cert: {:?}", cert_path); 106 | info!("loading key: {:?}", key_path); 107 | 108 | let key = fs::read(key_path.clone()).context("failed to read private key")?; 109 | let key = if key_path.extension().is_some_and(|x| x == "der") { 110 | PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) 111 | } else { 112 | rustls_pemfile::private_key(&mut &*key) 113 | .context("malformed PKCS #1 private key")? 114 | .ok_or_else(|| anyhow::Error::msg("no private keys found"))? 115 | }; 116 | let certs = fs::read(cert_path.clone()).context("failed to read certificate chain")?; 117 | let certs = if cert_path.extension().is_some_and(|x| x == "der") { 118 | vec![CertificateDer::from(certs)] 119 | } else { 120 | rustls_pemfile::certs(&mut &*certs) 121 | .collect::>() 122 | .context("invalid PEM-encoded certificate")? 123 | }; 124 | 125 | let mut server_crypto = rustls::ServerConfig::builder() 126 | .with_no_client_auth() 127 | .with_single_cert(certs, key)?; 128 | server_crypto.alpn_protocols = common::ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect(); 129 | 130 | let mut server_config = 131 | quinn::ServerConfig::with_crypto(Arc::new(QuicServerConfig::try_from(server_crypto)?)); 132 | let transport_config = Arc::get_mut(&mut server_config.transport).unwrap(); 133 | transport_config.max_concurrent_uni_streams(0_u8.into()); 134 | 135 | let remote = Arc::::from(relay_addr); 136 | 137 | let endpoint = quinn::Endpoint::server(server_config, listen_addr)?; 138 | eprintln!("listening on {}", endpoint.local_addr()?); 139 | 140 | while let Some(conn) = endpoint.accept().await { 141 | if options 142 | .connection_limit 143 | .is_some_and(|n| endpoint.open_connections() >= n) 144 | { 145 | info!("refusing due to open connection limit"); 146 | conn.refuse(); 147 | } else if Some(conn.remote_address()) == options.block { 148 | info!("refusing blocked client IP address"); 149 | conn.refuse(); 150 | } else if options.stateless_retry && !conn.remote_address_validated() { 151 | info!("requiring connection to validate its address"); 152 | conn.retry().unwrap(); 153 | } else { 154 | info!("accepting connection"); 155 | let fut = handle_connection(remote.clone(), conn); 156 | tokio::spawn(async move { 157 | if let Err(e) = fut.await { 158 | error!("connection failed: {reason}", reason = e.to_string()) 159 | } 160 | }); 161 | } 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | async fn handle_connection(remote: Arc, conn: quinn::Incoming) -> Result<()> { 168 | let bi_streams = conn.await?; 169 | 170 | async { 171 | info!("established"); 172 | 173 | // Each stream initiated by the client constitutes a new request. 174 | loop { 175 | // let (stream) = bi_streams.accept_bi().await 176 | let stream = match bi_streams.accept_bi().await { 177 | Err(quinn::ConnectionError::ApplicationClosed { .. }) => { 178 | info!("connection closed"); 179 | return Ok(()); 180 | } 181 | Err(e) => { 182 | return Err(e); 183 | } 184 | Ok(s) => s, 185 | }; 186 | tokio::spawn( 187 | transfer(remote.clone(), stream) 188 | .unwrap_or_else(move |e| error!("failed: {reason}", reason = e.to_string())), 189 | ); 190 | } 191 | } 192 | .await?; 193 | 194 | Ok(()) 195 | } 196 | 197 | async fn transfer( 198 | remote: Arc, 199 | inbound: (quinn::SendStream, quinn::RecvStream), 200 | ) -> Result<()> { 201 | let mut outbound = TcpStream::connect(remote.as_ref()).await?; 202 | 203 | let (mut wi, mut ri) = inbound; 204 | let (mut ro, mut wo) = outbound.split(); 205 | 206 | let client_to_server = tokio::io::copy(&mut ri, &mut wo); 207 | let server_to_client = tokio::io::copy(&mut ro, &mut wi); 208 | 209 | try_join(client_to_server, server_to_client).await?; 210 | 211 | Ok(()) 212 | } 213 | --------------------------------------------------------------------------------