mirror of
https://github.com/tun2proxy/tun2proxy.git
synced 2025-04-19 21:39:09 +00:00
Compare commits
57 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7121a80300 | ||
|
9e75475a23 | ||
|
7657f1603f | ||
|
a380817951 | ||
|
a2399c8b28 | ||
|
61bbafcf82 | ||
|
ca7cd25c4e | ||
|
68716bdc9f | ||
|
e556f7657b | ||
|
fd7dca9988 | ||
|
9a018f2393 | ||
|
c5d907551b | ||
|
6b038c2a80 | ||
|
5287bef3c0 | ||
|
04db15f553 | ||
|
f8c902b61c | ||
|
8ba2c1a2b7 | ||
|
e939f5f3dc | ||
|
ecd1ab80bf | ||
|
51de01854b | ||
|
bac54ec56c | ||
|
6034870264 | ||
|
e933e5d4c0 | ||
|
7136e2a20c | ||
|
2a8e31225c | ||
|
ea5ee834db | ||
|
4d4a0ce85c | ||
|
258637a52e | ||
|
a01de17b36 | ||
|
724557b30e | ||
|
7a7293effd | ||
|
46bf4434ef | ||
|
d37cb44b62 | ||
|
987635d3dc | ||
|
ebd3128778 | ||
|
ee4df8f97b | ||
|
7314906841 | ||
|
23d4e59367 | ||
|
28d54be638 | ||
|
8c98d1dc74 | ||
|
1a508918a2 | ||
|
c2382ee29b | ||
|
21355e37da | ||
|
e8143a691b | ||
|
53f60ffda6 | ||
|
9088cf6fe5 | ||
|
d7e3913450 | ||
|
52d814ce79 | ||
|
b4142453fd | ||
|
0aad0d1709 | ||
|
3fb02f0fc7 | ||
|
b9cf06da33 | ||
|
2ade72e79d | ||
|
e3cc5ea1ce | ||
|
b6bb9bedfc | ||
|
f823202b33 | ||
|
9aa2afb0fd |
32 changed files with 1686 additions and 500 deletions
20
.github/workflows/auto-merge.yaml
vendored
Normal file
20
.github/workflows/auto-merge.yaml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
name: Dependabot Auto Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
auto:
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Auto approve pull request, then squash and merge
|
||||
uses: ahmadnassri/action-dependabot-auto-merge@v2
|
||||
with:
|
||||
# target: minor
|
||||
# here `PAT_REPO_ADMIN` is a user's passkey provided by github.
|
||||
github-token: ${{ secrets.PAT_REPO_ADMIN }}
|
24
.github/workflows/publish-exe.yml
vendored
24
.github/workflows/publish-exe.yml
vendored
|
@ -15,6 +15,7 @@ jobs:
|
|||
attestations: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- x86_64-unknown-linux-gnu
|
||||
|
@ -33,7 +34,7 @@ jobs:
|
|||
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
host_os: ubuntu-latest
|
||||
host_os: ubuntu-22.04
|
||||
- target: x86_64-unknown-linux-musl
|
||||
host_os: ubuntu-latest
|
||||
- target: i686-unknown-linux-musl
|
||||
|
@ -72,14 +73,15 @@ jobs:
|
|||
rustup target add ${{ matrix.target }}
|
||||
fi
|
||||
cargo install cbindgen
|
||||
if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then
|
||||
if [[ "${{ contains(matrix.host_os, 'ubuntu') }}" == "true" && "${{ matrix.host_os }}" != "ubuntu-22.04" ]]; then
|
||||
sudo .github/workflows/install-cross.sh
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then
|
||||
if [[ "${{ contains(matrix.host_os, 'ubuntu') }}" == "true" && "${{ matrix.host_os }}" != "ubuntu-22.04" ]]; then
|
||||
cross build --all-features --release --target ${{ matrix.target }}
|
||||
else
|
||||
if [[ "${{ matrix.target }}" == "x86_64-win7-windows-msvc" || "${{ matrix.target }}" == "i686-win7-windows-msvc" ]]; then
|
||||
|
@ -90,21 +92,21 @@ jobs:
|
|||
cargo build --all-features --release --target ${{ matrix.target }}
|
||||
fi
|
||||
fi
|
||||
cbindgen --config cbindgen.toml -l C --cpp-compat -o target/tun2proxy-ffi.h
|
||||
cbindgen --config cbindgen.toml -o target/tun2proxy.h
|
||||
if [[ "${{ matrix.host_os }}" == "windows-latest" ]]; then
|
||||
powershell -Command "(Get-Item README.md).LastWriteTime = Get-Date"
|
||||
powershell -Command "(Get-Item target/${{ matrix.target }}/release/wintun.dll).LastWriteTime = Get-Date"
|
||||
powershell Compress-Archive -Path target/${{ matrix.target }}/release/tun2proxy-bin.exe, README.md, target/tun2proxy-ffi.h, target/${{ matrix.target }}/release/tun2proxy.dll, target/${{ matrix.target }}/release/wintun.dll -DestinationPath mypubdir4/tun2proxy-${{ matrix.target }}.zip
|
||||
powershell Compress-Archive -Path target/${{ matrix.target }}/release/tun2proxy-bin.exe, target/${{ matrix.target }}/release/udpgw-server.exe, README.md, target/tun2proxy.h, target/${{ matrix.target }}/release/tun2proxy.dll, target/${{ matrix.target }}/release/wintun.dll -DestinationPath mypubdir4/tun2proxy-${{ matrix.target }}.zip
|
||||
elif [[ "${{ matrix.host_os }}" == "macos-latest" ]]; then
|
||||
zip -j mypubdir4/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy-bin README.md target/tun2proxy-ffi.h target/${{ matrix.target }}/release/libtun2proxy.dylib
|
||||
zip -j mypubdir4/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy-bin target/${{ matrix.target }}/release/udpgw-server README.md target/tun2proxy.h target/${{ matrix.target }}/release/libtun2proxy.dylib
|
||||
if [[ "${{ matrix.target }}" == "x86_64-apple-darwin" ]]; then
|
||||
./build-aarch64-apple-ios.sh
|
||||
zip -r mypubdir4/tun2proxy-aarch64-apple-ios-xcframework.zip ./tun2proxy.xcframework/
|
||||
./build-apple.sh
|
||||
zip -r mypubdir4/tun2proxy-apple-xcframework.zip ./tun2proxy.xcframework/
|
||||
fi
|
||||
elif [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then
|
||||
zip -j mypubdir4/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy-bin README.md target/tun2proxy-ffi.h target/${{ matrix.target }}/release/libtun2proxy.so
|
||||
elif [[ "${{ contains(matrix.host_os, 'ubuntu') }}" == "true" ]]; then
|
||||
zip -j mypubdir4/tun2proxy-${{ matrix.target }}.zip target/${{ matrix.target }}/release/tun2proxy-bin target/${{ matrix.target }}/release/udpgw-server README.md target/tun2proxy.h target/${{ matrix.target }}/release/libtun2proxy.so
|
||||
if [[ "${{ matrix.target }}" == "x86_64-unknown-linux-gnu" ]]; then
|
||||
./build-android.sh
|
||||
cp ./tun2proxy-android-libs.zip ./mypubdir4/
|
||||
|
@ -112,20 +114,26 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bin-${{ matrix.target }}
|
||||
path: mypubdir4/*
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: mypubdir4/*
|
||||
|
||||
- name: Publish
|
||||
if: ${{ !cancelled() }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: mypubdir4/*
|
||||
|
||||
- name: Abort on error
|
||||
if: ${{ failure() }}
|
||||
run: echo "Some of jobs failed" && false
|
||||
|
|
47
.github/workflows/rust.yml
vendored
47
.github/workflows/rust.yml
vendored
|
@ -47,6 +47,53 @@ jobs:
|
|||
if: ${{ failure() }}
|
||||
run: echo "Some of jobs failed" && false
|
||||
|
||||
build_n_test_android:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install cargo ndk and rust compiler for android target
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo install --locked cargo-ndk
|
||||
rustup target add x86_64-linux-android
|
||||
- name: clippy
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo ndk -t x86_64 clippy --all-features -- -D warnings
|
||||
- name: Build
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo ndk -t x86_64 rustc --verbose --all-features --lib --crate-type=cdylib
|
||||
- name: Abort on error
|
||||
if: ${{ failure() }}
|
||||
run: echo "Android build job failed" && false
|
||||
|
||||
build_n_test_ios:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo lipo and rust compiler for ios target
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo install --locked cargo-lipo
|
||||
rustup target add x86_64-apple-ios aarch64-apple-ios
|
||||
- name: clippy
|
||||
if: ${{ !cancelled() }}
|
||||
run: cargo clippy --target x86_64-apple-ios --all-features -- -D warnings
|
||||
- name: Build
|
||||
if: ${{ !cancelled() }}
|
||||
run: |
|
||||
cargo lipo --verbose --all-features
|
||||
- name: Abort on error
|
||||
if: ${{ failure() }}
|
||||
run: echo "iOS build job failed" && false
|
||||
|
||||
semver:
|
||||
name: Check semver
|
||||
strategy:
|
||||
|
|
69
Cargo.toml
69
Cargo.toml
|
@ -1,69 +1,82 @@
|
|||
[package]
|
||||
name = "tun2proxy"
|
||||
version = "0.5.3"
|
||||
edition = "2021"
|
||||
version = "0.7.8"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/tun2proxy/tun2proxy"
|
||||
homepage = "https://github.com/tun2proxy/tun2proxy"
|
||||
authors = ["B. Blechschmidt", "ssrlive"]
|
||||
description = "Tunnel interface to proxy"
|
||||
readme = "README.md"
|
||||
rust-version = "1.80"
|
||||
rust-version = "1.85"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "cdylib", "lib"]
|
||||
|
||||
[[bin]]
|
||||
name = "tun2proxy-bin"
|
||||
path = "src/bin/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "udpgw-server"
|
||||
path = "src/bin/udpgw_server.rs"
|
||||
required-features = ["udpgw"]
|
||||
|
||||
[features]
|
||||
default = ["udpgw"]
|
||||
udpgw = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
base64 = { version = "0.22" }
|
||||
base64easy = "0.1"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4", features = ["derive", "wrap_help", "color"] }
|
||||
ctrlc2 = { version = "3", features = ["tokio", "termination"] }
|
||||
digest_auth = "0.3"
|
||||
dotenvy = "0.15"
|
||||
env_logger = "0.11"
|
||||
hashlink = "0.9"
|
||||
hickory-proto = "0.24"
|
||||
hashlink = "0.10"
|
||||
hickory-proto = "0.25"
|
||||
httparse = "1"
|
||||
ipstack = { version = "0.1" }
|
||||
ipstack = { version = "0.3", git = "https://github.com/ssrlive/ipstack.git", rev = "53c648e" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
mimalloc = { version = "0.1", default-features = false, optional = true }
|
||||
percent-encoding = "2"
|
||||
socks5-impl = { version = "0.5" }
|
||||
thiserror = "1"
|
||||
shlex = "1.3.0"
|
||||
socks5-impl = { version = "0.6", default-features = false, features = [
|
||||
"tokio",
|
||||
] }
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = "0.7"
|
||||
tproxy-config = { version = "6", default-features = false }
|
||||
tun2 = { version = "3", features = ["async"] }
|
||||
tun = { version = "0.7", features = ["async"] }
|
||||
udp-stream = { version = "0.0.12", default-features = false }
|
||||
unicase = "2"
|
||||
url = "2"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = "0.4"
|
||||
serde_json = "1"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
bincode = "1"
|
||||
bincode = "2"
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
android_logger = "0.15"
|
||||
jni = { version = "0.21", default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
daemonize = "0.5"
|
||||
nix = { version = "0.29", default-features = false, features = [
|
||||
"fs",
|
||||
"socket",
|
||||
"uio",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_os="android")'.dependencies]
|
||||
android_logger = "0.14"
|
||||
jni = { version = "0.21", default-features = false }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
daemonize = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-service = "0.7"
|
||||
windows-service = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1"
|
||||
|
||||
[[bin]]
|
||||
name = "tun2proxy-bin"
|
||||
path = "src/bin/main.rs"
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
# [profile.release]
|
||||
# strip = "symbols"
|
||||
|
|
62
README.md
62
README.md
|
@ -1,8 +1,10 @@
|
|||
[](https://github.com/tun2proxy/tun2proxy)
|
||||
|
||||
# tun2proxy
|
||||
A tunnel interface for HTTP and SOCKS proxies on Linux, Android, macOS, iOS and Windows.
|
||||
|
||||
[](https://crates.io/crates/tun2proxy)
|
||||

|
||||
[](https://docs.rs/tun2proxy)
|
||||
[](https://docs.rs/tun2proxy)
|
||||
[](https://crates.io/crates/tun2proxy)
|
||||
[](https://github.com/tun2proxy/tun2proxy/blob/master/LICENSE)
|
||||
|
@ -18,6 +20,7 @@ A tunnel interface for HTTP and SOCKS proxies on Linux, Android, macOS, iOS and
|
|||
- GFW evasion mechanism for certain use cases (see [issue #35](https://github.com/tun2proxy/tun2proxy/issues/35))
|
||||
- SOCKS5 UDP support
|
||||
- Native support for proxying DNS over TCP
|
||||
- UdpGW (UDP gateway) support for UDP over TCP, see the [wiki](https://github.com/tun2proxy/tun2proxy/wiki/UDP-gateway-feature) for more information
|
||||
|
||||
## Build
|
||||
Clone the repository and `cd` into the project folder. Then run the following:
|
||||
|
@ -129,28 +132,39 @@ Tunnel interface to proxy.
|
|||
Usage: tun2proxy-bin [OPTIONS] --proxy <URL> [ADMIN_COMMAND]...
|
||||
|
||||
Arguments:
|
||||
[ADMIN_COMMAND]... Specify a command to run with root-like capabilities in the new namespace when using `--unshare`.
|
||||
This could be useful to start additional daemons, e.g. `openvpn` instance
|
||||
[ADMIN_COMMAND]... Specify a command to run with root-like capabilities in the new namespace when using `--unshare`. This could be
|
||||
useful to start additional daemons, e.g. `openvpn` instance
|
||||
|
||||
Options:
|
||||
-p, --proxy <URL> Proxy URL in the form proto://[username[:password]@]host:port, where proto is one of
|
||||
socks4, socks5, http. For example: socks5://myname:password@127.0.0.1:1080
|
||||
socks4, socks5, http. Username and password are encoded in percent encoding. For example:
|
||||
socks5://myname:pass%40word@127.0.0.1:1080
|
||||
-t, --tun <name> Name of the tun interface, such as tun0, utun4, etc. If this option is not provided, the
|
||||
OS will generate a random one
|
||||
--tun-fd <fd> File descriptor of the tun interface
|
||||
--close-fd-on-drop <true or false> Set whether to close the received raw file descriptor on drop or not. This setting is
|
||||
dependent on [tun_fd] [possible values: true, false]
|
||||
--unshare Create a tun interface in a newly created unprivileged namespace while maintaining proxy
|
||||
connectivity via the global network namespace
|
||||
--unshare-pidfile <UNSHARE_PIDFILE> Create a pidfile of `unshare` process when using `--unshare`
|
||||
-6, --ipv6-enabled IPv6 enabled
|
||||
-s, --setup Routing and system setup, which decides whether to setup the routing and system
|
||||
configuration. This option is only available on Linux and requires root-like privileges.
|
||||
See `capabilities(7)`
|
||||
-d, --dns <strategy> DNS handling strategy [default: direct] [possible values: virtual, over-tcp, direct]
|
||||
--dns-addr <IP> DNS resolver address [default: 8.8.8.8]
|
||||
--virtual-dns-pool <CIDR> IP address pool to be used by virtual DNS in CIDR notation [default: 198.18.0.0/15]
|
||||
-b, --bypass <IP/CIDR> IPs used in routing setup which should bypass the tunnel, in the form of IP or IP/CIDR.
|
||||
Multiple IPs can be specified, e.g. --bypass 3.4.5.0/24 --bypass 5.6.7.8
|
||||
--tcp-timeout <seconds> TCP timeout in seconds [default: 600]
|
||||
--udp-timeout <seconds> UDP timeout in seconds [default: 10]
|
||||
-v, --verbosity <level> Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace]
|
||||
--daemonize Daemonize for unix family or run as Windows service
|
||||
--exit-on-fatal-error Exit immediately when fatal error occurs, useful for running as a service
|
||||
--max-sessions <number> Maximum number of sessions to be handled concurrently [default: 200]
|
||||
--udpgw-server <IP:PORT> UDP gateway server address, forwards UDP packets via specified TCP server
|
||||
--udpgw-connections <number> Max connections for the UDP gateway, default value is 5
|
||||
--udpgw-keepalive <seconds> Keepalive interval in seconds for the UDP gateway, default value is 30
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
@ -158,7 +172,8 @@ Currently, tun2proxy supports HTTP, SOCKS4/SOCKS4a and SOCKS5. A proxy is suppli
|
|||
URL format. For example, an HTTP proxy at `1.2.3.4:3128` with a username of `john.doe` and a password of `secret` is
|
||||
supplied as `--proxy http://john.doe:secret@1.2.3.4:3128`. This works analogously to curl's `--proxy` argument.
|
||||
|
||||
## Docker Support
|
||||
## Container Support
|
||||
### Docker
|
||||
Tun2proxy can serve as a proxy for other Docker containers. To make use of that feature, first build the image:
|
||||
|
||||
```bash
|
||||
|
@ -183,6 +198,36 @@ docker run -it \
|
|||
--network "container:tun2proxy" \
|
||||
ubuntu:latest
|
||||
```
|
||||
### Docker Compose
|
||||
|
||||
Write a `docker-compose.yaml` file with the following content:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
tun2proxy:
|
||||
volumes:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.default.disable_ipv6=0
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
container_name: tun2proxy
|
||||
image: ghcr.io/tun2proxy/tun2proxy:latest
|
||||
command: --proxy proto://[username[:password]@]host:port
|
||||
alpine:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
network_mode: container:tun2proxy
|
||||
image: alpine:latest
|
||||
command: apk add curl && curl ifconfig.icu && sleep 10
|
||||
```
|
||||
|
||||
Then run the compose file
|
||||
|
||||
```bash
|
||||
docker compose up -d tun2proxy
|
||||
docker compose up alpine
|
||||
```
|
||||
|
||||
## Configuration Tips
|
||||
### DNS
|
||||
|
@ -204,3 +249,10 @@ asked to open connections to IPv6 destinations. In such a case, you can disable
|
|||
either through `sysctl -w net.ipv6.conf.all.disable_ipv6=1` and `sysctl -w net.ipv6.conf.default.disable_ipv6=1`
|
||||
or through `ip -6 route del default`, which causes the `libc` resolver (and other software) to not issue DNS AAAA
|
||||
requests for IPv6 addresses.
|
||||
|
||||
## Contributors ✨
|
||||
Thanks goes to these wonderful people:
|
||||
|
||||
<a href="https://github.com/tun2proxy/tun2proxy/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=tun2proxy/tun2proxy" />
|
||||
</a>
|
||||
|
|
|
@ -10,7 +10,7 @@ cargo build --target aarch64-apple-ios --features mimalloc
|
|||
echo "Generating includes..."
|
||||
mkdir -p target/include/
|
||||
rm -rf target/include/*
|
||||
cbindgen --config cbindgen.toml -l C --cpp-compat -o target/include/tun2proxy.h
|
||||
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||
cat > target/include/tun2proxy.modulemap <<EOF
|
||||
framework module tun2proxy {
|
||||
umbrella header "tun2proxy.h"
|
||||
|
|
|
@ -10,7 +10,7 @@ cargo build --release --target aarch64-apple-ios --features mimalloc
|
|||
echo "Generating includes..."
|
||||
mkdir -p target/include/
|
||||
rm -rf target/include/*
|
||||
cbindgen --config cbindgen.toml -l C --cpp-compat -o target/include/tun2proxy.h
|
||||
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||
cat > target/include/tun2proxy.modulemap <<EOF
|
||||
framework module tun2proxy {
|
||||
umbrella header "tun2proxy.h"
|
||||
|
|
|
@ -108,7 +108,7 @@ function build_android() {
|
|||
cp $BASE/target/$target/${mode2}/lib${name}.a $android_libs/${target_dir}/lib${name}.a
|
||||
done
|
||||
|
||||
cbindgen -c $BASE/cbindgen.toml -l C --cpp-compat -o $android_libs/$name.h
|
||||
cbindgen -c $BASE/cbindgen.toml -o $android_libs/$name.h
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
|
|
@ -24,7 +24,7 @@ cargo build --release --target aarch64-apple-ios-sim
|
|||
echo "Generating includes..."
|
||||
mkdir -p target/include/
|
||||
rm -rf target/include/*
|
||||
cbindgen --config cbindgen.toml -l C --cpp-compat -o target/include/tun2proxy.h
|
||||
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||
cat > target/include/tun2proxy.modulemap <<EOF
|
||||
framework module tun2proxy {
|
||||
umbrella header "tun2proxy.h"
|
||||
|
|
16
build.rs
16
build.rs
|
@ -1,4 +1,13 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Ok(git_hash) = get_git_hash() {
|
||||
// Set the environment variables
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim());
|
||||
}
|
||||
|
||||
// Get the build time
|
||||
let build_time = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
println!("cargo:rustc-env=BUILD_TIME={}", build_time);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Ok(cargo_target_dir) = get_cargo_target_dir() {
|
||||
let mut f = std::fs::File::create(cargo_target_dir.join("build.log"))?;
|
||||
|
@ -85,3 +94,10 @@ fn get_crate_dir(crate_name: &str) -> Result<std::path::PathBuf, Box<dyn std::er
|
|||
}
|
||||
Ok(crate_dir.ok_or("crate_dir")?)
|
||||
}
|
||||
|
||||
fn get_git_hash() -> std::io::Result<String> {
|
||||
use std::process::Command;
|
||||
let git_hash = Command::new("git").args(["rev-parse", "--short", "HEAD"]).output()?.stdout;
|
||||
let git_hash = String::from_utf8(git_hash).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
Ok(git_hash)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
language = "C"
|
||||
cpp_compat = true
|
||||
|
||||
[export]
|
||||
include = [
|
||||
"tun2proxy_run_with_cli",
|
||||
"tun2proxy_with_fd_run",
|
||||
"tun2proxy_with_name_run",
|
||||
"tun2proxy_with_name_stop",
|
||||
"tun2proxy_with_fd_stop",
|
||||
"tun2proxy_stop",
|
||||
"tun2proxy_set_log_callback",
|
||||
"tun2proxy_set_traffic_status_callback",
|
||||
]
|
||||
exclude = [
|
||||
"Java_com_github_shadowsocks_bg_Tun2proxy_run",
|
||||
"Java_com_github_shadowsocks_bg_Tun2proxy_stop",
|
||||
"UdpFlag",
|
||||
]
|
||||
|
||||
[export.rename]
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
#![cfg(target_os = "android")]
|
||||
|
||||
use crate::{
|
||||
Args,
|
||||
args::ArgProxy,
|
||||
error::{Error, Result},
|
||||
Args,
|
||||
};
|
||||
use jni::{
|
||||
JNIEnv,
|
||||
objects::{JClass, JString},
|
||||
sys::{jboolean, jchar, jint},
|
||||
JNIEnv,
|
||||
};
|
||||
|
||||
/// # Safety
|
||||
|
@ -21,7 +21,7 @@ use jni::{
|
|||
/// - tun_mtu: the tun mtu
|
||||
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Tun2proxy_run(
|
||||
mut env: JNIEnv,
|
||||
_clazz: JClass,
|
||||
|
@ -52,15 +52,15 @@ pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Tun2proxy_run(
|
|||
.close_fd_on_drop(close_fd_on_drop)
|
||||
.dns(dns)
|
||||
.verbosity(verbosity);
|
||||
crate::mobile_api::mobile_run(args, tun_mtu, false)
|
||||
crate::general_api::general_run_for_api(args, tun_mtu, false)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Shutdown tun2proxy
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Tun2proxy_stop(_env: JNIEnv, _: JClass) -> jint {
|
||||
crate::mobile_api::mobile_stop()
|
||||
crate::general_api::tun2proxy_stop_internal()
|
||||
}
|
||||
|
||||
fn get_java_string(env: &mut JNIEnv, string: &JString) -> Result<String, Error> {
|
||||
|
|
54
src/apple.rs
54
src/apple.rs
|
@ -1,54 +0,0 @@
|
|||
#![cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
|
||||
|
||||
use crate::{
|
||||
args::{ArgDns, ArgProxy},
|
||||
ArgVerbosity, Args,
|
||||
};
|
||||
use std::os::raw::{c_char, c_int, c_ushort};
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
/// Parameters:
|
||||
/// - proxy_url: the proxy url, e.g. "socks5://127.0.0.1:1080"
|
||||
/// - tun_fd: the tun file descriptor, it will be owned by tun2proxy
|
||||
/// - close_fd_on_drop: whether close the tun_fd on drop
|
||||
/// - packet_information: whether exists packet information in tun_fd
|
||||
/// - tun_mtu: the tun mtu
|
||||
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tun2proxy_with_fd_run(
|
||||
proxy_url: *const c_char,
|
||||
tun_fd: c_int,
|
||||
close_fd_on_drop: bool,
|
||||
packet_information: bool,
|
||||
tun_mtu: c_ushort,
|
||||
dns_strategy: ArgDns,
|
||||
verbosity: ArgVerbosity,
|
||||
) -> c_int {
|
||||
log::set_max_level(verbosity.into());
|
||||
if let Err(err) = log::set_boxed_logger(Box::<crate::dump_logger::DumpLogger>::default()) {
|
||||
log::warn!("failed to set logger: {:?}", err);
|
||||
}
|
||||
|
||||
let proxy_url = std::ffi::CStr::from_ptr(proxy_url).to_str().unwrap();
|
||||
let proxy = ArgProxy::try_from(proxy_url).unwrap();
|
||||
|
||||
let mut args = Args::default();
|
||||
args.proxy(proxy)
|
||||
.tun_fd(Some(tun_fd))
|
||||
.close_fd_on_drop(close_fd_on_drop)
|
||||
.dns(dns_strategy)
|
||||
.verbosity(verbosity);
|
||||
|
||||
crate::mobile_api::mobile_run(args, tun_mtu, packet_information)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Shutdown the tun2proxy component.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tun2proxy_with_fd_stop() -> c_int {
|
||||
crate::mobile_api::mobile_stop()
|
||||
}
|
59
src/args.rs
59
src/args.rs
|
@ -9,7 +9,7 @@ use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
|||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
#[command(author, version, about = "Tunnel interface to proxy.", long_about = None)]
|
||||
#[command(author, version = version_info(), about = "Tunnel interface to proxy.", long_about = None)]
|
||||
pub struct Args {
|
||||
/// Proxy URL in the form proto://[username[:password]@]host:port,
|
||||
/// where proto is one of socks4, socks5, http.
|
||||
|
@ -30,10 +30,9 @@ pub struct Args {
|
|||
pub tun_fd: Option<i32>,
|
||||
|
||||
/// Set whether to close the received raw file descriptor on drop or not.
|
||||
/// This setting is passed to the tun2 crate.
|
||||
/// See [tun2::Configuration::close_fd_on_drop].
|
||||
/// This setting is dependent on [tun_fd].
|
||||
#[cfg(unix)]
|
||||
#[arg(long, value_name = "true or false", conflicts_with = "tun")]
|
||||
#[arg(long, value_name = "true or false", conflicts_with = "tun", requires = "tun_fd")]
|
||||
pub close_fd_on_drop: Option<bool>,
|
||||
|
||||
/// Create a tun interface in a newly created unprivileged namespace
|
||||
|
@ -111,6 +110,25 @@ pub struct Args {
|
|||
/// Maximum number of sessions to be handled concurrently
|
||||
#[arg(long, value_name = "number", default_value = "200")]
|
||||
pub max_sessions: usize,
|
||||
|
||||
/// UDP gateway server address, forwards UDP packets via specified TCP server
|
||||
#[cfg(feature = "udpgw")]
|
||||
#[arg(long, value_name = "IP:PORT")]
|
||||
pub udpgw_server: Option<SocketAddr>,
|
||||
|
||||
/// Max connections for the UDP gateway, default value is 5
|
||||
#[cfg(feature = "udpgw")]
|
||||
#[arg(long, value_name = "number", requires = "udpgw_server")]
|
||||
pub udpgw_connections: Option<usize>,
|
||||
|
||||
/// Keepalive interval in seconds for the UDP gateway, default value is 30
|
||||
#[cfg(feature = "udpgw")]
|
||||
#[arg(long, value_name = "seconds", requires = "udpgw_server")]
|
||||
pub udpgw_keepalive: Option<u64>,
|
||||
}
|
||||
|
||||
fn version_info() -> &'static str {
|
||||
concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), " ", env!("BUILD_TIME"), ")")
|
||||
}
|
||||
|
||||
fn validate_tun(p: &str) -> Result<String> {
|
||||
|
@ -154,6 +172,12 @@ impl Default for Args {
|
|||
daemonize: false,
|
||||
exit_on_fatal_error: false,
|
||||
max_sessions: 200,
|
||||
#[cfg(feature = "udpgw")]
|
||||
udpgw_server: None,
|
||||
#[cfg(feature = "udpgw")]
|
||||
udpgw_connections: None,
|
||||
#[cfg(feature = "udpgw")]
|
||||
udpgw_keepalive: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,8 +185,7 @@ impl Default for Args {
|
|||
impl Args {
|
||||
#[allow(clippy::let_and_return)]
|
||||
pub fn parse_args() -> Self {
|
||||
use clap::Parser;
|
||||
let args = Self::parse();
|
||||
let args = <Self as ::clap::Parser>::parse();
|
||||
#[cfg(target_os = "linux")]
|
||||
if !args.setup && args.tun.is_none() {
|
||||
eprintln!("Missing required argument, '--tun' must present when '--setup' is not used.");
|
||||
|
@ -181,6 +204,18 @@ impl Args {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "udpgw")]
|
||||
pub fn udpgw_server(&mut self, udpgw: SocketAddr) -> &mut Self {
|
||||
self.udpgw_server = Some(udpgw);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "udpgw")]
|
||||
pub fn udpgw_connections(&mut self, udpgw_connections: usize) -> &mut Self {
|
||||
self.udpgw_connections = Some(udpgw_connections);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub fn tun_fd(&mut self, tun_fd: Option<i32>) -> &mut Self {
|
||||
self.tun_fd = tun_fd;
|
||||
|
@ -363,17 +398,11 @@ impl TryFrom<&str> for ArgProxy {
|
|||
let e = format!("`{s}` does not contain a host");
|
||||
let host = url.host_str().ok_or(Error::from(e))?;
|
||||
|
||||
let mut url_host = String::from(host);
|
||||
let e = format!("`{s}` does not contain a port");
|
||||
let port = url.port().ok_or(Error::from(&e))?;
|
||||
url_host.push(':');
|
||||
url_host.push_str(port.to_string().as_str());
|
||||
let port = url.port_or_known_default().ok_or(Error::from(&e))?;
|
||||
|
||||
let e = format!("`{host}` could not be resolved");
|
||||
let mut addr_iter = url_host.to_socket_addrs().map_err(|_| Error::from(&e))?;
|
||||
|
||||
let e = format!("`{host}` does not resolve to a usable IP address");
|
||||
let addr = addr_iter.next().ok_or(Error::from(&e))?;
|
||||
let e2 = format!("`{host}` does not resolve to a usable IP address");
|
||||
let addr = (host, port).to_socket_addrs()?.next().ok_or(Error::from(&e2))?;
|
||||
|
||||
let credentials = if url.username() == "" && url.password().is_none() {
|
||||
None
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use tun2proxy::{Args, BoxError};
|
||||
use tun2proxy::{ArgVerbosity, Args, BoxError};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), BoxError> {
|
||||
fn main() -> Result<(), BoxError> {
|
||||
dotenvy::dotenv().ok();
|
||||
let args = Args::parse_args();
|
||||
|
||||
|
@ -24,11 +23,21 @@ async fn main() -> Result<(), BoxError> {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let default = format!("{:?},hickory_proto=warn", args.verbosity);
|
||||
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
|
||||
rt.block_on(main_async(args))
|
||||
}
|
||||
|
||||
async fn main_async(args: Args) -> Result<(), BoxError> {
|
||||
let ipstack = match args.verbosity {
|
||||
ArgVerbosity::Trace => ArgVerbosity::Debug,
|
||||
_ => args.verbosity,
|
||||
};
|
||||
let default = format!("{:?},hickory_proto=warn,ipstack={:?}", args.verbosity, ipstack);
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init();
|
||||
|
||||
let shutdown_token = tokio_util::sync::CancellationToken::new();
|
||||
let main_loop_handle = tokio::spawn({
|
||||
let args = args.clone();
|
||||
let shutdown_token = shutdown_token.clone();
|
||||
async move {
|
||||
#[cfg(target_os = "linux")]
|
||||
|
@ -36,18 +45,20 @@ async fn main() -> Result<(), BoxError> {
|
|||
if let Err(err) = namespace_proxy_main(args, shutdown_token).await {
|
||||
log::error!("namespace proxy error: {}", err);
|
||||
}
|
||||
return;
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn traffic_cb(status: *const tun2proxy::TrafficStatus, _: *mut std::ffi::c_void) {
|
||||
let status = &*status;
|
||||
let status = unsafe { &*status };
|
||||
log::debug!("Traffic: ▲ {} : ▼ {}", status.tx, status.rx);
|
||||
}
|
||||
unsafe { tun2proxy::tun2proxy_set_traffic_status_callback(1, Some(traffic_cb), std::ptr::null_mut()) };
|
||||
|
||||
if let Err(err) = tun2proxy::desktop_run_async(args, shutdown_token).await {
|
||||
log::error!("main loop error: {}", err);
|
||||
let ret = tun2proxy::general_run_async(args, tun::DEFAULT_MTU, cfg!(target_os = "macos"), shutdown_token).await;
|
||||
if let Err(err) = &ret {
|
||||
log::error!("main loop error: {err}");
|
||||
}
|
||||
ret
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -60,13 +71,19 @@ async fn main() -> Result<(), BoxError> {
|
|||
})
|
||||
.await;
|
||||
|
||||
main_loop_handle.await?;
|
||||
let tasks = main_loop_handle.await??;
|
||||
|
||||
if ctrlc_fired.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
log::info!("Ctrl-C fired, waiting the handler to finish...");
|
||||
ctrlc_handel.await.map_err(|err| err.to_string())?;
|
||||
}
|
||||
|
||||
if args.exit_on_fatal_error && tasks >= args.max_sessions {
|
||||
// Because `main_async` function perhaps stuck in `await` state, so we need to exit the process forcefully
|
||||
log::info!("Internal fatal error, max sessions reached ({tasks}/{})", args.max_sessions);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -75,7 +92,7 @@ async fn namespace_proxy_main(
|
|||
_args: Args,
|
||||
_shutdown_token: tokio_util::sync::CancellationToken,
|
||||
) -> Result<std::process::ExitStatus, tun2proxy::Error> {
|
||||
use nix::fcntl::{open, OFlag};
|
||||
use nix::fcntl::{OFlag, open};
|
||||
use nix::sys::stat::Mode;
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
|
|
269
src/bin/udpgw_server.rs
Normal file
269
src/bin/udpgw_server.rs
Normal file
|
@ -0,0 +1,269 @@
|
|||
use socks5_impl::protocol::AsyncStreamOperation;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::{
|
||||
io::AsyncWriteExt,
|
||||
net::{
|
||||
UdpSocket,
|
||||
tcp::{ReadHalf, WriteHalf},
|
||||
},
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use tun2proxy::{
|
||||
ArgVerbosity, BoxError, Error, Result,
|
||||
udpgw::{Packet, UdpFlag},
|
||||
};
|
||||
|
||||
pub(crate) const CLIENT_DISCONNECT_TIMEOUT: tokio::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
addr: SocketAddr,
|
||||
last_activity: std::time::Instant,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(addr: SocketAddr) -> Self {
|
||||
let last_activity = std::time::Instant::now();
|
||||
Self { addr, last_activity }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
#[command(author, version, about = "UDP Gateway Server for tun2proxy", long_about = None)]
|
||||
pub struct UdpGwArgs {
|
||||
/// UDP gateway listen address
|
||||
#[arg(short, long, value_name = "IP:PORT", default_value = "127.0.0.1:7300")]
|
||||
pub listen_addr: SocketAddr,
|
||||
|
||||
/// UDP mtu
|
||||
#[arg(short = 'm', long, value_name = "udp mtu", default_value = "10240")]
|
||||
pub udp_mtu: u16,
|
||||
|
||||
/// UDP timeout in seconds
|
||||
#[arg(short = 't', long, value_name = "seconds", default_value = "3")]
|
||||
pub udp_timeout: u64,
|
||||
|
||||
/// Daemonize for unix family or run as Windows service
|
||||
#[cfg(unix)]
|
||||
#[arg(short, long)]
|
||||
pub daemonize: bool,
|
||||
|
||||
/// Verbosity level
|
||||
#[arg(short, long, value_name = "level", value_enum, default_value = "info")]
|
||||
pub verbosity: ArgVerbosity,
|
||||
}
|
||||
|
||||
impl UdpGwArgs {
|
||||
pub fn parse_args() -> Self {
|
||||
<Self as ::clap::Parser>::parse()
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_error_response(tx: Sender<Packet>, conn_id: u16) {
|
||||
let error_packet = Packet::build_error_packet(conn_id);
|
||||
if let Err(e) = tx.send(error_packet).await {
|
||||
log::error!("send error response error {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_keepalive_response(tx: Sender<Packet>, conn_id: u16) {
|
||||
let keepalive_packet = Packet::build_keepalive_packet(conn_id);
|
||||
if let Err(e) = tx.send(keepalive_packet).await {
|
||||
log::error!("send keepalive response error {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send data field of packet from client to destination server and receive response,
|
||||
/// then wrap response data to the packet's data field and send packet back to client.
|
||||
async fn process_udp(udp_mtu: u16, udp_timeout: u64, tx: Sender<Packet>, mut packet: Packet) -> Result<()> {
|
||||
let Some(dst_addr) = &packet.address else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, "udp request address is None").into());
|
||||
};
|
||||
use std::net::ToSocketAddrs;
|
||||
let Some(dst_addr) = dst_addr.to_socket_addrs()?.next() else {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, "to_socket_addrs").into());
|
||||
};
|
||||
let std_sock = match dst_addr {
|
||||
std::net::SocketAddr::V6(_) => std::net::UdpSocket::bind("[::]:0")?,
|
||||
std::net::SocketAddr::V4(_) => std::net::UdpSocket::bind("0.0.0.0:0")?,
|
||||
};
|
||||
std_sock.set_nonblocking(true)?;
|
||||
#[cfg(unix)]
|
||||
nix::sys::socket::setsockopt(&std_sock, nix::sys::socket::sockopt::ReuseAddr, &true)?;
|
||||
let socket = UdpSocket::from_std(std_sock)?;
|
||||
// 1. send udp data to destination server
|
||||
socket.send_to(&packet.data, &dst_addr).await?;
|
||||
// 2. receive response from destination server
|
||||
let mut buf = vec![0u8; udp_mtu as usize];
|
||||
let (len, _addr) = tokio::time::timeout(tokio::time::Duration::from_secs(udp_timeout), socket.recv_from(&mut buf))
|
||||
.await
|
||||
.map_err(std::io::Error::from)??;
|
||||
packet.data = buf[..len].to_vec();
|
||||
// 3. send response back to client
|
||||
use std::io::{Error, ErrorKind::BrokenPipe};
|
||||
tx.send(packet).await.map_err(|e| Error::new(BrokenPipe, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mask_ip(ip: &str) -> String {
|
||||
if ip.len() <= 2 {
|
||||
return ip.to_string();
|
||||
}
|
||||
let mut masked_ip = String::new();
|
||||
for (i, c) in ip.chars().enumerate() {
|
||||
if i == 0 || i == ip.len() - 1 || c == '.' || c == ':' {
|
||||
masked_ip.push(c);
|
||||
} else {
|
||||
masked_ip.push('*');
|
||||
}
|
||||
}
|
||||
masked_ip
|
||||
}
|
||||
|
||||
fn mask_socket_addr(socket_addr: std::net::SocketAddr) -> String {
|
||||
match socket_addr {
|
||||
std::net::SocketAddr::V4(addr) => {
|
||||
let masked_ip = mask_ip(&addr.ip().to_string());
|
||||
format!("{}:{}", masked_ip, addr.port())
|
||||
}
|
||||
std::net::SocketAddr::V6(addr) => {
|
||||
let masked_ip = mask_ip(&addr.ip().to_string());
|
||||
format!("[{}]:{}", masked_ip, addr.port())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_client_udp_req(args: &UdpGwArgs, tx: Sender<Packet>, mut client: Client, mut reader: ReadHalf<'_>) -> std::io::Result<()> {
|
||||
let udp_timeout = args.udp_timeout;
|
||||
let udp_mtu = args.udp_mtu;
|
||||
|
||||
let masked_addr = mask_socket_addr(client.addr);
|
||||
|
||||
loop {
|
||||
let masked_addr = masked_addr.clone();
|
||||
// 1. read udpgw packet from client
|
||||
let res = tokio::time::timeout(tokio::time::Duration::from_secs(2), Packet::retrieve_from_async_stream(&mut reader)).await;
|
||||
let packet = match res {
|
||||
Ok(Ok(packet)) => packet,
|
||||
Ok(Err(e)) => {
|
||||
log::debug!("client {} retrieve_from_async_stream \"{}\"", masked_addr, e);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
if client.last_activity.elapsed() >= CLIENT_DISCONNECT_TIMEOUT {
|
||||
log::debug!("client {} last_activity elapsed \"{e}\"", masked_addr);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
client.last_activity = std::time::Instant::now();
|
||||
|
||||
let flags = packet.header.flags;
|
||||
let conn_id = packet.header.conn_id;
|
||||
if flags & UdpFlag::KEEPALIVE == UdpFlag::KEEPALIVE {
|
||||
log::trace!("client {} send keepalive", masked_addr);
|
||||
// 2. if keepalive packet, do nothing, send keepalive response to client
|
||||
send_keepalive_response(tx.clone(), conn_id).await;
|
||||
continue;
|
||||
}
|
||||
log::trace!("client {} received udp data {}", masked_addr, packet);
|
||||
|
||||
// 3. process client udpgw packet in a new task
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = process_udp(udp_mtu, udp_timeout, tx.clone(), packet).await {
|
||||
send_error_response(tx, conn_id).await;
|
||||
log::debug!("client {} process udp function \"{e}\"", masked_addr);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_to_client(addr: SocketAddr, mut writer: WriteHalf<'_>, mut rx: Receiver<Packet>) -> std::io::Result<()> {
|
||||
let masked_addr = mask_socket_addr(addr);
|
||||
loop {
|
||||
use std::io::{Error, ErrorKind::BrokenPipe};
|
||||
let packet = rx.recv().await.ok_or(Error::new(BrokenPipe, "recv error"))?;
|
||||
log::trace!("send response to client {} with {}", masked_addr, packet);
|
||||
let data: Vec<u8> = packet.into();
|
||||
let _r = writer.write(&data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
async fn main_async(args: UdpGwArgs) -> Result<(), BoxError> {
|
||||
log::info!("{} {} starting...", module_path!(), env!("CARGO_PKG_VERSION"));
|
||||
log::info!("UDP Gateway Server running at {}", args.listen_addr);
|
||||
|
||||
let shutdown_token = tokio_util::sync::CancellationToken::new();
|
||||
let main_loop_handle = tokio::spawn(run(args, shutdown_token.clone()));
|
||||
|
||||
let ctrlc_fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let ctrlc_fired_clone = ctrlc_fired.clone();
|
||||
let ctrlc_handel = ctrlc2::set_async_handler(async move {
|
||||
log::info!("Ctrl-C received, exiting...");
|
||||
ctrlc_fired_clone.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
shutdown_token.cancel();
|
||||
})
|
||||
.await;
|
||||
|
||||
let _ = main_loop_handle.await?;
|
||||
|
||||
if ctrlc_fired.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
log::info!("Ctrl-C fired, waiting the handler to finish...");
|
||||
ctrlc_handel.await.map_err(|err| err.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(args: UdpGwArgs, shutdown_token: tokio_util::sync::CancellationToken) -> crate::Result<()> {
|
||||
let tcp_listener = tokio::net::TcpListener::bind(args.listen_addr).await?;
|
||||
loop {
|
||||
let (mut tcp_stream, addr) = tokio::select! {
|
||||
v = tcp_listener.accept() => v?,
|
||||
_ = shutdown_token.cancelled() => break,
|
||||
};
|
||||
let client = Client::new(addr);
|
||||
let masked_addr = mask_socket_addr(addr);
|
||||
log::info!("client {} connected", masked_addr);
|
||||
let params = args.clone();
|
||||
tokio::spawn(async move {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel::<Packet>(100);
|
||||
let (tcp_read_stream, tcp_write_stream) = tcp_stream.split();
|
||||
let res = tokio::select! {
|
||||
v = process_client_udp_req(¶ms, tx, client, tcp_read_stream) => v,
|
||||
v = write_to_client(addr, tcp_write_stream, rx) => v,
|
||||
};
|
||||
log::info!("client {} disconnected with {:?}", masked_addr, res);
|
||||
});
|
||||
}
|
||||
Ok::<(), Error>(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), BoxError> {
|
||||
dotenvy::dotenv().ok();
|
||||
let args = UdpGwArgs::parse_args();
|
||||
|
||||
let default = format!("{:?}", args.verbosity);
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init();
|
||||
|
||||
#[cfg(unix)]
|
||||
if args.daemonize {
|
||||
let stdout = std::fs::File::create("/tmp/udpgw.out")?;
|
||||
let stderr = std::fs::File::create("/tmp/udpgw.err")?;
|
||||
let daemonize = daemonize::Daemonize::new()
|
||||
.working_directory("/tmp")
|
||||
.umask(0o777)
|
||||
.stdout(stdout)
|
||||
.stderr(stderr)
|
||||
.privileged_action(|| "Executed before drop privileges");
|
||||
let _ = daemonize
|
||||
.start()
|
||||
.map_err(|e| format!("Failed to daemonize process, error:{:?}", e))?;
|
||||
}
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
|
||||
rt.block_on(main_async(args))
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
#![cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
|
||||
use crate::{
|
||||
args::{ArgDns, ArgProxy},
|
||||
ArgVerbosity, Args,
|
||||
};
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use tproxy_config::{TproxyArgs, TUN_GATEWAY, TUN_IPV4, TUN_NETMASK};
|
||||
use tun2::{AbstractDevice, DEFAULT_MTU as MTU};
|
||||
|
||||
static TUN_QUIT: std::sync::Mutex<Option<tokio_util::sync::CancellationToken>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
/// Parameters:
|
||||
/// - proxy_url: the proxy url, e.g. "socks5://127.0.0.1:1080"
|
||||
/// - tun: the tun device name, e.g. "utun5"
|
||||
/// - bypass: the bypass IP/CIDR, e.g. "123.45.67.0/24"
|
||||
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||
/// - root_privilege: whether to run with root privilege
|
||||
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tun2proxy_with_name_run(
|
||||
proxy_url: *const c_char,
|
||||
tun: *const c_char,
|
||||
bypass: *const c_char,
|
||||
dns_strategy: ArgDns,
|
||||
_root_privilege: bool,
|
||||
verbosity: ArgVerbosity,
|
||||
) -> c_int {
|
||||
let shutdown_token = tokio_util::sync::CancellationToken::new();
|
||||
{
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if lock.is_some() {
|
||||
return -1;
|
||||
}
|
||||
*lock = Some(shutdown_token.clone());
|
||||
} else {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
log::set_max_level(verbosity.into());
|
||||
if let Err(err) = log::set_boxed_logger(Box::<crate::dump_logger::DumpLogger>::default()) {
|
||||
log::warn!("set logger error: {}", err);
|
||||
}
|
||||
|
||||
let proxy_url = std::ffi::CStr::from_ptr(proxy_url).to_str().unwrap();
|
||||
let proxy = ArgProxy::try_from(proxy_url).unwrap();
|
||||
let tun = std::ffi::CStr::from_ptr(tun).to_str().unwrap().to_string();
|
||||
|
||||
let mut args = Args::default();
|
||||
args.proxy(proxy).tun(tun).dns(dns_strategy).verbosity(verbosity);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
args.setup(_root_privilege);
|
||||
|
||||
if let Ok(bypass) = std::ffi::CStr::from_ptr(bypass).to_str() {
|
||||
args.bypass(bypass.parse().unwrap());
|
||||
}
|
||||
|
||||
let main_loop = async move {
|
||||
if let Err(err) = desktop_run_async(args, shutdown_token).await {
|
||||
log::error!("main loop error: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let exit_code = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
|
||||
Err(_e) => -3,
|
||||
Ok(rt) => match rt.block_on(main_loop) {
|
||||
Ok(_) => 0,
|
||||
Err(_e) => -4,
|
||||
},
|
||||
};
|
||||
|
||||
exit_code
|
||||
}
|
||||
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
pub async fn desktop_run_async(args: Args, shutdown_token: tokio_util::sync::CancellationToken) -> std::io::Result<()> {
|
||||
let bypass_ips = args.bypass.clone();
|
||||
|
||||
let mut tun_config = tun2::Configuration::default();
|
||||
tun_config.address(TUN_IPV4).netmask(TUN_NETMASK).mtu(MTU).up();
|
||||
tun_config.destination(TUN_GATEWAY);
|
||||
#[cfg(unix)]
|
||||
if let Some(fd) = args.tun_fd {
|
||||
tun_config.raw_fd(fd);
|
||||
if let Some(v) = args.close_fd_on_drop {
|
||||
tun_config.close_fd_on_drop(v);
|
||||
};
|
||||
} else if let Some(ref tun) = args.tun {
|
||||
tun_config.tun_name(tun);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if let Some(ref tun) = args.tun {
|
||||
tun_config.tun_name(tun);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
tun_config.platform_config(|cfg| {
|
||||
#[allow(deprecated)]
|
||||
cfg.packet_information(true);
|
||||
cfg.ensure_root_privileges(args.setup);
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
tun_config.platform_config(|cfg| {
|
||||
cfg.device_guid(12324323423423434234_u128);
|
||||
});
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let mut tproxy_args = TproxyArgs::new()
|
||||
.tun_dns(args.dns_addr)
|
||||
.proxy_addr(args.proxy.addr)
|
||||
.bypass_ips(&bypass_ips)
|
||||
.ipv6_default_route(args.ipv6_enabled);
|
||||
|
||||
#[allow(unused_mut, unused_assignments, unused_variables)]
|
||||
let mut setup = true;
|
||||
|
||||
let device = tun2::create_as_async(&tun_config)?;
|
||||
|
||||
if let Ok(tun_name) = device.tun_name() {
|
||||
tproxy_args = tproxy_args.tun_name(&tun_name);
|
||||
}
|
||||
|
||||
// TproxyState implements the Drop trait to restore network configuration,
|
||||
// so we need to assign it to a variable, even if it is not used.
|
||||
let mut _restore: Option<tproxy_config::TproxyState> = None;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
setup = args.setup;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
if setup {
|
||||
_restore = Some(tproxy_config::tproxy_setup(&tproxy_args)?);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut admin_command_args = args.admin_command.iter();
|
||||
if let Some(command) = admin_command_args.next() {
|
||||
let child = tokio::process::Command::new(command)
|
||||
.args(admin_command_args)
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Err(err) => {
|
||||
log::warn!("Failed to start admin process: {err}");
|
||||
}
|
||||
Ok(mut child) => {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = child.wait().await {
|
||||
log::warn!("Admin process terminated: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let join_handle = tokio::spawn(crate::run(device, MTU, args, shutdown_token));
|
||||
join_handle.await.map_err(std::io::Error::from)??;
|
||||
|
||||
Ok::<(), std::io::Error>(())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Shutdown the tun2proxy component.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn tun2proxy_with_name_stop() -> c_int {
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if let Some(shutdown_token) = lock.take() {
|
||||
shutdown_token.cancel();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
23
src/dns.rs
23
src/dns.rs
|
@ -1,21 +1,16 @@
|
|||
use hickory_proto::{
|
||||
op::{Message, MessageType, ResponseCode},
|
||||
rr::{record_type::RecordType, Name, RData, Record},
|
||||
rr::{
|
||||
Name, RData, Record,
|
||||
rdata::{A, AAAA},
|
||||
},
|
||||
};
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
|
||||
pub fn build_dns_response(mut request: Message, domain: &str, ip: IpAddr, ttl: u32) -> Result<Message, String> {
|
||||
let record = match ip {
|
||||
IpAddr::V4(ip) => {
|
||||
let mut record = Record::with(Name::from_str(domain)?, RecordType::A, ttl);
|
||||
record.set_data(Some(RData::A(ip.into())));
|
||||
record
|
||||
}
|
||||
IpAddr::V6(ip) => {
|
||||
let mut record = Record::with(Name::from_str(domain)?, RecordType::AAAA, ttl);
|
||||
record.set_data(Some(RData::AAAA(ip.into())));
|
||||
record
|
||||
}
|
||||
IpAddr::V4(ip) => Record::from_rdata(Name::from_str(domain)?, ttl, RData::A(A(ip))),
|
||||
IpAddr::V6(ip) => Record::from_rdata(Name::from_str(domain)?, ttl, RData::AAAA(AAAA(ip))),
|
||||
};
|
||||
|
||||
// We must indicate that this message is a response. Otherwise, implementations may not
|
||||
|
@ -27,9 +22,7 @@ pub fn build_dns_response(mut request: Message, domain: &str, ip: IpAddr, ttl: u
|
|||
}
|
||||
|
||||
pub fn remove_ipv6_entries(message: &mut Message) {
|
||||
message
|
||||
.answers_mut()
|
||||
.retain(|answer| !matches!(answer.data(), Some(RData::AAAA(_))));
|
||||
message.answers_mut().retain(|answer| !matches!(answer.data(), RData::AAAA(_)));
|
||||
}
|
||||
|
||||
pub fn extract_ipaddr_from_dns_message(message: &Message) -> Result<IpAddr, String> {
|
||||
|
@ -38,7 +31,7 @@ pub fn extract_ipaddr_from_dns_message(message: &Message) -> Result<IpAddr, Stri
|
|||
}
|
||||
let mut cname = None;
|
||||
for answer in message.answers() {
|
||||
match answer.data().ok_or("DNS response not contains answer data")? {
|
||||
match answer.data() {
|
||||
RData::A(addr) => {
|
||||
return Ok(IpAddr::V4((*addr).into()));
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub(crate) static DUMP_CALLBACK: Mutex<Option<DumpCallback>> = Mutex::new(None);
|
|||
/// # Safety
|
||||
///
|
||||
/// set dump log info callback.
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_set_log_callback(
|
||||
callback: Option<unsafe extern "C" fn(ArgVerbosity, *const c_char, *mut c_void)>,
|
||||
ctx: *mut c_void,
|
||||
|
@ -23,7 +23,7 @@ pub struct DumpCallback(Option<unsafe extern "C" fn(ArgVerbosity, *const c_char,
|
|||
impl DumpCallback {
|
||||
unsafe fn call(self, dump_level: ArgVerbosity, info: *const c_char) {
|
||||
if let Some(cb) = self.0 {
|
||||
cb(dump_level, info, self.1);
|
||||
unsafe { cb(dump_level, info, self.1) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ pub enum Error {
|
|||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[cfg(unix)]
|
||||
#[error("nix::errno::Errno {0:?}")]
|
||||
NixErrno(#[from] nix::errno::Errno),
|
||||
|
||||
|
@ -26,7 +26,7 @@ pub enum Error {
|
|||
IpStack(#[from] ipstack::IpStackError),
|
||||
|
||||
#[error("DnsProtoError {0:?}")]
|
||||
DnsProto(#[from] hickory_proto::error::ProtoError),
|
||||
DnsProto(#[from] hickory_proto::ProtoError),
|
||||
|
||||
#[error("httparse::Error {0:?}")]
|
||||
Httparse(#[from] httparse::Error),
|
||||
|
@ -43,10 +43,6 @@ pub enum Error {
|
|||
|
||||
#[error("std::num::ParseIntError {0:?}")]
|
||||
IntParseError(#[from] std::num::ParseIntError),
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("bincode::Error {0:?}")]
|
||||
BincodeError(#[from] bincode::Error),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
|
|
269
src/general_api.rs
Normal file
269
src/general_api.rs
Normal file
|
@ -0,0 +1,269 @@
|
|||
use crate::{
|
||||
ArgVerbosity, Args,
|
||||
args::{ArgDns, ArgProxy},
|
||||
};
|
||||
use std::os::raw::{c_char, c_int, c_ushort};
|
||||
|
||||
static TUN_QUIT: std::sync::Mutex<Option<tokio_util::sync::CancellationToken>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
/// Parameters:
|
||||
/// - proxy_url: the proxy url, e.g. "socks5://127.0.0.1:1080"
|
||||
/// - tun: the tun device name, e.g. "utun5"
|
||||
/// - bypass: the bypass IP/CIDR, e.g. "123.45.67.0/24"
|
||||
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||
/// - root_privilege: whether to run with root privilege
|
||||
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_with_name_run(
|
||||
proxy_url: *const c_char,
|
||||
tun: *const c_char,
|
||||
bypass: *const c_char,
|
||||
dns_strategy: ArgDns,
|
||||
_root_privilege: bool,
|
||||
verbosity: ArgVerbosity,
|
||||
) -> c_int {
|
||||
let proxy_url = unsafe { std::ffi::CStr::from_ptr(proxy_url) }.to_str().unwrap();
|
||||
let proxy = ArgProxy::try_from(proxy_url).unwrap();
|
||||
let tun = unsafe { std::ffi::CStr::from_ptr(tun) }.to_str().unwrap().to_string();
|
||||
|
||||
let mut args = Args::default();
|
||||
if let Ok(bypass) = unsafe { std::ffi::CStr::from_ptr(bypass) }.to_str() {
|
||||
args.bypass(bypass.parse().unwrap());
|
||||
}
|
||||
args.proxy(proxy).tun(tun).dns(dns_strategy).verbosity(verbosity);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
args.setup(_root_privilege);
|
||||
|
||||
general_run_for_api(args, tun::DEFAULT_MTU, false)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
/// Parameters:
|
||||
/// - proxy_url: the proxy url, e.g. "socks5://127.0.0.1:1080"
|
||||
/// - tun_fd: the tun file descriptor, it will be owned by tun2proxy
|
||||
/// - close_fd_on_drop: whether close the tun_fd on drop
|
||||
/// - packet_information: indicates whether exists packet information in packet from TUN device
|
||||
/// - tun_mtu: the tun mtu
|
||||
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||
#[cfg(unix)]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_with_fd_run(
|
||||
proxy_url: *const c_char,
|
||||
tun_fd: c_int,
|
||||
close_fd_on_drop: bool,
|
||||
packet_information: bool,
|
||||
tun_mtu: c_ushort,
|
||||
dns_strategy: ArgDns,
|
||||
verbosity: ArgVerbosity,
|
||||
) -> c_int {
|
||||
let proxy_url = unsafe { std::ffi::CStr::from_ptr(proxy_url) }.to_str().unwrap();
|
||||
let proxy = ArgProxy::try_from(proxy_url).unwrap();
|
||||
|
||||
let mut args = Args::default();
|
||||
args.proxy(proxy)
|
||||
.tun_fd(Some(tun_fd))
|
||||
.close_fd_on_drop(close_fd_on_drop)
|
||||
.dns(dns_strategy)
|
||||
.verbosity(verbosity);
|
||||
|
||||
general_run_for_api(args, tun_mtu, packet_information)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Run the tun2proxy component with command line arguments
|
||||
/// Parameters:
|
||||
/// - cli_args: The command line arguments,
|
||||
/// e.g. `tun2proxy-bin --setup --proxy socks5://127.0.0.1:1080 --bypass 98.76.54.0/24 --dns over-tcp --verbosity trace`
|
||||
/// - tun_mtu: The MTU of the TUN device, e.g. 1500
|
||||
/// - packet_information: Whether exists packet information in packet from TUN device
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_run_with_cli_args(cli_args: *const c_char, tun_mtu: c_ushort, packet_information: bool) -> c_int {
|
||||
let Ok(cli_args) = unsafe { std::ffi::CStr::from_ptr(cli_args) }.to_str() else {
|
||||
log::error!("Failed to convert CLI arguments to string");
|
||||
return -5;
|
||||
};
|
||||
let Some(args) = shlex::split(cli_args) else {
|
||||
log::error!("Failed to split CLI arguments");
|
||||
return -6;
|
||||
};
|
||||
let args = <Args as ::clap::Parser>::parse_from(args);
|
||||
general_run_for_api(args, tun_mtu, packet_information)
|
||||
}
|
||||
|
||||
pub fn general_run_for_api(args: Args, tun_mtu: u16, packet_information: bool) -> c_int {
|
||||
log::set_max_level(args.verbosity.into());
|
||||
if let Err(err) = log::set_boxed_logger(Box::<crate::dump_logger::DumpLogger>::default()) {
|
||||
log::debug!("set logger error: {}", err);
|
||||
}
|
||||
|
||||
let shutdown_token = tokio_util::sync::CancellationToken::new();
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if lock.is_some() {
|
||||
log::error!("tun2proxy already started");
|
||||
return -1;
|
||||
}
|
||||
*lock = Some(shutdown_token.clone());
|
||||
} else {
|
||||
log::error!("failed to lock tun2proxy quit token");
|
||||
return -2;
|
||||
}
|
||||
|
||||
let Ok(rt) = tokio::runtime::Builder::new_multi_thread().enable_all().build() else {
|
||||
log::error!("failed to create tokio runtime with");
|
||||
return -3;
|
||||
};
|
||||
match rt.block_on(async move {
|
||||
let ret = general_run_async(args.clone(), tun_mtu, packet_information, shutdown_token).await;
|
||||
match &ret {
|
||||
Ok(sessions) => {
|
||||
if args.exit_on_fatal_error && *sessions >= args.max_sessions {
|
||||
log::error!("Forced exit due to max sessions reached ({sessions}/{})", args.max_sessions);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
log::debug!("tun2proxy exited normally, current sessions: {sessions}");
|
||||
}
|
||||
Err(err) => log::error!("main loop error: {err}"),
|
||||
}
|
||||
ret
|
||||
}) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to run tun2proxy with error: {:?}", e);
|
||||
-4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the tun2proxy component with some arguments.
|
||||
pub async fn general_run_async(
|
||||
args: Args,
|
||||
tun_mtu: u16,
|
||||
_packet_information: bool,
|
||||
shutdown_token: tokio_util::sync::CancellationToken,
|
||||
) -> std::io::Result<usize> {
|
||||
let mut tun_config = tun::Configuration::default();
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
{
|
||||
use tproxy_config::{TUN_GATEWAY, TUN_IPV4, TUN_NETMASK};
|
||||
tun_config.address(TUN_IPV4).netmask(TUN_NETMASK).mtu(tun_mtu).up();
|
||||
tun_config.destination(TUN_GATEWAY);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(fd) = args.tun_fd {
|
||||
tun_config.raw_fd(fd);
|
||||
if let Some(v) = args.close_fd_on_drop {
|
||||
tun_config.close_fd_on_drop(v);
|
||||
};
|
||||
} else if let Some(ref tun) = args.tun {
|
||||
tun_config.tun_name(tun);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if let Some(ref tun) = args.tun {
|
||||
tun_config.tun_name(tun);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
tun_config.platform_config(|cfg| {
|
||||
#[allow(deprecated)]
|
||||
cfg.packet_information(true);
|
||||
cfg.ensure_root_privileges(args.setup);
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
tun_config.platform_config(|cfg| {
|
||||
cfg.device_guid(12324323423423434234_u128);
|
||||
});
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
tun_config.platform_config(|cfg| {
|
||||
cfg.packet_information(_packet_information);
|
||||
});
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
#[allow(unused_variables)]
|
||||
let mut tproxy_args = tproxy_config::TproxyArgs::new()
|
||||
.tun_dns(args.dns_addr)
|
||||
.proxy_addr(args.proxy.addr)
|
||||
.bypass_ips(&args.bypass)
|
||||
.ipv6_default_route(args.ipv6_enabled);
|
||||
|
||||
#[allow(unused_mut, unused_assignments, unused_variables)]
|
||||
let mut setup = true;
|
||||
|
||||
let device = tun::create_as_async(&tun_config)?;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
if let Ok(tun_name) = tun::AbstractDevice::tun_name(&*device) {
|
||||
// Above line is equivalent to: `use tun::AbstractDevice; if let Ok(tun_name) = device.tun_name() {`
|
||||
tproxy_args = tproxy_args.tun_name(&tun_name);
|
||||
}
|
||||
|
||||
// TproxyState implements the Drop trait to restore network configuration,
|
||||
// so we need to assign it to a variable, even if it is not used.
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
let mut _restore: Option<tproxy_config::TproxyState> = None;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
setup = args.setup;
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||
if setup {
|
||||
_restore = Some(tproxy_config::tproxy_setup(&tproxy_args)?);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let mut admin_command_args = args.admin_command.iter();
|
||||
if let Some(command) = admin_command_args.next() {
|
||||
let child = tokio::process::Command::new(command)
|
||||
.args(admin_command_args)
|
||||
.kill_on_drop(true)
|
||||
.spawn();
|
||||
|
||||
match child {
|
||||
Err(err) => {
|
||||
log::warn!("Failed to start admin process: {err}");
|
||||
}
|
||||
Ok(mut child) => {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = child.wait().await {
|
||||
log::warn!("Admin process terminated: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let join_handle = tokio::spawn(crate::run(device, tun_mtu, args, shutdown_token));
|
||||
Ok(join_handle.await.map_err(std::io::Error::from)??)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Shutdown the tun2proxy component.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_stop() -> c_int {
|
||||
tun2proxy_stop_internal()
|
||||
}
|
||||
|
||||
pub(crate) fn tun2proxy_stop_internal() -> c_int {
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if let Some(shutdown_token) = lock.take() {
|
||||
shutdown_token.cancel();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
|
@ -4,11 +4,10 @@ use crate::{
|
|||
proxy_handler::{ProxyHandler, ProxyHandlerManager},
|
||||
session_info::{IpProtocol, SessionInfo},
|
||||
};
|
||||
use base64::Engine;
|
||||
use httparse::Response;
|
||||
use socks5_impl::protocol::UserKey;
|
||||
use std::{
|
||||
collections::{hash_map::RandomState, HashMap, VecDeque},
|
||||
collections::{HashMap, VecDeque, hash_map::RandomState},
|
||||
iter::FromIterator,
|
||||
net::SocketAddr,
|
||||
str,
|
||||
|
@ -141,8 +140,7 @@ impl HttpConnection {
|
|||
.extend(format!("{}: {}\r\n", PROXY_AUTHORIZATION, response.to_header_string()).as_bytes());
|
||||
}
|
||||
AuthenticationScheme::Basic => {
|
||||
let cred = format!("{}:{}", credentials.username, credentials.password);
|
||||
let auth_b64 = base64::engine::general_purpose::STANDARD.encode(cred);
|
||||
let auth_b64 = base64easy::encode(credentials.to_string(), base64easy::EngineKind::Standard);
|
||||
self.server_outbuf
|
||||
.extend(format!("{}: Basic {}\r\n", PROXY_AUTHORIZATION, auth_b64).as_bytes());
|
||||
}
|
||||
|
@ -252,7 +250,7 @@ impl HttpConnection {
|
|||
}
|
||||
|
||||
// The HTTP/1.1 expected to be keep alive waiting for the next frame so, we must
|
||||
// compute the lenght of the response in order to detect the next frame (response)
|
||||
// compute the length of the response in order to detect the next frame (response)
|
||||
// [RFC-9112](https://datatracker.ietf.org/doc/html/rfc9112#body.content-length)
|
||||
|
||||
// Transfer-Encoding isn't supported yet
|
||||
|
|
225
src/lib.rs
225
src/lib.rs
|
@ -1,3 +1,5 @@
|
|||
#[cfg(feature = "udpgw")]
|
||||
use crate::udpgw::UdpGwClient;
|
||||
use crate::{
|
||||
directions::{IncomingDataEvent, IncomingDirection, OutgoingDirection},
|
||||
http::HttpManager,
|
||||
|
@ -9,6 +11,8 @@ use ipstack::stream::{IpStackStream, IpStackTcpStream, IpStackUdpStream};
|
|||
use proxy_handler::{ProxyHandler, ProxyHandlerManager};
|
||||
use socks::SocksProxyManager;
|
||||
pub use socks5_impl::protocol::UserKey;
|
||||
#[cfg(feature = "udpgw")]
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::ErrorKind,
|
||||
|
@ -18,59 +22,57 @@ use std::{
|
|||
use tokio::{
|
||||
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||
net::{TcpSocket, TcpStream, UdpSocket},
|
||||
sync::{mpsc::Receiver, Mutex},
|
||||
sync::{Mutex, mpsc::Receiver},
|
||||
};
|
||||
pub use tokio_util::sync::CancellationToken;
|
||||
use tproxy_config::is_private_ip;
|
||||
use udp_stream::UdpStream;
|
||||
#[cfg(feature = "udpgw")]
|
||||
use udpgw::{UDPGW_KEEPALIVE_TIME, UDPGW_MAX_CONNECTIONS, UdpGwClientStream, UdpGwResponse};
|
||||
|
||||
pub use {
|
||||
args::{ArgDns, ArgProxy, ArgVerbosity, Args, ProxyType},
|
||||
error::{BoxError, Error, Result},
|
||||
traffic_status::{tun2proxy_set_traffic_status_callback, TrafficStatus},
|
||||
traffic_status::{TrafficStatus, tun2proxy_set_traffic_status_callback},
|
||||
};
|
||||
|
||||
#[cfg(feature = "mimalloc")]
|
||||
#[global_allocator]
|
||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
pub use desktop_api::desktop_run_async;
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
pub use mobile_api::{desktop_run_async, mobile_run, mobile_stop};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use mobile_api::{mobile_run, mobile_stop};
|
||||
pub use general_api::general_run_async;
|
||||
|
||||
mod android;
|
||||
mod apple;
|
||||
mod args;
|
||||
mod desktop_api;
|
||||
mod directions;
|
||||
mod dns;
|
||||
mod dump_logger;
|
||||
mod error;
|
||||
mod general_api;
|
||||
mod http;
|
||||
mod mobile_api;
|
||||
mod no_proxy;
|
||||
mod proxy_handler;
|
||||
mod session_info;
|
||||
pub mod socket_transfer;
|
||||
mod socks;
|
||||
mod traffic_status;
|
||||
#[cfg(feature = "udpgw")]
|
||||
pub mod udpgw;
|
||||
mod virtual_dns;
|
||||
#[doc(hidden)]
|
||||
pub mod win_svc;
|
||||
|
||||
const DNS_PORT: u16 = 53;
|
||||
|
||||
static TASK_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
static TASK_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[cfg_attr(target_os = "linux", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
target_os = "linux",
|
||||
derive(bincode::Encode, bincode::Decode, serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
pub enum SocketProtocol {
|
||||
Tcp,
|
||||
Udp,
|
||||
|
@ -78,7 +80,10 @@ pub enum SocketProtocol {
|
|||
|
||||
#[allow(unused)]
|
||||
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug)]
|
||||
#[cfg_attr(target_os = "linux", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
target_os = "linux",
|
||||
derive(bincode::Encode, bincode::Decode, serde::Serialize, serde::Deserialize)
|
||||
)]
|
||||
pub enum SocketDomain {
|
||||
IpV4,
|
||||
IpV6,
|
||||
|
@ -149,7 +154,9 @@ async fn create_udp_stream(socket_queue: &Option<Arc<SocketQueue>>, peer: Socket
|
|||
/// * `mtu` - The MTU of the network device
|
||||
/// * `args` - The arguments to use
|
||||
/// * `shutdown_token` - The token to exit the server
|
||||
pub async fn run<D>(device: D, mtu: u16, args: Args, shutdown_token: CancellationToken) -> crate::Result<()>
|
||||
/// # Returns
|
||||
/// * The number of sessions while exiting
|
||||
pub async fn run<D>(device: D, mtu: u16, args: Args, shutdown_token: CancellationToken) -> crate::Result<usize>
|
||||
where
|
||||
D: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
|
@ -231,6 +238,24 @@ where
|
|||
|
||||
let mut ip_stack = ipstack::IpStack::new(ipstack_config, device);
|
||||
|
||||
#[cfg(feature = "udpgw")]
|
||||
let udpgw_client = args.udpgw_server.map(|addr| {
|
||||
log::info!("UDP Gateway enabled, server: {}", addr);
|
||||
use std::time::Duration;
|
||||
let client = Arc::new(UdpGwClient::new(
|
||||
mtu,
|
||||
args.udpgw_connections.unwrap_or(UDPGW_MAX_CONNECTIONS),
|
||||
args.udpgw_keepalive.map(Duration::from_secs).unwrap_or(UDPGW_KEEPALIVE_TIME),
|
||||
args.udp_timeout,
|
||||
addr,
|
||||
));
|
||||
let client_keepalive = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = client_keepalive.heartbeat_task().await;
|
||||
});
|
||||
client
|
||||
});
|
||||
|
||||
loop {
|
||||
let virtual_dns = virtual_dns.clone();
|
||||
let ip_stack_stream = tokio::select! {
|
||||
|
@ -242,10 +267,10 @@ where
|
|||
ip_stack_stream?
|
||||
}
|
||||
};
|
||||
let max_sessions = args.max_sessions as u64;
|
||||
let max_sessions = args.max_sessions;
|
||||
match ip_stack_stream {
|
||||
IpStackStream::Tcp(tcp) => {
|
||||
if TASK_COUNT.load(Relaxed) > max_sessions {
|
||||
if TASK_COUNT.load(Relaxed) >= max_sessions {
|
||||
if args.exit_on_fatal_error {
|
||||
log::info!("Too many sessions that over {max_sessions}, exiting...");
|
||||
break;
|
||||
|
@ -253,7 +278,7 @@ where
|
|||
log::warn!("Too many sessions that over {max_sessions}, dropping new session");
|
||||
continue;
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_add(1, Relaxed) + 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_add(1, Relaxed).saturating_add(1));
|
||||
let info = SessionInfo::new(tcp.local_addr(), tcp.peer_addr(), IpProtocol::Tcp);
|
||||
let domain_name = if let Some(virtual_dns) = &virtual_dns {
|
||||
let mut virtual_dns = virtual_dns.lock().await;
|
||||
|
@ -268,11 +293,11 @@ where
|
|||
if let Err(err) = handle_tcp_session(tcp, proxy_handler, socket_queue).await {
|
||||
log::error!("{} error \"{}\"", info, err);
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed) - 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||
});
|
||||
}
|
||||
IpStackStream::Udp(udp) => {
|
||||
if TASK_COUNT.load(Relaxed) > max_sessions {
|
||||
if TASK_COUNT.load(Relaxed) >= max_sessions {
|
||||
if args.exit_on_fatal_error {
|
||||
log::info!("Too many sessions that over {max_sessions}, exiting...");
|
||||
break;
|
||||
|
@ -280,7 +305,7 @@ where
|
|||
log::warn!("Too many sessions that over {max_sessions}, dropping new session");
|
||||
continue;
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_add(1, Relaxed) + 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_add(1, Relaxed).saturating_add(1));
|
||||
let mut info = SessionInfo::new(udp.local_addr(), udp.peer_addr(), IpProtocol::Udp);
|
||||
if info.dst.port() == DNS_PORT {
|
||||
if is_private_ip(info.dst.ip()) {
|
||||
|
@ -294,7 +319,7 @@ where
|
|||
if let Err(err) = handle_dns_over_tcp_session(udp, proxy_handler, socket_queue, ipv6_enabled).await {
|
||||
log::error!("{} error \"{}\"", info, err);
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed) - 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
@ -305,7 +330,7 @@ where
|
|||
log::error!("{} error \"{}\"", info, err);
|
||||
}
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed) - 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
@ -318,6 +343,28 @@ where
|
|||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(feature = "udpgw")]
|
||||
if let Some(udpgw) = udpgw_client.clone() {
|
||||
let tcp_src = match udp.peer_addr() {
|
||||
SocketAddr::V4(_) => SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)),
|
||||
SocketAddr::V6(_) => SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)),
|
||||
};
|
||||
let tcpinfo = SessionInfo::new(tcp_src, udpgw.get_udpgw_server_addr(), IpProtocol::Tcp);
|
||||
let proxy_handler = mgr.new_proxy_handler(tcpinfo, None, false).await?;
|
||||
let queue = socket_queue.clone();
|
||||
tokio::spawn(async move {
|
||||
let dst = info.dst; // real UDP destination address
|
||||
let dst_addr = match domain_name {
|
||||
Some(ref d) => socks5_impl::protocol::Address::from((d.clone(), dst.port())),
|
||||
None => dst.into(),
|
||||
};
|
||||
if let Err(e) = handle_udp_gateway_session(udp, udpgw, &dst_addr, proxy_handler, queue, ipv6_enabled).await {
|
||||
log::info!("Ending {} with \"{}\"", info, e);
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||
});
|
||||
continue;
|
||||
}
|
||||
match mgr.new_proxy_handler(info, domain_name, true).await {
|
||||
Ok(proxy_handler) => {
|
||||
let socket_queue = socket_queue.clone();
|
||||
|
@ -326,7 +373,7 @@ where
|
|||
if let Err(err) = handle_udp_associate_session(udp, ty, proxy_handler, socket_queue, ipv6_enabled).await {
|
||||
log::info!("Ending {} with \"{}\"", info, err);
|
||||
}
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed) - 1);
|
||||
log::trace!("Session count {}", TASK_COUNT.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -336,7 +383,7 @@ where
|
|||
}
|
||||
IpStackStream::UnknownTransport(u) => {
|
||||
let len = u.payload().len();
|
||||
log::info!("#0 unhandled transport - Ip Protocol 0x{:02X}, length {}", u.ip_protocol(), len);
|
||||
log::info!("#0 unhandled transport - Ip Protocol {:?}, length {}", u.ip_protocol(), len);
|
||||
continue;
|
||||
}
|
||||
IpStackStream::UnknownNetwork(pkt) => {
|
||||
|
@ -345,7 +392,7 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(TASK_COUNT.load(Relaxed))
|
||||
}
|
||||
|
||||
async fn handle_virtual_dns_session(mut udp: IpStackUdpStream, dns: Arc<Mutex<VirtualDns>>) -> crate::Result<()> {
|
||||
|
@ -436,6 +483,128 @@ async fn handle_tcp_session(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "udpgw")]
|
||||
async fn handle_udp_gateway_session(
|
||||
mut udp_stack: IpStackUdpStream,
|
||||
udpgw_client: Arc<UdpGwClient>,
|
||||
udp_dst: &socks5_impl::protocol::Address,
|
||||
proxy_handler: Arc<Mutex<dyn ProxyHandler>>,
|
||||
socket_queue: Option<Arc<SocketQueue>>,
|
||||
ipv6_enabled: bool,
|
||||
) -> crate::Result<()> {
|
||||
let proxy_server_addr = { proxy_handler.lock().await.get_server_addr() };
|
||||
let udp_mtu = udpgw_client.get_udp_mtu();
|
||||
let udp_timeout = udpgw_client.get_udp_timeout();
|
||||
|
||||
let mut stream = loop {
|
||||
match udpgw_client.pop_server_connection_from_queue().await {
|
||||
Some(stream) => {
|
||||
if stream.is_closed() {
|
||||
continue;
|
||||
} else {
|
||||
break stream;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut tcp_server_stream = create_tcp_stream(&socket_queue, proxy_server_addr).await?;
|
||||
if let Err(e) = handle_proxy_session(&mut tcp_server_stream, proxy_handler).await {
|
||||
return Err(format!("udpgw connection error: {}", e).into());
|
||||
}
|
||||
break UdpGwClientStream::new(tcp_server_stream);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tcp_local_addr = stream.local_addr();
|
||||
let sn = stream.serial_number();
|
||||
|
||||
log::info!("[UdpGw] Beginning stream {} {} -> {}", sn, &tcp_local_addr, udp_dst);
|
||||
|
||||
let Some(mut reader) = stream.get_reader() else {
|
||||
return Err("get reader failed".into());
|
||||
};
|
||||
|
||||
let Some(mut writer) = stream.get_writer() else {
|
||||
return Err("get writer failed".into());
|
||||
};
|
||||
|
||||
let mut tmp_buf = vec![0; udp_mtu.into()];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
len = udp_stack.read(&mut tmp_buf) => {
|
||||
let read_len = match len {
|
||||
Ok(0) => {
|
||||
log::info!("[UdpGw] Ending stream {} {} <> {}", sn, &tcp_local_addr, udp_dst);
|
||||
break;
|
||||
}
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
log::info!("[UdpGw] Ending stream {} {} <> {} with udp stack \"{}\"", sn, &tcp_local_addr, udp_dst, e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
crate::traffic_status::traffic_status_update(read_len, 0)?;
|
||||
let sn = stream.serial_number();
|
||||
if let Err(e) = UdpGwClient::send_udpgw_packet(ipv6_enabled, &tmp_buf[0..read_len], udp_dst, sn, &mut writer).await {
|
||||
log::info!("[UdpGw] Ending stream {} {} <> {} with send_udpgw_packet {}", sn, &tcp_local_addr, udp_dst, e);
|
||||
break;
|
||||
}
|
||||
log::debug!("[UdpGw] stream {} {} -> {} send len {}", sn, &tcp_local_addr, udp_dst, read_len);
|
||||
stream.update_activity();
|
||||
}
|
||||
ret = UdpGwClient::recv_udpgw_packet(udp_mtu, udp_timeout, &mut reader) => {
|
||||
if let Ok((len, _)) = ret {
|
||||
crate::traffic_status::traffic_status_update(0, len)?;
|
||||
}
|
||||
match ret {
|
||||
Err(e) => {
|
||||
log::warn!("[UdpGw] Ending stream {} {} <> {} with recv_udpgw_packet {}", sn, &tcp_local_addr, udp_dst, e);
|
||||
stream.close();
|
||||
break;
|
||||
}
|
||||
Ok((_, packet)) => match packet {
|
||||
//should not received keepalive
|
||||
UdpGwResponse::KeepAlive => {
|
||||
log::error!("[UdpGw] Ending stream {} {} <> {} with recv keepalive", sn, &tcp_local_addr, udp_dst);
|
||||
stream.close();
|
||||
break;
|
||||
}
|
||||
//server udp may be timeout,can continue to receive udp data?
|
||||
UdpGwResponse::Error => {
|
||||
log::info!("[UdpGw] Ending stream {} {} <> {} with recv udp error", sn, &tcp_local_addr, udp_dst);
|
||||
stream.update_activity();
|
||||
continue;
|
||||
}
|
||||
UdpGwResponse::TcpClose => {
|
||||
log::error!("[UdpGw] Ending stream {} {} <> {} with tcp closed", sn, &tcp_local_addr, udp_dst);
|
||||
stream.close();
|
||||
break;
|
||||
}
|
||||
UdpGwResponse::Data(data) => {
|
||||
use socks5_impl::protocol::StreamOperation;
|
||||
let len = data.len();
|
||||
let f = data.header.flags;
|
||||
log::debug!("[UdpGw] stream {sn} {} <- {} receive {f} len {len}", &tcp_local_addr, udp_dst);
|
||||
if let Err(e) = udp_stack.write_all(&data.data).await {
|
||||
log::error!("[UdpGw] Ending stream {} {} <> {} with send_udp_packet {}", sn, &tcp_local_addr, udp_dst, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stream.update_activity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !stream.is_closed() {
|
||||
udpgw_client.store_server_connection_full(stream, reader, writer).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_udp_associate_session(
|
||||
mut udp_stack: IpStackUdpStream,
|
||||
proxy_type: ProxyType,
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
#![cfg(any(target_os = "ios", target_os = "android", target_os = "macos"))]
|
||||
|
||||
use crate::Args;
|
||||
use std::os::raw::c_int;
|
||||
|
||||
static TUN_QUIT: std::sync::Mutex<Option<tokio_util::sync::CancellationToken>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// Dummy function to make the build pass.
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub async fn desktop_run_async(_: Args, _: tokio_util::sync::CancellationToken) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mobile_run(args: Args, tun_mtu: u16, _packet_information: bool) -> c_int {
|
||||
let shutdown_token = tokio_util::sync::CancellationToken::new();
|
||||
{
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if lock.is_some() {
|
||||
log::error!("tun2proxy already started");
|
||||
return -1;
|
||||
}
|
||||
*lock = Some(shutdown_token.clone());
|
||||
} else {
|
||||
log::error!("failed to lock tun2proxy quit token");
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
let block = async move {
|
||||
let mut config = tun2::Configuration::default();
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(fd) = args.tun_fd {
|
||||
config.raw_fd(fd);
|
||||
if let Some(v) = args.close_fd_on_drop {
|
||||
config.close_fd_on_drop(v);
|
||||
};
|
||||
} else if let Some(ref tun) = args.tun {
|
||||
config.tun_name(tun);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if let Some(ref tun) = args.tun {
|
||||
config.tun_name(tun);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
config.platform_config(|config| {
|
||||
config.packet_information(_packet_information);
|
||||
});
|
||||
|
||||
let device = tun2::create_as_async(&config).map_err(std::io::Error::from)?;
|
||||
let join_handle = tokio::spawn(crate::run(device, tun_mtu, args, shutdown_token));
|
||||
|
||||
join_handle.await.map_err(std::io::Error::from)?
|
||||
};
|
||||
|
||||
let exit_code = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
|
||||
Err(e) => {
|
||||
log::error!("failed to create tokio runtime with error: {:?}", e);
|
||||
-1
|
||||
}
|
||||
Ok(rt) => match rt.block_on(block) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
log::error!("failed to run tun2proxy with error: {:?}", e);
|
||||
-2
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
exit_code
|
||||
}
|
||||
|
||||
pub fn mobile_stop() -> c_int {
|
||||
if let Ok(mut lock) = TUN_QUIT.lock() {
|
||||
if let Some(shutdown_token) = lock.take() {
|
||||
shutdown_token.cancel();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
-1
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
#![cfg(target_os = "linux")]
|
||||
|
||||
use crate::{error, SocketDomain, SocketProtocol};
|
||||
use crate::{SocketDomain, SocketProtocol, error};
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{self, FdFlag},
|
||||
sys::socket::{cmsg_space, getsockopt, recvmsg, sendmsg, sockopt, ControlMessage, ControlMessageOwned, MsgFlags, SockType},
|
||||
sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, SockType, cmsg_space, getsockopt, recvmsg, sendmsg, sockopt},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
|
@ -16,14 +16,14 @@ use tokio::net::{TcpSocket, UdpSocket, UnixDatagram};
|
|||
|
||||
const REQUEST_BUFFER_SIZE: usize = 64;
|
||||
|
||||
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[derive(bincode::Encode, bincode::Decode, Hash, Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
struct Request {
|
||||
protocol: SocketProtocol,
|
||||
domain: SocketDomain,
|
||||
number: u32,
|
||||
}
|
||||
|
||||
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug, Hash, Copy, Clone, Eq, Serialize, Deserialize)]
|
||||
enum Response {
|
||||
Ok,
|
||||
}
|
||||
|
@ -135,14 +135,21 @@ where
|
|||
// Borrow socket as mut to prevent multiple simultaneous requests
|
||||
let socket = socket.deref_mut();
|
||||
|
||||
let mut request = [0u8; 1000];
|
||||
|
||||
// Send request
|
||||
let request = bincode::serialize(&Request {
|
||||
let size = bincode::encode_into_slice(
|
||||
Request {
|
||||
protocol: T::domain(),
|
||||
domain,
|
||||
number,
|
||||
})?;
|
||||
},
|
||||
&mut request,
|
||||
bincode::config::standard(),
|
||||
)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
socket.send(&request[..]).await?;
|
||||
socket.send(&request[..size]).await?;
|
||||
|
||||
// Receive response
|
||||
loop {
|
||||
|
@ -161,7 +168,9 @@ where
|
|||
|
||||
// Parse response
|
||||
let response = &msg.iovs().next().unwrap()[..msg.bytes];
|
||||
let response: Response = bincode::deserialize(response)?;
|
||||
let response: Response = bincode::decode_from_slice(response, bincode::config::standard())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?
|
||||
.0;
|
||||
if !matches!(response, Response::Ok) {
|
||||
return Err("Request for new sockets failed".into());
|
||||
}
|
||||
|
@ -194,10 +203,14 @@ pub async fn process_socket_requests(socket: &UnixDatagram) -> error::Result<()>
|
|||
|
||||
let len = socket.recv(&mut buf[..]).await?;
|
||||
|
||||
let request: Request = bincode::deserialize(&buf[..len])?;
|
||||
let request: Request = bincode::decode_from_slice(&buf[..len], bincode::config::standard())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?
|
||||
.0;
|
||||
|
||||
let response = Response::Ok;
|
||||
let buf = bincode::serialize(&response)?;
|
||||
let mut buf = [0u8; 1000];
|
||||
let size = bincode::encode_into_slice(response, &mut buf, bincode::config::standard())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
let mut owned_fd_buf: Vec<OwnedFd> = Vec::with_capacity(request.number as usize);
|
||||
for _ in 0..request.number {
|
||||
|
@ -223,7 +236,7 @@ pub async fn process_socket_requests(socket: &UnixDatagram) -> error::Result<()>
|
|||
|
||||
let raw_fd_buf: Vec<RawFd> = owned_fd_buf.iter().map(|fd| fd.as_raw_fd()).collect();
|
||||
let cmsg = ControlMessage::ScmRights(&raw_fd_buf[..]);
|
||||
let iov = [IoSlice::new(&buf[..])];
|
||||
let iov = [IoSlice::new(&buf[..size])];
|
||||
|
||||
sendmsg::<()>(socket.as_raw_fd(), &iov, &[cmsg], MsgFlags::empty(), None)?;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
proxy_handler::{ProxyHandler, ProxyHandlerManager},
|
||||
session_info::SessionInfo,
|
||||
};
|
||||
use socks5_impl::protocol::{self, handshake, password_method, Address, AuthMethod, StreamOperation, UserKey, Version};
|
||||
use socks5_impl::protocol::{self, Address, AuthMethod, StreamOperation, UserKey, Version, handshake, password_method};
|
||||
use std::{collections::VecDeque, net::SocketAddr, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use std::sync::{LazyLock, Mutex};
|
|||
/// # Safety
|
||||
///
|
||||
/// set traffic status callback.
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn tun2proxy_set_traffic_status_callback(
|
||||
send_interval_secs: u32,
|
||||
callback: Option<unsafe extern "C" fn(*const TrafficStatus, *mut c_void)>,
|
||||
|
@ -34,7 +34,7 @@ struct TrafficStatusCallback(Option<unsafe extern "C" fn(*const TrafficStatus, *
|
|||
impl TrafficStatusCallback {
|
||||
unsafe fn call(self, info: &TrafficStatus) {
|
||||
if let Some(cb) = self.0 {
|
||||
cb(info, self.1);
|
||||
unsafe { cb(info, self.1) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
578
src/udpgw.rs
Normal file
578
src/udpgw.rs
Normal file
|
@ -0,0 +1,578 @@
|
|||
use crate::error::Result;
|
||||
use socks5_impl::protocol::{Address, AsyncStreamOperation, BufMut, StreamOperation};
|
||||
use std::{collections::VecDeque, hash::Hash, net::SocketAddr, sync::atomic::Ordering::Relaxed};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{
|
||||
TcpStream,
|
||||
tcp::{OwnedReadHalf, OwnedWriteHalf},
|
||||
},
|
||||
sync::Mutex,
|
||||
time::{Duration, sleep},
|
||||
};
|
||||
|
||||
pub(crate) const UDPGW_LENGTH_FIELD_SIZE: usize = std::mem::size_of::<u16>();
|
||||
pub(crate) const UDPGW_MAX_CONNECTIONS: usize = 5;
|
||||
pub(crate) const UDPGW_KEEPALIVE_TIME: tokio::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct UdpFlag(pub u8);
|
||||
|
||||
impl UdpFlag {
|
||||
pub const ZERO: UdpFlag = UdpFlag(0x00);
|
||||
pub const KEEPALIVE: UdpFlag = UdpFlag(0x01);
|
||||
pub const ERR: UdpFlag = UdpFlag(0x20);
|
||||
pub const DATA: UdpFlag = UdpFlag(0x02);
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UdpFlag {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let flag = match self.0 {
|
||||
0x00 => "ZERO",
|
||||
0x01 => "KEEPALIVE",
|
||||
0x20 => "ERR",
|
||||
0x02 => "DATA",
|
||||
n => return write!(f, "Unknown UdpFlag(0x{:02X})", n),
|
||||
};
|
||||
write!(f, "{}", flag)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for UdpFlag {
|
||||
type Output = Self;
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
UdpFlag(self.0 & rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for UdpFlag {
|
||||
type Output = Self;
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
UdpFlag(self.0 | rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP Gateway Packet Format
|
||||
///
|
||||
/// The format is referenced from SOCKS5 packet format, with additional flags and connection ID fields.
|
||||
///
|
||||
/// `LEN`: This field is indicated the length of the packet, not including the length field itself.
|
||||
///
|
||||
/// `FLAGS`: This field is used to indicate the packet type. The flags are defined as follows:
|
||||
/// - `0x01`: Keepalive packet without address and data
|
||||
/// - `0x20`: Error packet without address and data
|
||||
/// - `0x02`: Data packet with address and data
|
||||
///
|
||||
/// `CONN_ID`: This field is used to indicate the unique connection ID for the packet.
|
||||
///
|
||||
/// `ATYP` & `DST.ADDR` & `DST.PORT`: This fields are used to indicate the remote address and port.
|
||||
/// It can be either an IPv4 address, an IPv6 address, or a domain name, depending on the `ATYP` field.
|
||||
/// The address format directly uses the address format of the [SOCKS5](https://datatracker.ietf.org/doc/html/rfc1928#section-4) protocol.
|
||||
/// - `ATYP`: Address Type, 1 byte, indicating the type of address ( 0x01-IPv4, 0x04-IPv6, or 0x03-domain name )
|
||||
/// - `DST.ADDR`: Destination Address. If `ATYP` is 0x01 or 0x04, it is 4 or 16 bytes of IP address;
|
||||
/// If `ATYP` is 0x03, it is a domain name, `DST.ADDR` is a variable length field,
|
||||
/// it begins with a 1-byte length field and then the domain name without null-termination,
|
||||
/// since the length field is 1 byte, the maximum length of the domain name is 255 bytes.
|
||||
/// - `DST.PORT`: Destination Port, 2 bytes, the port number of the destination address.
|
||||
///
|
||||
/// `DATA`: The data field, a variable length field, the length is determined by the `LEN` field.
|
||||
///
|
||||
/// All the digits fields are in big-endian byte order.
|
||||
///
|
||||
/// ```plain
|
||||
/// +-----+ +-------+---------+ +------+----------+----------+ +----------+
|
||||
/// | LEN | | FLAGS | CONN_ID | | ATYP | DST.ADDR | DST.PORT | | DATA |
|
||||
/// +-----+ +-------+---------+ +------+----------+----------+ +----------+
|
||||
/// | 2 | | 1 | 2 | | 1 | Variable | 2 | | Variable |
|
||||
/// +-----+ +-------+---------+ +------+----------+----------+ +----------+
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Packet {
|
||||
pub header: UdpgwHeader,
|
||||
pub address: Option<Address>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Packet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let addr = self.address.as_ref().map_or("None".to_string(), |addr| addr.to_string());
|
||||
let len = self.data.len();
|
||||
write!(f, "Packet {{ {}, address: {}, payload length: {} }}", self.header, addr, len)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Packet> for Vec<u8> {
|
||||
fn from(packet: Packet) -> Vec<u8> {
|
||||
(&packet).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Packet> for Vec<u8> {
|
||||
fn from(packet: &Packet) -> Vec<u8> {
|
||||
let mut bytes: Vec<u8> = vec![];
|
||||
packet.write_to_buf(&mut bytes);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Packet {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
if value.len() < UDPGW_LENGTH_FIELD_SIZE {
|
||||
return Err(std::io::ErrorKind::InvalidData.into());
|
||||
}
|
||||
let mut iter = std::io::Cursor::new(value);
|
||||
use tokio_util::bytes::Buf;
|
||||
let length = iter.get_u16();
|
||||
if value.len() < length as usize + UDPGW_LENGTH_FIELD_SIZE {
|
||||
return Err(std::io::ErrorKind::InvalidData.into());
|
||||
}
|
||||
let header = UdpgwHeader::retrieve_from_stream(&mut iter)?;
|
||||
let address = if header.flags & UdpFlag::DATA != UdpFlag::ZERO {
|
||||
Some(Address::retrieve_from_stream(&mut iter)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Packet::new(header, address, iter.chunk()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub fn new(header: UdpgwHeader, address: Option<Address>, data: &[u8]) -> Self {
|
||||
let data = data.to_vec();
|
||||
Packet { header, address, data }
|
||||
}
|
||||
|
||||
pub fn build_keepalive_packet(conn_id: u16) -> Self {
|
||||
Packet::new(UdpgwHeader::new(UdpFlag::KEEPALIVE, conn_id), None, &[])
|
||||
}
|
||||
|
||||
pub fn build_error_packet(conn_id: u16) -> Self {
|
||||
Packet::new(UdpgwHeader::new(UdpFlag::ERR, conn_id), None, &[])
|
||||
}
|
||||
|
||||
pub fn build_packet_from_address(conn_id: u16, remote_addr: &Address, data: &[u8]) -> std::io::Result<Self> {
|
||||
use socks5_impl::protocol::Address::{DomainAddress, SocketAddress};
|
||||
let packet = match remote_addr {
|
||||
SocketAddress(addr) => Packet::build_ip_packet(conn_id, *addr, data),
|
||||
DomainAddress(domain, port) => Packet::build_domain_packet(conn_id, *port, domain, data)?,
|
||||
};
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
pub fn build_ip_packet(conn_id: u16, remote_addr: SocketAddr, data: &[u8]) -> Self {
|
||||
let addr: Address = remote_addr.into();
|
||||
Packet::new(UdpgwHeader::new(UdpFlag::DATA, conn_id), Some(addr), data)
|
||||
}
|
||||
|
||||
pub fn build_domain_packet(conn_id: u16, port: u16, domain: &str, data: &[u8]) -> std::io::Result<Self> {
|
||||
if domain.len() > 255 {
|
||||
return Err(std::io::ErrorKind::InvalidInput.into());
|
||||
}
|
||||
let addr = Address::from((domain, port));
|
||||
Ok(Packet::new(UdpgwHeader::new(UdpFlag::DATA, conn_id), Some(addr), data))
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamOperation for Packet {
|
||||
fn retrieve_from_stream<R>(stream: &mut R) -> std::io::Result<Self>
|
||||
where
|
||||
R: std::io::Read,
|
||||
Self: Sized,
|
||||
{
|
||||
let mut buf = [0; UDPGW_LENGTH_FIELD_SIZE];
|
||||
stream.read_exact(&mut buf)?;
|
||||
let length = u16::from_be_bytes(buf) as usize;
|
||||
let header = UdpgwHeader::retrieve_from_stream(stream)?;
|
||||
let address = if header.flags & UdpFlag::DATA == UdpFlag::DATA {
|
||||
Some(Address::retrieve_from_stream(stream)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let read_len = header.len() + address.as_ref().map_or(0, |addr| addr.len());
|
||||
if length < read_len {
|
||||
return Err(std::io::ErrorKind::InvalidData.into());
|
||||
}
|
||||
let mut data = vec![0; length - read_len];
|
||||
stream.read_exact(&mut data)?;
|
||||
Ok(Packet::new(header, address, &data))
|
||||
}
|
||||
|
||||
fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
|
||||
let len = self.len() - UDPGW_LENGTH_FIELD_SIZE;
|
||||
buf.put_u16(len as u16);
|
||||
self.header.write_to_buf(buf);
|
||||
if let Some(addr) = &self.address {
|
||||
addr.write_to_buf(buf);
|
||||
}
|
||||
buf.put_slice(&self.data);
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
UDPGW_LENGTH_FIELD_SIZE + self.header.len() + self.address.as_ref().map_or(0, |addr| addr.len()) + self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncStreamOperation for Packet {
|
||||
async fn retrieve_from_async_stream<R>(r: &mut R) -> std::io::Result<Self>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + ?Sized,
|
||||
Self: Sized,
|
||||
{
|
||||
let mut buf = [0; UDPGW_LENGTH_FIELD_SIZE];
|
||||
r.read_exact(&mut buf).await?;
|
||||
let length = u16::from_be_bytes(buf) as usize;
|
||||
let header = UdpgwHeader::retrieve_from_async_stream(r).await?;
|
||||
let address = if header.flags & UdpFlag::DATA == UdpFlag::DATA {
|
||||
Some(Address::retrieve_from_async_stream(r).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let read_len = header.len() + address.as_ref().map_or(0, |addr| addr.len());
|
||||
if length < read_len {
|
||||
return Err(std::io::ErrorKind::InvalidData.into());
|
||||
}
|
||||
let mut data = vec![0; length - read_len];
|
||||
r.read_exact(&mut data).await?;
|
||||
Ok(Packet::new(header, address, &data))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct UdpgwHeader {
|
||||
pub flags: UdpFlag,
|
||||
pub conn_id: u16,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UdpgwHeader {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} conn_id: {}", self.flags, self.conn_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamOperation for UdpgwHeader {
|
||||
fn retrieve_from_stream<R>(stream: &mut R) -> std::io::Result<Self>
|
||||
where
|
||||
R: std::io::Read,
|
||||
Self: Sized,
|
||||
{
|
||||
let mut buf = [0; UdpgwHeader::static_len()];
|
||||
stream.read_exact(&mut buf)?;
|
||||
UdpgwHeader::try_from(&buf[..])
|
||||
}
|
||||
|
||||
fn write_to_buf<B: BufMut>(&self, buf: &mut B) {
|
||||
let bytes: Vec<u8> = self.into();
|
||||
buf.put_slice(&bytes);
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
Self::static_len()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncStreamOperation for UdpgwHeader {
|
||||
async fn retrieve_from_async_stream<R>(r: &mut R) -> std::io::Result<Self>
|
||||
where
|
||||
R: tokio::io::AsyncRead + Unpin + Send + ?Sized,
|
||||
Self: Sized,
|
||||
{
|
||||
let mut buf = [0; UdpgwHeader::static_len()];
|
||||
r.read_exact(&mut buf).await?;
|
||||
UdpgwHeader::try_from(&buf[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl UdpgwHeader {
|
||||
pub fn new(flags: UdpFlag, conn_id: u16) -> Self {
|
||||
UdpgwHeader { flags, conn_id }
|
||||
}
|
||||
|
||||
pub const fn static_len() -> usize {
|
||||
std::mem::size_of::<u8>() + std::mem::size_of::<u16>()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for UdpgwHeader {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
if value.len() < UdpgwHeader::static_len() {
|
||||
return Err(std::io::ErrorKind::InvalidData.into());
|
||||
}
|
||||
let conn_id = u16::from_be_bytes([value[1], value[2]]);
|
||||
Ok(UdpgwHeader::new(UdpFlag(value[0]), conn_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&UdpgwHeader> for Vec<u8> {
|
||||
fn from(header: &UdpgwHeader) -> Vec<u8> {
|
||||
let mut bytes = vec![0; header.len()];
|
||||
bytes[0] = header.flags.0;
|
||||
bytes[1..3].copy_from_slice(&header.conn_id.to_be_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum UdpGwResponse {
|
||||
KeepAlive,
|
||||
Error,
|
||||
TcpClose,
|
||||
Data(Packet),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UdpGwResponse {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UdpGwResponse::KeepAlive => write!(f, "KeepAlive"),
|
||||
UdpGwResponse::Error => write!(f, "Error"),
|
||||
UdpGwResponse::TcpClose => write!(f, "TcpClose"),
|
||||
UdpGwResponse::Data(packet) => write!(f, "Data({})", packet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SERIAL_NUMBER: std::sync::atomic::AtomicU16 = std::sync::atomic::AtomicU16::new(1);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UdpGwClientStream {
|
||||
local_addr: SocketAddr,
|
||||
writer: Option<OwnedWriteHalf>,
|
||||
reader: Option<OwnedReadHalf>,
|
||||
closed: bool,
|
||||
last_activity: std::time::Instant,
|
||||
serial_number: u16,
|
||||
}
|
||||
|
||||
impl UdpGwClientStream {
|
||||
pub fn close(&mut self) {
|
||||
self.closed = true;
|
||||
}
|
||||
|
||||
pub fn get_reader(&mut self) -> Option<OwnedReadHalf> {
|
||||
self.reader.take()
|
||||
}
|
||||
|
||||
pub fn set_reader(&mut self, reader: Option<OwnedReadHalf>) {
|
||||
self.reader = reader;
|
||||
}
|
||||
|
||||
pub fn set_writer(&mut self, writer: Option<OwnedWriteHalf>) {
|
||||
self.writer = writer;
|
||||
}
|
||||
|
||||
pub fn get_writer(&mut self) -> Option<OwnedWriteHalf> {
|
||||
self.writer.take()
|
||||
}
|
||||
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
|
||||
pub fn update_activity(&mut self) {
|
||||
self.last_activity = std::time::Instant::now();
|
||||
}
|
||||
|
||||
pub fn is_closed(&self) -> bool {
|
||||
self.closed
|
||||
}
|
||||
|
||||
pub fn serial_number(&self) -> u16 {
|
||||
self.serial_number
|
||||
}
|
||||
|
||||
pub fn new(tcp_server_stream: TcpStream) -> Self {
|
||||
let default = "0.0.0.0:0".parse::<SocketAddr>().unwrap();
|
||||
let local_addr = tcp_server_stream.local_addr().unwrap_or(default);
|
||||
let (reader, writer) = tcp_server_stream.into_split();
|
||||
let serial_number = SERIAL_NUMBER.fetch_add(1, Relaxed);
|
||||
UdpGwClientStream {
|
||||
local_addr,
|
||||
reader: Some(reader),
|
||||
writer: Some(writer),
|
||||
last_activity: std::time::Instant::now(),
|
||||
closed: false,
|
||||
serial_number,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UdpGwClient {
|
||||
udp_mtu: u16,
|
||||
max_connections: usize,
|
||||
udp_timeout: u64,
|
||||
keepalive_time: Duration,
|
||||
udpgw_server: SocketAddr,
|
||||
server_connections: Mutex<VecDeque<UdpGwClientStream>>,
|
||||
}
|
||||
|
||||
impl UdpGwClient {
|
||||
pub fn new(udp_mtu: u16, max_connections: usize, keepalive_time: Duration, udp_timeout: u64, udpgw_server: SocketAddr) -> Self {
|
||||
let server_connections = Mutex::new(VecDeque::with_capacity(max_connections));
|
||||
UdpGwClient {
|
||||
udp_mtu,
|
||||
max_connections,
|
||||
udp_timeout,
|
||||
udpgw_server,
|
||||
keepalive_time,
|
||||
server_connections,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_udp_mtu(&self) -> u16 {
|
||||
self.udp_mtu
|
||||
}
|
||||
|
||||
pub(crate) fn get_udp_timeout(&self) -> u64 {
|
||||
self.udp_timeout
|
||||
}
|
||||
|
||||
pub(crate) async fn pop_server_connection_from_queue(&self) -> Option<UdpGwClientStream> {
|
||||
self.server_connections.lock().await.pop_front()
|
||||
}
|
||||
|
||||
pub(crate) async fn store_server_connection(&self, stream: UdpGwClientStream) {
|
||||
if self.server_connections.lock().await.len() < self.max_connections {
|
||||
self.server_connections.lock().await.push_back(stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn store_server_connection_full(&self, mut stream: UdpGwClientStream, reader: OwnedReadHalf, writer: OwnedWriteHalf) {
|
||||
if self.server_connections.lock().await.len() < self.max_connections {
|
||||
stream.set_reader(Some(reader));
|
||||
stream.set_writer(Some(writer));
|
||||
self.server_connections.lock().await.push_back(stream);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_udpgw_server_addr(&self) -> SocketAddr {
|
||||
self.udpgw_server
|
||||
}
|
||||
|
||||
/// Heartbeat task asynchronous function to periodically check and maintain the active state of the server connection.
|
||||
pub(crate) async fn heartbeat_task(&self) -> std::io::Result<()> {
|
||||
loop {
|
||||
sleep(self.keepalive_time).await;
|
||||
let mut streams = Vec::new();
|
||||
|
||||
while let Some(stream) = self.pop_server_connection_from_queue().await {
|
||||
if !stream.is_closed() {
|
||||
streams.push(stream);
|
||||
}
|
||||
}
|
||||
|
||||
let (mut tx, mut rx) = (0, 0);
|
||||
|
||||
for mut stream in streams {
|
||||
if stream.last_activity.elapsed() < self.keepalive_time {
|
||||
self.store_server_connection(stream).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut stream_reader) = stream.get_reader() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(mut stream_writer) = stream.get_writer() else {
|
||||
continue;
|
||||
};
|
||||
let local_addr = stream_writer.local_addr()?;
|
||||
let sn = stream.serial_number();
|
||||
let keepalive_packet: Vec<u8> = Packet::build_keepalive_packet(sn).into();
|
||||
tx += keepalive_packet.len();
|
||||
if let Err(e) = stream_writer.write_all(&keepalive_packet).await {
|
||||
log::warn!("stream {} {:?} send keepalive failed: {}", sn, local_addr, e);
|
||||
continue;
|
||||
}
|
||||
match UdpGwClient::recv_udpgw_packet(self.udp_mtu, self.udp_timeout, &mut stream_reader).await {
|
||||
Ok((len, UdpGwResponse::KeepAlive)) => {
|
||||
stream.update_activity();
|
||||
self.store_server_connection_full(stream, stream_reader, stream_writer).await;
|
||||
log::trace!("stream {sn} {:?} send keepalive and recieve it successfully", local_addr);
|
||||
rx += len;
|
||||
}
|
||||
Ok((len, v)) => {
|
||||
log::debug!("stream {sn} {:?} keepalive unexpected response: {v}", local_addr);
|
||||
rx += len;
|
||||
}
|
||||
Err(e) => log::debug!("stream {sn} {:?} keepalive no response, error \"{e}\"", local_addr),
|
||||
}
|
||||
}
|
||||
crate::traffic_status::traffic_status_update(tx, rx)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the UDP response data.
|
||||
pub(crate) fn parse_udp_response(udp_mtu: u16, packet: Packet) -> Result<UdpGwResponse> {
|
||||
let flags = packet.header.flags;
|
||||
if flags & UdpFlag::ERR == UdpFlag::ERR {
|
||||
return Ok(UdpGwResponse::Error);
|
||||
}
|
||||
if flags & UdpFlag::KEEPALIVE == UdpFlag::KEEPALIVE {
|
||||
return Ok(UdpGwResponse::KeepAlive);
|
||||
}
|
||||
if packet.data.len() > udp_mtu as usize {
|
||||
return Err("too much data".into());
|
||||
}
|
||||
Ok(UdpGwResponse::Data(packet))
|
||||
}
|
||||
|
||||
/// Receives a UDP gateway packet.
|
||||
///
|
||||
/// This function is responsible for receiving packets from the UDP gateway
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `udp_mtu`: The maximum transmission unit size for UDP packets.
|
||||
/// - `udp_timeout`: The timeout in seconds for receiving UDP packets.
|
||||
/// - `stream`: A mutable reference to the UDP gateway client stream reader.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Result<UdpGwResponse>`: Returns a result type containing the parsed UDP gateway response, or an error if one occurs.
|
||||
pub(crate) async fn recv_udpgw_packet(udp_mtu: u16, udp_timeout: u64, stream: &mut OwnedReadHalf) -> Result<(usize, UdpGwResponse)> {
|
||||
let packet = tokio::time::timeout(
|
||||
tokio::time::Duration::from_secs(udp_timeout + 2),
|
||||
Packet::retrieve_from_async_stream(stream),
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::from)??;
|
||||
Ok((packet.len(), UdpGwClient::parse_udp_response(udp_mtu, packet)?))
|
||||
}
|
||||
|
||||
/// Sends a UDP gateway packet.
|
||||
///
|
||||
/// This function constructs and sends a UDP gateway packet based on the IPv6 enabled status, data length,
|
||||
/// remote address, domain (if any), connection ID, and the UDP gateway client writer stream.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ipv6_enabled` - Whether IPv6 is enabled
|
||||
/// * `data` - The data packet
|
||||
/// * `remote_addr` - Remote address
|
||||
/// * `conn_id` - Connection ID
|
||||
/// * `stream` - UDP gateway client writer stream
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(())` if the packet is sent successfully, otherwise returns an error.
|
||||
pub(crate) async fn send_udpgw_packet(
|
||||
ipv6_enabled: bool,
|
||||
data: &[u8],
|
||||
remote_addr: &socks5_impl::protocol::Address,
|
||||
conn_id: u16,
|
||||
stream: &mut OwnedWriteHalf,
|
||||
) -> Result<()> {
|
||||
if !ipv6_enabled && remote_addr.get_type() == socks5_impl::protocol::AddressType::IPv6 {
|
||||
return Err("ipv6 not support".into());
|
||||
}
|
||||
let out_data: Vec<u8> = Packet::build_packet_from_address(conn_id, remote_addr, data)?.into();
|
||||
stream.write_all(&out_data).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::error::Result;
|
||||
use hashlink::{linked_hash_map::RawEntryMut, LruCache};
|
||||
use hashlink::{LruCache, linked_hash_map::RawEntryMut};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::TryInto,
|
||||
|
|
|
@ -73,13 +73,21 @@ fn run_service(_arguments: Vec<std::ffi::OsString>) -> Result<(), crate::BoxErro
|
|||
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
|
||||
rt.block_on(async {
|
||||
unsafe extern "C" fn traffic_cb(status: *const crate::TrafficStatus, _: *mut std::ffi::c_void) {
|
||||
let status = &*status;
|
||||
let status = unsafe { &*status };
|
||||
log::debug!("Traffic: ▲ {} : ▼ {}", status.tx, status.rx);
|
||||
}
|
||||
unsafe { crate::tun2proxy_set_traffic_status_callback(1, Some(traffic_cb), std::ptr::null_mut()) };
|
||||
|
||||
if let Err(err) = crate::desktop_run_async(args, shutdown_token).await {
|
||||
log::error!("main loop error: {}", err);
|
||||
let ret = crate::general_run_async(args.clone(), tun::DEFAULT_MTU, false, shutdown_token).await;
|
||||
match &ret {
|
||||
Ok(sessions) => {
|
||||
if args.exit_on_fatal_error && *sessions >= args.max_sessions {
|
||||
log::error!("Forced exit due to max sessions reached ({sessions}/{})", args.max_sessions);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
log::debug!("tun2proxy exited normally, current sessions: {sessions}");
|
||||
}
|
||||
Err(err) => log::error!("main loop error: {err}"),
|
||||
}
|
||||
Ok::<(), crate::Error>(())
|
||||
})?;
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
requests
|
||||
python-dotenv
|
||||
psutil
|
|
@ -4,6 +4,7 @@ import os
|
|||
import subprocess
|
||||
import time
|
||||
import unittest
|
||||
import psutil
|
||||
|
||||
import dotenv
|
||||
import requests
|
||||
|
@ -33,6 +34,14 @@ def get_tool_path():
|
|||
default = default[0] if len(default) > 0 else 'tun2proxy-bin'
|
||||
return os.environ.get('TOOL_PATH', default)
|
||||
|
||||
def sudo_kill_process_and_children(proc):
|
||||
try:
|
||||
for child in psutil.Process(proc.pid).children(recursive=True):
|
||||
if child.name() == 'tun2proxy-bin':
|
||||
subprocess.run(['sudo', 'kill', str(child.pid)])
|
||||
subprocess.run(['sudo', 'kill', str(proc.pid)])
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
class Tun2ProxyTest(unittest.TestCase):
|
||||
@staticmethod
|
||||
|
@ -49,6 +58,7 @@ class Tun2ProxyTest(unittest.TestCase):
|
|||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
sudo_kill_process_and_children(p)
|
||||
p.terminate()
|
||||
p.wait()
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue