Showing preview only (269K chars total). Download the full file or copy to clipboard to get everything.
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: ''
---
<!-- Please try the latest release before filing a bug report -->
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
**Configuration**
Configuration used to reproduce the behavior:
**Logs**
<!-- Please upload full client and server logs if possible, with sensitive information masked.
If you encountered a panic, please re-run with `RUST_BACKTRACE=1` to provide the backtrace. -->
**Environment:**
- OS: <!-- Please fill in distribution if you're using linux-->
- `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**
<!-- describe the feature -->
**Use Case**
<!-- possible 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 <code@rapiz.me>"]
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

[](https://github.com/rapiz1/rathole/stargazers)
[](https://github.com/rapiz1/rathole/releases)

[](https://github.com/rapiz1/rathole/releases)
[](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 的服务器暴露在公网上。
<!-- TOC -->
- [rathole](#rathole)
- [Features](#features)
- [Quickstart](#quickstart)
- [Configuration](#configuration)
- [Logging](#logging)
- [Tuning](#tuning)
- [Benchmark](#benchmark)
- [Development Status](#development-status)
<!-- /TOC -->
## 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` 能带来的主要好处是更少的资源占用,而带宽和延迟不一定有显著的改善。




## Development Status
`rathole` 正在积极开发中
- [x] 支持 TLS
- [x] 支持 UDP
- [x] 热重载
- [ ] 用于配置的 HTTP APIs
[Out of Scope](./docs/out-of-scope.md) 列举了没有计划开发的特性并说明了原因。
================================================
FILE: README.md
================================================
# rathole

