Repository: rathole-org/rathole Branch: main Commit: 5a9dd6d93974 Files: 80 Total size: 249.8 KB Directory structure: gitextract_1djqu9jp/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── release.yml │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README-zh.md ├── README.md ├── benches/ │ └── scripts/ │ ├── http/ │ │ └── latency.sh │ └── mem/ │ ├── mem.sh │ └── plot.plt ├── build.rs ├── docs/ │ ├── benchmark.md │ ├── build-guide.md │ ├── img/ │ │ └── overview.excalidraw │ ├── internals.md │ ├── out-of-scope.md │ └── transport.md ├── examples/ │ ├── iperf3/ │ │ ├── client.toml │ │ └── server.toml │ ├── minimal/ │ │ ├── client.toml │ │ └── server.toml │ ├── noise_nk/ │ │ ├── client.toml │ │ └── server.toml │ ├── systemd/ │ │ ├── README.md │ │ ├── rathole@.service │ │ ├── ratholec.service │ │ ├── ratholec@.service │ │ ├── ratholes.service │ │ └── ratholes@.service │ ├── tls/ │ │ ├── client.toml │ │ ├── create_self_signed_cert.sh │ │ ├── identity.pfx │ │ ├── rootCA.crt │ │ ├── rootCA.key │ │ ├── server.crt │ │ ├── server.key │ │ └── server.toml │ ├── udp/ │ │ ├── client.toml │ │ └── server.toml │ ├── unified/ │ │ └── config.toml │ └── use_proxy/ │ └── client.toml ├── rust-toolchain ├── src/ │ ├── cli.rs │ ├── client.rs │ ├── config.rs │ ├── config_watcher.rs │ ├── constants.rs │ ├── helper.rs │ ├── lib.rs │ ├── main.rs │ ├── multi_map.rs │ ├── protocol.rs │ ├── server.rs │ └── transport/ │ ├── mod.rs │ ├── native_tls.rs │ ├── noise.rs │ ├── rustls.rs │ ├── tcp.rs │ └── websocket.rs └── tests/ ├── common/ │ └── mod.rs ├── config_test/ │ ├── invalid_config/ │ │ ├── missing_tls_client.toml │ │ ├── missing_tls_server.toml │ │ └── missing_tls_server2.toml │ └── valid_config/ │ └── full.toml ├── for_tcp/ │ ├── noise_transport.toml │ ├── tcp_transport.toml │ ├── tls_transport.toml │ ├── websocket_tls_transport.toml │ └── websocket_transport.toml ├── for_udp/ │ ├── noise_transport.toml │ ├── tcp_transport.toml │ ├── tls_transport.toml │ ├── websocket_tls_transport.toml │ └── websocket_transport.toml └── integration_test.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Directories /.git/ /.github/ /target/ /examples/ /docs/ /benches/ # Files .gitignore *.md LICENSE ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** **To Reproduce** Steps to reproduce the behavior: 1. 2. **Configuration** Configuration used to reproduce the behavior: **Logs** **Environment:** - OS: - `rathole --version` output: - CPU architecture: - rustc version: ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Ask for a new feature title: '' labels: enhancement assignees: '' --- **Feature Proposed** **Use Case** ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "*" workflow_dispatch: env: CARGO_TERM_COLOR: always jobs: release: name: Cross build for ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu exe: rathole cross: false - os: ubuntu-latest target: x86_64-unknown-linux-musl exe: rathole cross: false - os: ubuntu-latest target: aarch64-unknown-linux-musl exe: rathole cross: true - os: ubuntu-latest target: arm-unknown-linux-musleabi exe: rathole cross: true - os: ubuntu-latest target: arm-unknown-linux-musleabihf exe: rathole cross: true - os: ubuntu-latest target: armv7-unknown-linux-musleabihf exe: rathole cross: true - os: ubuntu-latest target: mips-unknown-linux-gnu exe: rathole cross: true - os: ubuntu-latest target: mips-unknown-linux-musl exe: rathole cross: true - os: ubuntu-latest target: mipsel-unknown-linux-gnu exe: rathole cross: true - os: ubuntu-latest target: mipsel-unknown-linux-musl exe: rathole cross: true - os: ubuntu-latest target: mips64-unknown-linux-gnuabi64 exe: rathole cross: true - os: ubuntu-latest target: mips64el-unknown-linux-gnuabi64 exe: rathole cross: true - os: macos-latest target: x86_64-apple-darwin exe: rathole cross: false - os: macos-latest target: aarch64-apple-darwin exe: rathole cross: false - os: windows-latest target: x86_64-pc-windows-msvc exe: rathole.exe cross: false steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal default: true - name: Install OpenSSL if: matrix.os == 'ubuntu-latest' run: sudo apt-get install pkg-config libssl-dev - name: Install OpenSSL if: matrix.os == 'macos-latest' run: brew install openssl@3 # Native build - name: Install target if: matrix.cross == false run: rustup target add ${{ matrix.target }} - name: Run tests if: matrix.cross == false && matrix.target != 'aarch64-apple-darwin' run: cargo test --release --target ${{ matrix.target }} --verbose - name: Build release if: matrix.cross == false run: cargo build --release --target ${{ matrix.target }} # Cross build - name: Install cross if: matrix.cross run: cargo install --version 0.2.5 cross - name: Run tests if: matrix.cross run: cross test --release --target ${{ matrix.target }} --verbose --features embedded --no-default-features - name: Build release if: matrix.cross run: cross build --release --target ${{ matrix.target }} --features embedded --no-default-features - name: Run UPX # Upx may not support some platforms. Ignore the errors continue-on-error: true # Disable upx for mips. See https://github.com/upx/upx/issues/387 if: matrix.os == 'ubuntu-latest' && !contains(matrix.target, 'mips') uses: crazy-max/ghaction-upx@v1 with: version: v4.0.2 files: target/${{ matrix.target }}/release/${{ matrix.exe }} args: -q --best --lzma - uses: actions/upload-artifact@v4 with: name: rathole-${{ matrix.target }} path: target/${{ matrix.target }}/release/${{ matrix.exe }} - name: Zip Release uses: TheDoctor0/zip-release@0.6.1 with: type: zip filename: rathole-${{ matrix.target }}.zip directory: target/${{ matrix.target }}/release/ path: ${{ matrix.exe }} - name: Publish uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: target/${{ matrix.target }}/release/rathole-${{ matrix.target }}.zip generate_release_notes: true draft: true docker: name: Publish to Docker Hub if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: release steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build uses: docker/build-push-action@v2 with: push: true platforms: linux/amd64, linux/arm64, linux/armhf, linux/armv7 tags: rapiz1/rathole:latest, rapiz1/rathole:${{ github.ref_name }} publish-crate: name: Publish to crates.io if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: release steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal - name: Publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }} run: cargo publish ================================================ FILE: .github/workflows/rust.yml ================================================ name: Rust on: pull_request: branches: ["*"] push: branches: ["main", "dev"] concurrency: # Documentation suggests ${{ github.head_ref }}, but that's only available on pull_request/pull_request_target triggers, so using ${{ github.ref }}. # On main, we want all builds to complete even if merging happens faster to make it easier to discover at which point something broke. group: ${{ github.ref == 'refs/heads/main' && format('ci-main-{0}', github.sha) || format('ci-{0}', github.ref) }} cancel-in-progress: true env: CARGO_TERM_COLOR: always jobs: lints: name: Lints runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal components: clippy - uses: Swatinem/rust-cache@v1 - name: Clippy run: cargo clippy -- -D warnings - name: Setup cargo-hack run: cargo install cargo-hack - name: Check all features run: > cargo hack check --feature-powerset --no-dev-deps --mutually-exclusive-features default,native-tls,websocket-native-tls,rustls,websocket-rustls build: name: Build for ${{ matrix.target }} runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest exe: rathole target: x86_64-unknown-linux-gnu - os: windows-latest exe: rathole.exe target: x86_64-pc-windows-msvc - os: macos-latest exe: rathole target: x86_64-apple-darwin - os: macos-latest exe: rathole target: aarch64-apple-darwin steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal - uses: Swatinem/rust-cache@v1 - name: Build run: cargo build - name: Run tests with native-tls run: cargo test --verbose - name: Run tests with rustls run: cargo test --verbose --no-default-features --features server,client,rustls,noise,websocket-rustls,hot-reload - uses: actions/upload-artifact@v4 with: name: rathole-${{ matrix.target }} path: target/debug/${{ matrix.exe }} ================================================ FILE: .gitignore ================================================ /target perf.data perf.data.old ================================================ FILE: .rustfmt.toml ================================================ imports_granularity = "module" ================================================ FILE: Cargo.toml ================================================ [package] name = "rathole" version = "0.5.0" edition = "2021" authors = ["Yujia Qiao "] description = "A reverse proxy for NAT traversal" license = "Apache-2.0" repository = "https://github.com/rapiz1/rathole" readme = "README.md" build = "build.rs" include = ["src/**/*", "LICENSE", "README.md", "build.rs"] [features] default = [ "server", "client", "native-tls", "noise", "websocket-native-tls", "hot-reload", ] # Run as a server server = [] # Run as a client client = [] # TLS support native-tls = ["tokio-native-tls"] rustls = [ "tokio-rustls", "rustls-pemfile", "rustls-native-certs", "p12", ] # Noise support noise = ["snowstorm", "base64"] # Websocket support websocket-native-tls = [ "tokio-tungstenite", "tokio-util", "futures-core", "futures-sink", "native-tls", ] websocket-rustls = [ "tokio-tungstenite", "tokio-util", "futures-core", "futures-sink", "rustls", ] # Configuration hot-reload support hot-reload = ["notify"] # Default feature releasing embedded devices # Cross-compiling with tls is hard. So we don't :( embedded = ["server", "client", "hot-reload", "noise"] # Feature to enable tokio-console. Disabled by default. # Don't enable it unless for debugging purposes. console = ["console-subscriber", "tokio/tracing"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.dev] panic = "abort" [profile.release] panic = "abort" lto = true codegen-units = 1 strip = true [profile.bench] debug = 1 [profile.minimal] inherits = "release" opt-level = "z" lto = true codegen-units = 1 [dependencies] tokio = { version = "1", features = ["full"] } bytes = { version = "1", features = ["serde"] } clap = { version = "3.0", features = ["derive"] } toml = "0.5" serde = { version = "1.0", features = ["derive"] } anyhow = "1.0" sha2 = "0.10" bincode = "1" lazy_static = "1.4" hex = "0.4" rand = "0.8" backoff = { version = "0.4", features = ["tokio"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } socket2 = { version = "0.4", features = ["all"] } fdlimit = "0.2" async-trait = "0.1" snowstorm = { version = "0.4", optional = true, features = [ "stream", ], default-features = false } base64 = { version = "0.13", optional = true } notify = { version = "5.0.0-pre.13", optional = true } console-subscriber = { version = "0.1", optional = true, features = [ "parking_lot", ] } atty = "0.2" async-http-proxy = { version = "1.2", features = [ "runtime-tokio", "basic-auth", ] } async-socks5 = "0.5" url = { version = "2.2", features = ["serde"] } tokio-tungstenite = { version = "0.20.1", optional = true } tokio-util = { version = "0.7.9", optional = true, features = ["io"] } futures-core = { version = "0.3.28", optional = true } futures-sink = { version = "0.3.28", optional = true } tokio-native-tls = { version = "0.3", optional = true } tokio-rustls = { version = "0.25", optional = true } rustls-native-certs = { version = "0.7", optional = true } rustls-pemfile = { version = "2.0", optional = true } p12 = { version = "0.6.3", optional = true } [target.'cfg(target_env = "musl")'.dependencies] openssl = { version = "0.10", features = ["vendored"], optional = true } [build-dependencies] vergen = { version = "7.4.2", default-features = false, features = [ "build", "git", "cargo", ] } anyhow = "1.0" ================================================ FILE: Dockerfile ================================================ FROM rust:bookworm as builder RUN apt update && apt install -y libssl-dev WORKDIR /home/rust/src COPY . . ARG FEATURES RUN cargo build --locked --release --features ${FEATURES:-default} RUN mkdir -p build-out/ RUN cp target/release/rathole build-out/ FROM gcr.io/distroless/cc-debian12 WORKDIR /app COPY --from=builder /home/rust/src/build-out/rathole . USER 1000:1000 ENTRYPOINT ["./rathole"] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README-zh.md ================================================ # rathole ![rathole-logo](./docs/img/rathole-logo.png) [![GitHub stars](https://img.shields.io/github/stars/rapiz1/rathole)](https://github.com/rapiz1/rathole/stargazers) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/rapiz1/rathole)](https://github.com/rapiz1/rathole/releases) ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/rapiz1/rathole/rust.yml?branch=main) [![GitHub all releases](https://img.shields.io/github/downloads/rapiz1/rathole/total)](https://github.com/rapiz1/rathole/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/rapiz1/rathole)](https://hub.docker.com/r/rapiz1/rathole) [English](README.md) | [简体中文](README-zh.md) 安全、稳定、高性能的内网穿透工具,用 Rust 语言编写 rathole,类似于 [frp](https://github.com/fatedier/frp) 和 [ngrok](https://github.com/inconshreveable/ngrok),可以让 NAT 后的设备上的服务通过具有公网 IP 的服务器暴露在公网上。 - [rathole](#rathole) - [Features](#features) - [Quickstart](#quickstart) - [Configuration](#configuration) - [Logging](#logging) - [Tuning](#tuning) - [Benchmark](#benchmark) - [Development Status](#development-status) ## Features - **高性能** 具有更高的吞吐量,高并发下更稳定。见[Benchmark](#benchmark) - **低资源消耗** 内存占用远低于同类工具。见[Benchmark](#benchmark)。[二进制文件最小](docs/build-guide.md)可以到 **~500KiB**,可以部署在嵌入式设备如路由器上。 - **安全性** 每个服务单独强制鉴权。Server 和 Client 负责各自的配置。使用 Noise Protocol 可以简单地配置传输加密,而不需要自签证书。同时也支持 TLS。 - **热重载** 支持配置文件热重载,动态修改端口转发服务。HTTP API 正在开发中。 ## Quickstart 一个全功能的 `rathole` 可以从 [release](https://github.com/rapiz1/rathole/releases) 页面下载。或者 [从源码编译](docs/build-guide.md) **获取其他平台和最小化的二进制文件**。 `rathole` 的使用和 frp 非常类似,如果你有后者的使用经验,那配置对你来说非常简单,区别只是转发服务的配置分离到了服务端和客户端,并且必须要设置 token。 使用 rathole 需要一个有公网 IP 的服务器,和一个在 NAT 或防火墙后的设备,其中有些服务需要暴露在互联网上。 假设你在家里的 NAT 后面有一个 NAS,并且想把它的 ssh 服务暴露在公网上: 1. 在有一个公网 IP 的服务器上 创建 `server.toml`,内容如下,并根据你的需要调整。 ```toml # server.toml [server] bind_addr = "0.0.0.0:2333" # `2333` 配置了服务端监听客户端连接的端口 [server.services.my_nas_ssh] token = "use_a_secret_that_only_you_know" # 用于验证的 token bind_addr = "0.0.0.0:5202" # `5202` 配置了将 `my_nas_ssh` 暴露给互联网的端口 ``` 然后运行: ```bash ./rathole server.toml ``` 2. 在 NAT 后面的主机(你的 NAS)上 创建 `client.toml`,内容如下,并根据你的需要进行调整。 ```toml # client.toml [client] remote_addr = "myserver.com:2333" # 服务器的地址。端口必须与 `server.bind_addr` 中的端口相同。 [client.services.my_nas_ssh] token = "use_a_secret_that_only_you_know" # 必须与服务器相同以通过验证 local_addr = "127.0.0.1:22" # 需要被转发的服务的地址 ``` 然后运行: ```bash ./rathole client.toml ``` 3. 现在 `rathole` 客户端会连接运行在 `myserver.com:2333`的 `rathole` 服务器,任何到 `myserver.com:5202` 的流量将被转发到客户端所在主机的 `22` 端口。 所以你可以 `ssh myserver.com:5202` 来 ssh 到你的 NAS。 [Systemd examples](./examples/systemd) 中提供了一些让 `rathole` 在 Linux 上作为后台服务运行的配置示例。 ## Configuration 如果只有一个 `[server]` 和 `[client]` 块存在的话,`rathole` 可以根据配置文件的内容自动决定在服务器模式或客户端模式下运行,就像 [Quickstart](#quickstart) 中的例子。 但 `[client]` 和 `[server]` 块也可以放在一个文件中。然后在服务器端,运行 `rathole --server config.toml`。在客户端,运行 `rathole --client config.toml` 来明确告诉 `rathole` 运行模式。 **推荐首先查看 [examples](./examples) 中的配置示例来快速理解配置格式**,如果有不清楚的地方再查阅完整配置格式。 关于如何配置 Noise Protocol 和 TLS 来进行加密传输,参见 [Transport](./docs/transport.md)。 下面是完整的配置格式。 ```toml [client] remote_addr = "example.com:2333" # Necessary. The address of the server default_token = "default_token_if_not_specify" # Optional. The default token of services, if they don't define their own ones heartbeat_timeout = 40 # Optional. Set to 0 to disable the application-layer heartbeat test. The value must be greater than `server.heartbeat_interval`. Default: 40 seconds retry_interval = 1 # Optional. The interval between retry to connect to the server. Default: 1 second [client.transport] # The whole block is optional. Specify which transport to use type = "tcp" # Optional. Possible values: ["tcp", "tls", "noise"]. Default: "tcp" [client.transport.tcp] # Optional. Also affects `noise` and `tls` proxy = "socks5://user:passwd@127.0.0.1:1080" # Optional. The proxy used to connect to the server. `http` and `socks5` is supported. nodelay = true # Optional. Override the `client.transport.nodelay` per service keepalive_secs = 20 # Optional. Specify `tcp_keepalive_time` in `tcp(7)`, if applicable. Default: 20 seconds keepalive_interval = 8 # Optional. Specify `tcp_keepalive_intvl` in `tcp(7)`, if applicable. Default: 8 seconds [client.transport.tls] # Necessary if `type` is "tls" trusted_root = "ca.pem" # Necessary. The certificate of CA that signed the server's certificate hostname = "example.com" # Optional. The hostname that the client uses to validate the certificate. If not set, fallback to `client.remote_addr` [client.transport.noise] # Noise protocol. See `docs/transport.md` for further explanation pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" # Optional. Default value as shown local_private_key = "key_encoded_in_base64" # Optional remote_public_key = "key_encoded_in_base64" # Optional [client.transport.websocket] # Necessary if `type` is "websocket" tls = true # If `true` then it will use settings in `client.transport.tls` [client.services.service1] # A service that needs forwarding. The name `service1` can change arbitrarily, as long as identical to the name in the server's configuration type = "tcp" # Optional. The protocol that needs forwarding. Possible values: ["tcp", "udp"]. Default: "tcp" token = "whatever" # Necessary if `client.default_token` not set local_addr = "127.0.0.1:1081" # Necessary. The address of the service that needs to be forwarded nodelay = true # Optional. Determine whether to enable TCP_NODELAY for data transmission, if applicable, to improve the latency but decrease the bandwidth. Default: true retry_interval = 1 # Optional. The interval between retry to connect to the server. Default: inherits the global config [client.services.service2] # Multiple services can be defined local_addr = "127.0.0.1:1082" [server] bind_addr = "0.0.0.0:2333" # Necessary. The address that the server listens for clients. Generally only the port needs to be change. default_token = "default_token_if_not_specify" # Optional heartbeat_interval = 30 # Optional. The interval between two application-layer heartbeat. Set to 0 to disable sending heartbeat. Default: 30 seconds [server.transport] # Same as `[client.transport]` type = "tcp" [server.transport.tcp] # Same as the client nodelay = true keepalive_secs = 20 keepalive_interval = 8 [server.transport.tls] # Necessary if `type` is "tls" pkcs12 = "identify.pfx" # Necessary. pkcs12 file of server's certificate and private key pkcs12_password = "password" # Necessary. Password of the pkcs12 file [server.transport.noise] # Same as `[client.transport.noise]` pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" local_private_key = "key_encoded_in_base64" remote_public_key = "key_encoded_in_base64" [server.transport.websocket] # Necessary if `type` is "websocket" tls = true # If `true` then it will use settings in `server.transport.tls` [server.services.service1] # The service name must be identical to the client side type = "tcp" # Optional. Same as the client `[client.services.X.type] token = "whatever" # Necessary if `server.default_token` not set bind_addr = "0.0.0.0:8081" # Necessary. The address of the service is exposed at. Generally only the port needs to be change. nodelay = true # Optional. Same as the client [server.services.service2] bind_addr = "0.0.0.1:8082" ``` ### Logging `rathole`,像许多其他 Rust 程序一样,使用环境变量来控制日志级别。 支持的 Logging Level 有 `info`, `warn`, `error`, `debug`, `trace` 比如将日志级别设置为 `error`: ```shell RUST_LOG=error ./rathole config.toml ``` 如果 `RUST_LOG` 不存在,默认的日志级别是 `info`。 ### Tuning 从 v0.4.7 开始, rathole 默认启用 TCP_NODELAY。这能够减少延迟并使交互式应用受益,比如 RDP,Minecraft 服务器。但它会减少一些带宽。 如果带宽更重要,比如网盘类应用,TCP_NODELAY 仍然可以通过配置 `nodelay = false` 关闭。 ## Benchmark rathole 的延迟与 [frp](https://github.com/fatedier/frp) 相近,在高并发情况下表现更好,能提供更大的带宽,内存占用更少。 关于测试进行的更多细节,参见单独页面 [Benchmark](./docs/benchmark.md)。 **但是,不要从这里得出结论,`rathole` 能让内网转发出来的服务快上数倍。** Benchmark 是在本地回环上进行的,其结果说明了任务受 CPU 限制时的结果。当用户的网络不是瓶颈时,用户能得到很大的提升。但是,对很多用户来说并不是这样。在这种情况下,`rathole` 能带来的主要好处是更少的资源占用,而带宽和延迟不一定有显著的改善。 ![http_throughput](./docs/img/http_throughput.svg) ![tcp_bitrate](./docs/img/tcp_bitrate.svg) ![udp_bitrate](./docs/img/udp_bitrate.svg) ![mem](./docs/img/mem-graph.png) ## Development Status `rathole` 正在积极开发中 - [x] 支持 TLS - [x] 支持 UDP - [x] 热重载 - [ ] 用于配置的 HTTP APIs [Out of Scope](./docs/out-of-scope.md) 列举了没有计划开发的特性并说明了原因。 ================================================ FILE: README.md ================================================ # rathole ![rathole-logo](./docs/img/rathole-logo.png) [![GitHub stars](https://img.shields.io/github/stars/rapiz1/rathole)](https://github.com/rapiz1/rathole/stargazers) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/rapiz1/rathole)](https://github.com/rapiz1/rathole/releases) ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/rapiz1/rathole/rust.yml?branch=main) [![GitHub all releases](https://img.shields.io/github/downloads/rapiz1/rathole/total)](https://github.com/rapiz1/rathole/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/rapiz1/rathole)](https://hub.docker.com/r/rapiz1/rathole) [![Join the chat at https://gitter.im/rapiz1/rathole](https://badges.gitter.im/rapiz1/rathole.svg)](https://gitter.im/rapiz1/rathole?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [English](README.md) | [简体中文](README-zh.md) A secure, stable and high-performance reverse proxy for NAT traversal, written in Rust rathole, like [frp](https://github.com/fatedier/frp) and [ngrok](https://github.com/inconshreveable/ngrok), can help to expose the service on the device behind the NAT to the Internet, via a server with a public IP. - [rathole](#rathole) - [Features](#features) - [Quickstart](#quickstart) - [Configuration](#configuration) - [Logging](#logging) - [Tuning](#tuning) - [Benchmark](#benchmark) - [Planning](#planning) ## Features - **High Performance** Much higher throughput can be achieved than frp, and more stable when handling a large volume of connections. See [Benchmark](#benchmark) - **Low Resource Consumption** Consumes much fewer memory than similar tools. See [Benchmark](#benchmark). [The binary can be](docs/build-guide.md) **as small as ~500KiB** to fit the constraints of devices, like embedded devices as routers. - **Security** Tokens of services are mandatory and service-wise. The server and clients are responsible for their own configs. With the optional Noise Protocol, encryption can be configured at ease. No need to create a self-signed certificate! TLS is also supported. - **Hot Reload** Services can be added or removed dynamically by hot-reloading the configuration file. HTTP API is WIP. ## Quickstart A full-powered `rathole` can be obtained from the [release](https://github.com/rapiz1/rathole/releases) page. Or [build from source](docs/build-guide.md) **for other platforms and minimizing the binary**. A [Docker image](https://hub.docker.com/r/rapiz1/rathole) is also available. The usage of `rathole` is very similar to frp. If you have experience with the latter, then the configuration is very easy for you. The only difference is that configuration of a service is split into the client side and the server side, and a token is mandatory. To use `rathole`, you need a server with a public IP, and a device behind the NAT, where some services that need to be exposed to the Internet. Assuming you have a NAS at home behind the NAT, and want to expose its ssh service to the Internet: 1. On the server which has a public IP Create `server.toml` with the following content and accommodate it to your needs. ```toml # server.toml [server] bind_addr = "0.0.0.0:2333" # `2333` specifies the port that rathole listens for clients [server.services.my_nas_ssh] token = "use_a_secret_that_only_you_know" # Token that is used to authenticate the client for the service. Change to an arbitrary value. bind_addr = "0.0.0.0:5202" # `5202` specifies the port that exposes `my_nas_ssh` to the Internet ``` Then run: ```bash ./rathole server.toml ``` 2. On the host which is behind the NAT (your NAS) Create `client.toml` with the following content and accommodate it to your needs. ```toml # client.toml [client] remote_addr = "myserver.com:2333" # The address of the server. The port must be the same with the port in `server.bind_addr` [client.services.my_nas_ssh] token = "use_a_secret_that_only_you_know" # Must be the same with the server to pass the validation local_addr = "127.0.0.1:22" # The address of the service that needs to be forwarded ``` Then run: ```bash ./rathole client.toml ``` 3. Now the client will try to connect to the server `myserver.com` on port `2333`, and any traffic to `myserver.com:5202` will be forwarded to the client's port `22`. So you can `ssh myserver.com:5202` to ssh to your NAS. To run `rathole` run as a background service on Linux, checkout the [systemd examples](./examples/systemd). ## Configuration `rathole` can automatically determine to run in the server mode or the client mode, according to the content of the configuration file, if only one of `[server]` and `[client]` block is present, like the example in [Quickstart](#quickstart). But the `[client]` and `[server]` block can also be put in one file. Then on the server side, run `rathole --server config.toml` and on the client side, run `rathole --client config.toml` to explicitly tell `rathole` the running mode. Before heading to the full configuration specification, it's recommend to skim [the configuration examples](./examples) to get a feeling of the configuration format. See [Transport](./docs/transport.md) for more details about encryption and the `transport` block. Here is the full configuration specification: ```toml [client] remote_addr = "example.com:2333" # Necessary. The address of the server default_token = "default_token_if_not_specify" # Optional. The default token of services, if they don't define their own ones heartbeat_timeout = 40 # Optional. Set to 0 to disable the application-layer heartbeat test. The value must be greater than `server.heartbeat_interval`. Default: 40 seconds retry_interval = 1 # Optional. The interval between retry to connect to the server. Default: 1 second [client.transport] # The whole block is optional. Specify which transport to use type = "tcp" # Optional. Possible values: ["tcp", "tls", "noise"]. Default: "tcp" [client.transport.tcp] # Optional. Also affects `noise` and `tls` proxy = "socks5://user:passwd@127.0.0.1:1080" # Optional. The proxy used to connect to the server. `http` and `socks5` is supported. nodelay = true # Optional. Determine whether to enable TCP_NODELAY, if applicable, to improve the latency but decrease the bandwidth. Default: true keepalive_secs = 20 # Optional. Specify `tcp_keepalive_time` in `tcp(7)`, if applicable. Default: 20 seconds keepalive_interval = 8 # Optional. Specify `tcp_keepalive_intvl` in `tcp(7)`, if applicable. Default: 8 seconds [client.transport.tls] # Necessary if `type` is "tls" trusted_root = "ca.pem" # Necessary. The certificate of CA that signed the server's certificate hostname = "example.com" # Optional. The hostname that the client uses to validate the certificate. If not set, fallback to `client.remote_addr` [client.transport.noise] # Noise protocol. See `docs/transport.md` for further explanation pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" # Optional. Default value as shown local_private_key = "key_encoded_in_base64" # Optional remote_public_key = "key_encoded_in_base64" # Optional [client.transport.websocket] # Necessary if `type` is "websocket" tls = true # If `true` then it will use settings in `client.transport.tls` [client.services.service1] # A service that needs forwarding. The name `service1` can change arbitrarily, as long as identical to the name in the server's configuration type = "tcp" # Optional. The protocol that needs forwarding. Possible values: ["tcp", "udp"]. Default: "tcp" token = "whatever" # Necessary if `client.default_token` not set local_addr = "127.0.0.1:1081" # Necessary. The address of the service that needs to be forwarded nodelay = true # Optional. Override the `client.transport.nodelay` per service retry_interval = 1 # Optional. The interval between retry to connect to the server. Default: inherits the global config [client.services.service2] # Multiple services can be defined local_addr = "127.0.0.1:1082" [server] bind_addr = "0.0.0.0:2333" # Necessary. The address that the server listens for clients. Generally only the port needs to be change. default_token = "default_token_if_not_specify" # Optional heartbeat_interval = 30 # Optional. The interval between two application-layer heartbeat. Set to 0 to disable sending heartbeat. Default: 30 seconds [server.transport] # Same as `[client.transport]` type = "tcp" [server.transport.tcp] # Same as the client nodelay = true keepalive_secs = 20 keepalive_interval = 8 [server.transport.tls] # Necessary if `type` is "tls" pkcs12 = "identify.pfx" # Necessary. pkcs12 file of server's certificate and private key pkcs12_password = "password" # Necessary. Password of the pkcs12 file [server.transport.noise] # Same as `[client.transport.noise]` pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" local_private_key = "key_encoded_in_base64" remote_public_key = "key_encoded_in_base64" [server.transport.websocket] # Necessary if `type` is "websocket" tls = true # If `true` then it will use settings in `server.transport.tls` [server.services.service1] # The service name must be identical to the client side type = "tcp" # Optional. Same as the client `[client.services.X.type] token = "whatever" # Necessary if `server.default_token` not set bind_addr = "0.0.0.0:8081" # Necessary. The address of the service is exposed at. Generally only the port needs to be change. nodelay = true # Optional. Same as the client [server.services.service2] bind_addr = "0.0.0.1:8082" ``` ### Logging `rathole`, like many other Rust programs, use environment variables to control the logging level. `info`, `warn`, `error`, `debug`, `trace` are available. ```shell RUST_LOG=error ./rathole config.toml ``` will run `rathole` with only error level logging. If `RUST_LOG` is not present, the default logging level is `info`. ### Tuning From v0.4.7, rathole enables TCP_NODELAY by default, which should benefit the latency and interactive applications like rdp, Minecraft servers. However, it slightly decreases the bandwidth. If the bandwidth is more important, TCP_NODELAY can be opted out with `nodelay = false`. ## Benchmark rathole has similar latency to [frp](https://github.com/fatedier/frp), but can handle a more connections, provide larger bandwidth, with less memory usage. For more details, see the separate page [Benchmark](./docs/benchmark.md). **However, don't take it from here that `rathole` can magically make your forwarded service faster several times than before.** The benchmark is done on local loopback, indicating the performance when the task is cpu-bounded. One can gain quite a improvement if the network is not the bottleneck. Unfortunately, that's not true for many users. In that case, the main benefit is lower resource consumption, while the bandwidth and the latency may not improved significantly. ![http_throughput](./docs/img/http_throughput.svg) ![tcp_bitrate](./docs/img/tcp_bitrate.svg) ![udp_bitrate](./docs/img/udp_bitrate.svg) ![mem](./docs/img/mem-graph.png) ## Planning - [ ] HTTP APIs for configuration [Out of Scope](./docs/out-of-scope.md) lists features that are not planned to be implemented and why. ================================================ FILE: benches/scripts/http/latency.sh ================================================ #!/bin/sh RATE="1 1000 2000 3000 4000" DURATION="60s" RATHOLE="http://127.0.0.1:5202" FRP="http://127.0.0.1:5203" echo warming up frp echo GET $FRP | vegeta attack -duration 10s > /dev/null for rate in $RATE; do name="frp-${rate}qps-$DURATION.bin" echo $name echo GET $FRP | vegeta attack -rate $rate -duration $DURATION > $name vegeta report $name done echo warming up rathole echo GET $RATHOLE | vegeta attack -duration 10s > /dev/null for rate in $RATE; do name="rathole-${rate}qps-$DURATION.bin" echo $name echo GET $RATHOLE | vegeta attack -rate $rate -duration $DURATION > $name vegeta report $name done ================================================ FILE: benches/scripts/mem/mem.sh ================================================ #!/bin/bash rm -v *-mem.log echo frp while true; do ps -C frpc -o rsz= >> frpc-mem.log sleep 1 done & while true; do ps -C frps -o rsz= >> frps-mem.log sleep 1 done & echo GET http://127.0.0.1:5203 | vegeta attack -duration 30s -rate 1000 > /dev/null sleep 10 kill $(jobs -p) echo rathole pid_s=$(ps aux | grep "rathole -s" | head -n 1 | awk '{print $2}') while true; do ps --pid $pid_s -o rsz= >> ratholec-mem.log sleep 1 done & pid_c=$(ps aux | grep "rathole -c" | head -n 1 | awk '{print $2}') while true; do ps --pid $pid_c -o rsz= >> ratholes-mem.log sleep 1 done & echo GET http://127.0.0.1:5202 | vegeta attack -duration 30s -rate 1000 > /dev/null sleep 10 kill $(jobs -p) gawk -i inplace '{print $1 "000"}' frpc-mem.log gawk -i inplace '{print $1 "000"}' frps-mem.log gawk -i inplace '{print $1 "000"}' ratholec-mem.log gawk -i inplace '{print $1 "000"}' ratholes-mem.log ================================================ FILE: benches/scripts/mem/plot.plt ================================================ set title "Memory Usage" font ",20" set term png small size 800,600 set key box outside set output "mem-graph.png" set ylabel "RSZ" set format y '%.0s%cB' set ytics nomirror set yrange [0:*] plot "frps-mem.log" using 1 with lines axes x1y1 title "frps RSZ", \ "frpc-mem.log" using 1 with lines axes x1y1 title "frpc RSZ", \ "ratholes-mem.log" using 1 with lines axes x1y1 title "ratholes RSZ", \ "ratholec-mem.log" using 1 with lines axes x1y1 title "ratholec RSZ" ================================================ FILE: build.rs ================================================ use anyhow::Result; use vergen::{vergen, Config, SemverKind}; fn main() -> Result<()> { let mut config = Config::default(); // Change the SEMVER output to the lightweight variant *config.git_mut().semver_kind_mut() = SemverKind::Lightweight; // Add a `-dirty` flag to the SEMVER output *config.git_mut().semver_dirty_mut() = Some("-dirty"); // Generate the instructions if let Err(e) = vergen(config) { eprintln!("error occurred while generating instructions: {:?}", e); let mut config = Config::default(); *config.git_mut().enabled_mut() = false; vergen(config) } else { Ok(()) } } ================================================ FILE: docs/benchmark.md ================================================ # Benchmark > Date: 2021/12/28 > > Version: commit 1180c7e538564efd69742f22e77453a1b74a5ed2 > > Arch Linux with 5.15.11-arch2-1 kernel > > Intel Xeon CPU E5-2620 @ 2.00GHz *2 > > 16GB RAM ## Bandwidth ![tcp_bitrate](./img/tcp_bitrate.svg) ![udp_bitrate](./img/udp_bitrate.svg) rathole with the following configuration: ```toml [client] remote_addr = "localhost:2333" default_token = "123" [client.services.bench-tcp] local_addr = "127.0.0.1:80" [client.services.bench-udp] type = "udp" local_addr = "127.0.0.1:80" [server] bind_addr = "0.0.0.0:2333" default_token = "123" [server.services.bench-tcp] bind_addr = "0.0.0.0:5202" [server.services.bench-udp] type = "udp" bind_addr = "0.0.0.0:5202" ``` frp 0.38.0 with the following configuration: ```ini [common] bind_port = 7000 authentication_method = token token = 1233 ``` ```ini # frpc.ini [common] server_addr = 127.0.0.1 server_port = 7000 authentication_method = token token = 1233 [bench-tcp] type = tcp local_ip = 127.0.0.1 local_port = 80 remote_port = 5203 [bench-udp] type = udp local_ip = 127.0.0.1 local_port = 80 remote_port = 5203 ``` ``` $ iperf3 -v iperf 3.10.1 (cJSON 1.7.13) Linux sig 5.15.7-arch1-1 #1 SMP PREEMPT Wed, 08 Dec 2021 14:33:16 +0000 x86_64 Optional features available: CPU affinity setting, IPv6 flow label, TCP congestion algorithm setting, sendfile / zerocopy, socket pacing, authentication, bind to device, support IPv4 don't fragment $ sudo iperf3 -s -p 80 ``` For rathole benchmark: ``` $ iperf3 -c 127.0.0.1 -p 5202 ``` For frp benchmark: ``` $ iperf3 -c 127.0.0.1 -p 5203 ``` ## HTTP nginx/1.20.2 listens on port 80, with the default test page. frp and rathole configuration is same with the previous section. [vegeta](https://github.com/tsenart/vegeta) is used to generate HTTP load. ### HTTP Throughput The following commands are used to benchmark rathole and frp. Note that if you want to do a benchmark yourself, `-max-workers` should be adjusted to get the accurate results for your machine. ``` echo 'GET http://127.0.0.1:5203' | vegeta attack -rate 0 -duration 30s -max-workers 48 echo 'GET http://127.0.0.1:5202' | vegeta attack -rate 0 -duration 30s -max-workers 48 ``` ![http_throughput](./img/http_throughput.svg) ### HTTP Latency `rathole` has very similar latency to `frp`, but can handle more connections Here's a table, latency is in ms |QPS|latency(rathole)|latency(frp)| |--|--|---| |1|2.113|2.55| |1000|1.723|1.742| |2000|1.845|1.749| |3000|2.064|2.011| |4000|2.569|7907| As you can see, for QPS from 1 to 3000, rathole and frp have nearly identical latency. But with QPS of 4000, frp starts reporting lots of errors and the latency grows to even seconds. This kind of reflects the throughput in the previous section. Thus, in terms of latency, rathole and frp are nearly the same. But rathole can handle more connections. [Script to benchmark latency](../benches/scripts/http/latency.sh) ## Memory Usage ![mem](./img/mem-graph.png) The graph shows the memory usage of frp and rathole when `vegeta attack -duration 30s -rate 1000` is executed. rathole uses much less memory than frp. [Script to benchmark memory](../benches/scripts/mem/mem.sh) ================================================ FILE: docs/build-guide.md ================================================ # Build Guide This is for those who want to build `rathole` themselves, possibly because the need of latest features or the minimal binary size. ## Build To use default build settings, run: ```sh cargo build --release ``` You may need to pre-install [openssl](https://docs.rs/openssl/latest/openssl/index.html) dependencies in Unix-like systems. ## Customize the Build `rathole` comes with lots of *crate features* that determine whether a certain feature will be compiled or not. Supported features can be checked out in `[features]` of [Cargo.toml](../Cargo.toml). For example, to build `rathole` with the `client` and `noise` feature: ```sh cargo build --release --no-default-features --features client,noise ``` ## Rustls Support `rathole` provides optional `rustls` support. It's an almost drop-in replacement of `native-tls` support. (See [Transport](transport.md) for more information.) To enable this, disable the default features and enable `rustls` feature. And for websocket feature, enable `websocket-rustls` feature as well. You can also use command line option for this. For example, to replace all default features with `rustls`: ```sh cargo build --release --no-default-features --features server,client,rustls,noise,websocket-rustls,hot-reload ``` Feature `rustls` and `websocket-rustls` cannot be enabled with `native-tls` and `websocket-native-tls` at the same time, as they are mutually exclusive. Enabling both will result in a compile error. (Note that default features contains `native-tls` and `websocket-native-tls`.) ## Minimalize the binary 1. Build with the `minimal` profile The `release` build profile optimize for the program running time, not the binary size. However, the `minimal` profile enables lots of optimization for the binary size to produce a much smaller binary. For example, to build `rathole` with `client` feature with the `minimal` profile: ```sh cargo build --profile minimal --no-default-features --features client ``` 2. `strip` and `upx` The binary that step 1 produces can be even smaller, by using `strip` and `upx` to remove the symbols and compress the binary. Like: ```sh strip rathole upx --best --lzma rathole ``` At the time of writting the build guide, the produced binary for `x86_64-unknown-linux-glibc` has the size of **574 KiB**, while `frpc` has the size of **~10 MiB**, which is much larger. ================================================ FILE: docs/img/overview.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", "version": 127, "versionNonce": 80643966, "isDeleted": false, "id": "_ROJe0KCjbnKQjLDcc-Ag", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 274.66668701171875, "y": 87.49995422363281, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 450.66668701171875, "height": 208.66667175292972, "seed": 1939336259, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "1Sorez2zxxKqyRilx21-m", "uJx77oj5eyZPw61wszaJN" ], "updated": 1639393963541 }, { "type": "text", "version": 248, "versionNonce": 1524512610, "isDeleted": false, "id": "X-BwNQGYSBy-tPINiKBCt", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 283.33331298828125, "y": 94.50007629394531, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 61, "height": 25, "seed": 429932333, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "updated": 1639393963541, "fontSize": 20, "fontFamily": 1, "text": "Server", "baseline": 18, "textAlign": "left", "verticalAlign": "top" }, { "type": "ellipse", "version": 166, "versionNonce": 1926031294, "isDeleted": false, "id": "5KLQ8EXnY3KjzuLRGbhJU", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 873.3333129882812, "y": 151.5, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 37.33331298828125, "height": 34.66667175292969, "seed": 565619875, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "-lU_z4mfDB58ZiJ8HlTxY" ], "updated": 1639393963541 }, { "type": "line", "version": 112, "versionNonce": 447019810, "isDeleted": false, "id": "ViC_qO7r1ED1cN1IlPe7s", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 892.6666259765625, "y": 188.16668701171875, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 0, "height": 34, "seed": 1032403459, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963541, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ 0, 34 ] ] }, { "type": "line", "version": 86, "versionNonce": 177553406, "isDeleted": false, "id": "8SCEaNme89qCxY-xAS0it", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 890, "y": 199.5, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 24, "height": 18.66668701171875, "seed": 773580109, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963541, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -24, 18.66668701171875 ] ] }, { "type": "line", "version": 110, "versionNonce": 73221858, "isDeleted": false, "id": "Kbl62J0jyfWlbTEVgMqJH", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 895.3333129882812, "y": 197.5, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 24, "height": 18, "seed": 464452045, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963541, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ 24, 18 ] ] }, { "type": "line", "version": 130, "versionNonce": 1881706558, "isDeleted": false, "id": "PRHAdurETJSYCaa5l6Iwa", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 892, "y": 222.16668701171875, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 14.66668701171875, "height": 25.33331298828125, "seed": 1595489411, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963542, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ -14.66668701171875, 25.33331298828125 ] ] }, { "type": "line", "version": 162, "versionNonce": 1888885410, "isDeleted": false, "id": "HkWBRTjPa-LTKYPbw8XS-", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 894, "y": 223.5, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 17.630663207545922, "height": 23.561635782942176, "seed": 1412110733, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963542, "startBinding": null, "endBinding": null, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": null, "points": [ [ 0, 0 ], [ 17.630663207545922, 23.561635782942176 ] ] }, { "type": "rectangle", "version": 307, "versionNonce": 1975983586, "isDeleted": false, "id": "2GauISsAXxBxXaARLdO2v", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 277.6666564941406, "y": 419.99999237060547, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 442, "height": 132.0000152587891, "seed": 1008142253, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [], "updated": 1639394067925 }, { "type": "text", "version": 375, "versionNonce": 69970238, "isDeleted": false, "id": "4t6IqDCz_2ovUHEWf3VyP", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 287.16668701171875, "y": 428.4999084472656, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 54, "height": 25, "seed": 1136307299, "groupIds": [], "strokeSharpness": "sharp", "boundElementIds": [ "uJx77oj5eyZPw61wszaJN" ], "updated": 1639394067925, "fontSize": 20, "fontFamily": 1, "text": "Client", "baseline": 18, "textAlign": "left", "verticalAlign": "top" }, { "type": "text", "version": 406, "versionNonce": 1298876478, "isDeleted": false, "id": "i-iOOSRyBhiISzIY5AG2O", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 650, "y": 135.50001525878906, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 72, "height": 40, "seed": 1004543373, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [ "1Sorez2zxxKqyRilx21-m" ], "updated": 1639394028864, "fontSize": 16, "fontFamily": 1, "text": "service1\nbind addr", "baseline": 34, "textAlign": "left", "verticalAlign": "top" }, { "type": "text", "version": 471, "versionNonce": 437130622, "isDeleted": false, "id": "Lld8m5f8AeGoMRmkfryGK", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 650, "y": 246.1667022705078, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 72, "height": 40, "seed": 1760597182, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [ "1Sorez2zxxKqyRilx21-m" ], "updated": 1639394090709, "fontSize": 16, "fontFamily": 1, "text": "service2\nbind addr", "baseline": 34, "textAlign": "left", "verticalAlign": "top" }, { "type": "text", "version": 212, "versionNonce": 1840891362, "isDeleted": false, "id": "5io-dv6h3U5ORt9DXFq37", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 308.66668701171875, "y": 250.83334350585938, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 72, "height": 40, "seed": 1771953379, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [ "wLQ-nby5mNnwfX9LnFrEt" ], "updated": 1639393963542, "fontSize": 16, "fontFamily": 1, "text": "server\nbind addr", "baseline": 34, "textAlign": "center", "verticalAlign": "top" }, { "type": "arrow", "version": 228, "versionNonce": 1728857058, "isDeleted": false, "id": "-lU_z4mfDB58ZiJ8HlTxY", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 870.7065228655306, "y": 194.87651239705974, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 129.96929122242796, "height": 1.4508687081649327, "seed": 585847683, "groupIds": [], "strokeSharpness": "round", "boundElementIds": [], "updated": 1639393963543, "startBinding": { "elementId": "5KLQ8EXnY3KjzuLRGbhJU", "focus": -1.5107931785090518, "gap": 15.791401051287782 }, "endBinding": { "elementId": "mR2qjxJFdOso9NGgCoq4h", "focus": 0.3208591338543321, "gap": 10.737231643102632 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ -129.96929122242796, -1.4508687081649327 ] ] }, { "id": "2DQbzxVigt_dM1muvXWYN", "type": "text", "x": 872.8333740234375, "y": 127, "width": 49, "height": 20, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1110579390, "version": 21, "versionNonce": 974179198, "isDeleted": false, "boundElementIds": null, "updated": 1639393963543, "text": "visitor", "fontSize": 16, "fontFamily": 1, "textAlign": "center", "verticalAlign": "top", "baseline": 14 }, { "id": "LU4D6A2Ugd1V9uKE6SwOC", "type": "arrow", "x": 696.2380319060499, "y": 189.44727288880773, "width": 134.59847736695497, "height": 6.61671213194353, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 275565346, "version": 351, "versionNonce": 481916706, "isDeleted": false, "boundElementIds": null, "updated": 1639393963543, "points": [ [ 0, 0 ], [ -134.59847736695497, 6.61671213194353 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "mR2qjxJFdOso9NGgCoq4h", "focus": 0.0597242207313621, "gap": 9.095342117387645 }, "endBinding": { "elementId": "NzpaVP1cgsvfg6KfdD99G", "focus": 0.1507396149689704, "gap": 3.4879322512409843 }, "startArrowhead": null, "endArrowhead": null }, { "id": "NzpaVP1cgsvfg6KfdD99G", "type": "diamond", "x": 372.66668701171875, "y": 152.5, "width": 184, "height": 84, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 891739170, "version": 42, "versionNonce": 1136678334, "isDeleted": false, "boundElementIds": [ "LU4D6A2Ugd1V9uKE6SwOC", "xm8fFB4fOVowURVtFEyfx", "E-k0fg9CKUsCbBcIFgpQN", "iqQRk3oncpFlTohh4RxWf", "S1o9eYMClf4Mrmfw9HlDs", "nbIlU5kICCXoOhMWP1aoq", "4mPQElLVeuU0MBB9zyNTL" ], "updated": 1639394052440 }, { "id": "6Ym2F9bT0rNpkiLbkm6Ku", "type": "text", "x": 414.6666564941406, "y": 185.50003051757812, "width": 112, "height": 20, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 162216574, "version": 33, "versionNonce": 199316542, "isDeleted": false, "boundElementIds": null, "updated": 1639393963543, "text": "rathole server", "fontSize": 16, "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", "baseline": 14 }, { "id": "72LJc8JYfizCW-59n-YiJ", "type": "diamond", "x": 313.9999694824219, "y": 456.83331298828125, "width": 172.66668701171875, "height": 74.66668701171876, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 627082402, "version": 203, "versionNonce": 1475968062, "isDeleted": false, "boundElementIds": [ "xm8fFB4fOVowURVtFEyfx", "CYPbqJ97T4dK8aTY2NoA6", "xQjRQnu2M-Lx4L_FApAWi", "ZBwjcWgJYIRx-XieGSul2", "DjwSuFQtjGNkkF4rl7myd", "8qillKpd5VKO0hrasQMVX" ], "updated": 1639394068076 }, { "id": "mR2qjxJFdOso9NGgCoq4h", "type": "rectangle", "x": 705.3333740234375, "y": 176.83335876464844, "width": 24.6666259765625, "height": 24.6666259765625, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1143103778, "version": 45, "versionNonce": 1343517282, "isDeleted": false, "boundElementIds": [ "-lU_z4mfDB58ZiJ8HlTxY", "LU4D6A2Ugd1V9uKE6SwOC" ], "updated": 1639393963543 }, { "id": "rp7H2PQFGWvQJIbz1y8IG", "type": "rectangle", "x": 705.3333740234375, "y": 220.83335876464844, "width": 24.6666259765625, "height": 24.6666259765625, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 764034658, "version": 77, "versionNonce": 1528280446, "isDeleted": false, "boundElementIds": [ "-lU_z4mfDB58ZiJ8HlTxY", "LU4D6A2Ugd1V9uKE6SwOC", "n9WWKSJRRhkFG2L3AY6W_", "4mPQElLVeuU0MBB9zyNTL" ], "updated": 1639394052440 }, { "id": "5H4DUHb4ELZWIIpXO32ix", "type": "rectangle", "x": 385.3333740234375, "y": 284.16668701171875, "width": 22, "height": 22, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1387104638, "version": 257, "versionNonce": 947580094, "isDeleted": false, "boundElementIds": [ "uJx77oj5eyZPw61wszaJN", "CYPbqJ97T4dK8aTY2NoA6", "E-k0fg9CKUsCbBcIFgpQN", "iqQRk3oncpFlTohh4RxWf", "xQjRQnu2M-Lx4L_FApAWi", "ZBwjcWgJYIRx-XieGSul2", "S1o9eYMClf4Mrmfw9HlDs", "nbIlU5kICCXoOhMWP1aoq" ], "updated": 1639393977422 }, { "id": "1-O9JOrs2pnONGNtZiH4B", "type": "text", "x": 349.1666564941406, "y": 482.8332824707031, "width": 105, "height": 20, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1412742590, "version": 106, "versionNonce": 541336190, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "text": "rathole client", "fontSize": 16, "fontFamily": 1, "textAlign": "center", "verticalAlign": "middle", "baseline": 14 }, { "id": "CYPbqJ97T4dK8aTY2NoA6", "type": "arrow", "x": 400.5230856224749, "y": 452.05962166754307, "width": 3.021429201284718, "height": 139.89293465582432, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 941766434, "version": 627, "versionNonce": 840863074, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "points": [ [ 0, 0 ], [ -3.021429201284718, -139.89293465582432 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "72LJc8JYfizCW-59n-YiJ", "focus": 0.012722437706498701, "gap": 4.777461928511805 }, "endBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": -0.07128881792747094, "gap": 6 }, "startArrowhead": null, "endArrowhead": null }, { "id": "iqQRk3oncpFlTohh4RxWf", "type": "arrow", "x": 411.88175007980965, "y": 278.4142786269854, "width": 41.1875896678809, "height": 38.07827569869187, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 316085374, "version": 241, "versionNonce": 705621374, "isDeleted": false, "boundElementIds": null, "updated": 1639393963544, "points": [ [ 0, 0 ], [ 41.1875896678809, -38.07827569869187 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": -0.111311585930796, "gap": 7.333343505859375 }, "endBinding": { "elementId": "NzpaVP1cgsvfg6KfdD99G", "focus": -0.4128416678755755, "gap": 8.3058554409374 }, "startArrowhead": null, "endArrowhead": null }, { "id": "xQjRQnu2M-Lx4L_FApAWi", "type": "arrow", "x": 415.03538422542744, "y": 453.3711527236708, "width": 10.189618736042235, "height": 139.87115272367078, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 419928446, "version": 136, "versionNonce": 325751074, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "points": [ [ 0, 0 ], [ -10.189618736042235, -139.87115272367078 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "72LJc8JYfizCW-59n-YiJ", "focus": 0.20468988783315253, "gap": 9.013184853545113 }, "endBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": -0.6081345501761519, "gap": 7.33331298828125 }, "startArrowhead": null, "endArrowhead": null }, { "id": "ZBwjcWgJYIRx-XieGSul2", "type": "arrow", "x": 381.63001651123903, "y": 456.0383959625252, "width": 2.158517689316966, "height": 145.46817028787365, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 39236898, "version": 133, "versionNonce": 1006640354, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "points": [ [ 0, 0 ], [ 2.158517689316966, -145.46817028787365 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "72LJc8JYfizCW-59n-YiJ", "focus": -0.22319845234228697, "gap": 8.153168060821436 }, "endBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": 1.1032903390278315, "gap": 4.666656494140625 }, "startArrowhead": null, "endArrowhead": null }, { "id": "S1o9eYMClf4Mrmfw9HlDs", "type": "arrow", "x": 418.66668701171875, "y": 287.5, "width": 38.666656494140625, "height": 43.33331298828125, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 247933282, "version": 30, "versionNonce": 1800972706, "isDeleted": false, "boundElementIds": null, "updated": 1639393973403, "points": [ [ 0, 0 ], [ 38.666656494140625, -43.33331298828125 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": 0.7442699983400397, "gap": 11.33331298828125 }, "endBinding": { "elementId": "NzpaVP1cgsvfg6KfdD99G", "focus": -0.4020068751542848, "gap": 10.019774658829391 }, "startArrowhead": null, "endArrowhead": null }, { "id": "nbIlU5kICCXoOhMWP1aoq", "type": "arrow", "x": 403.3333435058594, "y": 273.5, "width": 44, "height": 37.33331298828125, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 261563746, "version": 26, "versionNonce": 1384498722, "isDeleted": false, "boundElementIds": null, "updated": 1639393977422, "points": [ [ 0, 0 ], [ 44, -37.33331298828125 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "5H4DUHb4ELZWIIpXO32ix", "focus": -0.7734750559093652, "gap": 10.66668701171875 }, "endBinding": { "elementId": "NzpaVP1cgsvfg6KfdD99G", "focus": -0.6418956116222861, "gap": 6.895194123821575 }, "startArrowhead": null, "endArrowhead": null }, { "id": "7N39v3qK0fltyhClnuI_Q", "type": "ellipse", "x": 582.0000305175781, "y": 444.16668701171875, "width": 112.6666259765625, "height": 47.33331298828125, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1272534334, "version": 116, "versionNonce": 1964223486, "isDeleted": false, "boundElementIds": [ "DjwSuFQtjGNkkF4rl7myd" ], "updated": 1639394068076 }, { "id": "THrrqy4Axfy1vlF2wrI9s", "type": "ellipse", "x": 582.0000305175781, "y": 498.8333740234375, "width": 112.6666259765625, "height": 47.33331298828125, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 614595234, "version": 142, "versionNonce": 909853822, "isDeleted": false, "boundElementIds": [ "8qillKpd5VKO0hrasQMVX" ], "updated": 1639394068076 }, { "id": "WyAj01yc3DnhvWQG7tHd9", "type": "text", "x": 605.8333435058594, "y": 457.8333435058594, "width": 65, "height": 20, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 2040747710, "version": 45, "versionNonce": 172071906, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "text": "service 1", "fontSize": 16, "fontFamily": 1, "textAlign": "center", "verticalAlign": "middle", "baseline": 14 }, { "id": "CDfW7H0EVISeS0Zugsf8W", "type": "text", "x": 600.9999694824219, "y": 512.5000305175781, "width": 72, "height": 20, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "sharp", "seed": 1292334754, "version": 74, "versionNonce": 1763707710, "isDeleted": false, "boundElementIds": null, "updated": 1639394067926, "text": "service 2", "fontSize": 16, "fontFamily": 1, "textAlign": "center", "verticalAlign": "middle", "baseline": 14 }, { "id": "DjwSuFQtjGNkkF4rl7myd", "type": "arrow", "x": 493.3333435058594, "y": 489.0001220703125, "width": 80.49465291276579, "height": 19.591364584118082, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1489129058, "version": 177, "versionNonce": 1120784162, "isDeleted": false, "boundElementIds": null, "updated": 1639394068076, "points": [ [ 0, 0 ], [ 80.49465291276579, -19.591364584118082 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "72LJc8JYfizCW-59n-YiJ", "focus": 0.46790554502387516, "gap": 7.388222614736868 }, "endBinding": { "elementId": "7N39v3qK0fltyhClnuI_Q", "focus": 0.5164042977199623, "gap": 8.24035262874532 }, "startArrowhead": null, "endArrowhead": null }, { "id": "8qillKpd5VKO0hrasQMVX", "type": "arrow", "x": 497.3333435058594, "y": 496.33331298828125, "width": 78, "height": 22, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1551722530, "version": 144, "versionNonce": 1536814754, "isDeleted": false, "boundElementIds": null, "updated": 1639394068076, "points": [ [ 0, 0 ], [ 78, 22 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "72LJc8JYfizCW-59n-YiJ", "focus": -0.6747942752141096, "gap": 6.222408426625634 }, "endBinding": { "elementId": "THrrqy4Axfy1vlF2wrI9s", "focus": -0.4771879886646304, "gap": 7.177668745668626 }, "startArrowhead": null, "endArrowhead": null }, { "id": "n9WWKSJRRhkFG2L3AY6W_", "type": "arrow", "x": 876.3054169557988, "y": 206.15009644516743, "width": 139.63872994408007, "height": 28.349903554832565, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1631704318, "version": 50, "versionNonce": 1307848610, "isDeleted": false, "boundElementIds": null, "updated": 1639394048593, "points": [ [ 0, 0 ], [ -139.63872994408007, 28.349903554832565 ] ], "lastCommittedPoint": null, "startBinding": null, "endBinding": { "elementId": "rp7H2PQFGWvQJIbz1y8IG", "focus": 0.3498468485388594, "gap": 6.66668701171875 }, "startArrowhead": null, "endArrowhead": "arrow" }, { "id": "4mPQElLVeuU0MBB9zyNTL", "type": "arrow", "x": 696.6666870117188, "y": 231.83334350585938, "width": 140.66668701171875, "height": 30.666656494140625, "angle": 0, "strokeColor": "#000000", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "strokeSharpness": "round", "seed": 1042183102, "version": 16, "versionNonce": 392654114, "isDeleted": false, "boundElementIds": null, "updated": 1639394053746, "points": [ [ 0, 0 ], [ -140.66668701171875, -30.666656494140625 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "rp7H2PQFGWvQJIbz1y8IG", "focus": -0.21600645731035134, "gap": 8.66668701171875 }, "endBinding": { "elementId": "NzpaVP1cgsvfg6KfdD99G", "focus": -0.31535312984667163, "gap": 5.787735184532465 }, "startArrowhead": null, "endArrowhead": null } ], "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" }, "files": {} } ================================================ FILE: docs/internals.md ================================================ # Internals ![overview](./img/overview.png) ## Conceptions ### Service The entity whose traffic needs to be forwarded ### Server The host that runs `rathole` in the server mode ### Client The host behind the NAT that runs `rathole` in the client mode. It has some services that need to be forwarded. ### Visitor Who visists a *service*, via the *server* ### Control Channel A control channel is a TCP connection between the *server* and the *client* that only carries `rathole` control commands for one *service*. ### Data Channel A data channel is a TCP connection between the *server* and the *client* that only carries the encapsulated data that needs forwarding for one *service*. ## The Process *TODO: Add more details about the protocol* When `rathole` starts in the client mode, it creates connections to `server.common.bind_addr` for each service. These connection acts as control channels. When a control channel starts, the server challenge the client by a nonce, the client is required to authenticate as the service it wants to represent. Then the forwarding of that service is set up. When the server accepts a connection on a service's `bind_port`, it sends a control command to the client via the corresponding control channel. Then the client connects to the server to create a data channel. In this way, a forwarding is set up. The server also creates a few data channels in advance to improve the latency. ================================================ FILE: docs/out-of-scope.md ================================================ # Out of Scope `rathole` focuses on the forwarding for the NAT traversal, rather than being a all-in-one development tool or a load balancer or a gateway. It's designed to *be used with them*, not *replace them*. But that doesn't mean it's not useful for other purposes. In the future, more configuration APIs will be added and `rathole` can be used with an external dashboard. > Make each program do one thing well. - *Domain based forwarding for HTTP* Introducing these kind of features into `rathole` itself ultimately reinvent a nginx. Use nginx to do this and set `rathole` as the upstream. This method achieves better performance as well as flexibility. - *HTTP Request Logging* `rathole` doesn't interference with the application layer traffic. A right place for this kind of stuff is the web server, and a network capture tool. - *`frp`'s STCP or other setup that requires visitors' side configuration* If that kind of setup is possible, then there are a lot more tools available. You may want to consider secure tunnels like wireguard or zerotier. `rathole` primarily focuses on NAT traversal by forwarding, which doesn't require any setup for visitors. - *Caching `local_ip`'s DNS records* As responded in [issue #183](https://github.com/rapiz1/rathole/issues/183), `local_ip` cache is not feasible because we have no reliable way to detect ip change. Handle DNS TTL and so on should be done with a DNS server, not a client. Caching ip is generally dangerous for clients. If you care about the `local_ip` query you can set up a local DNS server and enable caching. Then the local lookup should be trivial. ================================================ FILE: docs/transport.md ================================================ # Security By default, `rathole` forwards traffic as it is. Different options can be enabled to secure the traffic. ## TLS Checkout the [example](../examples/tls) ### Client Normally, a self-signed certificate is used. In this case, the client needs to trust the CA. `trusted_root` is the path to the root CA's certificate PEM file. `hostname` is the hostname that the client used to validate aginst the certificate that the server presents. Note that it does not have to be the same with the `remote_addr` in `[client]`. ```toml [client.transport.tls] trusted_root = "example/tls/rootCA.crt" hostname = "localhost" ``` ### Server PKCS#12 archives are needed to run the server. It can be created using openssl like: ```sh openssl pkcs12 -export -out identity.pfx -inkey server.key -in server.crt -certfile ca_chain_certs.crt ``` Aruguments are: - `-inkey`: Server Private Key - `-in`: Server Certificate - `-certfile`: CA Certificate Creating self-signed certificate with one's own CA is a non-trival task. However, a script is provided under tls example folder for reference. ### Rustls Support `rathole` provides optional `rustls` support. [Build Guide](build-guide.md) demostrated this. One difference is that, the crate we use for loading PKCS#12 archives can only handle limited types of PBE algorithms. We only support PKCS#12 archives that they (crate `p12`) support. So we need to specify the legacy format (openssl 1.x format) when creating the PKCS#12 archive. In short, the command used with openssl 3 to create the PKCS#12 archive with `rustls` support is: ```sh openssl pkcs12 -export -out identity.pfx -inkey server.key -in server.crt -certfile ca_chain_certs.crt -legacy ``` ## Noise Protocol ### Quickstart for the Noise Protocl In one word, the [Noise Protocol](http://noiseprotocol.org/noise.html) is a lightweigt, easy to configure and drop-in replacement of TLS. No need to create a self-sign certificate to secure the connection. `rathole` comes with a reasonable default configuration for noise protocol. You can a glimpse of the minimal [example](../examples/noise_nk) for how it will look like. The default noise protocol that `rathole` uses, which is `Noise_NK_25519_ChaChaPoly_BLAKE2s`, providing the authentication of the server, just like TLS with properly configured certificates. So MITM is no more a problem. To use it, a X25519 keypair is needed. #### Generate a Keypair 1. Run `rathole --genkey`, which will generate a keypair using the default X25519 algorithm. It emits: ```sh $ rathole --genkey Private Key: cQ/vwIqNPJZmuM/OikglzBo/+jlYGrOt9i0k5h5vn1Q= Public Key: GQYTKSbWLBUSZiGfdWPSgek9yoOuaiwGD/GIX8Z1kkE= ``` (WARNING: Don't use the keypair from the Internet, including this one) 2. The server should keep the private key to identify itself. And the client should keep the public key, which is used to verify whether the peer is the authentic server. So relevant snippets of configuration are: ```toml # Client Side Configuration [client.transport] type = "noise" [client.transport.noise] remote_public_key = "GQYTKSbWLBUSZiGfdWPSgek9yoOuaiwGD/GIX8Z1kkE=" # Server Side Configuration [server.transport] type = "noise" [server.transport.noise] local_private_key = "cQ/vwIqNPJZmuM/OikglzBo/+jlYGrOt9i0k5h5vn1Q=" ``` Then `rathole` will run under the protection of the Noise Protocol. ## Specifying the Pattern of Noise Protocol The default configuration of Noise Protocol that comes with `rathole` satifies most use cases, which is described above. But there're other patterns that can be useful. ### No Authentication This configuration provides encryption of the traffic but provides no authentication, which means it's vulnerable to MITM attack, but is resistent to the sniffing and replay attack. If MITM attack is not one of the concerns, this is more convenient to use. ```toml # Server Side Configuration [server.transport.noise] pattern = "Noise_XX_25519_ChaChaPoly_BLAKE2s" # Client Side Configuration [client.transport.noise] pattern = "Noise_XX_25519_ChaChaPoly_BLAKE2s" ``` ### Bidirectional Authentication ```toml # Server Side Configuration [server.transport.noise] pattern = "Noise_KK_25519_ChaChaPoly_BLAKE2s" local_private_key = "server-priv-key-here" remote_public_key = "client-pub-key-here" # Client Side Configuration [client.transport.noise] pattern = "Noise_KK_25519_ChaChaPoly_BLAKE2s" local_private_key = "client-priv-key-here" remote_public_key = "server-pub-key-here" ``` ### Other Patterns To find out which pattern to use, refer to: - [7.5. Interactive handshake patterns (fundamental)](https://noiseprotocol.org/noise.html#interactive-handshake-patterns-fundamental) - [8. Protocol names and modifiers](https://noiseprotocol.org/noise.html#protocol-names-and-modifiers) Note that PSKs are not supported currently. Free to open an issue if you need it. ================================================ FILE: examples/iperf3/client.toml ================================================ [client] remote_addr = "localhost:2333" default_token = "123" [client.services.iperf3-udp] type = "udp" local_addr = "127.0.0.1:80" [client.services.iperf3-tcp] type = "tcp" local_addr = "127.0.0.1:80" ================================================ FILE: examples/iperf3/server.toml ================================================ [server] bind_addr = "0.0.0.0:2333" default_token = "123" [server.services.iperf3-udp] type = "udp" bind_addr = "0.0.0.0:5202" [server.services.iperf3-tcp] type = "tcp" bind_addr = "0.0.0.0:5202" ================================================ FILE: examples/minimal/client.toml ================================================ [client] remote_addr = "localhost:2333" default_token = "123" [client.services.foo1] local_addr = "127.0.0.1:80" ================================================ FILE: examples/minimal/server.toml ================================================ [server] bind_addr = "0.0.0.0:2333" default_token = "123" [server.services.foo1] bind_addr = "0.0.0.0:5202" ================================================ FILE: examples/noise_nk/client.toml ================================================ [client] remote_addr = "localhost:2333" default_token = "123" [client.transport] type = "noise" [client.transport.noise] remote_public_key = "xrpknQcAagcd/b9foMwxSCD+EindWxq450NEONk8XQo=" [client.services.foo1] local_addr = "127.0.0.1:80" ================================================ FILE: examples/noise_nk/server.toml ================================================ [server] bind_addr = "0.0.0.0:2333" default_token = "123" [server.transport] type = "noise" [server.transport.noise] local_private_key = "QLYMByBnjgM254zT6YKaBVvuAA61swyZfFxoA/SKZHM=" [server.services.foo1] bind_addr = "0.0.0.0:5202" ================================================ FILE: examples/systemd/README.md ================================================ ## Systemd Unit Examples The directory lists some systemd unit files for example, which can be used to run `rathole` as a service on Linux. [The `@` symbol in the name of unit files](https://superuser.com/questions/393423/the-symbol-and-systemctl-and-vsftpd) such as `rathole@.service` facilitates the management of multiple instances of `rathole`. For the naming of the example, `ratholes` stands for `rathole --server`, and `ratholec` stands for `rathole --client`, `rathole` is just `rathole`. For security, it is suggested to store configuration files with permission `600`, that is, only the owner can read the file, preventing arbitrary users on the system from accessing the secret tokens. ### With root privilege Assuming that `rathole` is installed in `/usr/bin/rathole`, and the configuration file is in `/etc/rathole/app1.toml`, the following steps show how to run an instance of `rathole --server` with root. 1. Create a service file. ```bash sudo cp ratholes@.service /etc/systemd/system/ ``` 2. Create the configuration file `app1.toml`. ```bash sudo mkdir -p /etc/rathole # And create the configuration file named `app1.toml` inside /etc/rathole ``` 3. Enable and start the service. ```bash sudo systemctl daemon-reload # Make sure systemd find the new unit sudo systemctl enable ratholes@app1 --now ``` ### Without root privilege Assuming that `rathole` is installed in `~/.local/bin/rathole`, and the configuration file is in `~/.local/etc/rathole/app1.toml`, the following steps show how to run an instance of `rathole --server` without root. 1. Edit the example service file as... ```txt # with root # ExecStart=/usr/bin/rathole -s /etc/rathole/%i.toml # without root ExecStart=%h/.local/bin/rathole -s %h/.local/etc/rathole/%i.toml ``` 2. Create a service file. ```bash mkdir -p ~/.config/systemd/user cp ratholes@.service ~/.config/systemd/user/ ``` 3. Create the configuration file `app1.toml`. ```bash mkdir -p ~/.local/etc/rathole # And create the configuration file named `app1.toml` inside ~/.local/etc/rathole ``` 4. Enable and start the service. ```bash systemctl --user daemon-reload # Make sure systemd find the new unit systemctl --user enable ratholes@app1 --now ``` ### Run multiple services To run multiple services at once, simply add another configuration, say `app2.toml` under `/etc/rathole` (`~/.local/etc/rathole` for non-root), then run `sudo systemctl enable ratholes@app2 --now` (`systemctl --user enable ratholes@app2 --now` for non-root) to start an instance for that configuration. The same applies to `ratholec@.service` for `rathole --client` and `rathole@.service` for `rathole`. ================================================ FILE: examples/systemd/rathole@.service ================================================ [Unit] Description=Rathole Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s LimitNOFILE=1048576 # with root ExecStart=/usr/bin/rathole /etc/rathole/%i.toml # without root # ExecStart=%h/.local/bin/rathole %h/.local/etc/rathole/%i.toml [Install] WantedBy=multi-user.target ================================================ FILE: examples/systemd/ratholec.service ================================================ [Unit] Description=Rathole Client Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s LimitNOFILE=1048576 # with root ExecStart=/usr/bin/rathole -c /etc/rathole/rathole.toml # without root # ExecStart=%h/.local/bin/rathole -c %h/.local/etc/rathole/rathole.toml [Install] WantedBy=multi-user.target ================================================ FILE: examples/systemd/ratholec@.service ================================================ [Unit] Description=Rathole Client Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s LimitNOFILE=1048576 # with root ExecStart=/usr/bin/rathole -c /etc/rathole/%i.toml # without root # ExecStart=%h/.local/bin/rathole -c %h/.local/etc/rathole/%i.toml [Install] WantedBy=multi-user.target ================================================ FILE: examples/systemd/ratholes.service ================================================ [Unit] Description=Rathole Server Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s LimitNOFILE=1048576 # with root ExecStart=/usr/bin/rathole -s /etc/rathole/rathole.toml # without root # ExecStart=%h/.local/bin/rathole -s %h/.local/etc/rathole/rathole.toml [Install] WantedBy=multi-user.target ================================================ FILE: examples/systemd/ratholes@.service ================================================ [Unit] Description=Rathole Server Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s LimitNOFILE=1048576 # with root ExecStart=/usr/bin/rathole -s /etc/rathole/%i.toml # without root # ExecStart=%h/.local/bin/rathole -s %h/.local/etc/rathole/%i.toml [Install] WantedBy=multi-user.target ================================================ FILE: examples/tls/client.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "123" [client.transport] type = "tls" [client.transport.tls] trusted_root = "examples/tls/rootCA.crt" hostname = "localhost" [client.services.foo1] local_addr = "127.0.0.1:80" ================================================ FILE: examples/tls/create_self_signed_cert.sh ================================================ #!/bin/sh # create CA openssl req -x509 \ -sha256 -days 356 \ -nodes \ -newkey rsa:2048 \ -subj "/CN=MyOwnCA/C=US/L=San Fransisco" \ -keyout rootCA.key -out rootCA.crt # create server private key openssl genrsa -out server.key 2048 # create certificate signing request (CSR) cat > csr.conf < cert.conf <, /// Run as a server #[clap(long, short, group = "mode")] pub server: bool, /// Run as a client #[clap(long, short, group = "mode")] pub client: bool, /// Generate a keypair for the use of the noise protocol /// /// The DH function to use is x25519 #[clap(long, arg_enum, value_name = "CURVE")] pub genkey: Option>, } ================================================ FILE: src/client.rs ================================================ use crate::config::{ClientConfig, ClientServiceConfig, Config, ServiceType, TransportType}; use crate::config_watcher::{ClientServiceChange, ConfigChange}; use crate::helper::udp_connect; use crate::protocol::Hello::{self, *}; use crate::protocol::{ self, read_ack, read_control_cmd, read_data_cmd, read_hello, Ack, Auth, ControlChannelCmd, DataChannelCmd, UdpTraffic, CURRENT_PROTO_VERSION, HASH_WIDTH_IN_BYTES, }; use crate::transport::{AddrMaybeCached, SocketOpts, TcpTransport, Transport}; use anyhow::{anyhow, bail, Context, Result}; use backoff::backoff::Backoff; use backoff::future::retry_notify; use backoff::ExponentialBackoff; use bytes::{Bytes, BytesMut}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use tokio::io::{self, copy_bidirectional, AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpStream, UdpSocket}; use tokio::sync::{broadcast, mpsc, oneshot, RwLock}; use tokio::time::{self, Duration, Instant}; use tracing::{debug, error, info, instrument, trace, warn, Instrument, Span}; #[cfg(feature = "noise")] use crate::transport::NoiseTransport; #[cfg(any(feature = "native-tls", feature = "rustls"))] use crate::transport::TlsTransport; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] use crate::transport::WebsocketTransport; use crate::constants::{run_control_chan_backoff, UDP_BUFFER_SIZE, UDP_SENDQ_SIZE, UDP_TIMEOUT}; // The entrypoint of running a client pub async fn run_client( config: Config, shutdown_rx: broadcast::Receiver, update_rx: mpsc::Receiver, ) -> Result<()> { let config = config.client.ok_or_else(|| { anyhow!( "Try to run as a client, but the configuration is missing. Please add the `[client]` block" ) })?; match config.transport.transport_type { TransportType::Tcp => { let mut client = Client::::from(config).await?; client.run(shutdown_rx, update_rx).await } TransportType::Tls => { #[cfg(any(feature = "native-tls", feature = "rustls"))] { let mut client = Client::::from(config).await?; client.run(shutdown_rx, update_rx).await } #[cfg(not(any(feature = "native-tls", feature = "rustls")))] crate::helper::feature_neither_compile("native-tls", "rustls") } TransportType::Noise => { #[cfg(feature = "noise")] { let mut client = Client::::from(config).await?; client.run(shutdown_rx, update_rx).await } #[cfg(not(feature = "noise"))] crate::helper::feature_not_compile("noise") } TransportType::Websocket => { #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] { let mut client = Client::::from(config).await?; client.run(shutdown_rx, update_rx).await } #[cfg(not(any(feature = "websocket-native-tls", feature = "websocket-rustls")))] crate::helper::feature_neither_compile("websocket-native-tls", "websocket-rustls") } } } type ServiceDigest = protocol::Digest; type Nonce = protocol::Digest; // Holds the state of a client struct Client { config: ClientConfig, service_handles: HashMap, transport: Arc, } impl Client { // Create a Client from `[client]` config block async fn from(config: ClientConfig) -> Result> { let transport = Arc::new(T::new(&config.transport).with_context(|| "Failed to create the transport")?); Ok(Client { config, service_handles: HashMap::new(), transport, }) } // The entrypoint of Client async fn run( &mut self, mut shutdown_rx: broadcast::Receiver, mut update_rx: mpsc::Receiver, ) -> Result<()> { for (name, config) in &self.config.services { // Create a control channel for each service defined let handle = ControlChannelHandle::new( (*config).clone(), self.config.remote_addr.clone(), self.transport.clone(), self.config.heartbeat_timeout, ); self.service_handles.insert(name.clone(), handle); } // Wait for the shutdown signal loop { tokio::select! { val = shutdown_rx.recv() => { match val { Ok(_) => {} Err(err) => { error!("Unable to listen for shutdown signal: {}", err); } } break; }, e = update_rx.recv() => { if let Some(e) = e { self.handle_hot_reload(e).await; } } } } // Shutdown all services for (_, handle) in self.service_handles.drain() { handle.shutdown(); } Ok(()) } async fn handle_hot_reload(&mut self, e: ConfigChange) { match e { ConfigChange::ClientChange(client_change) => match client_change { ClientServiceChange::Add(cfg) => { let name = cfg.name.clone(); let handle = ControlChannelHandle::new( cfg, self.config.remote_addr.clone(), self.transport.clone(), self.config.heartbeat_timeout, ); let _ = self.service_handles.insert(name, handle); } ClientServiceChange::Delete(s) => { let _ = self.service_handles.remove(&s); } }, ignored => warn!("Ignored {:?} since running as a client", ignored), } } } struct RunDataChannelArgs { session_key: Nonce, remote_addr: AddrMaybeCached, connector: Arc, socket_opts: SocketOpts, service: ClientServiceConfig, } async fn do_data_channel_handshake( args: Arc>, ) -> Result { // Retry at least every 100ms, at most for 10 seconds let backoff = ExponentialBackoff { max_interval: Duration::from_millis(100), max_elapsed_time: Some(Duration::from_secs(10)), ..Default::default() }; // Connect to remote_addr let mut conn: T::Stream = retry_notify( backoff, || async { args.connector .connect(&args.remote_addr) .await .with_context(|| format!("Failed to connect to {}", &args.remote_addr)) .map_err(backoff::Error::transient) }, |e, duration| { warn!("{:#}. Retry in {:?}", e, duration); }, ) .await?; T::hint(&conn, args.socket_opts); // Send nonce let v: &[u8; HASH_WIDTH_IN_BYTES] = args.session_key[..].try_into().unwrap(); let hello = Hello::DataChannelHello(CURRENT_PROTO_VERSION, v.to_owned()); conn.write_all(&bincode::serialize(&hello).unwrap()).await?; conn.flush().await?; Ok(conn) } async fn run_data_channel(args: Arc>) -> Result<()> { // Do the handshake let mut conn = do_data_channel_handshake(args.clone()).await?; // Forward match read_data_cmd(&mut conn).await? { DataChannelCmd::StartForwardTcp => { if args.service.service_type != ServiceType::Tcp { bail!("Expect TCP traffic. Please check the configuration.") } run_data_channel_for_tcp::(conn, &args.service.local_addr).await?; } DataChannelCmd::StartForwardUdp => { if args.service.service_type != ServiceType::Udp { bail!("Expect UDP traffic. Please check the configuration.") } run_data_channel_for_udp::(conn, &args.service.local_addr, args.service.prefer_ipv6).await?; } } Ok(()) } // Simply copying back and forth for TCP #[instrument(skip(conn))] async fn run_data_channel_for_tcp( mut conn: T::Stream, local_addr: &str, ) -> Result<()> { debug!("New data channel starts forwarding"); let mut local = TcpStream::connect(local_addr) .await .with_context(|| format!("Failed to connect to {}", local_addr))?; let _ = copy_bidirectional(&mut conn, &mut local).await; Ok(()) } // Things get a little tricker when it gets to UDP because it's connection-less. // A UdpPortMap must be maintained for recent seen incoming address, giving them // each a local port, which is associated with a socket. So just the sender // to the socket will work fine for the map's value. type UdpPortMap = Arc>>>; #[instrument(skip(conn))] async fn run_data_channel_for_udp(conn: T::Stream, local_addr: &str, prefer_ipv6: bool) -> Result<()> { debug!("New data channel starts forwarding"); let port_map: UdpPortMap = Arc::new(RwLock::new(HashMap::new())); // The channel stores UdpTraffic that needs to be sent to the server let (outbound_tx, mut outbound_rx) = mpsc::channel::(UDP_SENDQ_SIZE); // FIXME: https://github.com/tokio-rs/tls/issues/40 // Maybe this is our concern let (mut rd, mut wr) = io::split(conn); // Keep sending items from the outbound channel to the server tokio::spawn(async move { while let Some(t) = outbound_rx.recv().await { trace!("outbound {:?}", t); if let Err(e) = t .write(&mut wr) .await .with_context(|| "Failed to forward UDP traffic to the server") { debug!("{:?}", e); break; } } }); loop { // Read a packet from the server let hdr_len = rd.read_u8().await?; let packet = UdpTraffic::read(&mut rd, hdr_len) .await .with_context(|| "Failed to read UDPTraffic from the server")?; let m = port_map.read().await; if m.get(&packet.from).is_none() { // This packet is from a address we don't see for a while, // which is not in the UdpPortMap. // So set up a mapping (and a forwarder) for it // Drop the reader lock drop(m); // Grab the writer lock // This is the only thread that will try to grab the writer lock // So no need to worry about some other thread has already set up // the mapping between the gap of dropping the reader lock and // grabbing the writer lock let mut m = port_map.write().await; match udp_connect(local_addr, prefer_ipv6).await { Ok(s) => { let (inbound_tx, inbound_rx) = mpsc::channel(UDP_SENDQ_SIZE); m.insert(packet.from, inbound_tx); tokio::spawn(run_udp_forwarder( s, inbound_rx, outbound_tx.clone(), packet.from, port_map.clone(), )); } Err(e) => { error!("{:#}", e); } } } // Now there should be a udp forwarder that can receive the packet let m = port_map.read().await; if let Some(tx) = m.get(&packet.from) { let _ = tx.send(packet.data).await; } } } // Run a UdpSocket for the visitor `from` #[instrument(skip_all, fields(from))] async fn run_udp_forwarder( s: UdpSocket, mut inbound_rx: mpsc::Receiver, outbount_tx: mpsc::Sender, from: SocketAddr, port_map: UdpPortMap, ) -> Result<()> { debug!("Forwarder created"); let mut buf = BytesMut::new(); buf.resize(UDP_BUFFER_SIZE, 0); loop { tokio::select! { // Receive from the server data = inbound_rx.recv() => { if let Some(data) = data { s.send(&data).await?; } else { break; } }, // Receive from the service val = s.recv(&mut buf) => { let len = match val { Ok(v) => v, Err(_) => break }; let t = UdpTraffic{ from, data: Bytes::copy_from_slice(&buf[..len]) }; outbount_tx.send(t).await?; }, // No traffic for the duration of UDP_TIMEOUT, clean up the state _ = time::sleep(Duration::from_secs(UDP_TIMEOUT)) => { break; } } } let mut port_map = port_map.write().await; port_map.remove(&from); debug!("Forwarder dropped"); Ok(()) } // Control channel, using T as the transport layer struct ControlChannel { digest: ServiceDigest, // SHA256 of the service name service: ClientServiceConfig, // `[client.services.foo]` config block shutdown_rx: oneshot::Receiver, // Receives the shutdown signal remote_addr: String, // `client.remote_addr` transport: Arc, // Wrapper around the transport layer heartbeat_timeout: u64, // Application layer heartbeat timeout in secs } // Handle of a control channel // Dropping it will also drop the actual control channel struct ControlChannelHandle { shutdown_tx: oneshot::Sender, } impl ControlChannel { #[instrument(skip_all)] async fn run(&mut self) -> Result<()> { let mut remote_addr = AddrMaybeCached::new(&self.remote_addr); remote_addr.resolve().await?; let mut conn = self .transport .connect(&remote_addr) .await .with_context(|| format!("Failed to connect to {}", &self.remote_addr))?; T::hint(&conn, SocketOpts::for_control_channel()); // Send hello debug!("Sending hello"); let hello_send = Hello::ControlChannelHello(CURRENT_PROTO_VERSION, self.digest[..].try_into().unwrap()); conn.write_all(&bincode::serialize(&hello_send).unwrap()) .await?; conn.flush().await?; // Read hello debug!("Reading hello"); let nonce = match read_hello(&mut conn).await? { ControlChannelHello(_, d) => d, _ => { bail!("Unexpected type of hello"); } }; // Send auth debug!("Sending auth"); let mut concat = Vec::from(self.service.token.as_ref().unwrap().as_bytes()); concat.extend_from_slice(&nonce); let session_key = protocol::digest(&concat); let auth = Auth(session_key); conn.write_all(&bincode::serialize(&auth).unwrap()).await?; conn.flush().await?; // Read ack debug!("Reading ack"); match read_ack(&mut conn).await? { Ack::Ok => {} v => { return Err(anyhow!("{}", v)) .with_context(|| format!("Authentication failed: {}", self.service.name)); } } // Channel ready info!("Control channel established"); // Socket options for the data channel let socket_opts = SocketOpts::from_client_cfg(&self.service); let data_ch_args = Arc::new(RunDataChannelArgs { session_key, remote_addr, connector: self.transport.clone(), socket_opts, service: self.service.clone(), }); loop { tokio::select! { val = read_control_cmd(&mut conn) => { let val = val?; debug!( "Received {:?}", val); match val { ControlChannelCmd::CreateDataChannel => { let args = data_ch_args.clone(); tokio::spawn(async move { if let Err(e) = run_data_channel(args).await.with_context(|| "Failed to run the data channel") { warn!("{:#}", e); } }.instrument(Span::current())); }, ControlChannelCmd::HeartBeat => () } }, _ = time::sleep(Duration::from_secs(self.heartbeat_timeout)), if self.heartbeat_timeout != 0 => { return Err(anyhow!("Heartbeat timed out")) } _ = &mut self.shutdown_rx => { break; } } } info!("Control channel shutdown"); Ok(()) } } impl ControlChannelHandle { #[instrument(name="handle", skip_all, fields(service = %service.name))] fn new( service: ClientServiceConfig, remote_addr: String, transport: Arc, heartbeat_timeout: u64, ) -> ControlChannelHandle { let digest = protocol::digest(service.name.as_bytes()); info!("Starting {}", hex::encode(digest)); let (shutdown_tx, shutdown_rx) = oneshot::channel(); let mut retry_backoff = run_control_chan_backoff(service.retry_interval.unwrap()); let mut s = ControlChannel { digest, service, shutdown_rx, remote_addr, transport, heartbeat_timeout, }; tokio::spawn( async move { let mut start = Instant::now(); while let Err(err) = s .run() .await .with_context(|| "Failed to run the control channel") { if s.shutdown_rx.try_recv() != Err(oneshot::error::TryRecvError::Empty) { break; } if start.elapsed() > Duration::from_secs(3) { // The client runs for at least 3 secs and then disconnects retry_backoff.reset(); } if let Some(duration) = retry_backoff.next_backoff() { error!("{:#}. Retry in {:?}...", err, duration); time::sleep(duration).await; } else { // Should never reach panic!("{:#}. Break", err); } start = Instant::now(); } } .instrument(Span::current()), ); ControlChannelHandle { shutdown_tx } } fn shutdown(self) { // A send failure shows that the actor has already shutdown. let _ = self.shutdown_tx.send(0u8); } } ================================================ FILE: src/config.rs ================================================ use anyhow::{anyhow, bail, Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::ops::Deref; use std::path::Path; use tokio::fs; use url::Url; use crate::transport::{DEFAULT_KEEPALIVE_INTERVAL, DEFAULT_KEEPALIVE_SECS, DEFAULT_NODELAY}; /// Application-layer heartbeat interval in secs const DEFAULT_HEARTBEAT_INTERVAL_SECS: u64 = 30; const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 40; /// Client const DEFAULT_CLIENT_RETRY_INTERVAL_SECS: u64 = 1; /// String with Debug implementation that emits "MASKED" /// Used to mask sensitive strings when logging #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone)] pub struct MaskedString(String); impl Debug for MaskedString { fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.write_str("MASKED") } } impl Deref for MaskedString { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl From<&str> for MaskedString { fn from(s: &str) -> MaskedString { MaskedString(String::from(s)) } } #[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Default)] pub enum TransportType { #[default] #[serde(rename = "tcp")] Tcp, #[serde(rename = "tls")] Tls, #[serde(rename = "noise")] Noise, #[serde(rename = "websocket")] Websocket, } /// Per service config /// All Option are optional in configuration but must be Some value in runtime #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(deny_unknown_fields)] pub struct ClientServiceConfig { #[serde(rename = "type", default = "default_service_type")] pub service_type: ServiceType, #[serde(skip)] pub name: String, pub local_addr: String, #[serde(default)] // Default to false pub prefer_ipv6: bool, pub token: Option, pub nodelay: Option, pub retry_interval: Option, } impl ClientServiceConfig { pub fn with_name(name: &str) -> ClientServiceConfig { ClientServiceConfig { name: name.to_string(), ..Default::default() } } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)] pub enum ServiceType { #[serde(rename = "tcp")] #[default] Tcp, #[serde(rename = "udp")] Udp, } fn default_service_type() -> ServiceType { Default::default() } /// Per service config /// All Option are optional in configuration but must be Some value in runtime #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] #[serde(deny_unknown_fields)] pub struct ServerServiceConfig { #[serde(rename = "type", default = "default_service_type")] pub service_type: ServiceType, #[serde(skip)] pub name: String, pub bind_addr: String, pub token: Option, pub nodelay: Option, } impl ServerServiceConfig { pub fn with_name(name: &str) -> ServerServiceConfig { ServerServiceConfig { name: name.to_string(), ..Default::default() } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TlsConfig { pub hostname: Option, pub trusted_root: Option, pub pkcs12: Option, pub pkcs12_password: Option, } fn default_noise_pattern() -> String { String::from("Noise_NK_25519_ChaChaPoly_BLAKE2s") } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct NoiseConfig { #[serde(default = "default_noise_pattern")] pub pattern: String, pub local_private_key: Option, pub remote_public_key: Option, // TODO: Maybe psk can be added } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct WebsocketConfig { pub tls: bool, } fn default_nodelay() -> bool { DEFAULT_NODELAY } fn default_keepalive_secs() -> u64 { DEFAULT_KEEPALIVE_SECS } fn default_keepalive_interval() -> u64 { DEFAULT_KEEPALIVE_INTERVAL } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct TcpConfig { #[serde(default = "default_nodelay")] pub nodelay: bool, #[serde(default = "default_keepalive_secs")] pub keepalive_secs: u64, #[serde(default = "default_keepalive_interval")] pub keepalive_interval: u64, pub proxy: Option, } impl Default for TcpConfig { fn default() -> Self { Self { nodelay: default_nodelay(), keepalive_secs: default_keepalive_secs(), keepalive_interval: default_keepalive_interval(), proxy: None, } } } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)] #[serde(deny_unknown_fields)] pub struct TransportConfig { #[serde(rename = "type")] pub transport_type: TransportType, #[serde(default)] pub tcp: TcpConfig, pub tls: Option, pub noise: Option, pub websocket: Option, } fn default_heartbeat_timeout() -> u64 { DEFAULT_HEARTBEAT_TIMEOUT_SECS } fn default_client_retry_interval() -> u64 { DEFAULT_CLIENT_RETRY_INTERVAL_SECS } #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct ClientConfig { pub remote_addr: String, pub default_token: Option, pub prefer_ipv6: Option, pub services: HashMap, #[serde(default)] pub transport: TransportConfig, #[serde(default = "default_heartbeat_timeout")] pub heartbeat_timeout: u64, #[serde(default = "default_client_retry_interval")] pub retry_interval: u64, } fn default_heartbeat_interval() -> u64 { DEFAULT_HEARTBEAT_INTERVAL_SECS } #[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct ServerConfig { pub bind_addr: String, pub default_token: Option, pub services: HashMap, #[serde(default)] pub transport: TransportConfig, #[serde(default = "default_heartbeat_interval")] pub heartbeat_interval: u64, } #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] #[serde(deny_unknown_fields)] pub struct Config { pub server: Option, pub client: Option, } impl Config { fn from_str(s: &str) -> Result { let mut config: Config = toml::from_str(s).with_context(|| "Failed to parse the config")?; if let Some(server) = config.server.as_mut() { Config::validate_server_config(server)?; } if let Some(client) = config.client.as_mut() { Config::validate_client_config(client)?; } if config.server.is_none() && config.client.is_none() { Err(anyhow!("Neither of `[server]` or `[client]` is defined")) } else { Ok(config) } } fn validate_server_config(server: &mut ServerConfig) -> Result<()> { // Validate services for (name, s) in &mut server.services { s.name = name.clone(); if s.token.is_none() { s.token = server.default_token.clone(); if s.token.is_none() { bail!("The token of service {} is not set", name); } } } Config::validate_transport_config(&server.transport, true)?; Ok(()) } fn validate_client_config(client: &mut ClientConfig) -> Result<()> { // Validate services for (name, s) in &mut client.services { s.name = name.clone(); if s.token.is_none() { s.token = client.default_token.clone(); if s.token.is_none() { bail!("The token of service {} is not set", name); } } if s.retry_interval.is_none() { s.retry_interval = Some(client.retry_interval); } } Config::validate_transport_config(&client.transport, false)?; Ok(()) } fn validate_transport_config(config: &TransportConfig, is_server: bool) -> Result<()> { config .tcp .proxy .as_ref() .map_or(Ok(()), |u| match u.scheme() { "socks5" => Ok(()), "http" => Ok(()), _ => Err(anyhow!(format!("Unknown proxy scheme: {}", u.scheme()))), })?; match config.transport_type { TransportType::Tcp => Ok(()), TransportType::Tls => { let tls_config = config .tls .as_ref() .ok_or_else(|| anyhow!("Missing TLS configuration"))?; if is_server { tls_config .pkcs12 .as_ref() .and(tls_config.pkcs12_password.as_ref()) .ok_or_else(|| anyhow!("Missing `pkcs12` or `pkcs12_password`"))?; } Ok(()) } TransportType::Noise => { // The check is done in transport Ok(()) } TransportType::Websocket => Ok(()), } } pub async fn from_file(path: &Path) -> Result { let s: String = fs::read_to_string(path) .await .with_context(|| format!("Failed to read the config {:?}", path))?; Config::from_str(&s).with_context(|| { "Configuration is invalid. Please refer to the configuration specification." }) } } #[cfg(test)] mod tests { use super::*; use std::{fs, path::PathBuf}; use anyhow::Result; fn list_config_files>(root: T) -> Result> { let mut files = Vec::new(); for entry in fs::read_dir(root)? { let entry = entry?; let path = entry.path(); if path.is_file() { files.push(path); } else if path.is_dir() { files.append(&mut list_config_files(path)?); } } Ok(files) } fn get_all_example_config() -> Result> { Ok(list_config_files("./examples")? .into_iter() .filter(|x| x.ends_with(".toml")) .collect()) } #[test] fn test_example_config() -> Result<()> { let paths = get_all_example_config()?; for p in paths { let s = fs::read_to_string(p)?; Config::from_str(&s)?; } Ok(()) } #[test] fn test_valid_config() -> Result<()> { let paths = list_config_files("tests/config_test/valid_config")?; for p in paths { let s = fs::read_to_string(p)?; Config::from_str(&s)?; } Ok(()) } #[test] fn test_invalid_config() -> Result<()> { let paths = list_config_files("tests/config_test/invalid_config")?; for p in paths { let s = fs::read_to_string(p)?; assert!(Config::from_str(&s).is_err()); } Ok(()) } #[test] fn test_validate_server_config() -> Result<()> { let mut cfg = ServerConfig::default(); cfg.services.insert( "foo1".into(), ServerServiceConfig { service_type: ServiceType::Tcp, name: "foo1".into(), bind_addr: "127.0.0.1:80".into(), token: None, ..Default::default() }, ); // Missing the token assert!(Config::validate_server_config(&mut cfg).is_err()); // Use the default token cfg.default_token = Some("123".into()); assert!(Config::validate_server_config(&mut cfg).is_ok()); assert_eq!( cfg.services .get("foo1") .as_ref() .unwrap() .token .as_ref() .unwrap() .0, "123" ); // The default token won't override the service token cfg.services.get_mut("foo1").unwrap().token = Some("4".into()); assert!(Config::validate_server_config(&mut cfg).is_ok()); assert_eq!( cfg.services .get("foo1") .as_ref() .unwrap() .token .as_ref() .unwrap() .0, "4" ); Ok(()) } #[test] fn test_validate_client_config() -> Result<()> { let mut cfg = ClientConfig::default(); cfg.services.insert( "foo1".into(), ClientServiceConfig { service_type: ServiceType::Tcp, name: "foo1".into(), local_addr: "127.0.0.1:80".into(), token: None, ..Default::default() }, ); // Missing the token assert!(Config::validate_client_config(&mut cfg).is_err()); // Use the default token cfg.default_token = Some("123".into()); assert!(Config::validate_client_config(&mut cfg).is_ok()); assert_eq!( cfg.services .get("foo1") .as_ref() .unwrap() .token .as_ref() .unwrap() .0, "123" ); // The default token won't override the service token cfg.services.get_mut("foo1").unwrap().token = Some("4".into()); assert!(Config::validate_client_config(&mut cfg).is_ok()); assert_eq!( cfg.services .get("foo1") .as_ref() .unwrap() .token .as_ref() .unwrap() .0, "4" ); Ok(()) } } ================================================ FILE: src/config_watcher.rs ================================================ use crate::{ config::{ClientConfig, ClientServiceConfig, ServerConfig, ServerServiceConfig}, Config, }; use anyhow::{Context, Result}; use std::{ collections::HashMap, env, path::{Path, PathBuf}, }; use tokio::sync::{broadcast, mpsc}; use tracing::{error, info, instrument}; #[cfg(feature = "notify")] use notify::{EventKind, RecursiveMode, Watcher}; #[derive(Debug, PartialEq, Eq, Clone)] pub enum ConfigChange { General(Box), // Trigger a full restart ServerChange(ServerServiceChange), ClientChange(ClientServiceChange), } #[derive(Debug, PartialEq, Eq, Clone)] pub enum ClientServiceChange { Add(ClientServiceConfig), Delete(String), } #[derive(Debug, PartialEq, Eq, Clone)] pub enum ServerServiceChange { Add(ServerServiceConfig), Delete(String), } trait InstanceConfig: Clone { type ServiceConfig: PartialEq + Eq + Clone; fn equal_without_service(&self, rhs: &Self) -> bool; fn service_delete_change(s: String) -> ConfigChange; fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange; fn get_services(&self) -> &HashMap; } impl InstanceConfig for ServerConfig { type ServiceConfig = ServerServiceConfig; fn equal_without_service(&self, rhs: &Self) -> bool { let left = ServerConfig { services: Default::default(), ..self.clone() }; let right = ServerConfig { services: Default::default(), ..rhs.clone() }; left == right } fn service_delete_change(s: String) -> ConfigChange { ConfigChange::ServerChange(ServerServiceChange::Delete(s)) } fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange { ConfigChange::ServerChange(ServerServiceChange::Add(cfg)) } fn get_services(&self) -> &HashMap { &self.services } } impl InstanceConfig for ClientConfig { type ServiceConfig = ClientServiceConfig; fn equal_without_service(&self, rhs: &Self) -> bool { let left = ClientConfig { services: Default::default(), ..self.clone() }; let right = ClientConfig { services: Default::default(), ..rhs.clone() }; left == right } fn service_delete_change(s: String) -> ConfigChange { ConfigChange::ClientChange(ClientServiceChange::Delete(s)) } fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange { ConfigChange::ClientChange(ClientServiceChange::Add(cfg)) } fn get_services(&self) -> &HashMap { &self.services } } pub struct ConfigWatcherHandle { pub event_rx: mpsc::UnboundedReceiver, } impl ConfigWatcherHandle { pub async fn new(path: &Path, shutdown_rx: broadcast::Receiver) -> Result { let (event_tx, event_rx) = mpsc::unbounded_channel(); let origin_cfg = Config::from_file(path).await?; // Initial start event_tx .send(ConfigChange::General(Box::new(origin_cfg.clone()))) .unwrap(); tokio::spawn(config_watcher( path.to_owned(), shutdown_rx, event_tx, origin_cfg, )); Ok(ConfigWatcherHandle { event_rx }) } } // Fake config watcher when compiling without `notify` #[cfg(not(feature = "notify"))] async fn config_watcher( _path: PathBuf, mut shutdown_rx: broadcast::Receiver, _event_tx: mpsc::UnboundedSender, _old: Config, ) -> Result<()> { // Do nothing except waiting for ctrl-c let _ = shutdown_rx.recv().await; Ok(()) } #[cfg(feature = "notify")] #[instrument(skip(shutdown_rx, event_tx, old))] async fn config_watcher( path: PathBuf, mut shutdown_rx: broadcast::Receiver, event_tx: mpsc::UnboundedSender, mut old: Config, ) -> Result<()> { let (fevent_tx, mut fevent_rx) = mpsc::unbounded_channel(); let path = if path.is_absolute() { path } else { env::current_dir()?.join(path) }; let parent_path = path.parent().expect("config file should have a parent dir"); let path_clone = path.clone(); let mut watcher = notify::recommended_watcher(move |res: Result| match res { Ok(e) => { if matches!(e.kind, EventKind::Modify(_)) && e.paths .iter() .map(|x| x.file_name()) .any(|x| x == path_clone.file_name()) { let _ = fevent_tx.send(true); } } Err(e) => error!("watch error: {:#}", e), })?; watcher.watch(parent_path, RecursiveMode::NonRecursive)?; info!("Start watching the config"); loop { tokio::select! { e = fevent_rx.recv() => { match e { Some(_) => { info!("Rescan the configuration"); let new = match Config::from_file(&path).await.with_context(|| "The changed configuration is invalid. Ignored") { Ok(v) => v, Err(e) => { error!("{:#}", e); // If the config is invalid, just ignore it continue; } }; let events = calculate_events(&old, &new).into_iter().flatten(); for event in events { event_tx.send(event)?; } old = new; }, None => break } }, _ = shutdown_rx.recv() => break } } info!("Config watcher exiting"); Ok(()) } fn calculate_events(old: &Config, new: &Config) -> Option> { if old == new { return None; } if (old.server.is_some() != new.server.is_some()) || (old.client.is_some() != new.client.is_some()) { return Some(vec![ConfigChange::General(Box::new(new.clone()))]); } let mut ret = vec![]; if old.server != new.server { match calculate_instance_config_events( old.server.as_ref().unwrap(), new.server.as_ref().unwrap(), ) { Some(mut v) => ret.append(&mut v), None => return Some(vec![ConfigChange::General(Box::new(new.clone()))]), } } if old.client != new.client { match calculate_instance_config_events( old.client.as_ref().unwrap(), new.client.as_ref().unwrap(), ) { Some(mut v) => ret.append(&mut v), None => return Some(vec![ConfigChange::General(Box::new(new.clone()))]), } } Some(ret) } // None indicates a General change needed fn calculate_instance_config_events( old: &T, new: &T, ) -> Option> { if !old.equal_without_service(new) { return None; } let old = old.get_services(); let new = new.get_services(); let deletions = old .keys() .filter(|&name| new.get(name).is_none()) .map(|x| T::service_delete_change(x.to_owned())); let addition = new .iter() .filter(|(name, c)| old.get(*name) != Some(*c)) .map(|(_, c)| T::service_add_change(c.clone())); Some(deletions.chain(addition).collect()) } #[cfg(test)] mod test { use crate::config::ServerConfig; use super::*; // macro to create map or set literal macro_rules! collection { // map-like ($($k:expr => $v:expr),* $(,)?) => {{ use std::iter::{Iterator, IntoIterator}; Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*])) }}; } #[test] fn test_calculate_events() { struct Test { old: Config, new: Config, } let tests = [ Test { old: Config { server: Some(Default::default()), client: None, }, new: Config { server: Some(Default::default()), client: Some(Default::default()), }, }, Test { old: Config { server: Some(ServerConfig { bind_addr: String::from("127.0.0.1:2334"), ..Default::default() }), client: None, }, new: Config { server: Some(ServerConfig { bind_addr: String::from("127.0.0.1:2333"), services: collection!(String::from("foo") => Default::default()), ..Default::default() }), client: None, }, }, Test { old: Config { server: Some(Default::default()), client: None, }, new: Config { server: Some(ServerConfig { services: collection!(String::from("foo") => Default::default()), ..Default::default() }), client: None, }, }, Test { old: Config { server: Some(ServerConfig { services: collection!(String::from("foo") => Default::default()), ..Default::default() }), client: None, }, new: Config { server: Some(Default::default()), client: None, }, }, Test { old: Config { server: Some(ServerConfig { services: collection!(String::from("foo1") => ServerServiceConfig::with_name("foo1"), String::from("foo2") => ServerServiceConfig::with_name("foo2")), ..Default::default() }), client: Some(ClientConfig { services: collection!(String::from("foo1") => ClientServiceConfig::with_name("foo1"), String::from("foo2") => ClientServiceConfig::with_name("foo2")), ..Default::default() }), }, new: Config { server: Some(ServerConfig { services: collection!(String::from("bar1") => ServerServiceConfig::with_name("bar1"), String::from("foo2") => ServerServiceConfig::with_name("foo2")), ..Default::default() }), client: Some(ClientConfig { services: collection!(String::from("bar1") => ClientServiceConfig::with_name("bar1"), String::from("bar2") => ClientServiceConfig::with_name("bar2")), ..Default::default() }), }, }, ]; let mut expected = [ vec![ConfigChange::General(Box::new(tests[0].new.clone()))], vec![ConfigChange::General(Box::new(tests[1].new.clone()))], vec![ConfigChange::ServerChange(ServerServiceChange::Add( Default::default(), ))], vec![ConfigChange::ServerChange(ServerServiceChange::Delete( String::from("foo"), ))], vec![ ConfigChange::ServerChange(ServerServiceChange::Delete(String::from("foo1"))), ConfigChange::ServerChange(ServerServiceChange::Add( tests[4].new.server.as_ref().unwrap().services["bar1"].clone(), )), ConfigChange::ClientChange(ClientServiceChange::Delete(String::from("foo1"))), ConfigChange::ClientChange(ClientServiceChange::Delete(String::from("foo2"))), ConfigChange::ClientChange(ClientServiceChange::Add( tests[4].new.client.as_ref().unwrap().services["bar1"].clone(), )), ConfigChange::ClientChange(ClientServiceChange::Add( tests[4].new.client.as_ref().unwrap().services["bar2"].clone(), )), ], ]; assert_eq!(tests.len(), expected.len()); for i in 0..tests.len() { let mut actual = calculate_events(&tests[i].old, &tests[i].new).unwrap(); let get_key = |x: &ConfigChange| -> String { match x { ConfigChange::General(_) => String::from("g"), ConfigChange::ServerChange(sc) => match sc { ServerServiceChange::Add(c) => "s_add_".to_owned() + &c.name, ServerServiceChange::Delete(s) => "s_del_".to_owned() + s, }, ConfigChange::ClientChange(sc) => match sc { ClientServiceChange::Add(c) => "c_add_".to_owned() + &c.name, ClientServiceChange::Delete(s) => "c_del_".to_owned() + s, }, } }; actual.sort_by_cached_key(get_key); expected[i].sort_by_cached_key(get_key); assert_eq!(actual, expected[i]); } // No changes assert_eq!( calculate_events( &Config { server: Default::default(), client: None, }, &Config { server: Default::default(), client: None, }, ), None ); } } ================================================ FILE: src/constants.rs ================================================ use backoff::ExponentialBackoff; use std::time::Duration; // FIXME: Determine reasonable size /// UDP MTU. Currently far larger than necessary pub const UDP_BUFFER_SIZE: usize = 2048; pub const UDP_SENDQ_SIZE: usize = 1024; pub const UDP_TIMEOUT: u64 = 60; pub fn listen_backoff() -> ExponentialBackoff { ExponentialBackoff { max_elapsed_time: None, max_interval: Duration::from_secs(1), ..Default::default() } } pub fn run_control_chan_backoff(interval: u64) -> ExponentialBackoff { ExponentialBackoff { randomization_factor: 0.2, max_elapsed_time: None, multiplier: 3.0, max_interval: Duration::from_secs(interval), ..Default::default() } } ================================================ FILE: src/helper.rs ================================================ use anyhow::{anyhow, Context, Result}; use async_http_proxy::{http_connect_tokio, http_connect_tokio_with_basic_auth}; use backoff::{backoff::Backoff, Notify}; use socket2::{SockRef, TcpKeepalive}; use std::{future::Future, net::SocketAddr, time::Duration}; use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::{ net::{lookup_host, TcpStream, ToSocketAddrs, UdpSocket}, sync::broadcast, }; use tracing::trace; use url::Url; use crate::transport::AddrMaybeCached; // Tokio hesitates to expose this option...So we have to do it on our own :( // The good news is that using socket2 it can be easily done, without losing portability. // See https://github.com/tokio-rs/tokio/issues/3082 pub fn try_set_tcp_keepalive( conn: &TcpStream, keepalive_duration: Duration, keepalive_interval: Duration, ) -> Result<()> { let s = SockRef::from(conn); let keepalive = TcpKeepalive::new() .with_time(keepalive_duration) .with_interval(keepalive_interval); trace!( "Set TCP keepalive {:?} {:?}", keepalive_duration, keepalive_interval ); Ok(s.set_tcp_keepalive(&keepalive)?) } #[allow(dead_code)] pub fn feature_not_compile(feature: &str) -> ! { panic!( "The feature '{}' is not compiled in this binary. Please re-compile rathole", feature ) } #[allow(dead_code)] pub fn feature_neither_compile(feature1: &str, feature2: &str) -> ! { panic!( "Neither of the feature '{}' or '{}' is compiled in this binary. Please re-compile rathole", feature1, feature2 ) } pub async fn to_socket_addr(addr: A) -> Result { lookup_host(addr) .await? .next() .ok_or_else(|| anyhow!("Failed to lookup the host")) } pub fn host_port_pair(s: &str) -> Result<(&str, u16)> { let semi = s.rfind(':').expect("missing semicolon"); Ok((&s[..semi], s[semi + 1..].parse()?)) } /// Create a UDP socket and connect to `addr` pub async fn udp_connect(addr: A, prefer_ipv6: bool) -> Result { let (socket_addr, bind_addr); match prefer_ipv6 { false => { socket_addr = to_socket_addr(addr).await?; bind_addr = match socket_addr { SocketAddr::V4(_) => "0.0.0.0:0", SocketAddr::V6(_) => ":::0", }; }, true => { let all_host_addresses: Vec = lookup_host(addr).await?.collect(); // Try to find an IPv6 address match all_host_addresses.clone().iter().find(|x| x.is_ipv6()) { Some(socket_addr_ipv6) => { socket_addr = *socket_addr_ipv6; bind_addr = ":::0"; }, None => { let socket_addr_ipv4 = all_host_addresses.iter().find(|x| x.is_ipv4()); match socket_addr_ipv4 { None => return Err(anyhow!("Failed to lookup the host")), // fallback to IPv4 Some(socket_addr_ipv4) => { socket_addr = *socket_addr_ipv4; bind_addr = "0.0.0.0:0"; } } } } } }; let s = UdpSocket::bind(bind_addr).await?; s.connect(socket_addr).await?; s.connect(socket_addr).await?; Ok(s) } /// Create a TcpStream using a proxy /// e.g. socks5://user:pass@127.0.0.1:1080 http://127.0.0.1:8080 pub async fn tcp_connect_with_proxy( addr: &AddrMaybeCached, proxy: Option<&Url>, ) -> Result { if let Some(url) = proxy { let addr = &addr.addr; let mut s = TcpStream::connect(( url.host_str().expect("proxy url should have host field"), url.port().expect("proxy url should have port field"), )) .await?; let auth = if !url.username().is_empty() || url.password().is_some() { Some(async_socks5::Auth { username: url.username().into(), password: url.password().unwrap_or("").into(), }) } else { None }; match url.scheme() { "socks5" => { async_socks5::connect(&mut s, host_port_pair(addr)?, auth).await?; } "http" => { let (host, port) = host_port_pair(addr)?; match auth { Some(auth) => { http_connect_tokio_with_basic_auth( &mut s, host, port, &auth.username, &auth.password, ) .await? } None => http_connect_tokio(&mut s, host, port).await?, } } _ => panic!("unknown proxy scheme"), } Ok(s) } else { Ok(match addr.socket_addr { Some(s) => TcpStream::connect(s).await?, None => TcpStream::connect(&addr.addr).await?, }) } } // Wrapper of retry_notify pub async fn retry_notify_with_deadline( backoff: B, operation: Fn, notify: N, deadline: &mut broadcast::Receiver, ) -> Result where E: std::error::Error + Send + Sync + 'static, B: Backoff, Fn: FnMut() -> Fut, Fut: Future>>, N: Notify, { tokio::select! { v = backoff::future::retry_notify(backoff, operation, notify) => { v.map_err(anyhow::Error::new) } _ = deadline.recv() => { Err(anyhow!("shutdown")) } } } pub async fn write_and_flush(conn: &mut T, data: &[u8]) -> Result<()> where T: AsyncWrite + Unpin, { conn.write_all(data) .await .with_context(|| "Failed to write data")?; conn.flush().await.with_context(|| "Failed to flush data")?; Ok(()) } ================================================ FILE: src/lib.rs ================================================ mod cli; mod config; mod config_watcher; mod constants; mod helper; mod multi_map; mod protocol; mod transport; pub use cli::Cli; use cli::KeypairType; pub use config::Config; pub use constants::UDP_BUFFER_SIZE; use anyhow::Result; use tokio::sync::{broadcast, mpsc}; use tracing::{debug, info}; #[cfg(feature = "client")] mod client; #[cfg(feature = "client")] use client::run_client; #[cfg(feature = "server")] mod server; #[cfg(feature = "server")] use server::run_server; use crate::config_watcher::{ConfigChange, ConfigWatcherHandle}; const DEFAULT_CURVE: KeypairType = KeypairType::X25519; fn get_str_from_keypair_type(curve: KeypairType) -> &'static str { match curve { KeypairType::X25519 => "25519", KeypairType::X448 => "448", } } #[cfg(feature = "noise")] fn genkey(curve: Option) -> Result<()> { let curve = curve.unwrap_or(DEFAULT_CURVE); let builder = snowstorm::Builder::new( format!( "Noise_KK_{}_ChaChaPoly_BLAKE2s", get_str_from_keypair_type(curve) ) .parse()?, ); let keypair = builder.generate_keypair()?; println!("Private Key:\n{}\n", base64::encode(keypair.private)); println!("Public Key:\n{}", base64::encode(keypair.public)); Ok(()) } #[cfg(not(feature = "noise"))] fn genkey(curve: Option) -> Result<()> { crate::helper::feature_not_compile("nosie") } pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver) -> Result<()> { if args.genkey.is_some() { return genkey(args.genkey.unwrap()); } // Raise `nofile` limit on linux and mac fdlimit::raise_fd_limit(); // Spawn a config watcher. The watcher will send a initial signal to start the instance with a config let config_path = args.config_path.as_ref().unwrap(); let mut cfg_watcher = ConfigWatcherHandle::new(config_path, shutdown_rx).await?; // shutdown_tx owns the instance let (shutdown_tx, _) = broadcast::channel(1); // (The join handle of the last instance, The service update channel sender) let mut last_instance: Option<(tokio::task::JoinHandle<_>, mpsc::Sender)> = None; while let Some(e) = cfg_watcher.event_rx.recv().await { match e { ConfigChange::General(config) => { if let Some((i, _)) = last_instance { info!("General configuration change detected. Restarting..."); shutdown_tx.send(true)?; i.await??; } debug!("{:?}", config); let (service_update_tx, service_update_rx) = mpsc::channel(1024); last_instance = Some(( tokio::spawn(run_instance( *config, args.clone(), shutdown_tx.subscribe(), service_update_rx, )), service_update_tx, )); } ev => { info!("Service change detected. {:?}", ev); if let Some((_, service_update_tx)) = &last_instance { let _ = service_update_tx.send(ev).await; } } } } let _ = shutdown_tx.send(true); Ok(()) } async fn run_instance( config: Config, args: Cli, shutdown_rx: broadcast::Receiver, service_update: mpsc::Receiver, ) -> Result<()> { match determine_run_mode(&config, &args) { RunMode::Undetermine => panic!("Cannot determine running as a server or a client"), RunMode::Client => { #[cfg(not(feature = "client"))] crate::helper::feature_not_compile("client"); #[cfg(feature = "client")] run_client(config, shutdown_rx, service_update).await } RunMode::Server => { #[cfg(not(feature = "server"))] crate::helper::feature_not_compile("server"); #[cfg(feature = "server")] run_server(config, shutdown_rx, service_update).await } } } #[derive(PartialEq, Eq, Debug)] enum RunMode { Server, Client, Undetermine, } fn determine_run_mode(config: &Config, args: &Cli) -> RunMode { use RunMode::*; if args.client && args.server { Undetermine } else if args.client { Client } else if args.server { Server } else if config.client.is_some() && config.server.is_none() { Client } else if config.server.is_some() && config.client.is_none() { Server } else { Undetermine } } #[cfg(test)] mod tests { use super::*; #[test] fn test_determine_run_mode() { use config::*; use RunMode::*; struct T { cfg_s: bool, cfg_c: bool, arg_s: bool, arg_c: bool, run_mode: RunMode, } let tests = [ T { cfg_s: false, cfg_c: false, arg_s: false, arg_c: false, run_mode: Undetermine, }, T { cfg_s: true, cfg_c: false, arg_s: false, arg_c: false, run_mode: Server, }, T { cfg_s: false, cfg_c: true, arg_s: false, arg_c: false, run_mode: Client, }, T { cfg_s: true, cfg_c: true, arg_s: false, arg_c: false, run_mode: Undetermine, }, T { cfg_s: true, cfg_c: true, arg_s: true, arg_c: false, run_mode: Server, }, T { cfg_s: true, cfg_c: true, arg_s: false, arg_c: true, run_mode: Client, }, T { cfg_s: true, cfg_c: true, arg_s: true, arg_c: true, run_mode: Undetermine, }, ]; for t in tests { let config = Config { server: match t.cfg_s { true => Some(ServerConfig::default()), false => None, }, client: match t.cfg_c { true => Some(ClientConfig::default()), false => None, }, }; let args = Cli { config_path: Some(std::path::PathBuf::new()), server: t.arg_s, client: t.arg_c, ..Default::default() }; assert_eq!(determine_run_mode(&config, &args), t.run_mode); } } } ================================================ FILE: src/main.rs ================================================ use anyhow::Result; use clap::Parser; use rathole::{run, Cli}; use tokio::{signal, sync::broadcast}; use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() -> Result<()> { let args = Cli::parse(); let (shutdown_tx, shutdown_rx) = broadcast::channel::(1); tokio::spawn(async move { if let Err(e) = signal::ctrl_c().await { // Something really weird happened. So just panic panic!("Failed to listen for the ctrl-c signal: {:?}", e); } if let Err(e) = shutdown_tx.send(true) { // shutdown signal must be catched and handle properly // `rx` must not be dropped panic!("Failed to send shutdown signal: {:?}", e); } }); #[cfg(feature = "console")] { console_subscriber::init(); tracing::info!("console_subscriber enabled"); } #[cfg(not(feature = "console"))] { let is_atty = atty::is(atty::Stream::Stdout); let level = "info"; // if RUST_LOG not present, use `info` level tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::from(level)), ) .with_ansi(is_atty) .init(); } run(args, shutdown_rx).await } ================================================ FILE: src/multi_map.rs ================================================ use std::borrow::Borrow; use std::collections::HashMap; use std::hash::{Hash, Hasher}; struct RawItem(*mut (K1, K2, V)); unsafe impl Send for RawItem {} unsafe impl Sync for RawItem {} /// MultiMap is a hash map that can index an item by two keys /// For example, after an item with key (a, b) is insert, `map.get1(a)` and /// `map.get2(b)` both returns the item. Likewise the `remove1` and `remove2`. pub struct MultiMap { map1: HashMap, RawItem>, map2: HashMap, RawItem>, } struct Key(*const T); unsafe impl Send for Key {} unsafe impl Sync for Key {} impl Borrow for Key { fn borrow(&self) -> &T { unsafe { &*self.0 } } } impl Hash for Key { fn hash(&self, state: &mut H) { (self.borrow() as &T).hash(state) } } impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { (self.borrow() as &T).eq(other.borrow()) } } impl Eq for Key {} impl MultiMap { pub fn new() -> Self { MultiMap { map1: HashMap::new(), map2: HashMap::new(), } } } #[allow(dead_code)] impl MultiMap where K1: Hash + Eq + Send, K2: Hash + Eq + Send, V: Send, { pub fn insert(&mut self, k1: K1, k2: K2, v: V) -> Result<(), (K1, K2, V)> { if self.map1.contains_key(&k1) || self.map2.contains_key(&k2) { return Err((k1, k2, v)); } let item = Box::new((k1, k2, v)); let k1 = Key(&item.0); let k2 = Key(&item.1); let item = Box::into_raw(item); self.map1.insert(k1, RawItem(item)); self.map2.insert(k2, RawItem(item)); Ok(()) } pub fn get1(&self, k1: &K1) -> Option<&V> { let item = self.map1.get(k1)?; let item = unsafe { &*item.0 }; Some(&item.2) } pub fn get1_mut(&mut self, k1: &K1) -> Option<&mut V> { let item = self.map1.get(k1)?; let item = unsafe { &mut *item.0 }; Some(&mut item.2) } pub fn get2(&self, k2: &K2) -> Option<&V> { let item = self.map2.get(k2)?; let item = unsafe { &*item.0 }; Some(&item.2) } pub fn get_mut2(&mut self, k2: &K2) -> Option<&mut V> { let item = self.map2.get(k2)?; let item = unsafe { &mut *item.0 }; Some(&mut item.2) } pub fn remove1(&mut self, k1: &K1) -> Option { let item = self.map1.remove(k1)?; let item = unsafe { Box::from_raw(item.0) }; self.map2.remove(&item.1); Some(item.2) } pub fn remove2(&mut self, k2: &K2) -> Option { let item = self.map2.remove(k2)?; let item = unsafe { Box::from_raw(item.0) }; self.map1.remove(&item.0); Some(item.2) } } impl Drop for MultiMap { fn drop(&mut self) { self.map1.clear(); self.map2 .drain() .for_each(|(_, item)| drop(unsafe { Box::from_raw(item.0) })); } } ================================================ FILE: src/protocol.rs ================================================ pub const HASH_WIDTH_IN_BYTES: usize = 32; use anyhow::{bail, Context, Result}; use bytes::{Bytes, BytesMut}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tracing::trace; type ProtocolVersion = u8; const _PROTO_V0: u8 = 0u8; const PROTO_V1: u8 = 1u8; pub const CURRENT_PROTO_VERSION: ProtocolVersion = PROTO_V1; pub type Digest = [u8; HASH_WIDTH_IN_BYTES]; #[derive(Deserialize, Serialize, Debug)] pub enum Hello { ControlChannelHello(ProtocolVersion, Digest), // sha256sum(service name) or a nonce DataChannelHello(ProtocolVersion, Digest), // token provided by CreateDataChannel } #[derive(Deserialize, Serialize, Debug)] pub struct Auth(pub Digest); #[derive(Deserialize, Serialize, Debug)] pub enum Ack { Ok, ServiceNotExist, AuthFailed, } impl std::fmt::Display for Ack { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Ack::Ok => "Ok", Ack::ServiceNotExist => "Service not exist", Ack::AuthFailed => "Incorrect token", } ) } } #[derive(Deserialize, Serialize, Debug)] pub enum ControlChannelCmd { CreateDataChannel, HeartBeat, } #[derive(Deserialize, Serialize, Debug)] pub enum DataChannelCmd { StartForwardTcp, StartForwardUdp, } type UdpPacketLen = u16; // `u16` should be enough for any practical UDP traffic on the Internet #[derive(Deserialize, Serialize, Debug)] struct UdpHeader { from: SocketAddr, len: UdpPacketLen, } #[derive(Debug)] pub struct UdpTraffic { pub from: SocketAddr, pub data: Bytes, } impl UdpTraffic { pub async fn write(&self, writer: &mut T) -> Result<()> { let hdr = UdpHeader { from: self.from, len: self.data.len() as UdpPacketLen, }; let v = bincode::serialize(&hdr).unwrap(); trace!("Write {:?} of length {}", hdr, v.len()); writer.write_u8(v.len() as u8).await?; writer.write_all(&v).await?; writer.write_all(&self.data).await?; Ok(()) } #[allow(dead_code)] pub async fn write_slice( writer: &mut T, from: SocketAddr, data: &[u8], ) -> Result<()> { let hdr = UdpHeader { from, len: data.len() as UdpPacketLen, }; let v = bincode::serialize(&hdr).unwrap(); trace!("Write {:?} of length {}", hdr, v.len()); writer.write_u8(v.len() as u8).await?; writer.write_all(&v).await?; writer.write_all(data).await?; Ok(()) } pub async fn read(reader: &mut T, hdr_len: u8) -> Result { let mut buf = vec![0; hdr_len as usize]; reader .read_exact(&mut buf) .await .with_context(|| "Failed to read udp header")?; let hdr: UdpHeader = bincode::deserialize(&buf).with_context(|| "Failed to deserialize UdpHeader")?; trace!("hdr {:?}", hdr); let mut data = BytesMut::new(); data.resize(hdr.len as usize, 0); reader.read_exact(&mut data).await?; Ok(UdpTraffic { from: hdr.from, data: data.freeze(), }) } } pub fn digest(data: &[u8]) -> Digest { use sha2::{Digest, Sha256}; let d = Sha256::new().chain_update(data).finalize(); d.into() } struct PacketLength { hello: usize, ack: usize, auth: usize, c_cmd: usize, d_cmd: usize, } impl PacketLength { pub fn new() -> PacketLength { let username = "default"; let d = digest(username.as_bytes()); let hello = bincode::serialized_size(&Hello::ControlChannelHello(CURRENT_PROTO_VERSION, d)) .unwrap() as usize; let c_cmd = bincode::serialized_size(&ControlChannelCmd::CreateDataChannel).unwrap() as usize; let d_cmd = bincode::serialized_size(&DataChannelCmd::StartForwardTcp).unwrap() as usize; let ack = Ack::Ok; let ack = bincode::serialized_size(&ack).unwrap() as usize; let auth = bincode::serialized_size(&Auth(d)).unwrap() as usize; PacketLength { hello, ack, auth, c_cmd, d_cmd, } } } lazy_static! { static ref PACKET_LEN: PacketLength = PacketLength::new(); } pub async fn read_hello(conn: &mut T) -> Result { let mut buf = vec![0u8; PACKET_LEN.hello]; conn.read_exact(&mut buf) .await .with_context(|| "Failed to read hello")?; let hello = bincode::deserialize(&buf).with_context(|| "Failed to deserialize hello")?; match hello { Hello::ControlChannelHello(v, _) => { if v != CURRENT_PROTO_VERSION { bail!( "Protocol version mismatched. Expected {}, got {}. Please update `rathole`.", CURRENT_PROTO_VERSION, v ); } } Hello::DataChannelHello(v, _) => { if v != CURRENT_PROTO_VERSION { bail!( "Protocol version mismatched. Expected {}, got {}. Please update `rathole`.", CURRENT_PROTO_VERSION, v ); } } } Ok(hello) } pub async fn read_auth(conn: &mut T) -> Result { let mut buf = vec![0u8; PACKET_LEN.auth]; conn.read_exact(&mut buf) .await .with_context(|| "Failed to read auth")?; bincode::deserialize(&buf).with_context(|| "Failed to deserialize auth") } pub async fn read_ack(conn: &mut T) -> Result { let mut bytes = vec![0u8; PACKET_LEN.ack]; conn.read_exact(&mut bytes) .await .with_context(|| "Failed to read ack")?; bincode::deserialize(&bytes).with_context(|| "Failed to deserialize ack") } pub async fn read_control_cmd( conn: &mut T, ) -> Result { let mut bytes = vec![0u8; PACKET_LEN.c_cmd]; conn.read_exact(&mut bytes) .await .with_context(|| "Failed to read cmd")?; bincode::deserialize(&bytes).with_context(|| "Failed to deserialize control cmd") } pub async fn read_data_cmd( conn: &mut T, ) -> Result { let mut bytes = vec![0u8; PACKET_LEN.d_cmd]; conn.read_exact(&mut bytes) .await .with_context(|| "Failed to read cmd")?; bincode::deserialize(&bytes).with_context(|| "Failed to deserialize data cmd") } ================================================ FILE: src/server.rs ================================================ use crate::config::{Config, ServerConfig, ServerServiceConfig, ServiceType, TransportType}; use crate::config_watcher::{ConfigChange, ServerServiceChange}; use crate::constants::{listen_backoff, UDP_BUFFER_SIZE}; use crate::helper::{retry_notify_with_deadline, write_and_flush}; use crate::multi_map::MultiMap; use crate::protocol::Hello::{ControlChannelHello, DataChannelHello}; use crate::protocol::{ self, read_auth, read_hello, Ack, ControlChannelCmd, DataChannelCmd, Hello, UdpTraffic, HASH_WIDTH_IN_BYTES, }; use crate::transport::{SocketOpts, TcpTransport, Transport}; use anyhow::{anyhow, bail, Context, Result}; use backoff::backoff::Backoff; use backoff::ExponentialBackoff; use rand::RngCore; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::io::{self, copy_bidirectional, AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream, UdpSocket}; use tokio::sync::{broadcast, mpsc, RwLock}; use tokio::time; use tracing::{debug, error, info, info_span, instrument, warn, Instrument, Span}; #[cfg(feature = "noise")] use crate::transport::NoiseTransport; #[cfg(any(feature = "native-tls", feature = "rustls"))] use crate::transport::TlsTransport; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] use crate::transport::WebsocketTransport; type ServiceDigest = protocol::Digest; // SHA256 of a service name type Nonce = protocol::Digest; // Also called `session_key` const TCP_POOL_SIZE: usize = 8; // The number of cached connections for TCP servies const UDP_POOL_SIZE: usize = 2; // The number of cached connections for UDP services const CHAN_SIZE: usize = 2048; // The capacity of various chans const HANDSHAKE_TIMEOUT: u64 = 5; // Timeout for transport handshake // The entrypoint of running a server pub async fn run_server( config: Config, shutdown_rx: broadcast::Receiver, update_rx: mpsc::Receiver, ) -> Result<()> { let config = match config.server { Some(config) => config, None => { return Err(anyhow!("Try to run as a server, but the configuration is missing. Please add the `[server]` block")) } }; match config.transport.transport_type { TransportType::Tcp => { let mut server = Server::::from(config).await?; server.run(shutdown_rx, update_rx).await?; } TransportType::Tls => { #[cfg(any(feature = "native-tls", feature = "rustls"))] { let mut server = Server::::from(config).await?; server.run(shutdown_rx, update_rx).await?; } #[cfg(not(any(feature = "native-tls", feature = "rustls")))] crate::helper::feature_neither_compile("native-tls", "rustls") } TransportType::Noise => { #[cfg(feature = "noise")] { let mut server = Server::::from(config).await?; server.run(shutdown_rx, update_rx).await?; } #[cfg(not(feature = "noise"))] crate::helper::feature_not_compile("noise") } TransportType::Websocket => { #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] { let mut server = Server::::from(config).await?; server.run(shutdown_rx, update_rx).await?; } #[cfg(not(any(feature = "websocket-native-tls", feature = "websocket-rustls")))] crate::helper::feature_neither_compile("websocket-native-tls", "websocket-rustls") } } Ok(()) } // A hash map of ControlChannelHandles, indexed by ServiceDigest or Nonce // See also MultiMap type ControlChannelMap = MultiMap>; // Server holds all states of running a server struct Server { // `[server]` config config: Arc, // `[server.services]` config, indexed by ServiceDigest services: Arc>>, // Collection of contorl channels control_channels: Arc>>, // Wrapper around the transport layer transport: Arc, } // Generate a hash map of services which is indexed by ServiceDigest fn generate_service_hashmap( server_config: &ServerConfig, ) -> HashMap { let mut ret = HashMap::new(); for u in &server_config.services { ret.insert(protocol::digest(u.0.as_bytes()), (*u.1).clone()); } ret } impl Server { // Create a server from `[server]` pub async fn from(config: ServerConfig) -> Result> { let config = Arc::new(config); let services = Arc::new(RwLock::new(generate_service_hashmap(&config))); let control_channels = Arc::new(RwLock::new(ControlChannelMap::new())); let transport = Arc::new(T::new(&config.transport)?); Ok(Server { config, services, control_channels, transport, }) } // The entry point of Server pub async fn run( &mut self, mut shutdown_rx: broadcast::Receiver, mut update_rx: mpsc::Receiver, ) -> Result<()> { // Listen at `server.bind_addr` let l = self .transport .bind(&self.config.bind_addr) .await .with_context(|| "Failed to listen at `server.bind_addr`")?; info!("Listening at {}", self.config.bind_addr); // Retry at least every 100ms let mut backoff = ExponentialBackoff { max_interval: Duration::from_millis(100), max_elapsed_time: None, ..Default::default() }; // Wait for connections and shutdown signals loop { tokio::select! { // Wait for incoming control and data channels ret = self.transport.accept(&l) => { match ret { Err(err) => { // Detects whether it's an IO error if let Some(err) = err.downcast_ref::() { // If it is an IO error, then it's possibly an // EMFILE. So sleep for a while and retry // TODO: Only sleep for EMFILE, ENFILE, ENOMEM, ENOBUFS if let Some(d) = backoff.next_backoff() { error!("Failed to accept: {:#}. Retry in {:?}...", err, d); time::sleep(d).await; } else { // This branch will never be executed according to the current retry policy error!("Too many retries. Aborting..."); break; } } // If it's not an IO error, then it comes from // the transport layer, so just ignore it } Ok((conn, addr)) => { backoff.reset(); // Do transport handshake with a timeout match time::timeout(Duration::from_secs(HANDSHAKE_TIMEOUT), self.transport.handshake(conn)).await { Ok(conn) => { match conn.with_context(|| "Failed to do transport handshake") { Ok(conn) => { let services = self.services.clone(); let control_channels = self.control_channels.clone(); let server_config = self.config.clone(); tokio::spawn(async move { if let Err(err) = handle_connection(conn, services, control_channels, server_config).await { error!("{:#}", err); } }.instrument(info_span!("connection", %addr))); }, Err(e) => { error!("{:#}", e); } } }, Err(e) => { error!("Transport handshake timeout: {}", e); } } } } }, // Wait for the shutdown signal _ = shutdown_rx.recv() => { info!("Shuting down gracefully..."); break; }, e = update_rx.recv() => { if let Some(e) = e { self.handle_hot_reload(e).await; } } } } info!("Shutdown"); Ok(()) } async fn handle_hot_reload(&mut self, e: ConfigChange) { match e { ConfigChange::ServerChange(server_change) => match server_change { ServerServiceChange::Add(cfg) => { let hash = protocol::digest(cfg.name.as_bytes()); let mut wg = self.services.write().await; let _ = wg.insert(hash, cfg); let mut wg = self.control_channels.write().await; let _ = wg.remove1(&hash); } ServerServiceChange::Delete(s) => { let hash = protocol::digest(s.as_bytes()); let _ = self.services.write().await.remove(&hash); let mut wg = self.control_channels.write().await; let _ = wg.remove1(&hash); } }, ignored => warn!("Ignored {:?} since running as a server", ignored), } } } // Handle connections to `server.bind_addr` async fn handle_connection( mut conn: T::Stream, services: Arc>>, control_channels: Arc>>, server_config: Arc, ) -> Result<()> { // Read hello let hello = read_hello(&mut conn).await?; match hello { ControlChannelHello(_, service_digest) => { do_control_channel_handshake( conn, services, control_channels, service_digest, server_config, ) .await?; } DataChannelHello(_, nonce) => { do_data_channel_handshake(conn, control_channels, nonce).await?; } } Ok(()) } async fn do_control_channel_handshake( mut conn: T::Stream, services: Arc>>, control_channels: Arc>>, service_digest: ServiceDigest, server_config: Arc, ) -> Result<()> { info!("Try to handshake a control channel"); T::hint(&conn, SocketOpts::for_control_channel()); // Generate a nonce let mut nonce = vec![0u8; HASH_WIDTH_IN_BYTES]; rand::thread_rng().fill_bytes(&mut nonce); // Send hello let hello_send = Hello::ControlChannelHello( protocol::CURRENT_PROTO_VERSION, nonce.clone().try_into().unwrap(), ); conn.write_all(&bincode::serialize(&hello_send).unwrap()) .await?; conn.flush().await?; // Lookup the service let service_config = match services.read().await.get(&service_digest) { Some(v) => v, None => { conn.write_all(&bincode::serialize(&Ack::ServiceNotExist).unwrap()) .await?; bail!("No such a service {}", hex::encode(service_digest)); } } .to_owned(); let service_name = &service_config.name; // Calculate the checksum let mut concat = Vec::from(service_config.token.as_ref().unwrap().as_bytes()); concat.append(&mut nonce); // Read auth let protocol::Auth(d) = read_auth(&mut conn).await?; // Validate let session_key = protocol::digest(&concat); if session_key != d { conn.write_all(&bincode::serialize(&Ack::AuthFailed).unwrap()) .await?; debug!( "Expect {}, but got {}", hex::encode(session_key), hex::encode(d) ); bail!("Service {} failed the authentication", service_name); } else { let mut h = control_channels.write().await; // If there's already a control channel for the service, then drop the old one. // Because a control channel doesn't report back when it's dead, // the handle in the map could be stall, dropping the old handle enables // the client to reconnect. if h.remove1(&service_digest).is_some() { warn!( "Dropping previous control channel for service {}", service_name ); } // Send ack conn.write_all(&bincode::serialize(&Ack::Ok).unwrap()) .await?; conn.flush().await?; info!(service = %service_config.name, "Control channel established"); let handle = ControlChannelHandle::new(conn, service_config, server_config.heartbeat_interval); // Insert the new handle let _ = h.insert(service_digest, session_key, handle); } Ok(()) } async fn do_data_channel_handshake( conn: T::Stream, control_channels: Arc>>, nonce: Nonce, ) -> Result<()> { debug!("Try to handshake a data channel"); // Validate let control_channels_guard = control_channels.read().await; match control_channels_guard.get2(&nonce) { Some(handle) => { T::hint(&conn, SocketOpts::from_server_cfg(&handle.service)); // Send the data channel to the corresponding control channel handle .data_ch_tx .send(conn) .await .with_context(|| "Data channel for a stale control channel")?; } None => { warn!("Data channel has incorrect nonce"); } } Ok(()) } pub struct ControlChannelHandle { // Shutdown the control channel by dropping it _shutdown_tx: broadcast::Sender, data_ch_tx: mpsc::Sender, service: ServerServiceConfig, } impl ControlChannelHandle where T: 'static + Transport, { // Create a control channel handle, where the control channel handling task // and the connection pool task are created. #[instrument(name = "handle", skip_all, fields(service = %service.name))] fn new( conn: T::Stream, service: ServerServiceConfig, heartbeat_interval: u64, ) -> ControlChannelHandle { // Create a shutdown channel let (shutdown_tx, shutdown_rx) = broadcast::channel::(1); // Store data channels let (data_ch_tx, data_ch_rx) = mpsc::channel(CHAN_SIZE * 2); // Store data channel creation requests let (data_ch_req_tx, data_ch_req_rx) = mpsc::unbounded_channel(); // Cache some data channels for later use let pool_size = match service.service_type { ServiceType::Tcp => TCP_POOL_SIZE, ServiceType::Udp => UDP_POOL_SIZE, }; for _i in 0..pool_size { if let Err(e) = data_ch_req_tx.send(true) { error!("Failed to request data channel {}", e); }; } let shutdown_rx_clone = shutdown_tx.subscribe(); let bind_addr = service.bind_addr.clone(); match service.service_type { ServiceType::Tcp => tokio::spawn( async move { if let Err(e) = run_tcp_connection_pool::( bind_addr, data_ch_rx, data_ch_req_tx, shutdown_rx_clone, ) .await .with_context(|| "Failed to run TCP connection pool") { error!("{:#}", e); } } .instrument(Span::current()), ), ServiceType::Udp => tokio::spawn( async move { if let Err(e) = run_udp_connection_pool::( bind_addr, data_ch_rx, data_ch_req_tx, shutdown_rx_clone, ) .await .with_context(|| "Failed to run TCP connection pool") { error!("{:#}", e); } } .instrument(Span::current()), ), }; // Create the control channel let ch = ControlChannel:: { conn, shutdown_rx, data_ch_req_rx, heartbeat_interval, }; // Run the control channel tokio::spawn( async move { if let Err(err) = ch.run().await { error!("{:#}", err); } } .instrument(Span::current()), ); ControlChannelHandle { _shutdown_tx: shutdown_tx, data_ch_tx, service, } } } // Control channel, using T as the transport layer. P is TcpStream or UdpTraffic struct ControlChannel { conn: T::Stream, // The connection of control channel shutdown_rx: broadcast::Receiver, // Receives the shutdown signal data_ch_req_rx: mpsc::UnboundedReceiver, // Receives visitor connections heartbeat_interval: u64, // Application-layer heartbeat interval in secs } impl ControlChannel { async fn write_and_flush(&mut self, data: &[u8]) -> Result<()> { write_and_flush(&mut self.conn, data) .await .with_context(|| "Failed to write control cmds")?; Ok(()) } // Run a control channel #[instrument(skip_all)] async fn run(mut self) -> Result<()> { let create_ch_cmd = bincode::serialize(&ControlChannelCmd::CreateDataChannel).unwrap(); let heartbeat = bincode::serialize(&ControlChannelCmd::HeartBeat).unwrap(); // Wait for data channel requests and the shutdown signal loop { tokio::select! { val = self.data_ch_req_rx.recv() => { match val { Some(_) => { if let Err(e) = self.write_and_flush(&create_ch_cmd).await { error!("{:#}", e); break; } } None => { break; } } }, _ = time::sleep(Duration::from_secs(self.heartbeat_interval)), if self.heartbeat_interval != 0 => { if let Err(e) = self.write_and_flush(&heartbeat).await { error!("{:#}", e); break; } } // Wait for the shutdown signal _ = self.shutdown_rx.recv() => { break; } } } info!("Control channel shutdown"); Ok(()) } } fn tcp_listen_and_send( addr: String, data_ch_req_tx: mpsc::UnboundedSender, mut shutdown_rx: broadcast::Receiver, ) -> mpsc::Receiver { let (tx, rx) = mpsc::channel(CHAN_SIZE); tokio::spawn(async move { let l = retry_notify_with_deadline(listen_backoff(), || async { Ok(TcpListener::bind(&addr).await?) }, |e, duration| { error!("{:#}. Retry in {:?}", e, duration); }, &mut shutdown_rx).await .with_context(|| "Failed to listen for the service"); let l: TcpListener = match l { Ok(v) => v, Err(e) => { error!("{:#}", e); return; } }; info!("Listening at {}", &addr); // Retry at least every 1s let mut backoff = ExponentialBackoff { max_interval: Duration::from_secs(1), max_elapsed_time: None, ..Default::default() }; // Wait for visitors and the shutdown signal loop { tokio::select! { val = l.accept() => { match val { Err(e) => { // `l` is a TCP listener so this must be a IO error // Possibly a EMFILE. So sleep for a while error!("{}. Sleep for a while", e); if let Some(d) = backoff.next_backoff() { time::sleep(d).await; } else { // This branch will never be reached for current backoff policy error!("Too many retries. Aborting..."); break; } } Ok((incoming, addr)) => { // For every visitor, request to create a data channel if data_ch_req_tx.send(true).with_context(|| "Failed to send data chan create request").is_err() { // An error indicates the control channel is broken // So break the loop break; } backoff.reset(); debug!("New visitor from {}", addr); // Send the visitor to the connection pool let _ = tx.send(incoming).await; } } }, _ = shutdown_rx.recv() => { break; } } } info!("TCPListener shutdown"); }.instrument(Span::current())); rx } #[instrument(skip_all)] async fn run_tcp_connection_pool( bind_addr: String, mut data_ch_rx: mpsc::Receiver, data_ch_req_tx: mpsc::UnboundedSender, shutdown_rx: broadcast::Receiver, ) -> Result<()> { let mut visitor_rx = tcp_listen_and_send(bind_addr, data_ch_req_tx.clone(), shutdown_rx); let cmd = bincode::serialize(&DataChannelCmd::StartForwardTcp).unwrap(); 'pool: while let Some(mut visitor) = visitor_rx.recv().await { loop { if let Some(mut ch) = data_ch_rx.recv().await { if write_and_flush(&mut ch, &cmd).await.is_ok() { tokio::spawn(async move { let _ = copy_bidirectional(&mut ch, &mut visitor).await; }); break; } else { // Current data channel is broken. Request for a new one if data_ch_req_tx.send(true).is_err() { break 'pool; } } } else { break 'pool; } } } info!("Shutdown"); Ok(()) } #[instrument(skip_all)] async fn run_udp_connection_pool( bind_addr: String, mut data_ch_rx: mpsc::Receiver, _data_ch_req_tx: mpsc::UnboundedSender, mut shutdown_rx: broadcast::Receiver, ) -> Result<()> { // TODO: Load balance let l = retry_notify_with_deadline( listen_backoff(), || async { Ok(UdpSocket::bind(&bind_addr).await?) }, |e, duration| { warn!("{:#}. Retry in {:?}", e, duration); }, &mut shutdown_rx, ) .await .with_context(|| "Failed to listen for the service")?; info!("Listening at {}", &bind_addr); let cmd = bincode::serialize(&DataChannelCmd::StartForwardUdp).unwrap(); // Receive one data channel let mut conn = data_ch_rx .recv() .await .ok_or_else(|| anyhow!("No available data channels"))?; write_and_flush(&mut conn, &cmd).await?; let mut buf = [0u8; UDP_BUFFER_SIZE]; loop { tokio::select! { // Forward inbound traffic to the client val = l.recv_from(&mut buf) => { let (n, from) = val?; UdpTraffic::write_slice(&mut conn, from, &buf[..n]).await?; }, // Forward outbound traffic from the client to the visitor hdr_len = conn.read_u8() => { let t = UdpTraffic::read(&mut conn, hdr_len?).await?; l.send_to(&t.data, t.from).await?; } _ = shutdown_rx.recv() => { break; } } } debug!("UDP pool dropped"); Ok(()) } ================================================ FILE: src/transport/mod.rs ================================================ use crate::config::{ClientServiceConfig, ServerServiceConfig, TcpConfig, TransportConfig}; use crate::helper::{to_socket_addr, try_set_tcp_keepalive}; use anyhow::{Context, Result}; use async_trait::async_trait; use std::fmt::{Debug, Display}; use std::net::SocketAddr; use std::time::Duration; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::{TcpStream, ToSocketAddrs}; use tracing::{error, trace}; pub const DEFAULT_NODELAY: bool = true; pub const DEFAULT_KEEPALIVE_SECS: u64 = 20; pub const DEFAULT_KEEPALIVE_INTERVAL: u64 = 8; #[derive(Clone)] pub struct AddrMaybeCached { pub addr: String, pub socket_addr: Option, } impl AddrMaybeCached { pub fn new(addr: &str) -> AddrMaybeCached { AddrMaybeCached { addr: addr.to_string(), socket_addr: None, } } pub async fn resolve(&mut self) -> Result<()> { match to_socket_addr(&self.addr).await { Ok(s) => { self.socket_addr = Some(s); Ok(()) } Err(e) => Err(e), } } } impl Display for AddrMaybeCached { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.socket_addr { Some(s) => f.write_fmt(format_args!("{}", s)), None => f.write_str(&self.addr), } } } /// Specify a transport layer, like TCP, TLS #[async_trait] pub trait Transport: Debug + Send + Sync { type Acceptor: Send + Sync; type RawStream: Send + Sync; type Stream: 'static + AsyncRead + AsyncWrite + Unpin + Send + Sync + Debug; fn new(config: &TransportConfig) -> Result where Self: Sized; /// Provide the transport with socket options, which can be handled at the need of the transport fn hint(conn: &Self::Stream, opts: SocketOpts); async fn bind(&self, addr: T) -> Result; /// accept must be cancel safe async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)>; async fn handshake(&self, conn: Self::RawStream) -> Result; async fn connect(&self, addr: &AddrMaybeCached) -> Result; } mod tcp; pub use tcp::TcpTransport; #[cfg(all(feature = "native-tls", feature = "rustls"))] compile_error!("Only one of `native-tls` and `rustls` can be enabled"); #[cfg(feature = "native-tls")] mod native_tls; #[cfg(feature = "native-tls")] use native_tls as tls; #[cfg(feature = "rustls")] mod rustls; #[cfg(feature = "rustls")] use rustls as tls; #[cfg(any(feature = "native-tls", feature = "rustls"))] pub(crate) use tls::TlsTransport; #[cfg(feature = "noise")] mod noise; #[cfg(feature = "noise")] pub use noise::NoiseTransport; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] mod websocket; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] pub use websocket::WebsocketTransport; #[derive(Debug, Clone, Copy)] struct Keepalive { // tcp_keepalive_time if the underlying protocol is TCP pub keepalive_secs: u64, // tcp_keepalive_intvl if the underlying protocol is TCP pub keepalive_interval: u64, } #[derive(Debug, Clone, Copy)] pub struct SocketOpts { // None means do not change nodelay: Option, // keepalive must be Some or None at the same time, or the behavior will be platform-dependent keepalive: Option, } impl SocketOpts { fn none() -> SocketOpts { SocketOpts { nodelay: None, keepalive: None, } } /// Socket options for the control channel pub fn for_control_channel() -> SocketOpts { SocketOpts { nodelay: Some(true), // Always set nodelay for the control channel ..SocketOpts::none() // None means do not change. Keepalive is set by TcpTransport } } } impl SocketOpts { pub fn from_cfg(cfg: &TcpConfig) -> SocketOpts { SocketOpts { nodelay: Some(cfg.nodelay), keepalive: Some(Keepalive { keepalive_secs: cfg.keepalive_secs, keepalive_interval: cfg.keepalive_interval, }), } } pub fn from_client_cfg(cfg: &ClientServiceConfig) -> SocketOpts { SocketOpts { nodelay: cfg.nodelay, ..SocketOpts::none() } } pub fn from_server_cfg(cfg: &ServerServiceConfig) -> SocketOpts { SocketOpts { nodelay: cfg.nodelay, ..SocketOpts::none() } } pub fn apply(&self, conn: &TcpStream) { if let Some(v) = self.keepalive { let keepalive_duration = Duration::from_secs(v.keepalive_secs); let keepalive_interval = Duration::from_secs(v.keepalive_interval); if let Err(e) = try_set_tcp_keepalive(conn, keepalive_duration, keepalive_interval) .with_context(|| "Failed to set keepalive") { error!("{:#}", e); } } if let Some(nodelay) = self.nodelay { trace!("Set nodelay {}", nodelay); if let Err(e) = conn .set_nodelay(nodelay) .with_context(|| "Failed to set nodelay") { error!("{:#}", e); } } } } ================================================ FILE: src/transport/native_tls.rs ================================================ use crate::config::{TlsConfig, TransportConfig}; use crate::helper::host_port_pair; use crate::transport::{AddrMaybeCached, SocketOpts, TcpTransport, Transport}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use std::fs; use std::net::SocketAddr; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; use tokio_native_tls::native_tls::{self, Certificate, Identity}; pub(crate) use tokio_native_tls::TlsStream; use tokio_native_tls::{TlsAcceptor, TlsConnector}; #[derive(Debug)] pub struct TlsTransport { tcp: TcpTransport, config: TlsConfig, connector: Option, tls_acceptor: Option, } #[async_trait] impl Transport for TlsTransport { type Acceptor = TcpListener; type RawStream = TcpStream; type Stream = TlsStream; fn new(config: &TransportConfig) -> Result { let tcp = TcpTransport::new(config)?; let config = config .tls .as_ref() .ok_or_else(|| anyhow!("Missing tls config"))?; let connector = match config.trusted_root.as_ref() { Some(path) => { let s = fs::read_to_string(path) .with_context(|| "Failed to read the `tls.trusted_root`")?; let cert = Certificate::from_pem(s.as_bytes()) .with_context(|| "Failed to read certificate from `tls.trusted_root`")?; let connector = native_tls::TlsConnector::builder() .add_root_certificate(cert) .build()?; Some(TlsConnector::from(connector)) } None => { // if no trusted_root is specified, allow TlsConnector to use system default let connector = native_tls::TlsConnector::builder().build()?; Some(TlsConnector::from(connector)) } }; let tls_acceptor = match config.pkcs12.as_ref() { Some(path) => { let ident = Identity::from_pkcs12( &fs::read(path)?, config.pkcs12_password.as_ref().unwrap(), ) .with_context(|| "Failed to create identitiy")?; Some(TlsAcceptor::from( native_tls::TlsAcceptor::new(ident).unwrap(), )) } None => None, }; Ok(TlsTransport { tcp, config: config.clone(), connector, tls_acceptor, }) } fn hint(conn: &Self::Stream, opt: SocketOpts) { opt.apply(conn.get_ref().get_ref().get_ref()); } async fn bind(&self, addr: A) -> Result { let l = TcpListener::bind(addr) .await .with_context(|| "Failed to create tcp listener")?; Ok(l) } async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)> { self.tcp .accept(a) .await .with_context(|| "Failed to accept TCP connection") } async fn handshake(&self, conn: Self::RawStream) -> Result { let conn = self.tls_acceptor.as_ref().unwrap().accept(conn).await?; Ok(conn) } async fn connect(&self, addr: &AddrMaybeCached) -> Result { let conn = self.tcp.connect(addr).await?; let connector = self.connector.as_ref().unwrap(); Ok(connector .connect( self.config .hostname .as_deref() .unwrap_or(host_port_pair(&addr.addr)?.0), conn, ) .await?) } } #[cfg(feature = "websocket-native-tls")] pub(crate) fn get_tcpstream(s: &TlsStream) -> &TcpStream { s.get_ref().get_ref().get_ref() } ================================================ FILE: src/transport/noise.rs ================================================ use std::net::SocketAddr; use super::{AddrMaybeCached, SocketOpts, TcpTransport, Transport}; use crate::config::{NoiseConfig, TransportConfig}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use snowstorm::{Builder, NoiseParams, NoiseStream}; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; pub struct NoiseTransport { tcp: TcpTransport, config: NoiseConfig, params: NoiseParams, local_private_key: Vec, remote_public_key: Option>, } impl std::fmt::Debug for NoiseTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, "{:?}", self.config) } } impl NoiseTransport { fn builder(&self) -> Builder { let builder = Builder::new(self.params.clone()).local_private_key(&self.local_private_key); match &self.remote_public_key { Some(x) => builder.remote_public_key(x), None => builder, } } } #[async_trait] impl Transport for NoiseTransport { type Acceptor = TcpListener; type RawStream = TcpStream; type Stream = snowstorm::stream::NoiseStream; fn new(config: &TransportConfig) -> Result { let tcp = TcpTransport::new(config)?; let config = match &config.noise { Some(v) => v.clone(), None => return Err(anyhow!("Missing noise config")), }; let builder = Builder::new(config.pattern.parse()?); let remote_public_key = match &config.remote_public_key { Some(x) => { Some(base64::decode(x).with_context(|| "Failed to decode remote_public_key")?) } None => None, }; let local_private_key = match &config.local_private_key { Some(x) => base64::decode(x.as_bytes()) .with_context(|| "Failed to decode local_private_key")?, None => builder.generate_keypair()?.private, }; let params: NoiseParams = config.pattern.parse()?; Ok(NoiseTransport { tcp, config, params, local_private_key, remote_public_key, }) } fn hint(conn: &Self::Stream, opt: SocketOpts) { opt.apply(conn.get_inner()); } async fn bind(&self, addr: T) -> Result { Ok(TcpListener::bind(addr).await?) } async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)> { self.tcp .accept(a) .await .with_context(|| "Failed to accept TCP connection") } async fn handshake(&self, conn: Self::RawStream) -> Result { let conn = NoiseStream::handshake(conn, self.builder().build_responder()?) .await .with_context(|| "Failed to do noise handshake")?; Ok(conn) } async fn connect(&self, addr: &AddrMaybeCached) -> Result { let conn = self .tcp .connect(addr) .await .with_context(|| "Failed to connect TCP socket")?; let conn = NoiseStream::handshake(conn, self.builder().build_initiator()?) .await .with_context(|| "Failed to do noise handshake")?; return Ok(conn); } } ================================================ FILE: src/transport/rustls.rs ================================================ use crate::config::{TlsConfig, TransportConfig}; use crate::helper::host_port_pair; use crate::transport::{AddrMaybeCached, SocketOpts, TcpTransport, Transport}; use std::fmt::Debug; use std::fs; use std::net::SocketAddr; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer, ServerName}; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use p12::PFX; use tokio_rustls::rustls::{ClientConfig, RootCertStore, ServerConfig}; pub(crate) use tokio_rustls::TlsStream; use tokio_rustls::{TlsAcceptor, TlsConnector}; pub struct TlsTransport { tcp: TcpTransport, config: TlsConfig, connector: Option, tls_acceptor: Option, } // workaround for TlsConnector and TlsAcceptor not implementing Debug impl Debug for TlsTransport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TlsTransport") .field("tcp", &self.tcp) .field("config", &self.config) .finish() } } fn load_server_config(config: &TlsConfig) -> Result> { if let Some(pkcs12_path) = config.pkcs12.as_ref() { let buf = fs::read(pkcs12_path)?; let pfx = PFX::parse(buf.as_slice())?; let pass = config.pkcs12_password.as_ref().unwrap(); let certs = pfx.cert_bags(pass)?; let keys = pfx.key_bags(pass)?; let chain: Vec = certs.into_iter().map(CertificateDer::from).collect(); let key = PrivatePkcs8KeyDer::from(keys.into_iter().next().unwrap()); Ok(Some( ServerConfig::builder() .with_no_client_auth() .with_single_cert(chain, key.into())?, )) } else { Ok(None) } } fn load_client_config(config: &TlsConfig) -> Result> { let cert = if let Some(path) = config.trusted_root.as_ref() { rustls_pemfile::certs(&mut std::io::BufReader::new(fs::File::open(path).unwrap())) .map(|cert| cert.unwrap()) .next() .with_context(|| "Failed to read certificate")? } else { // read from native match rustls_native_certs::load_native_certs() { Ok(certs) => certs.into_iter().next().unwrap(), Err(e) => { eprintln!("Failed to load native certs: {}", e); return Ok(None); } } }; let mut root_certs = RootCertStore::empty(); root_certs.add(cert).unwrap(); Ok(Some( ClientConfig::builder() .with_root_certificates(root_certs) .with_no_client_auth(), )) } #[async_trait] impl Transport for TlsTransport { type Acceptor = TcpListener; type RawStream = TcpStream; type Stream = TlsStream; fn new(config: &TransportConfig) -> Result { let tcp = TcpTransport::new(config)?; let config = config .tls .as_ref() .ok_or_else(|| anyhow!("Missing tls config"))?; let connector = load_client_config(config) .unwrap() .map(|c| Arc::new(c).into()); let tls_acceptor = load_server_config(config) .unwrap() .map(|c| Arc::new(c).into()); Ok(TlsTransport { tcp, config: config.clone(), connector, tls_acceptor, }) } fn hint(conn: &Self::Stream, opt: SocketOpts) { opt.apply(conn.get_ref().0); } async fn bind(&self, addr: A) -> Result { let l = TcpListener::bind(addr) .await .with_context(|| "Failed to create tcp listener")?; Ok(l) } async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)> { self.tcp .accept(a) .await .with_context(|| "Failed to accept TCP connection") } async fn handshake(&self, conn: Self::RawStream) -> Result { let conn = self.tls_acceptor.as_ref().unwrap().accept(conn).await?; Ok(tokio_rustls::TlsStream::Server(conn)) } async fn connect(&self, addr: &AddrMaybeCached) -> Result { let conn = self.tcp.connect(addr).await?; let connector = self.connector.as_ref().unwrap(); let host_name = self .config .hostname .as_deref() .unwrap_or(host_port_pair(&addr.addr)?.0); Ok(tokio_rustls::TlsStream::Client( connector .connect(ServerName::try_from(host_name)?.to_owned(), conn) .await?, )) } } pub(crate) fn get_tcpstream(s: &TlsStream) -> &TcpStream { &s.get_ref().0 } ================================================ FILE: src/transport/tcp.rs ================================================ use crate::{ config::{TcpConfig, TransportConfig}, helper::tcp_connect_with_proxy, }; use super::{AddrMaybeCached, SocketOpts, Transport}; use anyhow::Result; use async_trait::async_trait; use std::net::SocketAddr; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; #[derive(Debug)] pub struct TcpTransport { socket_opts: SocketOpts, cfg: TcpConfig, } #[async_trait] impl Transport for TcpTransport { type Acceptor = TcpListener; type Stream = TcpStream; type RawStream = TcpStream; fn new(config: &TransportConfig) -> Result { Ok(TcpTransport { socket_opts: SocketOpts::from_cfg(&config.tcp), cfg: config.tcp.clone(), }) } fn hint(conn: &Self::Stream, opt: SocketOpts) { opt.apply(conn); } async fn bind(&self, addr: T) -> Result { Ok(TcpListener::bind(addr).await?) } async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)> { let (s, addr) = a.accept().await?; self.socket_opts.apply(&s); Ok((s, addr)) } async fn handshake(&self, conn: Self::RawStream) -> Result { Ok(conn) } async fn connect(&self, addr: &AddrMaybeCached) -> Result { let s = tcp_connect_with_proxy(addr, self.cfg.proxy.as_ref()).await?; self.socket_opts.apply(&s); Ok(s) } } ================================================ FILE: src/transport/websocket.rs ================================================ use core::result::Result; use std::io::{Error, ErrorKind}; use std::net::SocketAddr; use std::pin::Pin; use std::task::{ready, Context, Poll}; use super::{AddrMaybeCached, SocketOpts, TcpTransport, TlsTransport, Transport}; use crate::config::TransportConfig; use anyhow::anyhow; use async_trait::async_trait; use bytes::Bytes; use futures_core::stream::Stream; use futures_sink::Sink; use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, ReadBuf}; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; #[cfg(any(feature = "native-tls", feature = "rustls"))] use super::tls::get_tcpstream; #[cfg(any(feature = "native-tls", feature = "rustls"))] use super::tls::TlsStream; use tokio_tungstenite::tungstenite::protocol::{Message, WebSocketConfig}; use tokio_tungstenite::{accept_async_with_config, client_async_with_config, WebSocketStream}; use tokio_util::io::StreamReader; use url::Url; #[derive(Debug)] enum TransportStream { Insecure(TcpStream), Secure(TlsStream), } impl TransportStream { fn get_tcpstream(&self) -> &TcpStream { match self { TransportStream::Insecure(s) => s, TransportStream::Secure(s) => get_tcpstream(s), } } } impl AsyncRead for TransportStream { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { match self.get_mut() { TransportStream::Insecure(s) => Pin::new(s).poll_read(cx, buf), TransportStream::Secure(s) => Pin::new(s).poll_read(cx, buf), } } } impl AsyncWrite for TransportStream { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { match self.get_mut() { TransportStream::Insecure(s) => Pin::new(s).poll_write(cx, buf), TransportStream::Secure(s) => Pin::new(s).poll_write(cx, buf), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.get_mut() { TransportStream::Insecure(s) => Pin::new(s).poll_flush(cx), TransportStream::Secure(s) => Pin::new(s).poll_flush(cx), } } fn poll_shutdown( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { match self.get_mut() { TransportStream::Insecure(s) => Pin::new(s).poll_shutdown(cx), TransportStream::Secure(s) => Pin::new(s).poll_shutdown(cx), } } } #[derive(Debug)] struct StreamWrapper { inner: WebSocketStream, } impl Stream for StreamWrapper { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match Pin::new(&mut self.get_mut().inner).poll_next(cx) { Poll::Pending => Poll::Pending, Poll::Ready(None) => Poll::Ready(None), Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(Error::new(ErrorKind::Other, err)))) } Poll::Ready(Some(Ok(res))) => { if let Message::Binary(b) = res { Poll::Ready(Some(Ok(Bytes::from(b)))) } else { Poll::Ready(Some(Err(Error::new( ErrorKind::InvalidData, "unexpected frame", )))) } } } } fn size_hint(&self) -> (usize, Option) { self.inner.size_hint() } } #[derive(Debug)] pub struct WebsocketTunnel { inner: StreamReader, } impl AsyncRead for WebsocketTunnel { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_read(cx, buf) } } impl AsyncBufRead for WebsocketTunnel { fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner).poll_fill_buf(cx) } fn consume(self: Pin<&mut Self>, amt: usize) { Pin::new(&mut self.get_mut().inner).consume(amt) } } impl AsyncWrite for WebsocketTunnel { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { let sw = self.get_mut().inner.get_mut(); ready!(Pin::new(&mut sw.inner) .poll_ready(cx) .map_err(|err| Error::new(ErrorKind::Other, err)))?; match Pin::new(&mut sw.inner).start_send(Message::Binary(buf.to_vec())) { Ok(()) => Poll::Ready(Ok(buf.len())), Err(e) => Poll::Ready(Err(Error::new(ErrorKind::Other, e))), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner.get_mut().inner) .poll_flush(cx) .map_err(|err| Error::new(ErrorKind::Other, err)) } fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.get_mut().inner.get_mut().inner) .poll_close(cx) .map_err(|err| Error::new(ErrorKind::Other, err)) } } #[derive(Debug)] enum SubTransport { Secure(TlsTransport), Insecure(TcpTransport), } #[derive(Debug)] pub struct WebsocketTransport { sub: SubTransport, conf: WebSocketConfig, } #[async_trait] impl Transport for WebsocketTransport { type Acceptor = TcpListener; type RawStream = TcpStream; type Stream = WebsocketTunnel; fn new(config: &TransportConfig) -> anyhow::Result { let wsconfig = config .websocket .as_ref() .ok_or_else(|| anyhow!("Missing websocket config"))?; let conf = WebSocketConfig { write_buffer_size: 0, ..WebSocketConfig::default() }; let sub = match wsconfig.tls { true => SubTransport::Secure(TlsTransport::new(config)?), false => SubTransport::Insecure(TcpTransport::new(config)?), }; Ok(WebsocketTransport { sub, conf }) } fn hint(conn: &Self::Stream, opt: SocketOpts) { opt.apply(conn.inner.get_ref().inner.get_ref().get_tcpstream()) } async fn bind( &self, addr: A, ) -> anyhow::Result { TcpListener::bind(addr).await.map_err(Into::into) } async fn accept(&self, a: &Self::Acceptor) -> anyhow::Result<(Self::RawStream, SocketAddr)> { let (s, addr) = match &self.sub { SubTransport::Insecure(t) => t.accept(a).await?, SubTransport::Secure(t) => t.accept(a).await?, }; Ok((s, addr)) } async fn handshake(&self, conn: Self::RawStream) -> anyhow::Result { let tsream = match &self.sub { SubTransport::Insecure(t) => TransportStream::Insecure(t.handshake(conn).await?), SubTransport::Secure(t) => TransportStream::Secure(t.handshake(conn).await?), }; let wsstream = accept_async_with_config(tsream, Some(self.conf)).await?; let tun = WebsocketTunnel { inner: StreamReader::new(StreamWrapper { inner: wsstream }), }; Ok(tun) } async fn connect(&self, addr: &AddrMaybeCached) -> anyhow::Result { let u = format!("ws://{}", &addr.addr.as_str()); let url = Url::parse(&u).unwrap(); let tstream = match &self.sub { SubTransport::Insecure(t) => TransportStream::Insecure(t.connect(addr).await?), SubTransport::Secure(t) => TransportStream::Secure(t.connect(addr).await?), }; let (wsstream, _) = client_async_with_config(url, tstream, Some(self.conf)) .await .expect("failed to connect"); let tun = WebsocketTunnel { inner: StreamReader::new(StreamWrapper { inner: wsstream }), }; Ok(tun) } } ================================================ FILE: tests/common/mod.rs ================================================ use std::path::PathBuf; use anyhow::Result; use tokio::{ io::{self, AsyncReadExt, AsyncWriteExt}, net::{TcpListener, TcpStream, ToSocketAddrs}, sync::broadcast, }; pub const PING: &str = "ping"; pub const PONG: &str = "pong"; pub async fn run_rathole_server( config_path: &str, shutdown_rx: broadcast::Receiver, ) -> Result<()> { let cli = rathole::Cli { config_path: Some(PathBuf::from(config_path)), server: true, client: false, ..Default::default() }; rathole::run(cli, shutdown_rx).await } pub async fn run_rathole_client( config_path: &str, shutdown_rx: broadcast::Receiver, ) -> Result<()> { let cli = rathole::Cli { config_path: Some(PathBuf::from(config_path)), server: false, client: true, ..Default::default() }; rathole::run(cli, shutdown_rx).await } pub mod tcp { use super::*; pub async fn echo_server(addr: A) -> Result<()> { let l = TcpListener::bind(addr).await?; loop { let (conn, _addr) = l.accept().await?; tokio::spawn(async move { let _ = echo(conn).await; }); } } pub async fn pingpong_server(addr: A) -> Result<()> { let l = TcpListener::bind(addr).await?; loop { let (conn, _addr) = l.accept().await?; tokio::spawn(async move { let _ = pingpong(conn).await; }); } } async fn echo(conn: TcpStream) -> Result<()> { let (mut rd, mut wr) = conn.into_split(); io::copy(&mut rd, &mut wr).await?; Ok(()) } async fn pingpong(mut conn: TcpStream) -> Result<()> { let mut buf = [0u8; PING.len()]; while conn.read_exact(&mut buf).await? != 0 { assert_eq!(buf, PING.as_bytes()); conn.write_all(PONG.as_bytes()).await?; } Ok(()) } } pub mod udp { use rathole::UDP_BUFFER_SIZE; use tokio::net::UdpSocket; use tracing::debug; use super::*; pub async fn echo_server(addr: A) -> Result<()> { let l = UdpSocket::bind(addr).await?; debug!("UDP echo server listening"); let mut buf = [0u8; UDP_BUFFER_SIZE]; loop { let (n, addr) = l.recv_from(&mut buf).await?; debug!("Get {:?} from {}", &buf[..n], addr); l.send_to(&buf[..n], addr).await?; } } pub async fn pingpong_server(addr: A) -> Result<()> { let l = UdpSocket::bind(addr).await?; let mut buf = [0u8; UDP_BUFFER_SIZE]; loop { let (n, addr) = l.recv_from(&mut buf).await?; assert_eq!(&buf[..n], PING.as_bytes()); l.send_to(PONG.as_bytes(), addr).await?; } } } ================================================ FILE: tests/config_test/invalid_config/missing_tls_client.toml ================================================ [client] remote_addr = "example.com:2333" [client.transport] type = "tls" [client.services.service1] token = "whatever" local_addr = "127.0.0.1:1081" ================================================ FILE: tests/config_test/invalid_config/missing_tls_server.toml ================================================ [server] bind_addr = "0.0.0.0:2333" [server.transport] type = "tls" [server.transport.tls] pkcs12_password = "password" [server.services.service1] token = "whatever" bind_addr = "0.0.0.0:8081" ================================================ FILE: tests/config_test/invalid_config/missing_tls_server2.toml ================================================ [server] bind_addr = "0.0.0.0:2333" [server.transport] type = "tls" [server.services.service1] token = "whatever" bind_addr = "0.0.0.0:8081" ================================================ FILE: tests/config_test/valid_config/full.toml ================================================ [client] remote_addr = "example.com:2333" # Necessary. The address of the server default_token = "default_token_if_not_specify" # Optional. The default token of services, if they don't define their own ones [client.transport] type = "tcp" # Optional. Possible values: ["tcp", "tls"]. Default: "tcp" [client.transport.tls] # Necessary if `type` is "tls" trusted_root = "ca.pem" # Necessary. The certificate of CA that signed the server's certificate hostname = "example.com" # Optional. The hostname that the client uses to validate the certificate. If not set, fallback to `client.remote_addr` [client.transport.noise] # Noise protocol. See `docs/transport.md` for further explanation pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" # Optional. Default value as shown local_private_key = "key_encoded_in_base64" # Optional remote_public_key = "key_encoded_in_base64" # Optional [client.services.service1] # A service that needs forwarding. The name `service1` can change arbitrarily, as long as identical to the name in the server's configuration type = "tcp" # Optional. The protocol that needs forwarding. Possible values: ["tcp", "udp"]. Default: "tcp" token = "whatever" # Necessary if `client.default_token` not set prefer_ipv6 = false # Optional. If the client prefers to use IPv6 when connecting to the server (e.g.: When the client is behind an ISP's NAT). Default: false local_addr = "127.0.0.1:1081" # Necessary. The address of the service that needs to be forwarded [client.services.service2] # Multiple services can be defined local_addr = "127.0.0.1:1082" [server] bind_addr = "0.0.0.0:2333" # Necessary. The address that the server listens for clients. Generally only the port needs to be change. default_token = "default_token_if_not_specify" # Optional [server.transport] type = "tcp" # Same as `[client.transport]` [server.transport.tls] # Necessary if `type` is "tls" pkcs12 = "identify.pfx" # Necessary. pkcs12 file of server's certificate and private key pkcs12_password = "password" # Necessary. Password of the pkcs12 file [server.transport.noise] # Same as `[client.transport.noise]` pattern = "Noise_NK_25519_ChaChaPoly_BLAKE2s" local_private_key = "key_encoded_in_base64" remote_public_key = "key_encoded_in_base64" [server.services.service1] # The service name must be identical to the client side type = "tcp" # Optional. Same as the client `[client.services.X.type] token = "whatever" # Necesary if `server.default_token` not set bind_addr = "0.0.0.0:8081" # Necessary. The address of the service is exposed at. Generally only the port needs to be change. [server.services.service2] bind_addr = "0.0.0.1:8082" ================================================ FILE: tests/for_tcp/noise_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "default_token_if_not_specify" [client.transport] type = "noise" [client.transport.noise] remote_public_key = "mEnUEACy9UrTBmwoCJb6fcKWBRdvfD9XzuBVsroOLFg=" [client.services.echo] local_addr = "127.0.0.1:8080" [client.services.pingpong] local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2333" default_token = "default_token_if_not_specify" [server.transport] type = "noise" [server.transport.noise] local_private_key = "kQiSRtS3bs8BoGCJYgFnl1FLrTG1lV53Dj8jSjmg8tE=" [server.services.echo] bind_addr = "0.0.0.0:2334" [server.services.pingpong] bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_tcp/tcp_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "default_token_if_not_specify" [client.transport] type = "tcp" [client.services.echo] local_addr = "127.0.0.1:8080" [client.services.pingpong] local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2333" default_token = "default_token_if_not_specify" [server.transport] type = "tcp" [server.services.echo] bind_addr = "0.0.0.0:2334" [server.services.pingpong] bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_tcp/tls_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "default_token_if_not_specify" [client.transport] type = "tls" [client.transport.tls] trusted_root = "examples/tls/rootCA.crt" hostname = "localhost" [client.services.echo] local_addr = "127.0.0.1:8080" [client.services.pingpong] local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2333" default_token = "default_token_if_not_specify" [server.transport] type = "tls" [server.transport.tls] pkcs12 = "examples/tls/identity.pfx" pkcs12_password = "1234" [server.services.echo] bind_addr = "0.0.0.0:2334" [server.services.pingpong] bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_tcp/websocket_tls_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "default_token_if_not_specify" [client.transport] type = "websocket" [client.transport.tls] trusted_root = "examples/tls/rootCA.crt" hostname = "localhost" [client.transport.websocket] tls = true [client.services.echo] local_addr = "127.0.0.1:8080" [client.services.pingpong] local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2333" default_token = "default_token_if_not_specify" [server.transport] type = "websocket" [server.transport.tls] pkcs12 = "examples/tls/identity.pfx" pkcs12_password = "1234" [server.transport.websocket] tls = true [server.services.echo] bind_addr = "0.0.0.0:2334" [server.services.pingpong] bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_tcp/websocket_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2333" default_token = "default_token_if_not_specify" [client.transport] type = "websocket" [client.transport.websocket] tls = false [client.services.echo] local_addr = "127.0.0.1:8080" [client.services.pingpong] local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2333" default_token = "default_token_if_not_specify" [server.transport] type = "websocket" [server.transport.websocket] tls = false [server.services.echo] bind_addr = "0.0.0.0:2334" [server.services.pingpong] bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_udp/noise_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2332" default_token = "default_token_if_not_specify" [client.transport] type = "noise" [client.transport.noise] remote_public_key = "mEnUEACy9UrTBmwoCJb6fcKWBRdvfD9XzuBVsroOLFg=" [client.services.echo] type = "udp" local_addr = "127.0.0.1:8080" [client.services.pingpong] type = "udp" local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2332" default_token = "default_token_if_not_specify" [server.transport] type = "noise" [server.transport.noise] local_private_key = "kQiSRtS3bs8BoGCJYgFnl1FLrTG1lV53Dj8jSjmg8tE=" [server.services.echo] type = "udp" bind_addr = "0.0.0.0:2334" [server.services.pingpong] type = "udp" bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_udp/tcp_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2332" default_token = "default_token_if_not_specify" [client.transport] type = "tcp" [client.services.echo] type = "udp" local_addr = "127.0.0.1:8080" [client.services.pingpong] type = "udp" local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2332" default_token = "default_token_if_not_specify" [server.transport] type = "tcp" [server.services.echo] type = "udp" bind_addr = "0.0.0.0:2334" [server.services.pingpong] type = "udp" bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_udp/tls_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2332" default_token = "default_token_if_not_specify" [client.transport] type = "tls" [client.transport.tls] trusted_root = "examples/tls/rootCA.crt" hostname = "localhost" [client.services.echo] type = "udp" local_addr = "127.0.0.1:8080" [client.services.pingpong] type = "udp" local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2332" default_token = "default_token_if_not_specify" [server.transport] type = "tls" [server.transport.tls] pkcs12 = "examples/tls/identity.pfx" pkcs12_password = "1234" [server.services.echo] type = "udp" bind_addr = "0.0.0.0:2334" [server.services.pingpong] type = "udp" bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_udp/websocket_tls_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2332" default_token = "default_token_if_not_specify" [client.transport] type = "websocket" [client.transport.tls] trusted_root = "examples/tls/rootCA.crt" hostname = "localhost" [client.transport.websocket] tls = true [client.services.echo] type = "udp" local_addr = "127.0.0.1:8080" [client.services.pingpong] type = "udp" local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2332" default_token = "default_token_if_not_specify" [server.transport] type = "websocket" [server.transport.tls] pkcs12 = "examples/tls/identity.pfx" pkcs12_password = "1234" [server.transport.websocket] tls = true [server.services.echo] type = "udp" bind_addr = "0.0.0.0:2334" [server.services.pingpong] type = "udp" bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/for_udp/websocket_transport.toml ================================================ [client] remote_addr = "127.0.0.1:2332" default_token = "default_token_if_not_specify" [client.transport] type = "websocket" [client.transport.websocket] tls = false [client.services.echo] type = "udp" local_addr = "127.0.0.1:8080" [client.services.pingpong] type = "udp" local_addr = "127.0.0.1:8081" [server] bind_addr = "0.0.0.0:2332" default_token = "default_token_if_not_specify" [server.transport] type = "websocket" [server.transport.websocket] tls = false [server.services.echo] type = "udp" bind_addr = "0.0.0.0:2334" [server.services.pingpong] type = "udp" bind_addr = "0.0.0.0:2335" ================================================ FILE: tests/integration_test.rs ================================================ use anyhow::{Ok, Result}; use common::{run_rathole_client, PING, PONG}; use rand::Rng; use std::time::Duration; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::{TcpStream, UdpSocket}, sync::broadcast, time, }; use tracing::{debug, info, instrument}; use tracing_subscriber::EnvFilter; use crate::common::run_rathole_server; mod common; const ECHO_SERVER_ADDR: &str = "127.0.0.1:8080"; const PINGPONG_SERVER_ADDR: &str = "127.0.0.1:8081"; const ECHO_SERVER_ADDR_EXPOSED: &str = "127.0.0.1:2334"; const PINGPONG_SERVER_ADDR_EXPOSED: &str = "127.0.0.1:2335"; const HITTER_NUM: usize = 4; #[derive(Clone, Copy, Debug)] enum Type { Tcp, Udp, } fn init() { let level = "info"; let _ = tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::from(level)), ) .try_init(); } #[tokio::test] async fn tcp() -> Result<()> { init(); // Spawn a echo server tokio::spawn(async move { if let Err(e) = common::tcp::echo_server(ECHO_SERVER_ADDR).await { panic!("Failed to run the echo server for testing: {:?}", e); } }); // Spawn a pingpong server tokio::spawn(async move { if let Err(e) = common::tcp::pingpong_server(PINGPONG_SERVER_ADDR).await { panic!("Failed to run the pingpong server for testing: {:?}", e); } }); test("tests/for_tcp/tcp_transport.toml", Type::Tcp).await?; #[cfg(any( // FIXME: Self-signed certificate on macOS nativetls requires manual interference. all(target_os = "macos", feature = "rustls"), // On other OS accept run with either all(not(target_os = "macos"), any(feature = "native-tls", feature = "rustls")), ))] test("tests/for_tcp/tls_transport.toml", Type::Tcp).await?; #[cfg(feature = "noise")] test("tests/for_tcp/noise_transport.toml", Type::Tcp).await?; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] test("tests/for_tcp/websocket_transport.toml", Type::Tcp).await?; #[cfg(not(target_os = "macos"))] #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] test("tests/for_tcp/websocket_tls_transport.toml", Type::Tcp).await?; Ok(()) } #[tokio::test] async fn udp() -> Result<()> { init(); // Spawn a echo server tokio::spawn(async move { if let Err(e) = common::udp::echo_server(ECHO_SERVER_ADDR).await { panic!("Failed to run the echo server for testing: {:?}", e); } }); // Spawn a pingpong server tokio::spawn(async move { if let Err(e) = common::udp::pingpong_server(PINGPONG_SERVER_ADDR).await { panic!("Failed to run the pingpong server for testing: {:?}", e); } }); test("tests/for_udp/tcp_transport.toml", Type::Udp).await?; #[cfg(any( // FIXME: Self-signed certificate on macOS nativetls requires manual interference. all(target_os = "macos", feature = "rustls"), // On other OS accept run with either all(not(target_os = "macos"), any(feature = "native-tls", feature = "rustls")), ))] test("tests/for_udp/tls_transport.toml", Type::Udp).await?; #[cfg(feature = "noise")] test("tests/for_udp/noise_transport.toml", Type::Udp).await?; #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] test("tests/for_udp/websocket_transport.toml", Type::Udp).await?; #[cfg(not(target_os = "macos"))] #[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))] test("tests/for_udp/websocket_tls_transport.toml", Type::Udp).await?; Ok(()) } #[instrument] async fn test(config_path: &'static str, t: Type) -> Result<()> { if cfg!(not(all(feature = "client", feature = "server"))) { // Skip the test if the client or the server is not enabled return Ok(()); } let (client_shutdown_tx, client_shutdown_rx) = broadcast::channel(1); let (server_shutdown_tx, server_shutdown_rx) = broadcast::channel(1); // Start the client info!("start the client"); let client = tokio::spawn(async move { run_rathole_client(config_path, client_shutdown_rx) .await .unwrap(); }); // Sleep for 1 second. Expect the client keep retrying to reach the server time::sleep(Duration::from_secs(1)).await; // Start the server info!("start the server"); let server = tokio::spawn(async move { run_rathole_server(config_path, server_shutdown_rx) .await .unwrap(); }); time::sleep(Duration::from_millis(2500)).await; // Wait for the client to retry info!("echo"); echo_hitter(ECHO_SERVER_ADDR_EXPOSED, t).await.unwrap(); info!("pingpong"); pingpong_hitter(PINGPONG_SERVER_ADDR_EXPOSED, t) .await .unwrap(); // Simulate the client crash and restart info!("shutdown the client"); client_shutdown_tx.send(true)?; let _ = tokio::join!(client); info!("restart the client"); let client_shutdown_rx = client_shutdown_tx.subscribe(); let client = tokio::spawn(async move { run_rathole_client(config_path, client_shutdown_rx) .await .unwrap(); }); time::sleep(Duration::from_secs(1)).await; // Wait for the client to start info!("echo"); echo_hitter(ECHO_SERVER_ADDR_EXPOSED, t).await.unwrap(); info!("pingpong"); pingpong_hitter(PINGPONG_SERVER_ADDR_EXPOSED, t) .await .unwrap(); // Simulate the server crash and restart info!("shutdown the server"); server_shutdown_tx.send(true)?; let _ = tokio::join!(server); info!("restart the server"); let server_shutdown_rx = server_shutdown_tx.subscribe(); let server = tokio::spawn(async move { run_rathole_server(config_path, server_shutdown_rx) .await .unwrap(); }); time::sleep(Duration::from_millis(2500)).await; // Wait for the client to retry // Simulate heavy load info!("lots of echo and pingpong"); let mut v = Vec::new(); for _ in 0..HITTER_NUM / 2 { v.push(tokio::spawn(async move { echo_hitter(ECHO_SERVER_ADDR_EXPOSED, t).await.unwrap(); })); v.push(tokio::spawn(async move { pingpong_hitter(PINGPONG_SERVER_ADDR_EXPOSED, t) .await .unwrap(); })); } for h in v { assert!(tokio::join!(h).0.is_ok()); } // Shutdown info!("shutdown the server and the client"); server_shutdown_tx.send(true)?; client_shutdown_tx.send(true)?; let _ = tokio::join!(server, client); Ok(()) } async fn echo_hitter(addr: &'static str, t: Type) -> Result<()> { match t { Type::Tcp => tcp_echo_hitter(addr).await, Type::Udp => udp_echo_hitter(addr).await, } } async fn pingpong_hitter(addr: &'static str, t: Type) -> Result<()> { match t { Type::Tcp => tcp_pingpong_hitter(addr).await, Type::Udp => udp_pingpong_hitter(addr).await, } } async fn tcp_echo_hitter(addr: &'static str) -> Result<()> { let mut conn = TcpStream::connect(addr).await?; let mut wr = [0u8; 1024]; let mut rd = [0u8; 1024]; for _ in 0..100 { rand::thread_rng().fill(&mut wr); conn.write_all(&wr).await?; conn.read_exact(&mut rd).await?; assert_eq!(wr, rd); } Ok(()) } async fn udp_echo_hitter(addr: &'static str) -> Result<()> { let conn = UdpSocket::bind("127.0.0.1:0").await?; conn.connect(addr).await?; let mut wr = [0u8; 128]; let mut rd = [0u8; 128]; for _ in 0..3 { rand::thread_rng().fill(&mut wr); conn.send(&wr).await?; debug!("send"); conn.recv(&mut rd).await?; debug!("recv"); assert_eq!(wr, rd); } Ok(()) } async fn tcp_pingpong_hitter(addr: &'static str) -> Result<()> { let mut conn = TcpStream::connect(addr).await?; let wr = PING.as_bytes(); let mut rd = [0u8; PONG.len()]; for _ in 0..100 { conn.write_all(wr).await?; conn.read_exact(&mut rd).await?; assert_eq!(rd, PONG.as_bytes()); } Ok(()) } async fn udp_pingpong_hitter(addr: &'static str) -> Result<()> { let conn = UdpSocket::bind("127.0.0.1:0").await?; conn.connect(&addr).await?; let wr = PING.as_bytes(); let mut rd = [0u8; PONG.len()]; for _ in 0..3 { conn.send(wr).await?; debug!("ping"); conn.recv(&mut rd).await?; debug!("pong"); assert_eq!(rd, PONG.as_bytes()); } Ok(()) }