Compare commits

...

57 Commits
v0.4.0 ... main

Author SHA1 Message Date
sunmy2019 0ee0157b4b
docs: update docs for rustls (#337)
* Fix typos and update docs for rustls

* update description

* Remove placeholders in doc
2024-03-01 14:46:29 +08:00
Yujia Qiao 3ab540f19d
chore: update bug_report.md 2024-02-18 22:55:59 +08:00
sunmy2019 4ac53a5a39
feat: optional rustls support (#330)
* initial implementation of rustls support

* Refactor create_self_signed_cert.sh script

* resolve lint errors

* Fix handling of Option in tls.rs

* Update cargo-hack check command and feature dependencies

* fix missing point

* Add conditional check to skip test if client or server is not enabled

* clean up things

* fix for windows CI

* try fixing Windows CI

* Update src/main.rs

* Update src/transport/websocket.rs

* add missing messages

* split the tls mod

Co-authored-by: Ning Sun <n@sunng.info>
2024-02-18 17:17:17 +08:00
dependabot[bot] 7251759bda
chore(deps): bump h2 from 0.3.21 to 0.3.24 (#334)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.21 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-18 02:55:27 +00:00
dependabot[bot] ee7561c38d
chore(deps): bump snow from 0.9.3 to 0.9.6 (#333)
Bumps [snow](https://github.com/mcginty/snow) from 0.9.3 to 0.9.6.
- [Release notes](https://github.com/mcginty/snow/releases)
- [Commits](https://github.com/mcginty/snow/compare/v0.9.3...v0.9.6)

---
updated-dependencies:
- dependency-name: snow
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-18 10:50:47 +08:00
Vincent Young e4766e7d90
ci: support apple aarch64 (#294) 2024-02-18 02:25:45 +00:00
Ryan Dearing 63221028c9
fix: flush DataChannelCmd::StartForward* commands (#316)
Without flushing this may sit in a kernel buffer and we won't
know if the channel is still alive. This is particularly problematic
for the TCP connection pool.
2024-02-14 11:30:52 +08:00
sunmy2019 915bf4d21d
chore: update dependencies in Cargo.lock (#329) 2024-02-14 11:29:49 +08:00
Thomas Fournier 62114cde4c
fix: typo (#323) 2024-02-13 23:08:06 +08:00
zhfish 65b27f076c
chore: vendor openssl for musl (#301)
* Update Cargo.toml

add openssl's features for musl

* Update release.yml

* Update Cargo.toml

musl

* 更新 Cargo.toml

---------

Co-authored-by: Yujia Qiao <rapiz3142@gmail.com>
2023-11-05 13:39:16 +08:00
blueskea e08b2a9e92
docs: build with openssl (#303) 2023-11-05 13:38:39 +08:00
Yujia Qiao 84c04ab9df
fix: clippy (#302) 2023-11-03 11:37:43 +00:00
Yujia Qiao ebb764ae53
chore(bump): v0.5.0 2023-10-01 17:55:18 +08:00
Yujia Qiao 2ccb386cea
chore(test): update to be feature aware 2023-10-01 17:50:15 +08:00
Yujia Qiao 97541afaed
chore: fix build
- Drop tls for embedded devices in release
- Upgrade cross
- Specify rust 1.71
2023-10-01 17:13:17 +08:00
Yujia Qiao 3aa6557696
chore(bump): v0.4.9 2023-10-01 15:09:39 +08:00
I Putu Ariyasa 5946a18370
feat(transport): add websocket transport (#290) 2023-10-01 14:43:03 +08:00
Chieh Tai d2fe586f7b
chore: fix shield.io badge in README-zh.md (#272) 2023-07-07 18:57:55 +08:00
boenshao 7923dec9f5
docs: non root user with systemd (#269)
* doc: remove 'DynamicUser=yes', so the service can read config file with permission 600

* doc: add example for non-root systemd service

* fix(doc): should be 'app2.toml'
2023-07-07 18:57:26 +08:00
Yujia Qiao 9727e15377
chore: bump v0.4.8 2023-05-26 16:29:56 +08:00
dependabot[bot] 553ff381eb chore(deps): bump h2 from 0.3.15 to 0.3.18
Bumps [h2](https://github.com/hyperium/h2) from 0.3.15 to 0.3.18.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.15...v0.3.18)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 21:10:31 +08:00
reemaadeniyi d2960b5bde
chore: bump tracing-subscriber (#247)
Co-authored-by: evergreen-trading-systems <4870868+evergreen-trading-systems@users.noreply.github.com>
2023-05-06 12:57:13 +00:00
dependabot[bot] a1815900f9 chore(deps): bump openssl from 0.10.42 to 0.10.48
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.42 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.42...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 12:00:54 +08:00
dependabot[bot] 540ebfc33b chore(deps): bump tokio from 1.21.2 to 1.24.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.21.2 to 1.24.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/commits)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-27 11:05:29 +08:00
Yujia Qiao 2acd62454d
fix: clippy 2023-03-27 10:48:12 +08:00
Yujia Qiao 9479b9a3a9 ci: upgrade to upx v4.0.2 2023-03-07 23:34:42 +08:00
dependabot[bot] 80a7266212 chore(deps): bump libgit2-sys from 0.13.4+1.4.2 to 0.13.5+1.4.5
Bumps [libgit2-sys](https://github.com/rust-lang/git2-rs) from 0.13.4+1.4.2 to 0.13.5+1.4.5.
- [Release notes](https://github.com/rust-lang/git2-rs/releases)
- [Commits](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.4...libgit2-sys-0.13.5)

---
updated-dependencies:
- dependency-name: libgit2-sys
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-07 23:22:35 +08:00
Yujia Qiao 32a2c36fde
chore: fix the shield.io badge 2023-03-07 23:07:46 +08:00
Yujia Qiao 69d8185ab5
chore: updat README.md 2023-03-07 21:51:19 +08:00
Yujia Qiao d079d66223 chore: update tls cert for test 2023-03-07 21:38:20 +08:00
Yujia Qiao ee5c7b4a77
chore: bump v0.4.7 2022-11-30 11:49:01 +08:00
Yujia Qiao 1b5c892e24
fix: enable TCP_NODELAY by default (#210) 2022-11-30 11:42:16 +08:00
Yujia Qiao 96479e498d
chore: bump v0.4.6 2022-11-29 17:59:58 +08:00
Yujia Qiao d216d6380f
feat: configurable retry interval (#208) 2022-11-29 09:41:23 +00:00
Orhun Parmaksız 87d06c91b9
chore: bump `vergen` crate to support `SOURCE_DATE_EPOCH` (#204)
This commits updates the version of `vergen` which is responsible
for generating the build info. The basis of this change is to support
`SOURCE_DATE_EPOCH` timestamps for reproducible builds.
See <https://reproducible-builds.org/docs/source-date-epoch/>

Signed-off-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2022-11-06 21:10:01 +08:00
Yujia Qiao 353d195529
chore: bump v0.4.5 2022-11-05 15:13:37 +08:00
Yujia Qiao e4c6c8abce
ci: upgrade upx to v4.0.0 (#203) 2022-11-04 14:49:31 +00:00
Yujia Qiao bf842b43d3
fix: update Dockerfile (#201) 2022-11-04 14:39:48 +00:00
Vincent Young 8fb9304549
chore: aarch64-apple-darwin CI 2022-10-29 20:44:42 +08:00
Yujia Qiao 5396d9e64d
chore: bump v0.4.4 2022-09-16 16:52:22 +08:00
Yujia Qiao 76f5c7227f
chore: update dependencies (#195) 2022-09-15 11:54:50 +00:00
Yujia Qiao ea01c42da7
refactor: ConfigChange (#191) 2022-09-15 11:40:15 +00:00
Peter Neumark 187f4f0335
feat: Allow use of system default TLS trusted root by omitting the trusted_root client config parameter. (#192) 2022-09-14 14:55:38 +00:00
Yujia Qiao 064bdcab8e
fix: clippy (#187) 2022-09-04 22:04:20 +08:00
inclyc 67182fbc10
docs: [example][systemd] use `DynamicUser=yes` (#186)
This patch fixes a warning generated by some new version of systemd. Use
"User=nobody" seems to be considered unsafe. So maybe we need to fix it
in our example files.

● ratholec@hitmc.service - Rathole Client Service
     Loaded: loaded (/etc/systemd/system/ratholec@.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2022-09-03 23:38:43 CST; 1h 27min ago
   Main PID: 507903 (rathole)
      Tasks: 14 (limit: 76731)
     Memory: 6.9M
        CPU: 39.908s
     CGroup: /system.slice/system-ratholec.slice/ratholec@hitmc.service
             └─507903 /usr/local/bin/rathole -c /etc/rathole/hitmc.toml

Sep 03 23:38:43 <hostname> systemd[1]: Started Rathole Client Service.
...
Sep 03 23:39:25 <hostname> systemd[1]: /etc/systemd/system/ratholec@.service:7: Special user nobody configured, this is not safe!
                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Link: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=969329
Link: https://github.com/trojan-gfw/trojan/issues/612
Link: https://www.vvave.net/archives/fix-the-systemd-error-special-user-nobody-configured-this-is-not-safe.html
2022-09-04 19:54:21 +08:00
fernvenue 881701d68f Update out-of-scope.md 2022-08-30 11:06:19 +08:00
Yujia Qiao 2e9e7374bc
chore: bump v0.4.3 2022-08-11 20:51:56 +08:00
Yujia Qiao 1f2fc5b28f
feat: cache dns result for one session (#166) 2022-06-11 11:39:11 +08:00
Yujia Qiao ee39a8e31e
chore: bump v0.4.2 2022-05-23 20:31:03 +08:00
Yujia Qiao 8c9527406e
chore: update snowstorm (#165) 2022-05-21 16:26:47 +00:00
Yujia Qiao 15a4183ba7
chore: update dependencies (#163) 2022-05-20 15:40:57 +00:00
Yujia Qiao f8c415c558
chore: add `feature_request.md` issue template 2022-05-20 23:26:29 +08:00
Victor C 8665e6a2cf
fix: make watcher's path absolute for notify's MacOS fsevent implementation (#155) 2022-04-15 16:47:55 +08:00
Yujia Qiao 8a24723895
chore: bump v0.4.1 2022-03-28 19:42:49 +08:00
Yujia Qiao feb8c2dbfa
fix: restart when heartbeat times out (#147) 2022-03-28 16:42:09 +08:00
Takayuki Maeda 636bdbd604
revert(ci): disable incremental compilation (#140) (#144)
This reverts commit f9ee8ec2f9.
2022-03-25 11:57:12 +08:00
Takayuki Maeda f9ee8ec2f9
ci: disable incremental compilation (#140) 2022-03-15 13:53:19 +00:00
45 changed files with 2422 additions and 1157 deletions

View File

@ -28,3 +28,4 @@ If you encountered a panic, please re-run with `RUST_BACKTRACE=1` to provide the
- OS: <!-- Please fill in distribution if you're using linux-->
- `rathole --version` output:
- CPU architecture:
- rustc version:

View File

@ -0,0 +1,14 @@
---
name: Feature Request
about: Ask for a new feature
title: ''
labels: enhancement
assignees: ''
---
**Feature Proposed**
<!-- describe the feature -->
**Use Case**
<!-- possible use case -->

View File

@ -20,59 +20,104 @@ jobs:
- 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
toolchain: stable
- name: Install cross
run: cargo install --version 0.1.16 cross
# Since rust 1.72, some platforms are tier 3
toolchain: 1.71
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
run: cross test --release --target ${{ matrix.target }} --verbose
if: matrix.cross == false && matrix.target != 'aarch64-apple-darwin'
run: cargo test --release --target ${{ matrix.target }} --verbose
- name: Build release
run: cross build --release --target ${{ matrix.target }}
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
@ -80,7 +125,7 @@ jobs:
if: matrix.os == 'ubuntu-latest' && !contains(matrix.target, 'mips')
uses: crazy-max/ghaction-upx@v1
with:
version: v3.96
version: v4.0.2
files: target/${{ matrix.target }}/release/${{ matrix.exe }}
args: -q --best --lzma
- uses: actions/upload-artifact@v2

View File

@ -32,7 +32,9 @@ jobs:
- name: Setup cargo-hack
run: cargo install cargo-hack
- name: Check all features
run: cargo hack check --feature-powerset --no-dev-deps
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 }}
@ -49,6 +51,9 @@ jobs:
- os: macos-latest
exe: rathole
target: x86_64-apple-darwin
- os: macos-latest
exe: rathole
target: aarch64-apple-darwin
steps:
- uses: actions/checkout@v2
@ -59,8 +64,10 @@ jobs:
- uses: Swatinem/rust-cache@v1
- name: Build
run: cargo build
- name: Run tests
- 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@v2
with:
name: rathole-${{ matrix.target }}

1840
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "rathole"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
authors = ["Yujia Qiao <code@rapiz.me>"]
description = "A reverse proxy for NAT traversal"
@ -11,19 +11,55 @@ build = "build.rs"
include = ["src/**/*", "LICENSE", "README.md", "build.rs"]
[features]
default = ["server", "client", "tls", "noise", "hot-reload"]
default = [
"server",
"client",
"native-tls",
"noise",
"websocket-native-tls",
"hot-reload",
]
# Run as a server
server = []
# Run as a client
client = []
# TLS support
tls = ["tokio-native-tls"]
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"]
@ -61,20 +97,42 @@ hex = "0.4"
rand = "0.8"
backoff = { version = "0.4", features = ["tokio"] }
tracing = "0.1"
tracing-subscriber = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
socket2 = { version = "0.4", features = ["all"] }
fdlimit = "0.2"
tokio-native-tls = { version = "0.3", optional = true }
async-trait = "0.1"
snowstorm = { version = "0.3", optional = true, features = ["stream"], default-features = false }
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"] }
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-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"] }
[build-dependencies]
vergen = { version = "6.0", default-features = false, features = ["build", "git", "cargo"] }
vergen = { version = "7.4.2", default-features = false, features = [
"build",
"git",
"cargo",
] }
anyhow = "1.0"

View File

@ -1,11 +1,15 @@
FROM ekidd/rust-musl-builder:latest as builder
FROM rust:bookworm as builder
RUN apt update && apt install -y libssl-dev
WORKDIR /home/rust/src
COPY . .
RUN cargo build --locked --release
ARG FEATURES
RUN cargo build --locked --release --features ${FEATURES:-default}
RUN mkdir -p build-out/
RUN cp target/x86_64-unknown-linux-musl/release/rathole build-out/
RUN cp target/release/rathole build-out/
FROM scratch
FROM gcr.io/distroless/cc-debian12
WORKDIR /app
COPY --from=builder /home/rust/src/build-out/rathole .
USER 1000:1000

View File

@ -4,7 +4,7 @@
[![GitHub stars](https://img.shields.io/github/stars/rapiz1/rathole)](https://github.com/rapiz1/rathole/stargazers)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/rapiz1/rathole)](https://github.com/rapiz1/rathole/releases)
![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/rapiz1/rathole/Rust/main)
![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/rapiz1/rathole/rust.yml?branch=main)
[![GitHub all releases](https://img.shields.io/github/downloads/rapiz1/rathole/total)](https://github.com/rapiz1/rathole/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/rapiz1/rathole)](https://hub.docker.com/r/rapiz1/rathole)
@ -16,19 +16,21 @@ rathole类似于 [frp](https://github.com/fatedier/frp) 和 [ngrok](https://g
<!-- TOC -->
- [Features](#features)
- [Quickstart](#quickstart)
- [Configuration](#configuration)
- [Logging](#logging)
- [Benchmark](#benchmark)
- [Development Status](#development-status)
- [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**,可以部署在嵌入式设备如路由器上。
- **高性能** 具有更高的吞吐量,高并发下更稳定。见[Benchmark](#benchmark)
- **低资源消耗** 内存占用远低于同类工具。见[Benchmark](#benchmark)。[二进制文件最小](docs/build-guide.md)可以到 **~500KiB**,可以部署在嵌入式设备如路由器上。
- **安全性** 每个服务单独强制鉴权。Server 和 Client 负责各自的配置。使用 Noise Protocol 可以简单地配置传输加密,而不需要自签证书。同时也支持 TLS。
- **热重载** 支持配置文件热重载动态修改端口转发服务。HTTP API 正在开发中。
@ -89,7 +91,7 @@ local_addr = "127.0.0.1:22" # 需要被转发的服务的地址
## Configuration
如果只有一个 `[server]``[client]` 块存在的话,`rathole` 可以根据配置文件的内容自动决定在服务器模式或客户端模式下运行,就像 [Quickstart](#Quickstart) 中的例子。
如果只有一个 `[server]``[client]` 块存在的话,`rathole` 可以根据配置文件的内容自动决定在服务器模式或客户端模式下运行,就像 [Quickstart](#quickstart) 中的例子。
`[client]``[server]` 块也可以放在一个文件中。然后在服务器端,运行 `rathole --server config.toml`。在客户端,运行 `rathole --client config.toml` 来明确告诉 `rathole` 运行模式。
@ -104,13 +106,14 @@ local_addr = "127.0.0.1:22" # 需要被转发的服务的地址
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 = false # Optional. Determine whether to enable TCP_NODELAY, if applicable, to improve the latency but decrease the bandwidth. Default: false
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
@ -123,11 +126,15 @@ 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 = false # Optional. Determine whether to enable TCP_NODELAY for data transmission, if applicable, to improve the latency but decrease the bandwidth. Default: false
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"
@ -141,7 +148,7 @@ heartbeat_interval = 30 # Optional. The interval between two application-layer h
type = "tcp"
[server.transport.tcp] # Same as the client
nodelay = false
nodelay = true
keepalive_secs = 20
keepalive_interval = 8
@ -154,11 +161,14 @@ 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 = false # Optional. Same as the client
nodelay = true # Optional. Same as the client
[server.services.service2]
bind_addr = "0.0.0.1:8082"
@ -178,6 +188,12 @@ RUST_LOG=error ./rathole config.toml
如果 `RUST_LOG` 不存在,默认的日志级别是 `info`
### Tuning
从 v0.4.7 开始, rathole 默认启用 TCP_NODELAY。这能够减少延迟并使交互式应用受益比如 RDPMinecraft 服务器。但它会减少一些带宽。
如果带宽更重要比如网盘类应用TCP_NODELAY 仍然可以通过配置 `nodelay = false` 关闭。
## Benchmark
rathole 的延迟与 [frp](https://github.com/fatedier/frp) 相近,在高并发情况下表现更好,能提供更大的带宽,内存占用更少。

View File

@ -4,7 +4,7 @@
[![GitHub stars](https://img.shields.io/github/stars/rapiz1/rathole)](https://github.com/rapiz1/rathole/stargazers)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/rapiz1/rathole)](https://github.com/rapiz1/rathole/releases)
![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/rapiz1/rathole/Rust/main)
![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/rapiz1/rathole/rust.yml?branch=main)
[![GitHub all releases](https://img.shields.io/github/downloads/rapiz1/rathole/total)](https://github.com/rapiz1/rathole/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/rapiz1/rathole)](https://hub.docker.com/r/rapiz1/rathole)
[![Join the chat at https://gitter.im/rapiz1/rathole](https://badges.gitter.im/rapiz1/rathole.svg)](https://gitter.im/rapiz1/rathole?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -17,19 +17,21 @@ rathole, like [frp](https://github.com/fatedier/frp) and [ngrok](https://github.
<!-- TOC -->
- [Features](#features)
- [Quickstart](#quickstart)
- [Configuration](#configuration)
- [Logging](#logging)
- [Benchmark](#benchmark)
- [Development Status](#development-status)
- [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.
- **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.
@ -53,7 +55,7 @@ Create `server.toml` with the following content and accommodate it to your needs
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 a arbitrary value.
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
```
@ -91,7 +93,7 @@ To run `rathole` run as a background service on Linux, checkout the [systemd exa
## 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).
`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.
@ -106,13 +108,14 @@ Here is the full configuration specification:
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 = false # Optional. Determine whether to enable TCP_NODELAY, if applicable, to improve the latency but decrease the bandwidth. Default: false
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
@ -125,11 +128,15 @@ 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 = false # Optional. Determine whether to enable TCP_NODELAY for data transmission, if applicable, to improve the latency but decrease the bandwidth. Default: false
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"
@ -143,7 +150,7 @@ heartbeat_interval = 30 # Optional. The interval between two application-layer h
type = "tcp"
[server.transport.tcp] # Same as the client
nodelay = false
nodelay = true
keepalive_secs = 20
keepalive_interval = 8
@ -156,11 +163,14 @@ 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 = false # Optional. Same as the client
nodelay = true # Optional. Same as the client
[server.services.service2]
bind_addr = "0.0.0.1:8082"
@ -178,6 +188,12 @@ 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.
@ -191,13 +207,8 @@ For more details, see the separate page [Benchmark](./docs/benchmark.md).
![udp_bitrate](./docs/img/udp_bitrate.svg)
![mem](./docs/img/mem-graph.png)
## Development Status
## Planning
`rathole` is under active development. A load of features is on the way:
- [x] TLS support
- [x] UDP support
- [x] Hot reloading
- [ ] HTTP APIs for configuration
[Out of Scope](./docs/out-of-scope.md) lists features that are not planned to be implemented and why.

View File

@ -1,30 +1,54 @@
# 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
```
## Customize the build
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.
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
```
@ -33,7 +57,8 @@ cargo build --profile minimal --no-default-features --features client
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
```

View File

@ -30,5 +30,5 @@ When `rathole` starts in the client mode, it creates connections to `server.comm
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 contorl 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.
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.

View File

@ -17,3 +17,7 @@ But that doesn't mean it's not useful for other purposes. In the future, more co
- *`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.

View File

@ -3,26 +3,54 @@
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.
```
`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/ca-cert.pem"
hostname = "0.0.0.0"
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
```
openssl pkcs12 -export -out identity.pfx -inkey server-key.pem -in server-cert.pem -certfile ca_chain_certs.pem
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.
@ -30,12 +58,14 @@ In one word, the [Noise Protocol](http://noiseprotocol.org/noise.html) is a ligh
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=
@ -43,11 +73,13 @@ 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]
@ -65,9 +97,11 @@ 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
@ -99,6 +133,7 @@ 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)

View File

@ -2,12 +2,16 @@
The directory lists some systemd unit files for example, which can be used to run `rathole` as a service on Linux.
[The `@` symbol in name of unit files](https://superuser.com/questions/393423/the-symbol-and-systemctl-and-vsftpd) such as
[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`.
Assuming that `rathole` is installed in `/usr/bin/rathole`, and the configuration file is in `/etc/rathole/app1.toml`, the following steps shows how to run an instance of `rathole --server`.
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.
@ -22,14 +26,49 @@ sudo mkdir -p /etc/rathole
# And create the configuration file named `app1.toml` inside /etc/rathole
```
3. Enable and start the service
3. Enable and start the service.
```bash
sudo systemctl daemon-reload # Make sure systemd find the new unit
sudo systemctl enable ratholes@app1 --now
```
And if there's another configuration named `app2.toml` in `/etc/rathole`, then
`sudo systemctl enable ratholes@app2 --now` can start an instance for that configuration.
### Without root privilege
The same applies to `rathole --client` and `rathole`.
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`.

View File

@ -4,11 +4,14 @@ After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/rathole /etc/rathole/%i.toml
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

View File

@ -4,11 +4,14 @@ After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/rathole -c /etc/rathole/rathole.toml
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

View File

@ -4,11 +4,14 @@ After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/rathole -c /etc/rathole/%i.toml
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

View File

@ -4,11 +4,14 @@ After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/rathole -s /etc/rathole/rathole.toml
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

View File

@ -4,11 +4,14 @@ After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/usr/bin/rathole -s /etc/rathole/%i.toml
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

View File

@ -1,31 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIUXTmJtkI6aK16A8HPkP2IvowmSKwwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjAxMDIwODEzMzhaFw0yMzAy
MDMwODEzMzhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDAAq3LEmJigEuRT9sswUx6Kfc4T04oZvZTSYNIRrBF
Zcc/EGZF/t/k2ciGDSAB1mL2rUdIfWveQ/5kRCSFffX5qvKFkzogRQQjFPLFjfoC
lKXxvy/BOIwF786gvHbz5EI1dcAL+nRco3U6dHPdewvbQwX9cZrUD3pq+r1qlipY
w5rZL7Z5cNoczhRAgFhIBHvsgBazkkOB7PDUkmkYAYnw3uK+r4coAqnnfjpxoaCQ
dQi4JX2VvqOdgxzw9vIRqbL+p2NBPnVjcSj067Y9sxtfR3Xmt2dlMJuReFN8phnK
8GiYiuiYA01O84htjHt+A8oVYKalXdPeikoSgPmhoJCQQs0NkBzGCc33U7XEa6kM
j6Y81Id4uXAK5LxyVGo5zOEvOyF3EhceIJDeGS9NsGJyT757OuKrsCK0v8KNPsEh
VvrcngnRQOWFTg/rp/vSrj7S5i0NPjkEpRitxaYBOg40DXyG1GfYf1SvneXpT0gh
ZbgjipPrwvuZnJVqqIv1hVVNOKo7nJS24rZ/andZS8g6OE0bL9AlE1Sp2lMXuagJ
2haPa2rSFZPqNPrP9wh5KVreD9UNeTb37NbXWeZXwKR8v20GAWjb2QQKY92zlMpI
gmViEvJHrHbKVoU/8gyS9R7iL9JOehk3sqVhbjaDyouC9mosPrQFzp1frKvSlKNg
1wIDAQABo1MwUTAdBgNVHQ4EFgQU98MJp09MMFw5s4sacYozQFzTNFwwHwYDVR0j
BBgwFoAU98MJp09MMFw5s4sacYozQFzTNFwwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEABOtNqqKFEA3vynOFteZV+VquaRKqDuYn0doMMPH9cY20
4ASioa3aqbmvBiSTDsOdvgP6j5nSVEtQCt5P3fBRMa8a3YnTGPNx8uGPuOA+ZD+b
USR5FcXJHtkjSfpVF9DOZr34+khRpfHPEZQiaAAiKwaRnI4Gqhv6e6JoaimkQDYj
xcKw+f1NcCdhSTkpcx9K/Qfa0cXKSL+0Hwl5AbDMsnRAkKu62YKdOv36nnBOMc2S
6laNIx20nt8Evm3KBNDRiHAw8pwMGfnxCCG6hGo2IvYh6hOjZupVpP55iMgQUkfF
Gmvxe/4wjuPCvI/Liy0PFfiCHVKASWIiMWG8u8WfJUw1/4RFZu4l2LVVuJOujr6n
1k5vzIozuo6Ym8mKnnHQmYf5K9T/YuRW3EFa9Ar6/krjw6K/I97P+Wh/DVZiaGC5
n90ZcRj+abb+zOfz0AHTOp7zlr3w4si7AF3tZ9WhW2R0BC3wwmXygli0I6iMXE7E
tvXM5UwxLJoJen2fWqn75/91BifEqPWckPb1h14i73hAPVSte1wvstf8mER/DFSX
Is/GxAhRsZChHn2lEJsvPlrfyMxYwcXTTvd//sp+iOZjfky5vhRuMDUYsHx6/znT
q/rpT3CMnAVlMTf8n/0dY4mdcaQj0cRJfVnUlvZnhw0tJzCP3rH3smlpWloexds=
-----END CERTIFICATE-----

View File

@ -1,12 +1,12 @@
[client]
remote_addr = "localhost:2333"
remote_addr = "127.0.0.1:2333"
default_token = "123"
[client.transport]
type = "tls"
[client.transport.tls]
trusted_root = "examples/tls/ca-cert.pem"
hostname = "0.0.0.0"
trusted_root = "examples/tls/rootCA.crt"
hostname = "localhost"
[client.services.foo1]
local_addr = "127.0.0.1:80"

View File

@ -0,0 +1,63 @@
#!/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

Binary file not shown.

20
examples/tls/rootCA.crt Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDTzCCAjegAwIBAgIUHPYndZflmbDV/30C+BHQSiNvUTQwDQYJKoZIhvcNAQEL
BQAwNzEQMA4GA1UEAwwHTXlPd25DQTELMAkGA1UEBhMCVVMxFjAUBgNVBAcMDVNh
biBGcmFuc2lzY28wHhcNMjQwMjE1MDUwNDQ5WhcNMjUwMjA1MDUwNDQ5WjA3MRAw
DgYDVQQDDAdNeU93bkNBMQswCQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5z
aXNjbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKIx0LdgvDrXGoGw
XJ9s3Y+nr34NMPPLTbo/C2Yj1pD4mxZKK7d1VuwuBNM1h/WQLhA9+x4ZcKYZ1S1g
3BRMuAdm/ZJyeeI1QDRqUlZD16ehPnY0Zy9sZX7oMKVS0m7l8zDv4nvDp9prC5yf
8eoI7zoAWiMv/xPacYXFTAJbUb0VgovFyf3rzgIzs/NBF675FxrQtbhM2j4DdMkJ
9UwRi+qmqtH/Z/Ddy4oMkPflEgKSgDEidmqa552CRExO3c+1ZbMEzq8iOUZ3Vb+g
enfo0SwQUxQ9PEUOAd13siEXs51jZ7JqNmj1d/lEIbAuX8znWDqLYz9FUN4QNsim
8Q/trBcCAwEAAaNTMFEwHQYDVR0OBBYEFP7eOqvUgs8/LOMonEZ6ubRaLkQMMB8G
A1UdIwQYMBaAFP7eOqvUgs8/LOMonEZ6ubRaLkQMMA8GA1UdEwEB/wQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggEBABfLdbsbchr8Ep4mCv75ojWe11Mdd3Eg8EOePukC
w918zqU6dZMmbnLtoXFk6QgFZnvD5MpmU4/d/BmvL9+CJJ9mJPwR2Vb/rIOPXV13
+kjHo/NwNbw5TdmPMbneyCjMdxRqmYKGoWYwbsI09YCK5Cb0J2fYmMrcACSVIUvz
WC7CPPwTA3zvzf9xab+naoE1dbThRDGvVPXEFFOSMIXC0UzCvG0Lj3NTyXyu4XJ0
TUcQUlnptLSejb+uh/5MSqwnEoc1dm2mW/oij1Gqg29+6WNw6wPv/cnC7VvlY4Eu
CR9tvTjMNb7G6VRok9W0HJec6dNf3FJJ1pVzVL8bKI19G54=
-----END CERTIFICATE-----

View File

@ -1,15 +1,16 @@
use crate::config::{ClientConfig, ClientServiceConfig, Config, ServiceType, TransportType};
use crate::config_watcher::ServiceChange;
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::{SocketOpts, TcpTransport, Transport};
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 backoff::{backoff::Backoff, future::retry_notify};
use bytes::{Bytes, BytesMut};
use std::collections::HashMap;
use std::net::SocketAddr;
@ -22,8 +23,10 @@ use tracing::{debug, error, info, instrument, trace, warn, Instrument, Span};
#[cfg(feature = "noise")]
use crate::transport::NoiseTransport;
#[cfg(feature = "tls")]
#[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};
@ -31,35 +34,46 @@ use crate::constants::{run_control_chan_backoff, UDP_BUFFER_SIZE, UDP_SENDQ_SIZE
pub async fn run_client(
config: Config,
shutdown_rx: broadcast::Receiver<bool>,
service_rx: mpsc::Receiver<ServiceChange>,
update_rx: mpsc::Receiver<ConfigChange>,
) -> Result<()> {
let config = config.client.ok_or_else(|| anyhow!(
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, service_rx).await
client.run(shutdown_rx, update_rx).await
}
TransportType::Tls => {
#[cfg(feature = "tls")]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
{
let mut client = Client::<TlsTransport>::from(config).await?;
client.run(shutdown_rx, service_rx).await
client.run(shutdown_rx, update_rx).await
}
#[cfg(not(feature = "tls"))]
crate::helper::feature_not_compile("tls")
#[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, service_rx).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")
}
}
}
@ -89,7 +103,7 @@ impl<T: 'static + Transport> Client<T> {
async fn run(
&mut self,
mut shutdown_rx: broadcast::Receiver<bool>,
mut service_rx: mpsc::Receiver<ServiceChange>,
mut update_rx: mpsc::Receiver<ConfigChange>,
) -> Result<()> {
for (name, config) in &self.config.services {
// Create a control channel for each service defined
@ -114,24 +128,9 @@ impl<T: 'static + Transport> Client<T> {
}
break;
},
e = service_rx.recv() => {
e = update_rx.recv() => {
if let Some(e) = e {
match e {
ServiceChange::ClientAdd(s)=> {
let name = s.name.clone();
let handle = ControlChannelHandle::new(
s,
self.config.remote_addr.clone(),
self.transport.clone(),
self.config.heartbeat_timeout
);
let _ = self.service_handles.insert(name, handle);
},
ServiceChange::ClientDelete(s)=> {
let _ = self.service_handles.remove(&s);
},
_ => ()
}
self.handle_hot_reload(e).await;
}
}
}
@ -144,11 +143,32 @@ impl<T: 'static + Transport> Client<T> {
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: String,
remote_addr: AddrMaybeCached,
connector: Arc<T>,
socket_opts: SocketOpts,
service: ClientServiceConfig,
@ -383,9 +403,12 @@ struct ControlChannelHandle {
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(&self.remote_addr)
.connect(&remote_addr)
.await
.with_context(|| format!("Failed to connect to {}", &self.remote_addr))?;
T::hint(&conn, SocketOpts::for_control_channel());
@ -430,7 +453,6 @@ impl<T: 'static + Transport> ControlChannel<T> {
// Channel ready
info!("Control channel established");
let remote_addr = self.remote_addr.clone();
// Socket options for the data channel
let socket_opts = SocketOpts::from_client_cfg(&self.service);
let data_ch_args = Arc::new(RunDataChannelArgs {
@ -459,8 +481,7 @@ impl<T: 'static + Transport> ControlChannel<T> {
}
},
_ = time::sleep(Duration::from_secs(self.heartbeat_timeout)), if self.heartbeat_timeout != 0 => {
warn!("Heartbeat timed out");
break;
return Err(anyhow!("Heartbeat timed out"))
}
_ = &mut self.shutdown_rx => {
break;
@ -485,6 +506,9 @@ impl ControlChannelHandle {
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,
@ -496,7 +520,6 @@ impl ControlChannelHandle {
tokio::spawn(
async move {
let mut backoff = run_control_chan_backoff();
let mut start = Instant::now();
while let Err(err) = s
@ -510,10 +533,10 @@ impl ControlChannelHandle {
if start.elapsed() > Duration::from_secs(3) {
// The client runs for at least 3 secs and then disconnects
// Retry immediately
backoff.reset();
error!("{:#}. Retry...", err);
} else if let Some(duration) = backoff.next_backoff() {
retry_backoff.reset();
}
if let Some(duration) = retry_backoff.next_backoff() {
error!("{:#}. Retry in {:?}...", err, duration);
time::sleep(duration).await;
} else {

View File

@ -13,9 +13,12 @@ use crate::transport::{DEFAULT_KEEPALIVE_INTERVAL, DEFAULT_KEEPALIVE_SECS, DEFAU
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, Clone)]
#[derive(Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
pub struct MaskedString(String);
impl Debug for MaskedString {
@ -37,23 +40,22 @@ impl From<&str> for MaskedString {
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)]
#[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,
}
impl Default for TransportType {
fn default() -> TransportType {
TransportType::Tcp
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, 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 ClientServiceConfig {
#[serde(rename = "type", default = "default_service_type")]
@ -63,6 +65,7 @@ pub struct ClientServiceConfig {
pub local_addr: String,
pub token: Option<MaskedString>,
pub nodelay: Option<bool>,
pub retry_interval: Option<u64>,
}
impl ClientServiceConfig {
@ -74,25 +77,22 @@ impl ClientServiceConfig {
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default)]
pub enum ServiceType {
#[serde(rename = "tcp")]
#[default]
Tcp,
#[serde(rename = "udp")]
Udp,
}
impl Default for ServiceType {
fn default() -> Self {
ServiceType::Tcp
}
}
fn default_service_type() -> ServiceType {
Default::default()
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, 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")]
@ -112,7 +112,7 @@ impl ServerServiceConfig {
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TlsConfig {
pub hostname: Option<String>,
@ -125,7 +125,7 @@ fn default_noise_pattern() -> String {
String::from("Noise_NK_25519_ChaChaPoly_BLAKE2s")
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct NoiseConfig {
#[serde(default = "default_noise_pattern")]
@ -135,6 +135,12 @@ pub struct NoiseConfig {
// 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
}
@ -147,7 +153,7 @@ fn default_keepalive_interval() -> u64 {
DEFAULT_KEEPALIVE_INTERVAL
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct TcpConfig {
#[serde(default = "default_nodelay")]
@ -170,7 +176,7 @@ impl Default for TcpConfig {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct TransportConfig {
#[serde(rename = "type")]
@ -179,13 +185,18 @@ pub struct TransportConfig {
pub tcp: TcpConfig,
pub tls: Option<TlsConfig>,
pub noise: Option<NoiseConfig>,
pub websocket: Option<WebsocketConfig>,
}
fn default_heartbeat_timeout() -> u64 {
DEFAULT_HEARTBEAT_TIMEOUT_SECS
}
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
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,
@ -195,13 +206,15 @@ pub struct ClientConfig {
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, Clone)]
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone)]
#[serde(deny_unknown_fields)]
pub struct ServerConfig {
pub bind_addr: String,
@ -213,7 +226,7 @@ pub struct ServerConfig {
pub heartbeat_interval: u64,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub server: Option<ServerConfig>,
@ -266,6 +279,9 @@ impl Config {
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)?;
@ -296,11 +312,6 @@ impl Config {
.as_ref()
.and(tls_config.pkcs12_password.as_ref())
.ok_or_else(|| anyhow!("Missing `pkcs12` or `pkcs12_password`"))?;
} else {
tls_config
.trusted_root
.as_ref()
.ok_or_else(|| anyhow!("Missing `trusted_root`"))?;
}
Ok(())
}
@ -308,6 +319,7 @@ impl Config {
// The check is done in transport
Ok(())
}
TransportType::Websocket => Ok(()),
}
}

View File

@ -5,6 +5,7 @@ use crate::{
use anyhow::{Context, Result};
use std::{
collections::HashMap,
env,
path::{Path, PathBuf},
};
use tokio::sync::{broadcast, mpsc};
@ -13,36 +14,30 @@ use tracing::{error, info, instrument};
#[cfg(feature = "notify")]
use notify::{EventKind, RecursiveMode, Watcher};
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ConfigChange {
General(Box<Config>), // Trigger a full restart
ServiceChange(ServiceChange),
ServerChange(ServerServiceChange),
ClientChange(ClientServiceChange),
}
#[derive(Debug, PartialEq)]
pub enum ServiceChange {
ClientAdd(ClientServiceConfig),
ClientDelete(String),
ServerAdd(ServerServiceConfig),
ServerDelete(String),
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ClientServiceChange {
Add(ClientServiceConfig),
Delete(String),
}
impl From<ClientServiceConfig> for ServiceChange {
fn from(c: ClientServiceConfig) -> Self {
ServiceChange::ClientAdd(c)
}
}
impl From<ServerServiceConfig> for ServiceChange {
fn from(c: ServerServiceConfig) -> Self {
ServiceChange::ServerAdd(c)
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ServerServiceChange {
Add(ServerServiceConfig),
Delete(String),
}
trait InstanceConfig: Clone {
type ServiceConfig: Into<ServiceChange> + PartialEq + Clone;
type ServiceConfig: PartialEq + Eq + Clone;
fn equal_without_service(&self, rhs: &Self) -> bool;
fn to_service_change_delete(s: String) -> ServiceChange;
fn service_delete_change(s: String) -> ConfigChange;
fn service_add_change(cfg: Self::ServiceConfig) -> ConfigChange;
fn get_services(&self) -> &HashMap<String, Self::ServiceConfig>;
}
@ -61,8 +56,11 @@ impl InstanceConfig for ServerConfig {
left == right
}
fn to_service_change_delete(s: String) -> ServiceChange {
ServiceChange::ServerDelete(s)
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
@ -84,8 +82,11 @@ impl InstanceConfig for ClientConfig {
left == right
}
fn to_service_change_delete(s: String) -> ServiceChange {
ServiceChange::ClientDelete(s)
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
@ -139,6 +140,11 @@ async fn config_watcher(
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 =
@ -174,8 +180,9 @@ async fn config_watcher(
}
};
for event in calculate_events(&old, &new) {
event_tx.send(event)?;
let events = calculate_events(&old, &new).into_iter().flatten();
for event in events {
event_tx.send(event)?;
}
old = new;
@ -192,42 +199,40 @@ async fn config_watcher(
Ok(())
}
fn calculate_events(old: &Config, new: &Config) -> Vec<ConfigChange> {
fn calculate_events(old: &Config, new: &Config) -> Option<Vec<ConfigChange>> {
if old == new {
return vec![];
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 {
if old.server.is_some() != new.server.is_some() {
return vec![ConfigChange::General(Box::new(new.clone()))];
} else {
match calculate_instance_config_events(
old.server.as_ref().unwrap(),
new.server.as_ref().unwrap(),
) {
Some(mut v) => ret.append(&mut v),
None => return vec![ConfigChange::General(Box::new(new.clone()))],
}
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 {
if old.client.is_some() != new.client.is_some() {
return vec![ConfigChange::General(Box::new(new.clone()))];
} else {
match calculate_instance_config_events(
old.client.as_ref().unwrap(),
new.client.as_ref().unwrap(),
) {
Some(mut v) => ret.append(&mut v),
None => return vec![ConfigChange::General(Box::new(new.clone()))],
}
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()))]),
}
}
ret
Some(ret)
}
// None indicates a General change needed
@ -242,31 +247,17 @@ fn calculate_instance_config_events<T: InstanceConfig>(
let old = old.get_services();
let new = new.get_services();
let mut v = vec![];
v.append(&mut calculate_service_delete_events::<T>(old, new));
v.append(&mut calculate_service_add_events(old, new));
Some(v.into_iter().map(ConfigChange::ServiceChange).collect())
}
fn calculate_service_delete_events<T: InstanceConfig>(
old: &HashMap<String, T::ServiceConfig>,
new: &HashMap<String, T::ServiceConfig>,
) -> Vec<ServiceChange> {
old.keys()
let deletions = old
.keys()
.filter(|&name| new.get(name).is_none())
.map(|x| T::to_service_change_delete(x.to_owned()))
.collect()
}
.map(|x| T::service_delete_change(x.to_owned()));
fn calculate_service_add_events<T: PartialEq + Clone + Into<ServiceChange>>(
old: &HashMap<String, T>,
new: &HashMap<String, T>,
) -> Vec<ServiceChange> {
new.iter()
let addition = new
.iter()
.filter(|(name, c)| old.get(*name) != Some(*c))
.map(|(_, c)| c.clone().into())
.collect()
.map(|(_, c)| T::service_add_change(c.clone()));
Some(deletions.chain(addition).collect())
}
#[cfg(test)]
@ -372,23 +363,23 @@ mod test {
let mut expected = [
vec![ConfigChange::General(Box::new(tests[0].new.clone()))],
vec![ConfigChange::General(Box::new(tests[1].new.clone()))],
vec![ConfigChange::ServiceChange(ServiceChange::ServerAdd(
vec![ConfigChange::ServerChange(ServerServiceChange::Add(
Default::default(),
))],
vec![ConfigChange::ServiceChange(ServiceChange::ServerDelete(
vec![ConfigChange::ServerChange(ServerServiceChange::Delete(
String::from("foo"),
))],
vec![
ConfigChange::ServiceChange(ServiceChange::ServerDelete(String::from("foo1"))),
ConfigChange::ServiceChange(ServiceChange::ServerAdd(
ConfigChange::ServerChange(ServerServiceChange::Delete(String::from("foo1"))),
ConfigChange::ServerChange(ServerServiceChange::Add(
tests[4].new.server.as_ref().unwrap().services["bar1"].clone(),
)),
ConfigChange::ServiceChange(ServiceChange::ClientDelete(String::from("foo1"))),
ConfigChange::ServiceChange(ServiceChange::ClientDelete(String::from("foo2"))),
ConfigChange::ServiceChange(ServiceChange::ClientAdd(
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::ServiceChange(ServiceChange::ClientAdd(
ConfigChange::ClientChange(ClientServiceChange::Add(
tests[4].new.client.as_ref().unwrap().services["bar2"].clone(),
)),
],
@ -397,16 +388,18 @@ mod test {
assert_eq!(tests.len(), expected.len());
for i in 0..tests.len() {
let mut actual = calculate_events(&tests[i].old, &tests[i].new);
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::ServiceChange(sc) => match sc {
ServiceChange::ClientAdd(c) => "c_add_".to_owned() + &c.name,
ServiceChange::ClientDelete(s) => "c_del_".to_owned() + s,
ServiceChange::ServerAdd(c) => "s_add_".to_owned() + &c.name,
ServiceChange::ServerDelete(s) => "s_del_".to_owned() + s,
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,
},
}
};
@ -416,5 +409,20 @@ mod test {
assert_eq!(actual, expected[i]);
}
// No changes
assert_eq!(
calculate_events(
&Config {
server: Default::default(),
client: None,
},
&Config {
server: Default::default(),
client: None,
},
),
None
);
}
}

View File

@ -15,11 +15,12 @@ pub fn listen_backoff() -> ExponentialBackoff {
}
}
pub fn run_control_chan_backoff() -> ExponentialBackoff {
pub fn run_control_chan_backoff(interval: u64) -> ExponentialBackoff {
ExponentialBackoff {
randomization_factor: 0.1,
randomization_factor: 0.2,
max_elapsed_time: None,
max_interval: Duration::from_secs(1),
multiplier: 3.0,
max_interval: Duration::from_secs(interval),
..Default::default()
}
}

View File

@ -1,8 +1,9 @@
use anyhow::{anyhow, Result};
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,
@ -10,6 +11,8 @@ use tokio::{
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
@ -40,7 +43,15 @@ pub fn feature_not_compile(feature: &str) -> ! {
)
}
async fn to_socket_addr<A: ToSocketAddrs>(addr: A) -> Result<SocketAddr> {
#[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()
@ -68,8 +79,12 @@ pub async fn udp_connect<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket> {
/// 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: &str, proxy: Option<&Url>) -> Result<TcpStream> {
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"),
@ -108,7 +123,10 @@ pub async fn tcp_connect_with_proxy(addr: &str, proxy: Option<&Url>) -> Result<T
}
Ok(s)
} else {
Ok(TcpStream::connect(addr).await?)
Ok(match addr.socket_addr {
Some(s) => TcpStream::connect(s).await?,
None => TcpStream::connect(&addr.addr).await?,
})
}
}
@ -135,3 +153,14 @@ where
}
}
}
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(())
}

View File

@ -10,7 +10,6 @@ mod transport;
pub use cli::Cli;
use cli::KeypairType;
pub use config::Config;
use config_watcher::ServiceChange;
pub use constants::UDP_BUFFER_SIZE;
use anyhow::Result;
@ -76,7 +75,7 @@ pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> Result<()
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<ServiceChange>)> = None;
let mut last_instance: Option<(tokio::task::JoinHandle<_>, mpsc::Sender<ConfigChange>)> = None;
while let Some(e) = cfg_watcher.event_rx.recv().await {
match e {
@ -84,7 +83,7 @@ pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> Result<()
if let Some((i, _)) = last_instance {
info!("General configuration change detected. Restarting...");
shutdown_tx.send(true)?;
i.await?;
i.await??;
}
debug!("{:?}", config);
@ -101,10 +100,10 @@ pub async fn run(args: Cli, shutdown_rx: broadcast::Receiver<bool>) -> Result<()
service_update_tx,
));
}
ConfigChange::ServiceChange(service_event) => {
info!("Service change detcted. {:?}", service_event);
ev => {
info!("Service change detected. {:?}", ev);
if let Some((_, service_update_tx)) = &last_instance {
let _ = service_update_tx.send(service_event).await;
let _ = service_update_tx.send(ev).await;
}
}
}
@ -119,9 +118,9 @@ async fn run_instance(
config: Config,
args: Cli,
shutdown_rx: broadcast::Receiver<bool>,
service_update: mpsc::Receiver<ServiceChange>,
) {
let ret: Result<()> = match determine_run_mode(&config, &args) {
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"))]
@ -135,8 +134,7 @@ async fn run_instance(
#[cfg(feature = "server")]
run_server(config, shutdown_rx, service_update).await
}
};
ret.unwrap();
}
}
#[derive(PartialEq, Eq, Debug)]

View File

@ -112,8 +112,7 @@ impl UdpTraffic {
}
pub async fn read<T: AsyncRead + Unpin>(reader: &mut T, hdr_len: u8) -> Result<UdpTraffic> {
let mut buf = Vec::new();
buf.resize(hdr_len as usize, 0);
let mut buf = vec![0; hdr_len as usize];
reader
.read_exact(&mut buf)
.await

View File

@ -1,7 +1,7 @@
use crate::config::{Config, ServerConfig, ServerServiceConfig, ServiceType, TransportType};
use crate::config_watcher::ServiceChange;
use crate::config_watcher::{ConfigChange, ServerServiceChange};
use crate::constants::{listen_backoff, UDP_BUFFER_SIZE};
use crate::helper::retry_notify_with_deadline;
use crate::helper::{retry_notify_with_deadline, write_and_flush};
use crate::multi_map::MultiMap;
use crate::protocol::Hello::{ControlChannelHello, DataChannelHello};
use crate::protocol::{
@ -25,8 +25,10 @@ use tracing::{debug, error, info, info_span, instrument, warn, Instrument, Span}
#[cfg(feature = "noise")]
use crate::transport::NoiseTransport;
#[cfg(feature = "tls")]
#[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`
@ -40,7 +42,7 @@ const HANDSHAKE_TIMEOUT: u64 = 5; // Timeout for transport handshake
pub async fn run_server(
config: Config,
shutdown_rx: broadcast::Receiver<bool>,
service_rx: mpsc::Receiver<ServiceChange>,
update_rx: mpsc::Receiver<ConfigChange>,
) -> Result<()> {
let config = match config.server {
Some(config) => config,
@ -52,26 +54,35 @@ pub async fn run_server(
match config.transport.transport_type {
TransportType::Tcp => {
let mut server = Server::<TcpTransport>::from(config).await?;
server.run(shutdown_rx, service_rx).await?;
server.run(shutdown_rx, update_rx).await?;
}
TransportType::Tls => {
#[cfg(feature = "tls")]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
{
let mut server = Server::<TlsTransport>::from(config).await?;
server.run(shutdown_rx, service_rx).await?;
server.run(shutdown_rx, update_rx).await?;
}
#[cfg(not(feature = "tls"))]
crate::helper::feature_not_compile("tls")
#[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, service_rx).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(())
@ -124,7 +135,7 @@ impl<T: 'static + Transport> Server<T> {
pub async fn run(
&mut self,
mut shutdown_rx: broadcast::Receiver<bool>,
mut service_rx: mpsc::Receiver<ServiceChange>,
mut update_rx: mpsc::Receiver<ConfigChange>,
) -> Result<()> {
// Listen at `server.bind_addr`
let l = self
@ -198,7 +209,7 @@ impl<T: 'static + Transport> Server<T> {
info!("Shuting down gracefully...");
break;
},
e = service_rx.recv() => {
e = update_rx.recv() => {
if let Some(e) = e {
self.handle_hot_reload(e).await;
}
@ -211,24 +222,26 @@ impl<T: 'static + Transport> Server<T> {
Ok(())
}
async fn handle_hot_reload(&mut self, e: ServiceChange) {
async fn handle_hot_reload(&mut self, e: ConfigChange) {
match e {
ServiceChange::ServerAdd(s) => {
let hash = protocol::digest(s.name.as_bytes());
let mut wg = self.services.write().await;
let _ = wg.insert(hash, s);
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);
}
ServiceChange::ServerDelete(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);
}
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);
}
_ => (),
let mut wg = self.control_channels.write().await;
let _ = wg.remove1(&hash);
}
},
ignored => warn!("Ignored {:?} since running as a server", ignored),
}
}
}
@ -290,7 +303,7 @@ async fn do_control_channel_handshake<T: 'static + Transport>(
None => {
conn.write_all(&bincode::serialize(&Ack::ServiceNotExist).unwrap())
.await?;
bail!("No such a service {}", hex::encode(&service_digest));
bail!("No such a service {}", hex::encode(service_digest));
}
}
.to_owned();
@ -485,14 +498,9 @@ struct ControlChannel<T: Transport> {
impl<T: Transport> ControlChannel<T> {
async fn write_and_flush(&mut self, data: &[u8]) -> Result<()> {
self.conn
.write_all(data)
write_and_flush(&mut self.conn, data)
.await
.with_context(|| "Failed to write control cmds")?;
self.conn
.flush()
.await
.with_context(|| "Failed to flush control cmds")?;
Ok(())
}
// Run a control channel
@ -627,7 +635,7 @@ async fn run_tcp_connection_pool<T: Transport>(
'pool: while let Some(mut visitor) = visitor_rx.recv().await {
loop {
if let Some(mut ch) = data_ch_rx.recv().await {
if ch.write_all(&cmd).await.is_ok() {
if write_and_flush(&mut ch, &cmd).await.is_ok() {
tokio::spawn(async move {
let _ = copy_bidirectional(&mut ch, &mut visitor).await;
});
@ -677,7 +685,7 @@ async fn run_udp_connection_pool<T: Transport>(
.recv()
.await
.ok_or_else(|| anyhow!("No available data channels"))?;
conn.write_all(&cmd).await?;
write_and_flush(&mut conn, &cmd).await?;
let mut buf = [0u8; UDP_BUFFER_SIZE];
loop {

View File

@ -1,19 +1,53 @@
use crate::config::{ClientServiceConfig, ServerServiceConfig, TcpConfig, TransportConfig};
use crate::helper::try_set_tcp_keepalive;
use crate::helper::{to_socket_addr, try_set_tcp_keepalive};
use anyhow::{Context, Result};
use async_trait::async_trait;
use std::fmt::Debug;
use std::fmt::{Debug, Display};
use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::{TcpStream, ToSocketAddrs};
use tracing::{error, trace};
pub const DEFAULT_NODELAY: bool = false;
pub const DEFAULT_NODELAY: bool = true;
pub const DEFAULT_KEEPALIVE_SECS: u64 = 20;
pub const DEFAULT_KEEPALIVE_INTERVAL: u64 = 8;
#[derive(Clone)]
pub struct AddrMaybeCached {
pub addr: String,
pub socket_addr: Option<SocketAddr>,
}
impl AddrMaybeCached {
pub fn new(addr: &str) -> AddrMaybeCached {
AddrMaybeCached {
addr: addr.to_string(),
socket_addr: None,
}
}
pub async fn resolve(&mut self) -> Result<()> {
match to_socket_addr(&self.addr).await {
Ok(s) => {
self.socket_addr = Some(s);
Ok(())
}
Err(e) => Err(e),
}
}
}
impl Display for AddrMaybeCached {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.socket_addr {
Some(s) => f.write_fmt(format_args!("{}", s)),
None => f.write_str(&self.addr),
}
}
}
/// Specify a transport layer, like TCP, TLS
#[async_trait]
pub trait Transport: Debug + Send + Sync {
@ -30,21 +64,37 @@ pub trait Transport: Debug + Send + Sync {
/// accept must be cancel safe
async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)>;
async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream>;
async fn connect(&self, addr: &str) -> Result<Self::Stream>;
async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream>;
}
mod tcp;
pub use tcp::TcpTransport;
#[cfg(feature = "tls")]
mod tls;
#[cfg(feature = "tls")]
pub use tls::TlsTransport;
#[cfg(all(feature = "native-tls", feature = "rustls"))]
compile_error!("Only one of `native-tls` and `rustls` can be enabled");
#[cfg(feature = "native-tls")]
mod native_tls;
#[cfg(feature = "native-tls")]
use native_tls as tls;
#[cfg(feature = "rustls")]
mod rustls;
#[cfg(feature = "rustls")]
use rustls as tls;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub(crate) use tls::TlsTransport;
#[cfg(feature = "noise")]
mod noise;
#[cfg(feature = "noise")]
pub use noise::NoiseTransport;
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
mod websocket;
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
pub use websocket::WebsocketTransport;
#[derive(Debug, Clone, Copy)]
struct Keepalive {
// tcp_keepalive_time if the underlying protocol is TCP

View File

@ -1,14 +1,14 @@
use std::net::SocketAddr;
use super::{SocketOpts, TcpTransport, Transport};
use crate::config::{TlsConfig, TransportConfig};
use crate::helper::host_port_pair;
use crate::transport::{AddrMaybeCached, SocketOpts, TcpTransport, Transport};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use std::fs;
use std::net::SocketAddr;
use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
use tokio_native_tls::native_tls::{self, Certificate, Identity};
use tokio_native_tls::{TlsAcceptor, TlsConnector, TlsStream};
pub(crate) use tokio_native_tls::TlsStream;
use tokio_native_tls::{TlsAcceptor, TlsConnector};
#[derive(Debug)]
pub struct TlsTransport {
@ -26,7 +26,10 @@ impl Transport for TlsTransport {
fn new(config: &TransportConfig) -> Result<Self> {
let tcp = TcpTransport::new(config)?;
let config = config.tls.as_ref().ok_or_else(|| anyhow!("Missing tls config"))?;
let config = config
.tls
.as_ref()
.ok_or_else(|| anyhow!("Missing tls config"))?;
let connector = match config.trusted_root.as_ref() {
Some(path) => {
@ -39,7 +42,11 @@ impl Transport for TlsTransport {
.build()?;
Some(TlsConnector::from(connector))
}
None => None,
None => {
// if no trusted_root is specified, allow TlsConnector to use system default
let connector = native_tls::TlsConnector::builder().build()?;
Some(TlsConnector::from(connector))
}
};
let tls_acceptor = match config.pkcs12.as_ref() {
@ -87,7 +94,7 @@ impl Transport for TlsTransport {
Ok(conn)
}
async fn connect(&self, addr: &str) -> Result<Self::Stream> {
async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
let conn = self.tcp.connect(addr).await?;
let connector = self.connector.as_ref().unwrap();
@ -96,9 +103,14 @@ impl Transport for TlsTransport {
self.config
.hostname
.as_deref()
.unwrap_or(host_port_pair(addr)?.0),
.unwrap_or(host_port_pair(&addr.addr)?.0),
conn,
)
.await?)
}
}
#[cfg(feature = "websocket-native-tls")]
pub(crate) fn get_tcpstream(s: &TlsStream<TcpStream>) -> &TcpStream {
s.get_ref().get_ref().get_ref()
}

View File

@ -1,6 +1,6 @@
use std::net::SocketAddr;
use super::{SocketOpts, TcpTransport, Transport};
use super::{AddrMaybeCached, SocketOpts, TcpTransport, Transport};
use crate::config::{NoiseConfig, TransportConfig};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
@ -92,7 +92,7 @@ impl Transport for NoiseTransport {
Ok(conn)
}
async fn connect(&self, addr: &str) -> Result<Self::Stream> {
async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
let conn = self
.tcp
.connect(addr)

156
src/transport/rustls.rs Normal file
View File

@ -0,0 +1,156 @@
use crate::config::{TlsConfig, TransportConfig};
use crate::helper::host_port_pair;
use crate::transport::{AddrMaybeCached, SocketOpts, TcpTransport, Transport};
use std::fmt::Debug;
use std::fs;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer, ServerName};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use p12::PFX;
use tokio_rustls::rustls::{ClientConfig, RootCertStore, ServerConfig};
pub(crate) use tokio_rustls::TlsStream;
use tokio_rustls::{TlsAcceptor, TlsConnector};
pub struct TlsTransport {
tcp: TcpTransport,
config: TlsConfig,
connector: Option<TlsConnector>,
tls_acceptor: Option<TlsAcceptor>,
}
// workaround for TlsConnector and TlsAcceptor not implementing Debug
impl Debug for TlsTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TlsTransport")
.field("tcp", &self.tcp)
.field("config", &self.config)
.finish()
}
}
fn load_server_config(config: &TlsConfig) -> Result<Option<ServerConfig>> {
if let Some(pkcs12_path) = config.pkcs12.as_ref() {
let buf = fs::read(pkcs12_path)?;
let pfx = PFX::parse(buf.as_slice())?;
let pass = config.pkcs12_password.as_ref().unwrap();
let certs = pfx.cert_bags(pass)?;
let keys = pfx.key_bags(pass)?;
let chain: Vec<CertificateDer> = certs.into_iter().map(CertificateDer::from).collect();
let key = PrivatePkcs8KeyDer::from(keys.into_iter().next().unwrap());
Ok(Some(
ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(chain, key.into())?,
))
} else {
Ok(None)
}
}
fn load_client_config(config: &TlsConfig) -> Result<Option<ClientConfig>> {
let cert = if let Some(path) = config.trusted_root.as_ref() {
rustls_pemfile::certs(&mut std::io::BufReader::new(fs::File::open(path).unwrap()))
.map(|cert| cert.unwrap())
.next()
.with_context(|| "Failed to read certificate")?
} else {
// read from native
match rustls_native_certs::load_native_certs() {
Ok(certs) => certs.into_iter().next().unwrap(),
Err(e) => {
eprintln!("Failed to load native certs: {}", e);
return Ok(None);
}
}
};
let mut root_certs = RootCertStore::empty();
root_certs.add(cert).unwrap();
Ok(Some(
ClientConfig::builder()
.with_root_certificates(root_certs)
.with_no_client_auth(),
))
}
#[async_trait]
impl Transport for TlsTransport {
type Acceptor = TcpListener;
type RawStream = TcpStream;
type Stream = TlsStream<TcpStream>;
fn new(config: &TransportConfig) -> Result<Self> {
let tcp = TcpTransport::new(config)?;
let config = config
.tls
.as_ref()
.ok_or_else(|| anyhow!("Missing tls config"))?;
let connector = load_client_config(config)
.unwrap()
.map(|c| Arc::new(c).into());
let tls_acceptor = load_server_config(config)
.unwrap()
.map(|c| Arc::new(c).into());
Ok(TlsTransport {
tcp,
config: config.clone(),
connector,
tls_acceptor,
})
}
fn hint(conn: &Self::Stream, opt: SocketOpts) {
opt.apply(conn.get_ref().0);
}
async fn bind<A: ToSocketAddrs + Send + Sync>(&self, addr: A) -> Result<Self::Acceptor> {
let l = TcpListener::bind(addr)
.await
.with_context(|| "Failed to create tcp listener")?;
Ok(l)
}
async fn accept(&self, a: &Self::Acceptor) -> Result<(Self::RawStream, SocketAddr)> {
self.tcp
.accept(a)
.await
.with_context(|| "Failed to accept TCP connection")
}
async fn handshake(&self, conn: Self::RawStream) -> Result<Self::Stream> {
let conn = self.tls_acceptor.as_ref().unwrap().accept(conn).await?;
Ok(tokio_rustls::TlsStream::Server(conn))
}
async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
let conn = self.tcp.connect(addr).await?;
let connector = self.connector.as_ref().unwrap();
let host_name = self
.config
.hostname
.as_deref()
.unwrap_or(host_port_pair(&addr.addr)?.0);
Ok(tokio_rustls::TlsStream::Client(
connector
.connect(ServerName::try_from(host_name)?.to_owned(), conn)
.await?,
))
}
}
pub(crate) fn get_tcpstream(s: &TlsStream<TcpStream>) -> &TcpStream {
&s.get_ref().0
}

View File

@ -3,7 +3,7 @@ use crate::{
helper::tcp_connect_with_proxy,
};
use super::{SocketOpts, Transport};
use super::{AddrMaybeCached, SocketOpts, Transport};
use anyhow::Result;
use async_trait::async_trait;
use std::net::SocketAddr;
@ -46,7 +46,7 @@ impl Transport for TcpTransport {
Ok(conn)
}
async fn connect(&self, addr: &str) -> Result<Self::Stream> {
async fn connect(&self, addr: &AddrMaybeCached) -> Result<Self::Stream> {
let s = tcp_connect_with_proxy(addr, self.cfg.proxy.as_ref()).await?;
self.socket_opts.apply(&s);
Ok(s)

254
src/transport/websocket.rs Normal file
View File

@ -0,0 +1,254 @@
use core::result::Result;
use std::io::{Error, ErrorKind};
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use super::{AddrMaybeCached, SocketOpts, TcpTransport, TlsTransport, Transport};
use crate::config::TransportConfig;
use anyhow::anyhow;
use async_trait::async_trait;
use bytes::Bytes;
use futures_core::stream::Stream;
use futures_sink::Sink;
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::{TcpListener, TcpStream, ToSocketAddrs};
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use super::tls::get_tcpstream;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use super::tls::TlsStream;
use tokio_tungstenite::tungstenite::protocol::{Message, WebSocketConfig};
use tokio_tungstenite::{accept_async_with_config, client_async_with_config, WebSocketStream};
use tokio_util::io::StreamReader;
use url::Url;
#[derive(Debug)]
enum TransportStream {
Insecure(TcpStream),
Secure(TlsStream<TcpStream>),
}
impl TransportStream {
fn get_tcpstream(&self) -> &TcpStream {
match self {
TransportStream::Insecure(s) => s,
TransportStream::Secure(s) => get_tcpstream(s),
}
}
}
impl AsyncRead for TransportStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
match self.get_mut() {
TransportStream::Insecure(s) => Pin::new(s).poll_read(cx, buf),
TransportStream::Secure(s) => Pin::new(s).poll_read(cx, buf),
}
}
}
impl AsyncWrite for TransportStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
match self.get_mut() {
TransportStream::Insecure(s) => Pin::new(s).poll_write(cx, buf),
TransportStream::Secure(s) => Pin::new(s).poll_write(cx, buf),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
match self.get_mut() {
TransportStream::Insecure(s) => Pin::new(s).poll_flush(cx),
TransportStream::Secure(s) => Pin::new(s).poll_flush(cx),
}
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
match self.get_mut() {
TransportStream::Insecure(s) => Pin::new(s).poll_shutdown(cx),
TransportStream::Secure(s) => Pin::new(s).poll_shutdown(cx),
}
}
}
#[derive(Debug)]
struct StreamWrapper {
inner: WebSocketStream<TransportStream>,
}
impl Stream for StreamWrapper {
type Item = Result<Bytes, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.get_mut().inner).poll_next(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some(Err(err))) => {
Poll::Ready(Some(Err(Error::new(ErrorKind::Other, err))))
}
Poll::Ready(Some(Ok(res))) => {
if let Message::Binary(b) = res {
Poll::Ready(Some(Ok(Bytes::from(b))))
} else {
Poll::Ready(Some(Err(Error::new(
ErrorKind::InvalidData,
"unexpected frame",
))))
}
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
#[derive(Debug)]
pub struct WebsocketTunnel {
inner: StreamReader<StreamWrapper, Bytes>,
}
impl AsyncRead for WebsocketTunnel {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
}
}
impl AsyncBufRead for WebsocketTunnel {
fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<&[u8]>> {
Pin::new(&mut self.get_mut().inner).poll_fill_buf(cx)
}
fn consume(self: Pin<&mut Self>, amt: usize) {
Pin::new(&mut self.get_mut().inner).consume(amt)
}
}
impl AsyncWrite for WebsocketTunnel {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
let sw = self.get_mut().inner.get_mut();
ready!(Pin::new(&mut sw.inner)
.poll_ready(cx)
.map_err(|err| Error::new(ErrorKind::Other, err)))?;
match Pin::new(&mut sw.inner).start_send(Message::Binary(buf.to_vec())) {
Ok(()) => Poll::Ready(Ok(buf.len())),
Err(e) => Poll::Ready(Err(Error::new(ErrorKind::Other, e))),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Pin::new(&mut self.get_mut().inner.get_mut().inner)
.poll_flush(cx)
.map_err(|err| Error::new(ErrorKind::Other, err))
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Pin::new(&mut self.get_mut().inner.get_mut().inner)
.poll_close(cx)
.map_err(|err| Error::new(ErrorKind::Other, err))
}
}
#[derive(Debug)]
enum SubTransport {
Secure(TlsTransport),
Insecure(TcpTransport),
}
#[derive(Debug)]
pub struct WebsocketTransport {
sub: SubTransport,
conf: WebSocketConfig,
}
#[async_trait]
impl Transport for WebsocketTransport {
type Acceptor = TcpListener;
type RawStream = TcpStream;
type Stream = WebsocketTunnel;
fn new(config: &TransportConfig) -> anyhow::Result<Self> {
let wsconfig = config
.websocket
.as_ref()
.ok_or_else(|| anyhow!("Missing websocket config"))?;
let conf = WebSocketConfig {
write_buffer_size: 0,
..WebSocketConfig::default()
};
let sub = match wsconfig.tls {
true => SubTransport::Secure(TlsTransport::new(config)?),
false => SubTransport::Insecure(TcpTransport::new(config)?),
};
Ok(WebsocketTransport { sub, conf })
}
fn hint(conn: &Self::Stream, opt: SocketOpts) {
opt.apply(conn.inner.get_ref().inner.get_ref().get_tcpstream())
}
async fn bind<A: ToSocketAddrs + Send + Sync>(
&self,
addr: A,
) -> anyhow::Result<Self::Acceptor> {
TcpListener::bind(addr).await.map_err(Into::into)
}
async fn accept(&self, a: &Self::Acceptor) -> anyhow::Result<(Self::RawStream, SocketAddr)> {
let (s, addr) = match &self.sub {
SubTransport::Insecure(t) => t.accept(a).await?,
SubTransport::Secure(t) => t.accept(a).await?,
};
Ok((s, addr))
}
async fn handshake(&self, conn: Self::RawStream) -> anyhow::Result<Self::Stream> {
let tsream = match &self.sub {
SubTransport::Insecure(t) => TransportStream::Insecure(t.handshake(conn).await?),
SubTransport::Secure(t) => TransportStream::Secure(t.handshake(conn).await?),
};
let wsstream = accept_async_with_config(tsream, Some(self.conf)).await?;
let tun = WebsocketTunnel {
inner: StreamReader::new(StreamWrapper { inner: wsstream }),
};
Ok(tun)
}
async fn connect(&self, addr: &AddrMaybeCached) -> anyhow::Result<Self::Stream> {
let u = format!("ws://{}", &addr.addr.as_str());
let url = Url::parse(&u).unwrap();
let tstream = match &self.sub {
SubTransport::Insecure(t) => TransportStream::Insecure(t.connect(addr).await?),
SubTransport::Secure(t) => TransportStream::Secure(t.connect(addr).await?),
};
let (wsstream, _) = client_async_with_config(url, tstream, Some(self.conf))
.await
.expect("failed to connect");
let tun = WebsocketTunnel {
inner: StreamReader::new(StreamWrapper { inner: wsstream }),
};
Ok(tun)
}
}

View File

@ -5,8 +5,8 @@ default_token = "default_token_if_not_specify"
[client.transport]
type = "tls"
[client.transport.tls]
trusted_root = "examples/tls/ca-cert.pem"
hostname = "0.0.0.0"
trusted_root = "examples/tls/rootCA.crt"
hostname = "localhost"
[client.services.echo]
local_addr = "127.0.0.1:8080"

View File

@ -0,0 +1,33 @@
[client]
remote_addr = "127.0.0.1:2333"
default_token = "default_token_if_not_specify"
[client.transport]
type = "websocket"
[client.transport.tls]
trusted_root = "examples/tls/rootCA.crt"
hostname = "localhost"
[client.transport.websocket]
tls = true
[client.services.echo]
local_addr = "127.0.0.1:8080"
[client.services.pingpong]
local_addr = "127.0.0.1:8081"
[server]
bind_addr = "0.0.0.0:2333"
default_token = "default_token_if_not_specify"
[server.transport]
type = "websocket"
[server.transport.tls]
pkcs12 = "examples/tls/identity.pfx"
pkcs12_password = "1234"
[server.transport.websocket]
tls = true
[server.services.echo]
bind_addr = "0.0.0.0:2334"
[server.services.pingpong]
bind_addr = "0.0.0.0:2335"

View File

@ -0,0 +1,27 @@
[client]
remote_addr = "127.0.0.1:2333"
default_token = "default_token_if_not_specify"
[client.transport]
type = "websocket"
[client.transport.websocket]
tls = false
[client.services.echo]
local_addr = "127.0.0.1:8080"
[client.services.pingpong]
local_addr = "127.0.0.1:8081"
[server]
bind_addr = "0.0.0.0:2333"
default_token = "default_token_if_not_specify"
[server.transport]
type = "websocket"
[server.transport.websocket]
tls = false
[server.services.echo]
bind_addr = "0.0.0.0:2334"
[server.services.pingpong]
bind_addr = "0.0.0.0:2335"

View File

@ -5,8 +5,8 @@ default_token = "default_token_if_not_specify"
[client.transport]
type = "tls"
[client.transport.tls]
trusted_root = "examples/tls/ca-cert.pem"
hostname = "0.0.0.0"
trusted_root = "examples/tls/rootCA.crt"
hostname = "localhost"
[client.services.echo]
type = "udp"

View File

@ -0,0 +1,37 @@
[client]
remote_addr = "127.0.0.1:2332"
default_token = "default_token_if_not_specify"
[client.transport]
type = "websocket"
[client.transport.tls]
trusted_root = "examples/tls/rootCA.crt"
hostname = "localhost"
[client.transport.websocket]
tls = true
[client.services.echo]
type = "udp"
local_addr = "127.0.0.1:8080"
[client.services.pingpong]
type = "udp"
local_addr = "127.0.0.1:8081"
[server]
bind_addr = "0.0.0.0:2332"
default_token = "default_token_if_not_specify"
[server.transport]
type = "websocket"
[server.transport.tls]
pkcs12 = "examples/tls/identity.pfx"
pkcs12_password = "1234"
[server.transport.websocket]
tls = true
[server.services.echo]
type = "udp"
bind_addr = "0.0.0.0:2334"
[server.services.pingpong]
type = "udp"
bind_addr = "0.0.0.0:2335"

View File

@ -0,0 +1,31 @@
[client]
remote_addr = "127.0.0.1:2332"
default_token = "default_token_if_not_specify"
[client.transport]
type = "websocket"
[client.transport.websocket]
tls = false
[client.services.echo]
type = "udp"
local_addr = "127.0.0.1:8080"
[client.services.pingpong]
type = "udp"
local_addr = "127.0.0.1:8081"
[server]
bind_addr = "0.0.0.0:2332"
default_token = "default_token_if_not_specify"
[server.transport]
type = "websocket"
[server.transport.websocket]
tls = false
[server.services.echo]
type = "udp"
bind_addr = "0.0.0.0:2334"
[server.services.pingpong]
type = "udp"
bind_addr = "0.0.0.0:2335"

View File

@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Ok, Result};
use common::{run_rathole_client, PING, PONG};
use rand::Rng;
use std::time::Duration;
@ -57,9 +57,19 @@ async fn tcp() -> Result<()> {
test("tests/for_tcp/tcp_transport.toml", Type::Tcp).await?;
// FIXME: Self-signed certificate on Mac requires mannual interference. Disable CI for now
#[cfg(not(target_os = "macos"))]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
test("tests/for_tcp/tls_transport.toml", Type::Tcp).await?;
#[cfg(feature = "noise")]
test("tests/for_tcp/noise_transport.toml", Type::Tcp).await?;
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
test("tests/for_tcp/websocket_transport.toml", Type::Tcp).await?;
#[cfg(not(target_os = "macos"))]
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
test("tests/for_tcp/websocket_tls_transport.toml", Type::Tcp).await?;
Ok(())
}
@ -84,14 +94,29 @@ async fn udp() -> Result<()> {
test("tests/for_udp/tcp_transport.toml", Type::Udp).await?;
// See above
#[cfg(not(target_os = "macos"))]
#[cfg(any(feature = "native-tls", feature = "rustls"))]
test("tests/for_udp/tls_transport.toml", Type::Udp).await?;
#[cfg(feature = "noise")]
test("tests/for_udp/noise_transport.toml", Type::Udp).await?;
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
test("tests/for_udp/websocket_transport.toml", Type::Udp).await?;
#[cfg(not(target_os = "macos"))]
#[cfg(any(feature = "websocket-native-tls", feature = "websocket-rustls"))]
test("tests/for_udp/websocket_tls_transport.toml", Type::Udp).await?;
Ok(())
}
#[instrument]
async fn test(config_path: &'static str, t: Type) -> Result<()> {
if cfg!(not(all(feature = "client", feature = "server"))) {
// Skip the test if the client or the server is not enabled
return Ok(());
}
let (client_shutdown_tx, client_shutdown_rx) = broadcast::channel(1);
let (server_shutdown_tx, server_shutdown_rx) = broadcast::channel(1);