[](https://github.com/rapiz1/rathole/stargazers)
[](https://github.com/rapiz1/rathole/releases)

[](https://github.com/rapiz1/rathole/releases)
[](https://hub.docker.com/r/rapiz1/rathole)
[](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.
<!-- TOC -->
- [rathole](#rathole)
- [Features](#features)
- [Quickstart](#quickstart)
- [Configuration](#configuration)
- [Logging](#logging)
- [Tuning](#tuning)
- [Benchmark](#benchmark)
- [Planning](#planning)
<!-- /TOC -->
## 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.




## 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


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 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

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

## 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 <<EOF
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn
[ dn ]
C = US
ST = California
L = San Fransisco
O = Someone
OU = Someone
CN = localhost
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = localhost
EOF
openssl req -new -key server.key -out server.csr -config csr.conf
# create server cert
cat > cert.conf <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
EOF
openssl x509 -req \
-in server.csr \
-CA rootCA.crt -CAkey rootCA.key \
-out server.crt \
-days 365 \
-sha256 -extfile cert.conf
# create pkcs12
openssl pkcs12 -export -out identity.pfx -inkey server.key -in server.crt -certfile rootCA.crt \
-passout pass:1234 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES
# clean up
rm server.csr csr.conf cert.conf
================================================
FILE: examples/tls/rootCA.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDTzCCAjegAwIBAgIUOSG1er7cfoTq6uMOe3r0tcSZREMwDQYJKoZIhvcNAQEL
BQAwNzEQMA4GA1UEAwwHTXlPd25DQTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNh
biBGcmFuc2lzY28wHhcNMjUwNzI2MTI0MzIwWhcNMjYwNzE3MTI0MzIwWjA3MRAw
DgYDVQQDDAdNeU93bkNBMQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5z
aXNjbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOqTqiNhaLU5iKRL
USb4LxgxYdS4Ir8sC0usNToVivCNSugm0OGdwCgB/ARm4lQzRm1P0zfgOOO+Sm03
QhjhV9Ds8c4JiECLT+S0EG7aNLgtCBx2xrIw0K+XeTbY/bVZTYIK6q9kWcK3r1WX
ItZXHAArbNRo995jwz2iE//Kq0nPDeyP3PUrsqrsdpy/oZLXtIWmtzxJskM99zi8
xBTH7uJnosSOLuo31o59Qag/SPOQbEUpl5w/9KxMUgWZxBz04mQE3KAlU4PFPYBV
48035S+24bd8T03d0DyqTI34dGweFKV3aaWYT40l3ivXLtrqFKQZOrSeQMAXd8At
1qInNJECAwEAAaNTMFEwHQYDVR0OBBYEFMkX5ozB+FrRgh36PnPe2Ps9L7igMB8G
A1UdIwQYMBaAFMkX5ozB+FrRgh36PnPe2Ps9L7igMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBAC/BqqAPGOfkMk6mBZRyOWWoTWFo4keiJBDmyAIq
0A8v9WtDKr/V4/Y2Y/SLdLO2nP58jomnFguYICERJXsEJeDdMIP/4v3+htVTMZDl
4iExaxZEgLFv1bnaGhsaHhytzuDHWZMpqp427ZXCFTPEzT/iFH4waTNaYpXWh26M
GXzl1iyGoIy9KOe2p+2sN7XAjroGIF9JKQt7rCXJ8Om0Uxvn/Ic7HPEFJ6RAAUMU
dmOaDJq0wsi9AxmqJl0m2Z+MJjPN717BNzavOebCa8xrlrXzlruHDrwo4StLG2wi
35eSgJLyz5hecm2pQLSRJfL+Xcp8iMVupZFvO92pWKmjk+s=
-----END CERTIFICATE-----
================================================
FILE: examples/tls/rootCA.key
================================================
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqk6ojYWi1OYik
S1Em+C8YMWHUuCK/LAtLrDU6FYrwjUroJtDhncAoAfwEZuJUM0ZtT9M34Djjvkpt
N0IY4VfQ7PHOCYhAi0/ktBBu2jS4LQgcdsayMNCvl3k22P21WU2CCuqvZFnCt69V
lyLWVxwAK2zUaPfeY8M9ohP/yqtJzw3sj9z1K7Kq7Hacv6GS17SFprc8SbJDPfc4
vMQUx+7iZ6LEji7qN9aOfUGoP0jzkGxFKZecP/SsTFIFmcQc9OJkBNygJVODxT2A
VePNN+UvtuG3fE9N3dA8qkyN+HRsHhSld2mlmE+NJd4r1y7a6hSkGTq0nkDAF3fA
LdaiJzSRAgMBAAECggEASE417mvzI5FVbhcNL67mjVWDa1dK1pST8sJlMb88MYPL
6B0226SNe4eJEC5Ka1vWxJELcTi7MHASbvHOfO7Q68RtkG3dws9uU/ew7Qpzn6W5
z1RJUNm9KbLOGTrvkTuyqXgF+QQ8qsmF0SMiCOGW7vJzvFGSvckQgGn6MildfQUv
fBzRPsGOQXFces2bhC+92FH6u2HzMN/CBhoBt0HowyFjs+Gh20iB/7rkXMZ3Re/H
7jIwCkhPCKMlXbMYlzuot+d1tEXEgKSo81tjet7wqp3dZNOum0oIiZPrLMZyMHeD
BHSXjChCQs8iSyaH2xTrihsrhP0VIfLLjTlHsoaXVQKBgQD+4UIeG4ztos+p/wdt
yaHEHNjp0/LGLfaCACREFBGIsov8qkRrNqwE9Lqrd4VPOcGyDqY81rWPTIeOSArs
pjkD352o73fjVCbSh3TgOZuEhCZNxJVL8cxTTB+JTrlW2xiDNN3pT2mhSqGwnb9I
ncd30FlywQ9PC9pSGjyMhqbAMwKBgQDrm5CwxrPU2N+uHGglKhO8OQFD6uJqVIdn
w7cuywxyNnigf3mU5wFQ3nJ7VBxTxcHUDYDA8fdqKj/xUC/uvUECMjtCFvPP724J
80K8GYEwIH0Mm+QeRtViSAP3icq0f6070us9Y+jF6zWhsjmsABGPXlQBR8kpJX7C
eM4ChatkKwKBgQD5QppCj3b3P24PNhWxzKqi/AM2Scz2+yREZpcQ7P9ozBQS+QEI
SONZxWx5G94HaNiApcr1XJUamyFGiDYG0ViY7Stmdyqr6zQ8V7R5RF1O7132V/YZ
21KTc7KzuYWP55zFVAJhlQQcdLxD2UGkZokYJ24Sx7OM/m54NKhaVaTl+QKBgGpb
6sByouHWGXvO5RDJ4ujYTwLq+NUJXarxBjPAg9jXUDMb+LXIZqasFMAp4zPKFUr1
4Ya4dHlfo0f7a/f5RWyJYojeNahLrMAfKaQiW1hvgiP8B8nHLjLU0b2gXXqIHJri
B0HKZV1bZfWdsD0+Nq8i5QdC8cN6YrPFtzIaR5nLAoGBAJquKGdlCZErAWVnBOpj
CujXpMBQJa7+ZWY5q+19s99jzffSdyMhuHbVf3NxE0fu8+A3csZDDlW2wyIIM6tk
HKxJSUapv7MDiA1nz3KlewJEr9XAHnapNciur6EgxEKPhjY86I76b28xR2Tdj4zY
wI+K8vATq4tFiSqM4hil+4WG
-----END PRIVATE KEY-----
================================================
FILE: examples/tls/server.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDpzCCAo+gAwIBAgIUPP/pKlmn1kOLpd31HXRSjtw2muQwDQYJKoZIhvcNAQEL
BQAwNzEQMA4GA1UEAwwHTXlPd25DQTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNh
biBGcmFuc2lzY28wHhcNMjUwNzI2MTI0MzIwWhcNMjYwNzI2MTI0MzIwWjByMQsw
CQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZy
YW5zaXNjbzEQMA4GA1UECgwHU29tZW9uZTEQMA4GA1UECwwHU29tZW9uZTESMBAG
A1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
p1LrbPzkvfwPzcBgORrmVxzkB+TKPdg+P5Ef6IrtlX3juyjUKMPK67v0zDwNKKyI
9LYND4KHImhgBcDc4M1B5y5WdALFR6XkJhcRtdm8RWY4FYalI0Q9ZbIzDZVVtIK5
kJx7r3t60rlSuHC55f3cJpek+BNAtqf/XiDgVAtSp7EECdkODFbWKhOLnln3Lvvs
6xkOOpSy7grYWUHMmUCv2LdLZuripjWJdT4Vkm+jTR+m1cnItruJZ3uGI2xmLRwy
gg9BkWG6Yn6Sr8is9I22yiFRKW/6SYkCZ19VwCzOahuQR5t0K1DrPjjI1nCbMaak
9fcRnz8X0JRQ5NloaGyynQIDAQABo3AwbjAfBgNVHSMEGDAWgBTJF+aMwfha0YId
+j5z3tj7PS+4oDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE8DAUBgNVHREEDTALggls
b2NhbGhvc3QwHQYDVR0OBBYEFNXm+idP/nizMy9EbGzf64KoObQNMA0GCSqGSIb3
DQEBCwUAA4IBAQB3MhVio45UevtifaWfzQqhbK+QYUrKTUVzvFzkdnIB+c5xkl5p
MaYZkWaRZxg9ZAKRIv+sNWw9sCBFtXsnxHyaTfwXx6qE5nPHBgSWUogIRh1aJadY
w9QxCyBwO72aUguDskGYNX7fP69RpxFQXJYlwfpH/Z9UMTb7dE78iuU2GpiHxLql
4oeMvCGqDV3d9aDBBOqNu/WsU9wcvakDF8E0Vx45kU0ze/Fr2R1LYagsQ0vaabPi
w6X9V1iUMuvpsiqcWjdj5WgMRXqRd5+CrDmXG3dsDBWB0LMkU0Vm1s3hso/9clx2
znZJJliEWwvHnBTcuXUQBNJP10NxWnDeV7tl
-----END CERTIFICATE-----
================================================
FILE: examples/tls/server.key
================================================
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnUuts/OS9/A/N
wGA5GuZXHOQH5Mo92D4/kR/oiu2VfeO7KNQow8rru/TMPA0orIj0tg0PgociaGAF
wNzgzUHnLlZ0AsVHpeQmFxG12bxFZjgVhqUjRD1lsjMNlVW0grmQnHuve3rSuVK4
cLnl/dwml6T4E0C2p/9eIOBUC1KnsQQJ2Q4MVtYqE4ueWfcu++zrGQ46lLLuCthZ
QcyZQK/Yt0tm6uKmNYl1PhWSb6NNH6bVyci2u4lne4YjbGYtHDKCD0GRYbpifpKv
yKz0jbbKIVEpb/pJiQJnX1XALM5qG5BHm3QrUOs+OMjWcJsxpqT19xGfPxfQlFDk
2WhobLKdAgMBAAECggEAGC+LKiDP64pAuovhHL//pX3elCmE9jWIoeWXSCS0vNQK
k+YN7KqIbVSoyNCxUjzGOyEJeEF0yQtvPcWn57KjoQf4pSI1Y+rdtIfHHpMPfLSO
zMW+nv9hJI8ChCce0U3IBtgnpLDjiwwQoephZJYyOT3YMaeOxhg4eGGmK1/LncNj
ot4OORaDHINq25Mt21BxhYaBG8p4DRfyb0HVXU9bQcjbLQViDFcDnoQB2F/lz4EO
V9r+IxE0/TKaGCqyEKhCjW303mBWlHpU66U+ykCZBLUmieOYmWEecp+liUYFo874
TiM3UXj7mQ1sBOkIrIhnYNLKPF5pxdsRvVIadIlyfQKBgQDW/YGL8ODpCllfdMcr
CymZj2zoKSwG2/N3Ee1G0EO2areYEiUlhImtz6tif4Z4Zjli7ZaoojfkGT++4U+L
6gH6A0Att+KMFkDbaiTPbaUXYywSYkhj+il/oz6aumqfP+hy9qCLT1wXnryMpH06
4YEZkwp7OuK8KiwRkJs46YDwbwKBgQDHPcAW3xiGfEVPWRui3FWj+NgnaUagPvOV
8XeVSwk6XW4RfJdISVdT1gKtjPV8UJ8kSogW6tVrWtZ73fd/uLrYwdCp9btBu7Dv
rA6nMeg8rLPr0Aq9Obv+ANk/TtgFqQjfZqqbxISspzazM1rBQq+/njXt8jFwliSW
/CX6WVU7swKBgQDG1l2rVRRe1ICGRZYzXDaUXM9oBTQ//8u0U3M1bEdD/n1g+185
zNQdWhVzmuh+kGUA9ybBPo8curF3VCFjEQHU/o6r+gdgcvB3PjtfUVRARiursRSs
yuD4uL2dE06rjMrrEOi5D6PoAJr4JOXhwFDLm3A1OugbCZIKiMjXITdspwKBgFTw
M0rM0yTKJ7YbE9gLPItJ08SBcfVwwOFkbol70rRKDllwFwJfGdaIvt2D4UedCysq
hvfWJyO8NwHZb+DIPQeZIL7EHo94V4blf92xPgNX0OAv8dQXn9g6PmNp1lgbZsfu
eb8sOS9tnbkppIANUOVMqksFXCRWLcUcO4iNuvNXAoGANA/bSU1uJTw0tQMyewcL
X0kskpHiEICH131yQgXsOI38uGAz7F4/i8OSQ77hNSF2b7XcNwf2rNkybswyulMu
4SfhKDHslFSHW3iMXNDxh3YHk71I+n9cXVjPtW5WuxFn7plJM6zvhzPy2xpGqoPL
FUw5z8qWROqGOrXenjAWdnw=
-----END PRIVATE KEY-----
================================================
FILE: examples/tls/server.toml
================================================
[server]
bind_addr = "0.0.0.0:2333"
default_token = "123"
[server.transport]
type = "tls"
[server.transport.tls]
pkcs12 = "examples/tls/identity.pfx"
pkcs12_password = "1234"
[server.services.foo1]
bind_addr = "0.0.0.0:5202"
================================================
FILE: examples/udp/client.toml
================================================
[client]
remote_addr = "localhost:2333"
default_token = "123"
[client.services.foo1]
type = "udp"
local_addr = "127.0.0.1:80"
================================================
FILE: examples/udp/server.toml
================================================
[server]
bind_addr = "0.0.0.0:2333"
default_token = "123"
[server.services.foo1]
type = "udp"
bind_addr = "0.0.0.0:5202"
================================================
FILE: examples/unified/config.toml
================================================
# rathole configuration can put in one file as long as running mode is specified via cli
[client]
remote_addr = "localhost:2333"
default_token = "123"
[client.services.foo1]
local_addr = "127.0.0.1:80"
[server]
bind_addr = "0.0.0.0:2333"
default_token = "123"
[server.services.foo1]
bind_addr = "0.0.0.0:5202"
================================================
FILE: examples/use_proxy/client.toml
================================================
[client]
remote_addr = "127.0.0.1:2333"
default_token = "123"
[client.services.foo1]
local_addr = "127.0.0.1:80"
[client.transport]
type = "tcp"
[client.transport.tcp]
# `proxy` controls how the client connect to the server
# Use socks5 proxy at 127.0.0.1, with port 1080, username 'myuser' and password 'mypass'
proxy = "socks5://myuser:mypass@127.0.0.1:1080"
# Use http proxy. Similar to socks5 proxy
# proxy = "http://myuser:mypass@127.0.0.1:8080"
================================================
FILE: rust-toolchain
================================================
1.71.0
================================================
FILE: src/cli.rs
================================================
use clap::{AppSettings, ArgGroup, Parser};
use lazy_static::lazy_static;
#[derive(clap::ArgEnum, Clone, Debug, Copy)]
pub enum KeypairType {
X25519,
X448,
}
lazy_static! {
static ref VERSION: &'static str =
option_env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT").unwrap_or(env!("VERGEN_BUILD_SEMVER"));
static ref LONG_VERSION: String = format!(
"
Build Timestamp: {}
Build Version: {}
Commit SHA: {:?}
Commit Date: {:?}
Commit Branch: {:?}
cargo Target Triple: {}
cargo Profile: {}
cargo Features: {}
",
env!("VERGEN_BUILD_TIMESTAMP"),
env!("VERGEN_BUILD_SEMVER"),
option_env!("VERGEN_GIT_SHA"),
option_env!("VERGEN_GIT_COMMIT_TIMESTAMP"),
option_env!("VERGEN_GIT_BRANCH"),
env!("VERGEN_CARGO_TARGET_TRIPLE"),
env!("VERGEN_CARGO_PROFILE"),
env!("VERGEN_CARGO_FEATURES")
);
}
#[derive(Parser, Debug, Default, Clone)]
#[clap(
about,
version(*VERSION),
long_version(LONG_VERSION.as_str()),
setting(AppSettings::DeriveDisplayOrder)
)]
#[clap(group(
ArgGroup::new("cmds")
.required(true)
.args(&["CONFIG", "genkey"]),
))]
pub struct Cli {
/// The path to the configuration file
///
/// Running as a client or a server is automatically determined
/// according to the configuration file.
#[clap(parse(from_os_str), name = "CONFIG")]
pub config_path: Option<std::path::PathBuf>,
/// 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<Option<KeypairType>>,
}
================================================
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<bool>,
update_rx: mpsc::Receiver<ConfigChange>,
) -> 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::<TcpTransport>::from(config).await?;
client.run(shutdown_rx, update_rx).await
}
TransportType::Tls => {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
{
let mut client = Client::<TlsTransport>::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::<NoiseTransport>::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::<WebsocketTransport>::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<T: Transport> {
config: ClientConfig,
service_handles: HashMap<String, ControlChannelHandle>,
transport: Arc<T>,
}
impl<T: 'static + Transport> Client<T> {
// Create a Client from `[client]` config block
async fn from(config: ClientConfig) -> Result<Client<T>> {
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<bool>,
mut update_rx: mpsc::Receiver<ConfigChange>,
) -> 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<T: Transport> {
session_key: Nonce,
remote_addr: AddrMaybeCached,
connector: Arc<T>,
socket_opts: SocketOpts,
service: ClientServiceConfig,
}
async fn do_data_channel_handshake<T: Transport>(
args: Arc<RunDataChannelArgs<T>>,
) -> Result<T::Stream> {
// 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<T: Transport>(args: Arc<RunDataChannelArgs<T>>) -> 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::<T>(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::<T>(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<T: Transport>(
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<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>;
#[instrument(skip(conn))]
async fn run_data_channel_for_udp<T: Transport>(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::<UdpTraffic>(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<Bytes>,
outbount_tx: mpsc::Sender<UdpTraffic>,
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<T: Transport> {
digest: ServiceDigest, // SHA256 of the service name
service: ClientServiceConfig, // `[client.services.foo]` config block
shutdown_rx: oneshot::Receiver<u8>, // Receives the shutdown signal
remote_addr: String, // `client.remote_addr`
transport: Arc<T>, // 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<u8>,
}
impl<T: 'static + Transport> ControlChannel<T> {
#[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<T: 'static + Transport>(
service: ClientServiceConfig,
remote_addr: String,
transport: Arc<T>,
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<MaskedString>,
pub nodelay: Option<bool>,
pub retry_interval: Option<u64>,
}
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<MaskedString>,
pub nodelay: Option<bool>,
}
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<String>,
pub trusted_root: Option<String>,
pub pkcs12: Option<String>,
pub pkcs12_password: Option<MaskedString>,
}
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<MaskedString>,
pub remote_public_key: Option<String>,
// 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<Url>,
}
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<TlsConfig>,
pub noise: Option<NoiseConfig>,
pub websocket: Option<WebsocketConfig>,
}
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<MaskedString>,
pub prefer_ipv6: Option<bool>,
pub services: HashMap<String, ClientServiceConfig>,
#[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<MaskedString>,
pub services: HashMap<String, ServerServiceConfig>,
#[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<ServerConfig>,
pub client: Option<ClientConfig>,
}
impl Config {
fn from_str(s: &str) -> Result<Config> {
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<Config> {
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<T: AsRef<Path>>(root: T) -> Result<Vec<PathBuf>> {
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<Vec<PathBuf>> {
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<Config>), // 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<String, Self::ServiceConfig>;
}
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<String, Self::ServiceConfig> {
&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<String, Self::ServiceConfig> {
&self.services
}
}
pub struct ConfigWatcherHandle {
pub event_rx: mpsc::UnboundedReceiver<ConfigChange>,
}
impl ConfigWatcherHandle {
pub async fn new(path: &Path, shutdown_rx: broadcast::Receiver<bool>) -> Result<Self> {
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<bool>,
_event_tx: mpsc::UnboundedSender<ConfigChange>,
_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<bool>,
event_tx: mpsc::UnboundedSender<ConfigChange>,
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<notify::Event, _>| 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<Vec<ConfigChange>> {
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<T: InstanceConfig>(
old: &T,
new: &T,
) -> Option<Vec<ConfigChange>> {
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<A: ToSocketAddrs>(addr: A) -> Result<SocketAddr> {
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<A: ToSocketAddrs>(addr: A, prefer_ipv6: bool) -> Result<UdpSocket> {
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<SocketAddr> = 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<TcpStream> {
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<I, E, Fn, Fut, B, N>(
backoff: B,
operation: Fn,
notify: N,
deadline: &mut broadcast::Receiver<bool>,
) -> Result<I>
where
E: std::error::Error + Send + Sync + 'static,
B: Backoff,
Fn: FnMut() -> Fut,
Fut: Future<Output = std::result::Result<I, backoff::Error<E>>>,
N: Notify<E>,
{
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<T>(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<KeypairType>) -> 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<KeypairType>) -> Result<()> {
crate::helper::feature_not_compile("nosie")
}
pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> 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<ConfigChange>)> = 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<bool>,
service_update: mpsc::Receiver<ConfigChange>,
) -> 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::<bool>(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<K1, K2, V>(*mut (K1, K2, V));
unsafe impl<K1, K2, V> Send for RawItem<K1, K2, V> {}
unsafe impl<K1, K2, V> Sync for RawItem<K1, K2, V> {}
/// 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<K1, K2, V> {
map1: HashMap<Key<K1>, RawItem<K1, K2, V>>,
map2: HashMap<Key<K2>, RawItem<K1, K2, V>>,
}
struct Key<T>(*const T);
unsafe impl<T> Send for Key<T> {}
unsafe impl<T> Sync for Key<T> {}
impl<T> Borrow<T> for Key<T> {
fn borrow(&self) -> &T {
unsafe { &*self.0 }
}
}
impl<T: Hash> Hash for Key<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
(self.borrow() as &T).hash(state)
}
}
impl<T: PartialEq> PartialEq for Key<T> {
fn eq(&self, other: &Self) -> bool {
(self.borrow() as &T).eq(other.borrow())
}
}
impl<T: Eq> Eq for Key<T> {}
impl<K1, K2, V> MultiMap<K1, K2, V> {
pub fn new() -> Self {
MultiMap {
map1: HashMap::new(),
map2: HashMap::new(),
}
}
}
#[allow(dead_code)]
impl<K1, K2, V> MultiMap<K1, K2, V>
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<V> {
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<V> {
let item = self.map2.remove(k2)?;
let item = unsafe { Box::from_raw(item.0) };
self.map1.remove(&item.0);
Some(item.2)
}
}
impl<K1, K2, V> Drop for MultiMap<K1, K2, V> {
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<T: AsyncWrite + Unpin>(&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<T: AsyncWrite + Unpin>(
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<T: AsyncRead + Unpin>(reader: &mut T, hdr_len: u8) -> Result<UdpTraffic> {
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<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T) -> Result<Hello> {
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<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T) -> Result<Auth> {
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<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T) -> Result<Ack> {
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<T: AsyncRead + AsyncWrite + Unpin>(
conn: &mut T,
) -> Result<ControlChannelCmd> {
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<T: AsyncRead + AsyncWrite + Unpin>(
conn: &mut T,
) -> Result<DataChannelCmd> {
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<bool>,
update_rx: mpsc::Receiver<ConfigChange>,
) -> 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::<TcpTransport>::from(config).await?;
server.run(shutdown_rx, update_rx).await?;
}
TransportType::Tls => {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
{
let mut server = Server::<TlsTransport>::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::<NoiseTransport>::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::<WebsocketTransport>::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<T> = MultiMap<ServiceDigest, Nonce, ControlChannelHandle<T>>;
// Server holds all states of running a server
struct Server<T: Transport> {
// `[server]` config
config: Arc<ServerConfig>,
// `[server.services]` config, indexed by ServiceDigest
services: Arc<RwLock<HashMap<ServiceDigest, ServerServiceConfig>>>,
// Collection of contorl channels
control_channels: Arc<RwLock<ControlChannelMap<T>>>,
// Wrapper around the transport layer
transport: Arc<T>,
}
// Generate a hash map of services which is indexed by ServiceDigest
fn generate_service_hashmap(
server_config: &ServerConfig,
) -> HashMap<ServiceDigest, ServerServiceConfig> {
let mut ret = HashMap::new();
for u in &server_config.services {
ret.insert(protocol::digest(u.0.as_bytes()), (*u.1).clone());
}
ret
}
impl<T: 'static + Transport> Server<T> {
// Create a server from `[server]`
pub async fn from(config: ServerConfig) -> Result<Server<T>> {
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<bool>,
mut update_rx: mpsc::Receiver<ConfigChange>,
) -> 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::<io::Error>() {
// 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<T: 'static + Transport>(
mut conn: T::Stream,
services: Arc<RwLock<HashMap<ServiceDigest, ServerServiceConfig>>>,
control_channels: Arc<RwLock<ControlChannelMap<T>>>,
server_config: Arc<ServerConfig>,
) -> Result<()> {
// Read hello
let hello = read_hello(&mut conn).await?;
match hello {
ControlChannelHello(_, service_digest) => {
do_control_channel_handshake(
conn,
services,
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
SYMBOL INDEX (302 symbols across 20 files)
FILE: build.rs
function main (line 4) | fn main() -> Result<()> {
FILE: src/cli.rs
type KeypairType (line 5) | pub enum KeypairType {
type Cli (line 47) | pub struct Cli {
FILE: src/client.rs
function run_client (line 34) | pub async fn run_client(
type ServiceDigest (line 80) | type ServiceDigest = protocol::Digest;
type Nonce (line 81) | type Nonce = protocol::Digest;
type Client (line 84) | struct Client<T: Transport> {
function from (line 92) | async fn from(config: ClientConfig) -> Result<Client<T>> {
function run (line 103) | async fn run(
function handle_hot_reload (line 147) | async fn handle_hot_reload(&mut self, e: ConfigChange) {
type RunDataChannelArgs (line 169) | struct RunDataChannelArgs<T: Transport> {
function do_data_channel_handshake (line 177) | async fn do_data_channel_handshake<T: Transport>(
function run_data_channel (line 214) | async fn run_data_channel<T: Transport>(args: Arc<RunDataChannelArgs<T>>...
function run_data_channel_for_tcp (line 238) | async fn run_data_channel_for_tcp<T: Transport>(
type UdpPortMap (line 255) | type UdpPortMap = Arc<RwLock<HashMap<SocketAddr, mpsc::Sender<Bytes>>>>;
function run_data_channel_for_udp (line 258) | async fn run_data_channel_for_udp<T: Transport>(conn: T::Stream, local_a...
function run_udp_forwarder (line 336) | async fn run_udp_forwarder(
type ControlChannel (line 388) | struct ControlChannel<T: Transport> {
type ControlChannelHandle (line 399) | struct ControlChannelHandle {
method new (line 499) | fn new<T: 'static + Transport>(
method shutdown (line 556) | fn shutdown(self) {
function run (line 405) | async fn run(&mut self) -> Result<()> {
FILE: src/config.rs
constant DEFAULT_HEARTBEAT_INTERVAL_SECS (line 13) | const DEFAULT_HEARTBEAT_INTERVAL_SECS: u64 = 30;
constant DEFAULT_HEARTBEAT_TIMEOUT_SECS (line 14) | const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 40;
constant DEFAULT_CLIENT_RETRY_INTERVAL_SECS (line 17) | const DEFAULT_CLIENT_RETRY_INTERVAL_SECS: u64 = 1;
type MaskedString (line 22) | pub struct MaskedString(String);
method from (line 38) | fn from(s: &str) -> MaskedString {
method fmt (line 25) | fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt...
type Target (line 31) | type Target = str;
method deref (line 32) | fn deref(&self) -> &Self::Target {
type TransportType (line 44) | pub enum TransportType {
type ClientServiceConfig (line 60) | pub struct ClientServiceConfig {
method with_name (line 74) | pub fn with_name(name: &str) -> ClientServiceConfig {
type ServiceType (line 83) | pub enum ServiceType {
function default_service_type (line 91) | fn default_service_type() -> ServiceType {
type ServerServiceConfig (line 99) | pub struct ServerServiceConfig {
method with_name (line 110) | pub fn with_name(name: &str) -> ServerServiceConfig {
type TlsConfig (line 119) | pub struct TlsConfig {
function default_noise_pattern (line 126) | fn default_noise_pattern() -> String {
type NoiseConfig (line 132) | pub struct NoiseConfig {
type WebsocketConfig (line 142) | pub struct WebsocketConfig {
function default_nodelay (line 146) | fn default_nodelay() -> bool {
function default_keepalive_secs (line 150) | fn default_keepalive_secs() -> u64 {
function default_keepalive_interval (line 154) | fn default_keepalive_interval() -> u64 {
type TcpConfig (line 160) | pub struct TcpConfig {
method default (line 171) | fn default() -> Self {
type TransportConfig (line 183) | pub struct TransportConfig {
function default_heartbeat_timeout (line 193) | fn default_heartbeat_timeout() -> u64 {
function default_client_retry_interval (line 197) | fn default_client_retry_interval() -> u64 {
type ClientConfig (line 203) | pub struct ClientConfig {
function default_heartbeat_interval (line 216) | fn default_heartbeat_interval() -> u64 {
type ServerConfig (line 222) | pub struct ServerConfig {
type Config (line 234) | pub struct Config {
method from_str (line 240) | fn from_str(s: &str) -> Result<Config> {
method validate_server_config (line 258) | fn validate_server_config(server: &mut ServerConfig) -> Result<()> {
method validate_client_config (line 275) | fn validate_client_config(client: &mut ClientConfig) -> Result<()> {
method validate_transport_config (line 295) | fn validate_transport_config(config: &TransportConfig, is_server: bool...
method from_file (line 329) | pub async fn from_file(path: &Path) -> Result<Config> {
function list_config_files (line 346) | fn list_config_files<T: AsRef<Path>>(root: T) -> Result<Vec<PathBuf>> {
function get_all_example_config (line 360) | fn get_all_example_config() -> Result<Vec<PathBuf>> {
function test_example_config (line 368) | fn test_example_config() -> Result<()> {
function test_valid_config (line 378) | fn test_valid_config() -> Result<()> {
function test_invalid_config (line 388) | fn test_invalid_config() -> Result<()> {
function test_validate_server_config (line 398) | fn test_validate_server_config() -> Result<()> {
function test_validate_client_config (line 448) | fn test_validate_client_config() -> Result<()> {
FILE: src/config_watcher.rs
type ConfigChange (line 18) | pub enum ConfigChange {
type ClientServiceChange (line 25) | pub enum ClientServiceChange {
type ServerServiceChange (line 31) | pub enum ServerServiceChange {
type InstanceConfig (line 36) | trait InstanceConfig: Clone {
method equal_without_service (line 38) | fn equal_without_service(&self, rhs: &Self) -> bool;
method service_delete_change (line 39) | fn service_delete_change(s: String) -> ConfigChange;
method service_add_change (line 40) | fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange;
method get_services (line 41) | fn get_services(&self) -> &HashMap<String, Self::ServiceConfig>;
type ServiceConfig (line 45) | type ServiceConfig = ServerServiceConfig;
method equal_without_service (line 46) | fn equal_without_service(&self, rhs: &Self) -> bool {
method service_delete_change (line 59) | fn service_delete_change(s: String) -> ConfigChange {
method service_add_change (line 62) | fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange {
method get_services (line 65) | fn get_services(&self) -> &HashMap<String, Self::ServiceConfig> {
type ServiceConfig (line 71) | type ServiceConfig = ClientServiceConfig;
method equal_without_service (line 72) | fn equal_without_service(&self, rhs: &Self) -> bool {
method service_delete_change (line 85) | fn service_delete_change(s: String) -> ConfigChange {
method service_add_change (line 88) | fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange {
method get_services (line 91) | fn get_services(&self) -> &HashMap<String, Self::ServiceConfig> {
type ConfigWatcherHandle (line 96) | pub struct ConfigWatcherHandle {
method new (line 101) | pub async fn new(path: &Path, shutdown_rx: broadcast::Receiver<bool>) ...
function config_watcher (line 123) | async fn config_watcher(
function config_watcher (line 136) | async fn config_watcher(
function calculate_events (line 202) | fn calculate_events(old: &Config, new: &Config) -> Option<Vec<ConfigChan...
function calculate_instance_config_events (line 239) | fn calculate_instance_config_events<T: InstanceConfig>(
function test_calculate_events (line 279) | fn test_calculate_events() {
FILE: src/constants.rs
constant UDP_BUFFER_SIZE (line 6) | pub const UDP_BUFFER_SIZE: usize = 2048;
constant UDP_SENDQ_SIZE (line 7) | pub const UDP_SENDQ_SIZE: usize = 1024;
constant UDP_TIMEOUT (line 8) | pub const UDP_TIMEOUT: u64 = 60;
function listen_backoff (line 10) | pub fn listen_backoff() -> ExponentialBackoff {
function run_control_chan_backoff (line 18) | pub fn run_control_chan_backoff(interval: u64) -> ExponentialBackoff {
FILE: src/helper.rs
function try_set_tcp_keepalive (line 19) | pub fn try_set_tcp_keepalive(
function feature_not_compile (line 39) | pub fn feature_not_compile(feature: &str) -> ! {
function feature_neither_compile (line 47) | pub fn feature_neither_compile(feature1: &str, feature2: &str) -> ! {
function to_socket_addr (line 54) | pub async fn to_socket_addr<A: ToSocketAddrs>(addr: A) -> Result<SocketA...
function host_port_pair (line 61) | pub fn host_port_pair(s: &str) -> Result<(&str, u16)> {
function udp_connect (line 67) | pub async fn udp_connect<A: ToSocketAddrs>(addr: A, prefer_ipv6: bool) -...
function tcp_connect_with_proxy (line 111) | pub async fn tcp_connect_with_proxy(
function retry_notify_with_deadline (line 163) | pub async fn retry_notify_with_deadline<I, E, Fn, Fut, B, N>(
function write_and_flush (line 186) | pub async fn write_and_flush<T>(conn: &mut T, data: &[u8]) -> Result<()>
FILE: src/lib.rs
constant DEFAULT_CURVE (line 31) | const DEFAULT_CURVE: KeypairType = KeypairType::X25519;
function get_str_from_keypair_type (line 33) | fn get_str_from_keypair_type(curve: KeypairType) -> &'static str {
function genkey (line 41) | fn genkey(curve: Option<KeypairType>) -> Result<()> {
function genkey (line 58) | fn genkey(curve: Option<KeypairType>) -> Result<()> {
function run (line 62) | pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> R...
function run_instance (line 117) | async fn run_instance(
type RunMode (line 141) | enum RunMode {
function determine_run_mode (line 147) | fn determine_run_mode(config: &Config, args: &Cli) -> RunMode {
function test_determine_run_mode (line 169) | fn test_determine_run_mode() {
FILE: src/main.rs
function main (line 8) | async fn main() -> Result<()> {
FILE: src/multi_map.rs
type RawItem (line 5) | struct RawItem<K1, K2, V>(*mut (K1, K2, V));
type MultiMap (line 12) | pub struct MultiMap<K1, K2, V> {
type Key (line 17) | struct Key<T>(*const T);
function borrow (line 23) | fn borrow(&self) -> &T {
method hash (line 29) | fn hash<H: Hasher>(&self, state: &mut H) {
method eq (line 35) | fn eq(&self, other: &Self) -> bool {
function new (line 43) | pub fn new() -> Self {
function insert (line 58) | pub fn insert(&mut self, k1: K1, k2: K2, v: V) -> Result<(), (K1, K2, V)> {
function get1 (line 71) | pub fn get1(&self, k1: &K1) -> Option<&V> {
function get1_mut (line 77) | pub fn get1_mut(&mut self, k1: &K1) -> Option<&mut V> {
function get2 (line 83) | pub fn get2(&self, k2: &K2) -> Option<&V> {
function get_mut2 (line 89) | pub fn get_mut2(&mut self, k2: &K2) -> Option<&mut V> {
function remove1 (line 95) | pub fn remove1(&mut self, k1: &K1) -> Option<V> {
function remove2 (line 102) | pub fn remove2(&mut self, k2: &K2) -> Option<V> {
method drop (line 111) | fn drop(&mut self) {
FILE: src/protocol.rs
constant HASH_WIDTH_IN_BYTES (line 1) | pub const HASH_WIDTH_IN_BYTES: usize = 32;
type ProtocolVersion (line 11) | type ProtocolVersion = u8;
constant _PROTO_V0 (line 12) | const _PROTO_V0: u8 = 0u8;
constant PROTO_V1 (line 13) | const PROTO_V1: u8 = 1u8;
constant CURRENT_PROTO_VERSION (line 15) | pub const CURRENT_PROTO_VERSION: ProtocolVersion = PROTO_V1;
type Digest (line 17) | pub type Digest = [u8; HASH_WIDTH_IN_BYTES];
type Hello (line 20) | pub enum Hello {
type Auth (line 26) | pub struct Auth(pub Digest);
type Ack (line 29) | pub enum Ack {
method fmt (line 36) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type ControlChannelCmd (line 50) | pub enum ControlChannelCmd {
type DataChannelCmd (line 56) | pub enum DataChannelCmd {
type UdpPacketLen (line 61) | type UdpPacketLen = u16;
type UdpHeader (line 63) | struct UdpHeader {
type UdpTraffic (line 69) | pub struct UdpTraffic {
method write (line 75) | pub async fn write<T: AsyncWrite + Unpin>(&self, writer: &mut T) -> Re...
method write_slice (line 93) | pub async fn write_slice<T: AsyncWrite + Unpin>(
method read (line 114) | pub async fn read<T: AsyncRead + Unpin>(reader: &mut T, hdr_len: u8) -...
function digest (line 137) | pub fn digest(data: &[u8]) -> Digest {
type PacketLength (line 143) | struct PacketLength {
method new (line 152) | pub fn new() -> PacketLength {
function read_hello (line 178) | pub async fn read_hello<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T)...
function read_auth (line 209) | pub async fn read_auth<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T) ...
function read_ack (line 217) | pub async fn read_ack<T: AsyncRead + AsyncWrite + Unpin>(conn: &mut T) -...
function read_control_cmd (line 225) | pub async fn read_control_cmd<T: AsyncRead + AsyncWrite + Unpin>(
function read_data_cmd (line 235) | pub async fn read_data_cmd<T: AsyncRead + AsyncWrite + Unpin>(
FILE: src/server.rs
type ServiceDigest (line 33) | type ServiceDigest = protocol::Digest;
type Nonce (line 34) | type Nonce = protocol::Digest;
constant TCP_POOL_SIZE (line 36) | const TCP_POOL_SIZE: usize = 8;
constant UDP_POOL_SIZE (line 37) | const UDP_POOL_SIZE: usize = 2;
constant CHAN_SIZE (line 38) | const CHAN_SIZE: usize = 2048;
constant HANDSHAKE_TIMEOUT (line 39) | const HANDSHAKE_TIMEOUT: u64 = 5;
function run_server (line 42) | pub async fn run_server(
type ControlChannelMap (line 93) | type ControlChannelMap<T> = MultiMap<ServiceDigest, Nonce, ControlChanne...
type Server (line 96) | struct Server<T: Transport> {
function generate_service_hashmap (line 109) | fn generate_service_hashmap(
function from (line 121) | pub async fn from(config: ServerConfig) -> Result<Server<T>> {
function run (line 135) | pub async fn run(
function handle_hot_reload (line 225) | async fn handle_hot_reload(&mut self, e: ConfigChange) {
function handle_connection (line 250) | async fn handle_connection<T: 'static + Transport>(
function do_control_channel_handshake (line 276) | async fn do_control_channel_handshake<T: 'static + Transport>(
function do_data_channel_handshake (line 361) | async fn do_data_channel_handshake<T: 'static + Transport>(
type ControlChannelHandle (line 388) | pub struct ControlChannelHandle<T: Transport> {
function new (line 402) | fn new(
type ControlChannel (line 492) | struct ControlChannel<T: Transport> {
function write_and_flush (line 500) | async fn write_and_flush(&mut self, data: &[u8]) -> Result<()> {
function run (line 508) | async fn run(mut self) -> Result<()> {
function tcp_listen_and_send (line 547) | fn tcp_listen_and_send(
function run_tcp_connection_pool (line 626) | async fn run_tcp_connection_pool<T: Transport>(
function run_udp_connection_pool (line 660) | async fn run_udp_connection_pool<T: Transport>(
FILE: src/transport/mod.rs
constant DEFAULT_NODELAY (line 12) | pub const DEFAULT_NODELAY: bool = true;
constant DEFAULT_KEEPALIVE_SECS (line 14) | pub const DEFAULT_KEEPALIVE_SECS: u64 = 20;
constant DEFAULT_KEEPALIVE_INTERVAL (line 15) | pub const DEFAULT_KEEPALIVE_INTERVAL: u64 = 8;
type AddrMaybeCached (line 18) | pub struct AddrMaybeCached {
method new (line 24) | pub fn new(addr: &str) -> AddrMaybeCached {
method resolve (line 31) | pub async fn resolve(&mut self) -> Result<()> {
method fmt (line 43) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type Transport (line 53) | pub trait Transport: Debug + Send + Sync {
method new (line 58) | fn new(config: &TransportConfig) -> Result<Self>
method hint (line 62) | fn hint(conn: &Self::Stream, opts: SocketOpts);
method bind (line 63) | async fn bind<T: ToSocketAddrs + Send + Sync>(&self, addr: T) -> Resul...
method accept (line 65) | async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream,...
method handshake (line 66) | async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream>;
method connect (line 67) | async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream>;
type Keepalive (line 99) | struct Keepalive {
type SocketOpts (line 107) | pub struct SocketOpts {
method none (line 115) | fn none() -> SocketOpts {
method for_control_channel (line 123) | pub fn for_control_channel() -> SocketOpts {
method from_cfg (line 132) | pub fn from_cfg(cfg: &TcpConfig) -> SocketOpts {
method from_client_cfg (line 142) | pub fn from_client_cfg(cfg: &ClientServiceConfig) -> SocketOpts {
method from_server_cfg (line 149) | pub fn from_server_cfg(cfg: &ServerServiceConfig) -> SocketOpts {
method apply (line 156) | pub fn apply(&self, conn: &TcpStream) {
FILE: src/transport/native_tls.rs
type TlsTransport (line 14) | pub struct TlsTransport {
type Acceptor (line 23) | type Acceptor = TcpListener;
type RawStream (line 24) | type RawStream = TcpStream;
type Stream (line 25) | type Stream = TlsStream<TcpStream>;
method new (line 27) | fn new(config: &TransportConfig) -> Result<Self> {
method hint (line 74) | fn hint(conn: &Self::Stream, opt: SocketOpts) {
method bind (line 78) | async fn bind<A: ToSocketAddrs + Send + Sync>(&self, addr: A) -> Result<...
method accept (line 85) | async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, S...
method handshake (line 92) | async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream> {
method connect (line 97) | async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
function get_tcpstream (line 114) | pub(crate) fn get_tcpstream(s: &TlsStream<TcpStream>) -> &TcpStream {
FILE: src/transport/noise.rs
type NoiseTransport (line 10) | pub struct NoiseTransport {
method fmt (line 19) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt:...
method builder (line 25) | fn builder(&self) -> Builder {
type Acceptor (line 36) | type Acceptor = TcpListener;
type RawStream (line 37) | type RawStream = TcpStream;
type Stream (line 38) | type Stream = snowstorm::stream::NoiseStream<TcpStream>;
method new (line 40) | fn new(config: &TransportConfig) -> Result<Self> {
method hint (line 73) | fn hint(conn: &Self::Stream, opt: SocketOpts) {
method bind (line 77) | async fn bind<T: ToSocketAddrs + Send + Sync>(&self, addr: T) -> Result<...
method accept (line 81) | async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, S...
method handshake (line 88) | async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream> {
method connect (line 95) | async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
FILE: src/transport/rustls.rs
type TlsTransport (line 18) | pub struct TlsTransport {
method fmt (line 27) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function load_server_config (line 35) | fn load_server_config(config: &TlsConfig) -> Result<Option<ServerConfig>> {
function load_client_config (line 57) | fn load_client_config(config: &TlsConfig) -> Result<Option<ClientConfig>> {
type Acceptor (line 86) | type Acceptor = TcpListener;
type RawStream (line 87) | type RawStream = TcpStream;
type Stream (line 88) | type Stream = TlsStream<TcpStream>;
method new (line 90) | fn new(config: &TransportConfig) -> Result<Self> {
method hint (line 112) | fn hint(conn: &Self::Stream, opt: SocketOpts) {
method bind (line 116) | async fn bind<A: ToSocketAddrs + Send + Sync>(&self, addr: A) -> Result<...
method accept (line 123) | async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, S...
method handshake (line 130) | async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream> {
method connect (line 135) | async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
function get_tcpstream (line 154) | pub(crate) fn get_tcpstream(s: &TlsStream<TcpStream>) -> &TcpStream {
FILE: src/transport/tcp.rs
type TcpTransport (line 13) | pub struct TcpTransport {
type Acceptor (line 20) | type Acceptor = TcpListener;
type Stream (line 21) | type Stream = TcpStream;
type RawStream (line 22) | type RawStream = TcpStream;
method new (line 24) | fn new(config: &TransportConfig) -> Result<Self> {
method hint (line 31) | fn hint(conn: &Self::Stream, opt: SocketOpts) {
method bind (line 35) | async fn bind<T: ToSocketAddrs + Send + Sync>(&self, addr: T) -> Result<...
method accept (line 39) | async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, S...
method handshake (line 45) | async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream> {
method connect (line 49) | async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
FILE: src/transport/websocket.rs
type TransportStream (line 28) | enum TransportStream {
method get_tcpstream (line 34) | fn get_tcpstream(&self) -> &TcpStream {
method poll_read (line 43) | fn poll_read(
method poll_write (line 56) | fn poll_write(
method poll_flush (line 67) | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result...
method poll_shutdown (line 74) | fn poll_shutdown(
type StreamWrapper (line 86) | struct StreamWrapper {
type Item (line 91) | type Item = Result<Bytes, Error>;
method poll_next (line 93) | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<...
method size_hint (line 113) | fn size_hint(&self) -> (usize, Option<usize>) {
type WebsocketTunnel (line 119) | pub struct WebsocketTunnel {
method poll_read (line 124) | fn poll_read(
method poll_fill_buf (line 134) | fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std...
method consume (line 138) | fn consume(self: Pin<&mut Self>, amt: usize) {
method poll_write (line 144) | fn poll_write(
method poll_flush (line 160) | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result...
method poll_shutdown (line 166) | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Res...
type SubTransport (line 174) | enum SubTransport {
type WebsocketTransport (line 180) | pub struct WebsocketTransport {
type Acceptor (line 187) | type Acceptor = TcpListener;
type RawStream (line 188) | type RawStream = TcpStream;
type Stream (line 189) | type Stream = WebsocketTunnel;
method new (line 191) | fn new(config: &TransportConfig) -> anyhow::Result<Self> {
method hint (line 208) | fn hint(conn: &Self::Stream, opt: SocketOpts) {
method bind (line 212) | async fn bind<A: ToSocketAddrs + Send + Sync>(
method accept (line 219) | async fn accept(&self, a: &Self::Acceptor) -> anyhow::Result<(Self::RawS...
method handshake (line 227) | async fn handshake(&self, conn: Self::RawStream) -> anyhow::Result<Self:...
method connect (line 239) | async fn connect(&self, addr: &AddrMaybeCached) -> anyhow::Result<Self::...
FILE: tests/common/mod.rs
constant PING (line 10) | pub const PING: &str = "ping";
constant PONG (line 11) | pub const PONG: &str = "pong";
function run_rathole_server (line 13) | pub async fn run_rathole_server(
function run_rathole_client (line 26) | pub async fn run_rathole_client(
function echo_server (line 42) | pub async fn echo_server<A: ToSocketAddrs>(addr: A) -> Result<()> {
function pingpong_server (line 53) | pub async fn pingpong_server<A: ToSocketAddrs>(addr: A) -> Result<()> {
function echo (line 64) | async fn echo(conn: TcpStream) -> Result<()> {
function pingpong (line 71) | async fn pingpong(mut conn: TcpStream) -> Result<()> {
function echo_server (line 90) | pub async fn echo_server<A: ToSocketAddrs>(addr: A) -> Result<()> {
function pingpong_server (line 102) | pub async fn pingpong_server<A: ToSocketAddrs>(addr: A) -> Result<()> {
FILE: tests/integration_test.rs
constant ECHO_SERVER_ADDR (line 18) | const ECHO_SERVER_ADDR: &str = "127.0.0.1:8080";
constant PINGPONG_SERVER_ADDR (line 19) | const PINGPONG_SERVER_ADDR: &str = "127.0.0.1:8081";
constant ECHO_SERVER_ADDR_EXPOSED (line 20) | const ECHO_SERVER_ADDR_EXPOSED: &str = "127.0.0.1:2334";
constant PINGPONG_SERVER_ADDR_EXPOSED (line 21) | const PINGPONG_SERVER_ADDR_EXPOSED: &str = "127.0.0.1:2335";
constant HITTER_NUM (line 22) | const HITTER_NUM: usize = 4;
type Type (line 25) | enum Type {
function init (line 30) | fn init() {
function tcp (line 40) | async fn tcp() -> Result<()> {
function udp (line 81) | async fn udp() -> Result<()> {
function test (line 122) | async fn test(config_path: &'static str, t: Type) -> Result<()> {
function echo_hitter (line 224) | async fn echo_hitter(addr: &'static str, t: Type) -> Result<()> {
function pingpong_hitter (line 231) | async fn pingpong_hitter(addr: &'static str, t: Type) -> Result<()> {
function tcp_echo_hitter (line 238) | async fn tcp_echo_hitter(addr: &'static str) -> Result<()> {
function udp_echo_hitter (line 253) | async fn udp_echo_hitter(addr: &'static str) -> Result<()> {
function tcp_pingpong_hitter (line 273) | async fn tcp_pingpong_hitter(addr: &'static str) -> Result<()> {
function udp_pingpong_hitter (line 288) | async fn udp_pingpong_hitter(addr: &'static str) -> Result<()> {
Condensed preview — 80 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (273K chars).
[
{
"path": ".dockerignore",
"chars": 101,
"preview": "# Directories\n/.git/\n/.github/\n/target/\n/examples/\n/docs/\n/benches/\n\n# Files\n.gitignore\n*.md\nLICENSE\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 730,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n<!-- Please try"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 197,
"preview": "---\nname: Feature Request\nabout: Ask for a new feature\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Feature Propo"
},
{
"path": ".github/workflows/release.yml",
"chars": 5655,
"preview": "name: Release\n\non:\n push:\n tags:\n - \"*\"\n\n workflow_dispatch:\n\nenv:\n CARGO_TERM_COLOR: always\n\njobs:\n release"
},
{
"path": ".github/workflows/rust.yml",
"chars": 2265,
"preview": "name: Rust\n\non:\n pull_request:\n branches: [\"*\"]\n push:\n branches: [\"main\", \"dev\"]\n\nconcurrency:\n # Documentatio"
},
{
"path": ".gitignore",
"chars": 32,
"preview": "/target\nperf.data\nperf.data.old\n"
},
{
"path": ".rustfmt.toml",
"chars": 31,
"preview": "imports_granularity = \"module\"\n"
},
{
"path": "Cargo.toml",
"chars": 3450,
"preview": "[package]\nname = \"rathole\"\nversion = \"0.5.0\"\nedition = \"2021\"\nauthors = [\"Yujia Qiao <code@rapiz.me>\"]\ndescription = \"A "
},
{
"path": "Dockerfile",
"chars": 397,
"preview": "FROM rust:bookworm as builder\nRUN apt update && apt install -y libssl-dev\nWORKDIR /home/rust/src\nCOPY . .\nARG FEATURES\nR"
},
{
"path": "LICENSE",
"chars": 10173,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README-zh.md",
"chars": 8445,
"preview": "# rathole\n\n\n\n[\n\n[ -> Result<()> {\n let mut config = Config::de"
},
{
"path": "docs/benchmark.md",
"chars": 3183,
"preview": "# Benchmark\n\n> Date: 2021/12/28\n>\n> Version: commit 1180c7e538564efd69742f22e77453a1b74a5ed2\n> \n> Arch Linux with 5.15.1"
},
{
"path": "docs/build-guide.md",
"chars": 2386,
"preview": "# Build Guide\n\nThis is for those who want to build `rathole` themselves, possibly because the need of latest features or"
},
{
"path": "docs/img/overview.excalidraw",
"chars": 33955,
"preview": "{\n \"type\": \"excalidraw\",\n \"version\": 2,\n \"source\": \"https://excalidraw.com\",\n \"elements\": [\n {\n \"type\": \"rec"
},
{
"path": "docs/internals.md",
"chars": 1438,
"preview": "# Internals\n\n\n\n## Conceptions\n### Service\nThe entity whose traffic needs to be forwarded\n"
},
{
"path": "docs/out-of-scope.md",
"chars": 1636,
"preview": "# Out of Scope\n\n`rathole` focuses on the forwarding for the NAT traversal, rather than being a all-in-one development to"
},
{
"path": "docs/transport.md",
"chars": 4869,
"preview": "# Security\n\nBy default, `rathole` forwards traffic as it is. Different options can be enabled to secure the traffic.\n\n##"
},
{
"path": "examples/iperf3/client.toml",
"chars": 204,
"preview": "[client]\nremote_addr = \"localhost:2333\"\ndefault_token = \"123\"\n\n[client.services.iperf3-udp]\ntype = \"udp\"\nlocal_addr = \"1"
},
{
"path": "examples/iperf3/server.toml",
"chars": 198,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\"\ndefault_token = \"123\"\n\n[server.services.iperf3-udp]\ntype = \"udp\"\nbind_addr = \"0.0.0."
},
{
"path": "examples/minimal/client.toml",
"chars": 114,
"preview": "[client]\nremote_addr = \"localhost:2333\"\ndefault_token = \"123\"\n\n[client.services.foo1]\nlocal_addr = \"127.0.0.1:80\"\n"
},
{
"path": "examples/minimal/server.toml",
"chars": 109,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\"\ndefault_token = \"123\"\n\n[server.services.foo1]\nbind_addr = \"0.0.0.0:5202\"\n"
},
{
"path": "examples/noise_nk/client.toml",
"chars": 241,
"preview": "[client]\nremote_addr = \"localhost:2333\"\ndefault_token = \"123\"\n\n[client.transport]\ntype = \"noise\"\n[client.transport.noise"
},
{
"path": "examples/noise_nk/server.toml",
"chars": 236,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\"\ndefault_token = \"123\"\n\n[server.transport]\ntype = \"noise\"\n[server.transport.noise]\nlo"
},
{
"path": "examples/systemd/README.md",
"chars": 2654,
"preview": "## Systemd Unit Examples\n\nThe directory lists some systemd unit files for example, which can be used to run `rathole` as"
},
{
"path": "examples/systemd/rathole@.service",
"chars": 310,
"preview": "[Unit]\nDescription=Rathole Service\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nRestartSec=5s\nLimitNOF"
},
{
"path": "examples/systemd/ratholec.service",
"chars": 333,
"preview": "[Unit]\nDescription=Rathole Client Service\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nRestartSec=5s\nL"
},
{
"path": "examples/systemd/ratholec@.service",
"chars": 323,
"preview": "[Unit]\nDescription=Rathole Client Service\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nRestartSec=5s\nL"
},
{
"path": "examples/systemd/ratholes.service",
"chars": 333,
"preview": "[Unit]\nDescription=Rathole Server Service\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nRestartSec=5s\nL"
},
{
"path": "examples/systemd/ratholes@.service",
"chars": 323,
"preview": "[Unit]\nDescription=Rathole Server Service\nAfter=network.target\n\n[Service]\nType=simple\nRestart=on-failure\nRestartSec=5s\nL"
},
{
"path": "examples/tls/client.toml",
"chars": 234,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\"\ndefault_token = \"123\"\n\n[client.transport]\ntype = \"tls\"\n[client.transport.tls]\ntr"
},
{
"path": "examples/tls/create_self_signed_cert.sh",
"chars": 1329,
"preview": "#!/bin/sh\n\n# create CA \nopenssl req -x509 \\\n -sha256 -days 356 \\\n -nodes \\\n -newkey rsa"
},
{
"path": "examples/tls/rootCA.crt",
"chars": 1208,
"preview": "-----BEGIN CERTIFICATE-----\nMIIDTzCCAjegAwIBAgIUOSG1er7cfoTq6uMOe3r0tcSZREMwDQYJKoZIhvcNAQEL\nBQAwNzEQMA4GA1UEAwwHTXlPd25"
},
{
"path": "examples/tls/rootCA.key",
"chars": 1704,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqk6ojYWi1OYik\nS1Em+C8YMWHUuCK/LAtLrDU6FYr"
},
{
"path": "examples/tls/server.crt",
"chars": 1326,
"preview": "-----BEGIN CERTIFICATE-----\nMIIDpzCCAo+gAwIBAgIUPP/pKlmn1kOLpd31HXRSjtw2muQwDQYJKoZIhvcNAQEL\nBQAwNzEQMA4GA1UEAwwHTXlPd25"
},
{
"path": "examples/tls/server.key",
"chars": 1704,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnUuts/OS9/A/N\nwGA5GuZXHOQH5Mo92D4/kR/oiu2"
},
{
"path": "examples/tls/server.toml",
"chars": 227,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\"\ndefault_token = \"123\"\n\n[server.transport]\ntype = \"tls\"\n[server.transport.tls]\npkcs12"
},
{
"path": "examples/udp/client.toml",
"chars": 127,
"preview": "[client]\nremote_addr = \"localhost:2333\"\ndefault_token = \"123\"\n\n[client.services.foo1]\ntype = \"udp\"\nlocal_addr = \"127.0.0"
},
{
"path": "examples/udp/server.toml",
"chars": 122,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\"\ndefault_token = \"123\"\n\n[server.services.foo1]\ntype = \"udp\"\nbind_addr = \"0.0.0.0:5202"
},
{
"path": "examples/unified/config.toml",
"chars": 314,
"preview": "# rathole configuration can put in one file as long as running mode is specified via cli\n\n[client]\nremote_addr = \"localh"
},
{
"path": "examples/use_proxy/client.toml",
"chars": 453,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\"\ndefault_token = \"123\"\n\n[client.services.foo1]\nlocal_addr = \"127.0.0.1:80\"\n\n[clie"
},
{
"path": "rust-toolchain",
"chars": 6,
"preview": "1.71.0"
},
{
"path": "src/cli.rs",
"chars": 1885,
"preview": "use clap::{AppSettings, ArgGroup, Parser};\nuse lazy_static::lazy_static;\n\n#[derive(clap::ArgEnum, Clone, Debug, Copy)]\np"
},
{
"path": "src/client.rs",
"chars": 19490,
"preview": "use crate::config::{ClientConfig, ClientServiceConfig, Config, ServiceType, TransportType};\nuse crate::config_watcher::{"
},
{
"path": "src/config.rs",
"chars": 14195,
"preview": "use anyhow::{anyhow, bail, Context, Result};\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\nuse std"
},
{
"path": "src/config_watcher.rs",
"chars": 13981,
"preview": "use crate::{\n config::{ClientConfig, ClientServiceConfig, ServerConfig, ServerServiceConfig},\n Config,\n};\nuse anyh"
},
{
"path": "src/constants.rs",
"chars": 726,
"preview": "use backoff::ExponentialBackoff;\nuse std::time::Duration;\n\n// FIXME: Determine reasonable size\n/// UDP MTU. Currently fa"
},
{
"path": "src/helper.rs",
"chars": 6110,
"preview": "use anyhow::{anyhow, Context, Result};\nuse async_http_proxy::{http_connect_tokio, http_connect_tokio_with_basic_auth};\nu"
},
{
"path": "src/lib.rs",
"chars": 6968,
"preview": "mod cli;\nmod config;\nmod config_watcher;\nmod constants;\nmod helper;\nmod multi_map;\nmod protocol;\nmod transport;\n\npub use"
},
{
"path": "src/main.rs",
"chars": 1316,
"preview": "use anyhow::Result;\nuse clap::Parser;\nuse rathole::{run, Cli};\nuse tokio::{signal, sync::broadcast};\nuse tracing_subscri"
},
{
"path": "src/multi_map.rs",
"chars": 3150,
"preview": "use std::borrow::Borrow;\nuse std::collections::HashMap;\nuse std::hash::{Hash, Hasher};\n\nstruct RawItem<K1, K2, V>(*mut ("
},
{
"path": "src/protocol.rs",
"chars": 6896,
"preview": "pub const HASH_WIDTH_IN_BYTES: usize = 32;\n\nuse anyhow::{bail, Context, Result};\nuse bytes::{Bytes, BytesMut};\nuse lazy_"
},
{
"path": "src/server.rs",
"chars": 25859,
"preview": "use crate::config::{Config, ServerConfig, ServerServiceConfig, ServiceType, TransportType};\nuse crate::config_watcher::{"
},
{
"path": "src/transport/mod.rs",
"chars": 5342,
"preview": "use crate::config::{ClientServiceConfig, ServerServiceConfig, TcpConfig, TransportConfig};\nuse crate::helper::{to_socket"
},
{
"path": "src/transport/native_tls.rs",
"chars": 3890,
"preview": "use crate::config::{TlsConfig, TransportConfig};\nuse crate::helper::host_port_pair;\nuse crate::transport::{AddrMaybeCach"
},
{
"path": "src/transport/noise.rs",
"chars": 3345,
"preview": "use std::net::SocketAddr;\n\nuse super::{AddrMaybeCached, SocketOpts, TcpTransport, Transport};\nuse crate::config::{NoiseC"
},
{
"path": "src/transport/rustls.rs",
"chars": 4876,
"preview": "use crate::config::{TlsConfig, TransportConfig};\nuse crate::helper::host_port_pair;\nuse crate::transport::{AddrMaybeCach"
},
{
"path": "src/transport/tcp.rs",
"chars": 1460,
"preview": "use crate::{\n config::{TcpConfig, TransportConfig},\n helper::tcp_connect_with_proxy,\n};\n\nuse super::{AddrMaybeCach"
},
{
"path": "src/transport/websocket.rs",
"chars": 8188,
"preview": "use core::result::Result;\nuse std::io::{Error, ErrorKind};\nuse std::net::SocketAddr;\nuse std::pin::Pin;\nuse std::task::{"
},
{
"path": "tests/common/mod.rs",
"chars": 2883,
"preview": "use std::path::PathBuf;\n\nuse anyhow::Result;\nuse tokio::{\n io::{self, AsyncReadExt, AsyncWriteExt},\n net::{TcpList"
},
{
"path": "tests/config_test/invalid_config/missing_tls_client.toml",
"chars": 156,
"preview": "[client]\nremote_addr = \"example.com:2333\"\n\n[client.transport]\ntype = \"tls\" \n\n[client.services.service1] \ntoken = \"whatev"
},
{
"path": "tests/config_test/invalid_config/missing_tls_server.toml",
"chars": 202,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\" \n\n[server.transport]\ntype = \"tls\" \n[server.transport.tls] \npkcs12_password = \"passwo"
},
{
"path": "tests/config_test/invalid_config/missing_tls_server2.toml",
"chars": 148,
"preview": "[server]\nbind_addr = \"0.0.0.0:2333\" \n\n[server.transport]\ntype = \"tls\" \n\n[server.services.service1] \ntoken = \"whatever\" \n"
},
{
"path": "tests/config_test/valid_config/full.toml",
"chars": 2652,
"preview": "[client]\nremote_addr = \"example.com:2333\" # Necessary. The address of the server\ndefault_token = \"default_token_if_not_s"
},
{
"path": "tests/for_tcp/noise_transport.toml",
"chars": 655,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"noi"
},
{
"path": "tests/for_tcp/tcp_transport.toml",
"chars": 467,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"tcp"
},
{
"path": "tests/for_tcp/tls_transport.toml",
"chars": 639,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"tls"
},
{
"path": "tests/for_tcp/websocket_tls_transport.toml",
"chars": 733,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"web"
},
{
"path": "tests/for_tcp/websocket_transport.toml",
"chars": 563,
"preview": "[client]\nremote_addr = \"127.0.0.1:2333\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"web"
},
{
"path": "tests/for_udp/noise_transport.toml",
"chars": 707,
"preview": "[client]\nremote_addr = \"127.0.0.1:2332\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"noi"
},
{
"path": "tests/for_udp/tcp_transport.toml",
"chars": 519,
"preview": "[client]\nremote_addr = \"127.0.0.1:2332\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"tcp"
},
{
"path": "tests/for_udp/tls_transport.toml",
"chars": 691,
"preview": "[client]\nremote_addr = \"127.0.0.1:2332\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"tls"
},
{
"path": "tests/for_udp/websocket_tls_transport.toml",
"chars": 784,
"preview": "[client]\nremote_addr = \"127.0.0.1:2332\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"web"
},
{
"path": "tests/for_udp/websocket_transport.toml",
"chars": 614,
"preview": "[client]\nremote_addr = \"127.0.0.1:2332\" \ndefault_token = \"default_token_if_not_specify\" \n\n[client.transport]\ntype = \"web"
},
{
"path": "tests/integration_test.rs",
"chars": 8725,
"preview": "use anyhow::{Ok, Result};\nuse common::{run_rathole_client, PING, PONG};\nuse rand::Rng;\nuse std::time::Duration;\nuse toki"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the rathole-org/rathole GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 80 files (249.8 KB), approximately 71.0k tokens, and a symbol index with 302 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.