mirror of
https://github.com/tun2proxy/tun2proxy.git
synced 2025-06-30 12:39:57 +00:00
Compare commits
416 commits
r15703a482
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
a8ebe0b9be | ||
|
31b0972801 | ||
|
9a6c96cf8b | ||
|
b5fbaa2d19 | ||
|
3baa41a1fb | ||
|
0cf4427ef6 | ||
|
bc00dcc5ae | ||
|
d87562b8d3 | ||
|
fa09daabac | ||
|
b36473ced9 | ||
|
584bdc17ed | ||
|
1880396822 | ||
|
8b4ecabd8f | ||
|
fbc47a3001 | ||
|
88d31ce168 | ||
|
ddebf5ee50 | ||
|
8cdb4f535d | ||
|
6a5692cea0 | ||
|
3dc8f222cb | ||
|
7c32b62727 | ||
|
cf4a565f93 | ||
|
54f7dbc81b | ||
|
b71f479bf3 | ||
|
2ead13a3f4 | ||
|
88423039c6 | ||
|
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 | ||
|
918e6137ab | ||
|
d093973160 | ||
|
4ef71a5b4c | ||
|
b03032b8cd | ||
|
c991006f4c | ||
|
fe32a65291 | ||
|
93e15e0a8b | ||
|
b74aeab182 | ||
|
c9b24a865c | ||
|
2396d769d2 | ||
|
b24d48a042 | ||
|
6c8ae7a33f | ||
|
77d651dc70 | ||
|
febd654f35 | ||
|
143f203fde | ||
|
a5bc8f49b4 | ||
|
1ccba18273 | ||
|
607d709c03 | ||
|
e817257866 | ||
|
c583e884b5 | ||
|
1e6c6f4f66 | ||
|
c167f45a5e | ||
|
02b15951b6 | ||
|
6dadc1504a | ||
|
187e251142 | ||
|
15646925a7 | ||
|
beb3d364a8 | ||
|
8334acd085 | ||
|
1e7f649192 | ||
|
8c28f2e000 | ||
|
3f76ccec97 | ||
|
f787ff6d23 | ||
|
1dd6746bbc | ||
|
6567b6bc00 | ||
|
016aaa6128 | ||
|
824b443d2b | ||
|
06ed994655 | ||
|
e879599e6b | ||
|
0ca92dcdc2 | ||
|
635c7e557f | ||
|
15fe95a2c6 | ||
|
d5a404fda7 | ||
|
3b2adf92cb | ||
|
1ba8f8b167 | ||
|
48f527ad81 | ||
|
060ca5740f | ||
|
bb1a1fe286 | ||
|
d8d40b09de | ||
|
ea0c10a5c1 | ||
|
01ba8f382f | ||
|
b525d3f99e | ||
|
b8c22db037 | ||
|
dbf960884d | ||
|
b0432c7659 | ||
|
628e6cba84 | ||
|
203cfba302 | ||
|
9d9c152b54 | ||
|
3b5f526728 | ||
|
1789259f6f | ||
|
4243057fbf | ||
|
07ffbe057c | ||
|
4554d3bc55 | ||
|
a082a6f45b | ||
|
4b0ca087eb | ||
|
1023f00d12 | ||
|
30a54329e4 | ||
|
e604dec01c | ||
|
d062b1b66a | ||
|
e6360d83a7 | ||
|
588364d060 | ||
|
3202e7bbd2 | ||
|
3980b985f2 | ||
|
64dd43c6f3 | ||
|
0f241325ad | ||
|
5e32994f91 | ||
|
04a0555101 | ||
|
a9ef8f658b | ||
|
8438eddc95 | ||
|
c36c4ecf1b | ||
|
03f98a0741 | ||
|
8aa2a66942 | ||
|
f418ca4fe7 | ||
|
d5d847fa92 | ||
|
09994d43cc | ||
|
2df59ae596 | ||
|
7bee2e0968 | ||
|
58364580f5 | ||
|
18f4689d21 | ||
|
ba1615fcd1 | ||
|
92011edd43 | ||
|
84c03426f2 | ||
|
e582d6cbec | ||
|
c1d93942cc | ||
|
18044a8056 | ||
|
ebbe939f85 | ||
|
0239a225a1 | ||
|
40368dd232 | ||
|
4f5a128972 | ||
|
e8469f0aee | ||
|
af6a8a3cb0 | ||
|
f9f5401ba4 | ||
|
56be614334 | ||
|
181497e709 | ||
|
a08b3338c3 | ||
|
d351b5031c | ||
|
050f8c0e65 | ||
|
5e99c9f874 | ||
|
361cf95f4e | ||
|
74e5220d08 | ||
|
b7e59b130e | ||
|
ce0c02b3bf | ||
|
4adc38c726 | ||
|
eab795e61c | ||
|
715a85920c | ||
|
c430d76534 | ||
|
3fe47d92ec | ||
|
c9272609b8 | ||
|
3a156f5837 | ||
|
9841987031 | ||
|
bd96807bf8 | ||
|
7cb251c190 | ||
|
989406d00c | ||
|
d3e77e6c17 | ||
|
fb7b6862e5 | ||
|
7e7aadb04b | ||
|
4ab6f1a9bc | ||
|
01a0d9164d | ||
|
b3314f5abc | ||
|
ee63dc1559 | ||
|
3628533c8b | ||
|
a52dccd827 | ||
|
444e72689c | ||
|
12efc5f392 | ||
|
1d49ec87ad | ||
|
5c228ca07e | ||
|
1b859a5374 | ||
|
a5db99b03b | ||
|
498a43b471 | ||
|
d03e3c268d | ||
|
91fcd07733 | ||
|
bd27833c29 | ||
|
cfbc5fabb1 | ||
|
b11e49b455 | ||
|
129450a9db | ||
|
977c3ce518 | ||
|
a1083273ee | ||
|
a26621bbcd | ||
|
e9c378099e | ||
|
9f60eee2e1 | ||
|
5514da71f9 | ||
|
a317a3fc9e | ||
|
2a9775ce2e | ||
|
2434c62524 | ||
|
9a4bd9f800 | ||
|
ea5ffff82c | ||
|
8a67915388 | ||
|
8067394003 | ||
|
4454ccc811 | ||
|
3e373677bc | ||
|
9c4fa4260a | ||
|
337619169e | ||
|
61ed6d62c4 | ||
|
0edd07479d | ||
|
2b3463c55c | ||
|
97c4aa5137 | ||
|
ebec547ccb | ||
|
e5041e6d9e | ||
|
4016e401b2 | ||
|
07ec58532d | ||
|
67c2aa1a22 | ||
|
3879e04327 | ||
|
0e654eb4bd | ||
|
9396db4a52 | ||
|
fe85ecd15c | ||
|
c4ed29b234 | ||
|
286ce0ca6d | ||
|
980ae0172e | ||
|
e3494d921c | ||
|
0ab52c623b | ||
|
e08a0f683d | ||
|
9b27dd2df2 | ||
|
c6f9610eb3 | ||
|
989c42ee61 | ||
|
a9a562029f | ||
|
5d722fc2a3 | ||
|
b50cac82c0 | ||
|
299b51667d | ||
|
cea0e0fa27 | ||
|
6169014564 | ||
|
c1ea5f1af2 | ||
|
d75488f1d8 | ||
|
fc4d29dd2e | ||
|
c0c7fda891 | ||
|
05cb35fabb | ||
|
03aa70f3c2 | ||
|
a54e6ae23e | ||
|
d4568c4676 | ||
|
b5d8f0ee48 | ||
|
cc46526af0 | ||
|
c723adce4f | ||
|
c1b322a01e | ||
|
f175813cc8 | ||
|
ef6f67b975 | ||
|
8b014322fc | ||
|
b8dab403e9 | ||
|
59fa5b155e | ||
|
2122cc0ba8 | ||
|
538e40d05b | ||
|
5bd62d3101 | ||
|
e5a645638a | ||
|
11995d525b | ||
|
0e3b45be4a | ||
|
a17d9587d6 | ||
|
abcff395d8 | ||
|
0044756f78 | ||
|
bbb8d3b244 | ||
|
4b42413ab0 | ||
|
c41f3c46a0 | ||
|
eac0ee90eb | ||
|
dc7fc3990c | ||
|
02b85739cb | ||
|
d04344238a | ||
|
4014c9891c | ||
|
11d4e4a0dc | ||
|
d7861128f4 | ||
|
e518355756 | ||
|
72a00af0ed | ||
|
cdbed3ed9b | ||
|
edb775941e | ||
|
3b5f803da8 | ||
|
d7d69ce927 | ||
|
9880741dc1 | ||
|
2211ec6d7a | ||
|
49dca1b535 | ||
|
1f5586b880 | ||
|
df7ecfd6a9 | ||
|
641363e0bc | ||
|
b2505dcfd7 | ||
|
8b566b66d7 | ||
|
fb86172ecc | ||
|
40f8870033 | ||
|
0f3903f455 | ||
|
d42d3a8287 | ||
|
0d1677fb73 | ||
|
89aeffe195 | ||
|
10ade80488 | ||
|
17566451cf | ||
|
3c09c2699d | ||
|
0f67dd6981 | ||
|
3543472c38 | ||
|
b244286e4d | ||
|
aa059e0dd5 | ||
|
b0e275ec08 | ||
|
5301cf8d37 | ||
|
d5b76c18cc | ||
|
9ad22fc419 | ||
|
6439cc7b43 | ||
|
60b9683fac | ||
|
04d4faff68 | ||
|
01157915b3 | ||
|
b019ace2e1 | ||
|
334514cfc1 | ||
|
1bea9ba9ea | ||
|
119c9fef99 | ||
|
30420059cc | ||
|
da87fa8d5a | ||
|
46ca342aba | ||
|
d00a18c865 | ||
|
57851f029e | ||
|
489d5fec00 | ||
|
94835c41a4 | ||
|
382c2ac6e3 | ||
|
507def8f29 | ||
|
855aaa04fa | ||
|
64ab4b503c | ||
|
ca5b550e44 | ||
|
3720c41a6b | ||
|
ff9c258fbd | ||
|
4d9b10fd1c | ||
|
b92f2efd81 | ||
|
3b9207fb7a | ||
|
c8b13fc404 | ||
|
da665b3825 | ||
|
41feb84c29 | ||
|
5fbce82032 | ||
|
5bb4bbf022 | ||
|
30d7217374 | ||
|
5ce2e85919 | ||
|
4ebd019cb5 | ||
|
1031f586f7 | ||
|
8d835dc96d | ||
|
a00f4b1a8b | ||
|
6e81e78dfb | ||
|
c61b6c74cd | ||
|
ab9f8011f0 | ||
|
3e26675919 | ||
|
a292be4bd8 | ||
|
1dc827e84c | ||
|
e6b1e93cd0 | ||
|
45dae79263 | ||
|
86429ee8eb | ||
|
6767076a6b | ||
|
75bfdcc95a | ||
|
e5d1cfbef1 | ||
|
fb28783598 | ||
|
ad72147ff4 | ||
|
5e218c2130 | ||
|
034417f525 | ||
|
0c45714a45 | ||
|
0027c5ac4e | ||
|
b838583bf1 | ||
|
d94cc90663 | ||
|
42878c29fd | ||
|
f67d8b23a8 | ||
|
cba6ba7318 | ||
|
7442abece5 | ||
|
62a04229db | ||
|
fb3ad33b53 | ||
|
500f6ef21f | ||
|
9437308283 | ||
|
cb1babebd4 | ||
|
b669b9de22 | ||
|
c0cff1da58 | ||
|
fd48be5feb | ||
|
70cea8e11f | ||
|
2cf7c9cdea | ||
|
1a53e2bb52 | ||
|
14279a482c | ||
|
3fc112fc2c | ||
|
44122f3c68 | ||
|
7818829760 | ||
|
10a674d1c9 | ||
|
0be39345a8 | ||
|
6d9767db42 |
66 changed files with 5907 additions and 2488 deletions
9
.cargo/config.toml
Normal file
9
.cargo/config.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[registries.crates-io]
|
||||||
|
protocol = "sparse"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
# target = ["x86_64-unknown-linux-gnu"]
|
||||||
|
# target = ["aarch64-linux-android"]
|
||||||
|
# target = ["aarch64-apple-ios"]
|
||||||
|
# target = ["x86_64-pc-windows-msvc"]
|
||||||
|
# target = ["x86_64-apple-darwin"]
|
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
.gitignore
|
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
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 }}
|
26
.github/workflows/close-stale-issues.yml
vendored
Normal file
26
.github/workflows/close-stale-issues.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: Close stale issues and PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *" # run a cron job every day at midnight
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close stale issues and PRs
|
||||||
|
uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
|
stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||||
|
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||||
|
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
|
||||||
|
days-before-issue-stale: 30
|
||||||
|
days-before-pr-stale: 45
|
||||||
|
days-before-issue-close: 5
|
||||||
|
days-before-pr-close: 10
|
||||||
|
stale-issue-label: 'no-issue-activity'
|
||||||
|
exempt-issue-labels: 'keep-open,awaiting-approval,work-in-progress'
|
||||||
|
stale-pr-label: 'no-pr-activity'
|
||||||
|
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||||
|
# only-labels: 'awaiting-feedback,awaiting-answers'
|
50
.github/workflows/format-build.yml
vendored
50
.github/workflows/format-build.yml
vendored
|
@ -1,50 +0,0 @@
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
name: Build and Formatting Tests
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
name: Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
name: Rustfmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- run: rustup component add rustfmt
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
|
|
||||||
clippy:
|
|
||||||
name: Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- run: rustup component add clippy
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
args: -- -D warnings
|
|
11
.github/workflows/install-cross.sh
vendored
Executable file
11
.github/workflows/install-cross.sh
vendored
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
curl -s https://api.github.com/repos/cross-rs/cross/releases/latest \
|
||||||
|
| grep cross-x86_64-unknown-linux-gnu.tar.gz \
|
||||||
|
| cut -d : -f 2,3 \
|
||||||
|
| tr -d \" \
|
||||||
|
| wget -qi -
|
||||||
|
|
||||||
|
tar -zxvf cross-x86_64-unknown-linux-gnu.tar.gz -C /usr/bin
|
||||||
|
rm -f cross-x86_64-unknown-linux-gnu.tar.gz
|
||||||
|
|
72
.github/workflows/publish-docker.yml
vendored
Normal file
72
.github/workflows/publish-docker.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
name: Publish Docker Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: [ 'v*.*.*' ]
|
||||||
|
|
||||||
|
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
# This also contains the owner, i.e. tun2proxy/tun2proxy.
|
||||||
|
IMAGE_PATH: ${{ github.repository }}
|
||||||
|
IMAGE_NAME: ${{ github.event.repository.name }}
|
||||||
|
DEFAULT_OS: scratch
|
||||||
|
|
||||||
|
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
name: Build and push Docker image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ 'scratch', 'ubuntu', 'alpine' ]
|
||||||
|
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
#
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Add support for more platforms with QEMU (optional)
|
||||||
|
# https://github.com/docker/setup-qemu-action
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||||
|
- name: Extract metadata (tags, labels) for Docker Image
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
# We publish the images with an OS-suffix.
|
||||||
|
# The image based on a default OS is also published without a suffix.
|
||||||
|
images: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_PATH }}-${{ matrix.os }}
|
||||||
|
${{ env.DEFAULT_OS == matrix.os && format('{0}/{1}', env.REGISTRY, env.IMAGE_PATH) || '' }}
|
||||||
|
|
||||||
|
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||||
|
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
||||||
|
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
target: ${{ env.IMAGE_NAME }}-${{ matrix.os }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
149
.github/workflows/publish-exe.yml
vendored
149
.github/workflows/publish-exe.yml
vendored
|
@ -1,33 +1,140 @@
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
name: Build and publish executable
|
name: Publish Releases
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_publish:
|
build_publish:
|
||||||
name: Build and publish executable
|
name: Publishing Tasks
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
- i686-unknown-linux-musl
|
||||||
|
- aarch64-unknown-linux-gnu
|
||||||
|
- armv7-unknown-linux-musleabi
|
||||||
|
- armv7-unknown-linux-musleabihf
|
||||||
|
- x86_64-apple-darwin
|
||||||
|
- aarch64-apple-darwin
|
||||||
|
- x86_64-pc-windows-msvc
|
||||||
|
- i686-pc-windows-msvc
|
||||||
|
- aarch64-pc-windows-msvc
|
||||||
|
- x86_64-win7-windows-msvc
|
||||||
|
- i686-win7-windows-msvc
|
||||||
|
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
host_os: ubuntu-22.04
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
host_os: ubuntu-latest
|
||||||
|
- target: i686-unknown-linux-musl
|
||||||
|
host_os: ubuntu-latest
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
host_os: ubuntu-latest
|
||||||
|
- target: armv7-unknown-linux-musleabi
|
||||||
|
host_os: ubuntu-latest
|
||||||
|
- target: armv7-unknown-linux-musleabihf
|
||||||
|
host_os: ubuntu-latest
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
host_os: macos-latest
|
||||||
|
- target: aarch64-apple-darwin
|
||||||
|
host_os: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
host_os: windows-latest
|
||||||
|
- target: i686-pc-windows-msvc
|
||||||
|
host_os: windows-latest
|
||||||
|
- target: aarch64-pc-windows-msvc
|
||||||
|
host_os: windows-latest
|
||||||
|
- target: x86_64-win7-windows-msvc
|
||||||
|
host_os: windows-latest
|
||||||
|
- target: i686-win7-windows-msvc
|
||||||
|
host_os: windows-latest
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.host_os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Prepare
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir mypubdir4
|
||||||
|
if [[ "${{ matrix.target }}" != "x86_64-win7-windows-msvc" && "${{ matrix.target }}" != "i686-win7-windows-msvc" ]]; then
|
||||||
|
rustup target add ${{ matrix.target }}
|
||||||
|
fi
|
||||||
|
cargo install cbindgen
|
||||||
|
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 [[ "${{ 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
|
||||||
|
rustup toolchain install nightly
|
||||||
|
rustup component add rust-src --toolchain nightly
|
||||||
|
cargo +nightly build --release -Z build-std --target ${{ matrix.target }}
|
||||||
|
else
|
||||||
|
cargo build --all-features --release --target ${{ matrix.target }}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
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, 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 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 [[ "${{ 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/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
name: bin-${{ matrix.target }}
|
||||||
toolchain: stable
|
path: mypubdir4/*
|
||||||
override: true
|
|
||||||
- uses: actions-rs/cargo@v1
|
- name: Generate artifact attestation
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/attest-build-provenance@v1
|
||||||
with:
|
with:
|
||||||
command: build
|
subject-path: mypubdir4/*
|
||||||
args: --release --target x86_64-unknown-linux-gnu
|
|
||||||
- name: Rename
|
|
||||||
run: mkdir build && mv target/x86_64-unknown-linux-gnu/release/tun2proxy build/tun2proxy-x86_64
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
name: Automated build of ${{ github.sha }}
|
files: mypubdir4/*
|
||||||
files: build/*
|
|
||||||
draft: false
|
- name: Abort on error
|
||||||
prerelease: true
|
if: ${{ failure() }}
|
||||||
body: This is an automated build of commit ${{ github.sha }}.
|
run: echo "Some of jobs failed" && false
|
||||||
tag_name: r${{ github.sha }}
|
|
||||||
|
|
115
.github/workflows/rust.yml
vendored
Normal file
115
.github/workflows/rust.yml
vendored
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
name: Push or PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # Every Sunday at midnight UTC
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_n_test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: rustfmt
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
- name: check
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: cargo check --verbose
|
||||||
|
|
||||||
|
- name: clippy
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
run: |
|
||||||
|
cargo build --verbose --tests --all-features
|
||||||
|
cargo clean
|
||||||
|
cargo build --verbose
|
||||||
|
|
||||||
|
- name: Abort on error
|
||||||
|
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:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Check semver
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||||
|
- name: Abort on error
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: echo "Semver check failed" && false
|
43
.github/workflows/tests.yml
vendored
43
.github/workflows/tests.yml
vendored
|
@ -3,6 +3,8 @@ on:
|
||||||
types: [submitted]
|
types: [submitted]
|
||||||
push:
|
push:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request_target:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
|
|
||||||
|
@ -10,17 +12,34 @@ jobs:
|
||||||
proxy_tests:
|
proxy_tests:
|
||||||
name: Proxy Tests
|
name: Proxy Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'safe to test')) && github.actor != 'dependabot[bot]' && github.actor != 'github-actions[bot]'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
- name: Populate .env
|
||||||
profile: minimal
|
env:
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --no-run
|
|
||||||
- env:
|
|
||||||
DOTENV: ${{ secrets.DOTENV }}
|
DOTENV: ${{ secrets.DOTENV }}
|
||||||
run: echo "$DOTENV" > .env && sudo -E /home/runner/.cargo/bin/cargo test
|
run: |
|
||||||
|
echo "$DOTENV" > tests/.env
|
||||||
|
ln -s tests/.env
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Create virtual environment
|
||||||
|
run: python -m venv venv
|
||||||
|
|
||||||
|
- name: Activate virtual environment and install dependencies
|
||||||
|
run: |
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r tests/requirements.txt
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
source venv/bin/activate
|
||||||
|
python tests/tests.py
|
||||||
|
|
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -1,9 +1,14 @@
|
||||||
|
tun2proxy-android-libs.zip
|
||||||
|
tun2proxy-android-libs/
|
||||||
|
tun2proxy.xcframework/
|
||||||
|
.env
|
||||||
|
project.xcworkspace/
|
||||||
|
xcuserdata/
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
.VSCodeCounter/
|
||||||
build/
|
build/
|
||||||
tmp/
|
tmp/
|
||||||
.*
|
|
||||||
*.secret
|
|
||||||
*.iml
|
|
||||||
!/.github
|
|
||||||
/target
|
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
manual-test.sh
|
target/
|
||||||
|
.idea/
|
||||||
|
|
11
.pre-commit-config.yaml
Normal file
11
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v2.3.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- repo: https://github.com/rhysd/actionlint
|
||||||
|
rev: v1.7.7
|
||||||
|
hooks:
|
||||||
|
- id: actionlint
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,12 +0,0 @@
|
||||||
# Changelog for Tun2Proxy
|
|
||||||
|
|
||||||
## 0.1.1
|
|
||||||
|
|
||||||
- Updated dependencies:
|
|
||||||
- `chrono`: v0.4, ready for next planned release ;
|
|
||||||
- `clap`: last version ;
|
|
||||||
- `mio`: v0.8 + rename renamed feature (os-util became os-ext) + some fixes due to removal of `TcpSocket` type ;
|
|
||||||
- `smoltcp`: set v0.8 but from crates.io, plus old reference could not work.
|
|
||||||
- Fixes:
|
|
||||||
- Removed typo from Cargo.toml ;
|
|
||||||
- Clippy.
|
|
98
Cargo.toml
98
Cargo.toml
|
@ -1,28 +1,82 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["B. Blechschmidt"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "tun2proxy"
|
name = "tun2proxy"
|
||||||
version = "0.1.1"
|
version = "0.7.11"
|
||||||
|
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.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]
|
[dependencies]
|
||||||
base64 = { version = "0.21" }
|
async-trait = "0.1"
|
||||||
clap = { version = "4.1", features = ["derive"] }
|
base64easy = "0.1"
|
||||||
ctrlc = "3.2"
|
chrono = "0.4"
|
||||||
|
clap = { version = "4", features = ["derive", "wrap_help", "color"] }
|
||||||
|
ctrlc2 = { version = "3.6.5", features = ["async", "termination"] }
|
||||||
|
digest_auth = "0.3"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
fork = "0.1"
|
hashlink = "0.10"
|
||||||
hashlink = "0.8"
|
hickory-proto = "0.25"
|
||||||
libc = "0.2"
|
httparse = "1"
|
||||||
log = "0.4"
|
ipstack = { version = "0.4" }
|
||||||
mio = { version = "0.8", features = ["os-poll", "net", "os-ext"] }
|
log = { version = "0.4", features = ["std"] }
|
||||||
nix = { version = "0.26", features = ["process", "signal"] }
|
mimalloc = { version = "0.1", default-features = false, optional = true }
|
||||||
prctl = "1.0"
|
percent-encoding = "2"
|
||||||
smoltcp = { version = "0.9", git = "https://github.com/smoltcp-rs/smoltcp.git", features = ["std"] }
|
shlex = "1.3.0"
|
||||||
thiserror = "1.0"
|
socks5-impl = { version = "0.7", default-features = false, features = [
|
||||||
url = "2.3"
|
"tokio",
|
||||||
|
] }
|
||||||
|
thiserror = "2"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
|
tproxy-config = { version = "7", default-features = false }
|
||||||
|
tun = { version = "0.8", features = ["async"] }
|
||||||
|
udp-stream = { version = "0.0.12", default-features = false }
|
||||||
|
unicase = "2"
|
||||||
|
url = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[target.'cfg(target_os="android")'.dependencies]
|
||||||
ctor = "0.1"
|
android_logger = "0.15"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
jni = { version = "0.21", default-features = false }
|
||||||
serial_test = "1.0"
|
|
||||||
test-log = "0.2"
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
|
bincode = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os="windows")'.dependencies]
|
||||||
|
windows-service = "0.8"
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
daemonize = "0.5"
|
||||||
|
nix = { version = "0.30", default-features = false, features = [
|
||||||
|
"fs",
|
||||||
|
"socket",
|
||||||
|
"uio",
|
||||||
|
] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
chrono = "0.4"
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
# [profile.release]
|
||||||
|
# strip = "symbols"
|
||||||
|
|
61
Dockerfile
Normal file
61
Dockerfile
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
####################################################################################################
|
||||||
|
# This is a multi-stage Dockerfile.
|
||||||
|
# Build with `docker buildx build -t <image-tag> --target <stage> .`
|
||||||
|
# For example, to build the Alpine-based image while naming it tun2proxy, run:
|
||||||
|
# `docker buildx build -t tun2proxy --target tun2proxy-alpine .`
|
||||||
|
####################################################################################################
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## glibc builder
|
||||||
|
####################################################################################################
|
||||||
|
FROM rust:latest AS glibc-builder
|
||||||
|
|
||||||
|
WORKDIR /worker
|
||||||
|
COPY ./ .
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## musl builder
|
||||||
|
####################################################################################################
|
||||||
|
FROM rust:latest AS musl-builder
|
||||||
|
|
||||||
|
WORKDIR /worker
|
||||||
|
COPY ./ .
|
||||||
|
RUN ARCH=$(rustc -vV | sed -nE 's/host:\s*([^-]+).*/\1/p') \
|
||||||
|
&& rustup target add "$ARCH-unknown-linux-musl" \
|
||||||
|
&& cargo build --release --target "$ARCH-unknown-linux-musl"
|
||||||
|
|
||||||
|
RUN mkdir /.etc \
|
||||||
|
&& touch /.etc/resolv.conf \
|
||||||
|
&& mkdir /.tmp \
|
||||||
|
&& chmod 777 /.tmp \
|
||||||
|
&& chmod +t /.tmp
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## Alpine image
|
||||||
|
####################################################################################################
|
||||||
|
FROM alpine:latest AS tun2proxy-alpine
|
||||||
|
|
||||||
|
COPY --from=musl-builder /worker/target/*/release/tun2proxy-bin /usr/bin/tun2proxy-bin
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tun2proxy-bin", "--setup"]
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## Ubuntu image
|
||||||
|
####################################################################################################
|
||||||
|
FROM ubuntu:latest AS tun2proxy-ubuntu
|
||||||
|
|
||||||
|
COPY --from=glibc-builder /worker/target/release/tun2proxy-bin /usr/bin/tun2proxy-bin
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tun2proxy-bin", "--setup"]
|
||||||
|
|
||||||
|
####################################################################################################
|
||||||
|
## OS-less image (default)
|
||||||
|
####################################################################################################
|
||||||
|
FROM scratch AS tun2proxy-scratch
|
||||||
|
|
||||||
|
COPY --from=musl-builder ./tmp /tmp
|
||||||
|
COPY --from=musl-builder ./etc /etc
|
||||||
|
COPY --from=musl-builder /worker/target/*/release/tun2proxy-bin /usr/bin/tun2proxy-bin
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tun2proxy-bin", "--setup"]
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) B. Blechschmidt and contributors
|
Copyright (c) @ssrlive, B. Blechschmidt and contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
195
README.md
195
README.md
|
@ -1,5 +1,26 @@
|
||||||
|
[](https://github.com/tun2proxy/tun2proxy)
|
||||||
|
|
||||||
# tun2proxy
|
# tun2proxy
|
||||||
A tunnel interface for HTTP and SOCKS proxies on Linux based on [smoltcp](https://github.com/smoltcp-rs/smoltcp).
|
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)
|
||||||
|
|
||||||
|
> Additional information can be found in the [wiki](https://github.com/tun2proxy/tun2proxy/wiki)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- HTTP proxy support (unauthenticated, basic and digest auth)
|
||||||
|
- SOCKS4 and SOCKS5 support (unauthenticated, username/password auth)
|
||||||
|
- SOCKS4a and SOCKS5h support (through the virtual DNS feature)
|
||||||
|
- Minimal configuration setup for routing all traffic
|
||||||
|
- IPv4 and IPv6 support
|
||||||
|
- 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
|
## Build
|
||||||
Clone the repository and `cd` into the project folder. Then run the following:
|
Clone the repository and `cd` into the project folder. Then run the following:
|
||||||
|
@ -7,22 +28,55 @@ Clone the repository and `cd` into the project folder. Then run the following:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Building Framework for Apple Devices
|
||||||
|
To build an XCFramework for macOS and iOS, run the following:
|
||||||
|
```
|
||||||
|
./build-apple.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install from binary
|
||||||
|
|
||||||
|
Download the binary from [releases](https://github.com/tun2proxy/tun2proxy/releases) and put it in your `PATH`.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Authenticity Verification</summary>
|
||||||
|
|
||||||
|
Since v0.2.23 [build provenance attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds#verifying-artifact-attestations-with-the-github-cli)
|
||||||
|
are supported. These allow you to ensure that the builds have been generated from the code on GitHub through the GitHub
|
||||||
|
CI/CD pipeline. To verify the authenticity of the build files, you can use the [GitHub CLI](https://cli.github.com/):
|
||||||
|
```shell
|
||||||
|
gh attestation verify <*.zip file> --owner tun2proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Install from source
|
||||||
|
|
||||||
|
If you have [rust](https://rustup.rs/) toolchain installed, this should work:
|
||||||
|
```shell
|
||||||
|
cargo install tun2proxy
|
||||||
|
```
|
||||||
|
> Note: In Windows, you need to copy [wintun](https://www.wintun.net/) DLL to the same directory as the binary.
|
||||||
|
> It's `%USERPROFILE%\.cargo\bin` by default.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
## Automated Setup
|
## Automated Setup
|
||||||
Using `--setup auto`, you can have tun2proxy configure your system to automatically route all traffic through the
|
Using `--setup`, you can have tun2proxy configure your system to automatically route all traffic through the
|
||||||
specified proxy. This requires running the tool as root and will roughly perform the steps outlined in the section
|
specified proxy. This requires running the tool as root and will roughly perform the steps outlined in the section
|
||||||
describing the manual setup, except that a bind mount is used to overlay the `/etc/resolv.conf` file.
|
describing the manual setup, except that a bind mount is used to overlay the `/etc/resolv.conf` file.
|
||||||
|
|
||||||
You would then run the tool as follows:
|
You would then run the tool as follows:
|
||||||
```bash
|
```bash
|
||||||
./target/release/tun2proxy --setup auto --proxy "socks5://1.2.3.4:1080"
|
sudo ./target/release/tun2proxy-bin --setup --proxy "socks5://1.2.3.4:1080"
|
||||||
```
|
```
|
||||||
|
|
||||||
Apart from SOCKS5, SOCKS4 and HTTP are supported.
|
Apart from SOCKS5, SOCKS4 and HTTP are supported.
|
||||||
|
|
||||||
Note that if your proxy is a non-global IP address (e.g. because the proxy is provided by some tunneling tool running
|
Note that if your proxy is a non-global IP address (e.g. because the proxy is provided by some tunneling tool running
|
||||||
locally), you will additionally need to provide the public IP address of the server through which the traffic is
|
locally), you will additionally need to provide the public IP address of the server through which the traffic is
|
||||||
actually tunneled. In such a case, the tool will tell you to specify the address through `--setup-ip <address>` if you
|
actually tunneled. In such a case, the tool will tell you to specify the address through `--bypass <IP/CIDR>` if you
|
||||||
wish to make use of the automated setup feature.
|
wish to make use of the automated setup feature.
|
||||||
|
|
||||||
## Manual Setup
|
## Manual Setup
|
||||||
|
@ -32,15 +86,16 @@ A standard setup, which would route all traffic from your system through the tun
|
||||||
PROXY_TYPE=SOCKS5
|
PROXY_TYPE=SOCKS5
|
||||||
PROXY_IP=1.2.3.4
|
PROXY_IP=1.2.3.4
|
||||||
PROXY_PORT=1080
|
PROXY_PORT=1080
|
||||||
|
BYPASS_IP=123.45.67.89
|
||||||
|
|
||||||
# Create a tunnel interface named tun0 which your user can bind to,
|
# Create a tunnel interface named tun0 which you can bind to,
|
||||||
# so we don't need to run tun2proxy as root.
|
# so we don't need to run tun2proxy as root.
|
||||||
sudo ip tuntap add name tun0 mode tun user $USER
|
sudo ip tuntap add name tun0 mode tun
|
||||||
sudo ip link set tun0 up
|
sudo ip link set tun0 up
|
||||||
|
|
||||||
# To prevent a routing loop, we add a route to the proxy server that behaves
|
# To prevent a routing loop, we add a route to the proxy server that behaves
|
||||||
# like the default route.
|
# like the default route.
|
||||||
sudo ip route add "$PROXY_IP" $(ip route | grep '^default' | cut -d ' ' -f 2-)
|
sudo ip route add "$BYPASS_IP" $(ip route | grep '^default' | cut -d ' ' -f 2-)
|
||||||
|
|
||||||
# Route all your traffic through tun0 without interfering with the default route.
|
# Route all your traffic through tun0 without interfering with the default route.
|
||||||
sudo ip route add 128.0.0.0/1 dev tun0
|
sudo ip route add 128.0.0.0/1 dev tun0
|
||||||
|
@ -53,17 +108,14 @@ sudo ip route add 8000::/1 dev tun0
|
||||||
# Make sure that DNS queries are routed through the tunnel.
|
# Make sure that DNS queries are routed through the tunnel.
|
||||||
sudo sh -c "echo nameserver 198.18.0.1 > /etc/resolv.conf"
|
sudo sh -c "echo nameserver 198.18.0.1 > /etc/resolv.conf"
|
||||||
|
|
||||||
./target/release/tun2proxy --tun tun0 --proxy "$PROXY_TYPE://$PROXY_IP:$PROXY_PORT"
|
./target/release/tun2proxy-bin --tun tun0 --proxy "$PROXY_TYPE://$PROXY_IP:$PROXY_PORT"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that if you paste these commands into a shell script, which you then run with `sudo`, you might want to replace
|
This tool implements a virtual DNS feature that is used by switch `--dns virtual`. When a DNS packet to port 53 is detected, an IP
|
||||||
`$USER` with `$SUDO_USER`.
|
|
||||||
|
|
||||||
This tool implements a virtual DNS feature that is used by default. When a DNS packet to port 53 is detected, an IP
|
|
||||||
address from `198.18.0.0/15` is chosen and mapped to the query name. Connections destined for an IP address from that
|
address from `198.18.0.0/15` is chosen and mapped to the query name. Connections destined for an IP address from that
|
||||||
range will supply the proxy with the mapped query name instead of the IP address. Since many proxies do not support UDP,
|
range will supply the proxy with the mapped query name instead of the IP address. Since many proxies do not support UDP,
|
||||||
this enables an out-of-the-box experience in most cases, without relying on third-party resolvers or applications.
|
this enables an out-of-the-box experience in most cases, without relying on third-party resolvers or applications.
|
||||||
Depending on your use case, you may want to disable this feature using `--dns none`.
|
Depending on your use case, you may want to disable this feature using `--dns direct`.
|
||||||
In that case, you might need an additional tool like [dnsproxy](https://github.com/AdguardTeam/dnsproxy) that is
|
In that case, you might need an additional tool like [dnsproxy](https://github.com/AdguardTeam/dnsproxy) that is
|
||||||
configured to listen on a local UDP port and communicates with a third-party upstream DNS server via TCP.
|
configured to listen on a local UDP port and communicates with a third-party upstream DNS server via TCP.
|
||||||
|
|
||||||
|
@ -77,14 +129,42 @@ sudo ip link del tun0
|
||||||
```
|
```
|
||||||
Tunnel interface to proxy.
|
Tunnel interface to proxy.
|
||||||
|
|
||||||
Usage: tun2proxy [OPTIONS] --proxy <URL>
|
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
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-t, --tun <name> Name of the tun interface [default: tun0]
|
-p, --proxy <URL> Proxy URL in the form proto://[username[:password]@]host:port, where proto is one of
|
||||||
-p, --proxy <URL> Proxy URL in the form proto://[username[:password]@]host:port
|
socks4, socks5, http. Username and password are encoded in percent encoding. For example:
|
||||||
-d, --dns <method> DNS handling [default: virtual] [possible values: virtual, none]
|
socks5://myname:pass%40word@127.0.0.1:1080
|
||||||
-s, --setup <method> Routing and system setup [possible values: auto]
|
-t, --tun <name> Name of the tun interface, such as tun0, utun4, etc. If this option is not provided, the
|
||||||
--setup-ip <IP> Public proxy IP used in routing setup
|
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 requires root-like privileges on every platform.
|
||||||
|
It is very important on Linux, 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
|
-h, --help Print help
|
||||||
-V, --version Print version
|
-V, --version Print version
|
||||||
```
|
```
|
||||||
|
@ -92,6 +172,72 @@ 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
|
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.
|
supplied as `--proxy http://john.doe:secret@1.2.3.4:3128`. This works analogously to curl's `--proxy` argument.
|
||||||
|
|
||||||
|
## Container Support
|
||||||
|
### Docker
|
||||||
|
Tun2proxy can serve as a proxy for other Docker containers. To make use of that feature, first build the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build -t tun2proxy .
|
||||||
|
```
|
||||||
|
|
||||||
|
This will build an image containing a statically linked `tun2proxy` binary (based on `musl`) without OS.
|
||||||
|
|
||||||
|
Alternatively, you can build images based on Ubuntu or Alpine as follows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build -t tun2proxy --target tun2proxy-ubuntu .
|
||||||
|
docker buildx build -t tun2proxy --target tun2proxy-alpine .
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, start a container from the tun2proxy image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-v /dev/net/tun:/dev/net/tun \
|
||||||
|
--sysctl net.ipv6.conf.default.disable_ipv6=0 \
|
||||||
|
--cap-add NET_ADMIN \
|
||||||
|
--name tun2proxy \
|
||||||
|
tun2proxy --proxy proto://[username[:password]@]host:port
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then provide the running container's network to another worker container by sharing the network namespace (like kubernetes sidecar):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it \
|
||||||
|
--network "container:tun2proxy" \
|
||||||
|
ubuntu:latest
|
||||||
|
```
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
Create 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-ubuntu: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
|
## Configuration Tips
|
||||||
### DNS
|
### DNS
|
||||||
When DNS resolution is performed by a service on your machine or through a server in your local network, DNS resolution
|
When DNS resolution is performed by a service on your machine or through a server in your local network, DNS resolution
|
||||||
|
@ -113,8 +259,9 @@ either through `sysctl -w net.ipv6.conf.all.disable_ipv6=1` and `sysctl -w net.i
|
||||||
or through `ip -6 route del default`, which causes the `libc` resolver (and other software) to not issue DNS AAAA
|
or through `ip -6 route del default`, which causes the `libc` resolver (and other software) to not issue DNS AAAA
|
||||||
requests for IPv6 addresses.
|
requests for IPv6 addresses.
|
||||||
|
|
||||||
## TODO
|
## Contributors ✨
|
||||||
- Improve handling of half-open connections
|
Thanks goes to these wonderful people:
|
||||||
- Increase error robustness (reduce `unwrap` and `expect` usage)
|
|
||||||
- UDP support for SOCKS
|
<a href="https://github.com/tun2proxy/tun2proxy/graphs/contributors">
|
||||||
- Native support for proxying DNS over TCP or TLS
|
<img src="https://contrib.rocks/image?repo=tun2proxy/tun2proxy" />
|
||||||
|
</a>
|
||||||
|
|
22
apple/readme.md
Normal file
22
apple/readme.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
Build iOS xcframework
|
||||||
|
----------------
|
||||||
|
|
||||||
|
# Install Rust build tools
|
||||||
|
|
||||||
|
- Install Xcode Command Line Tools: `xcode-select --install`
|
||||||
|
- Install Rust programming language: `curl https://sh.rustup.rs -sSf | sh`
|
||||||
|
- Install iOS target support: `rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios`
|
||||||
|
- Install cbindgen tool: `cargo install cbindgen`
|
||||||
|
|
||||||
|
# Building iOS xcframework
|
||||||
|
|
||||||
|
Run the following command in zsh (or bash):
|
||||||
|
```bash
|
||||||
|
cd tun2proxy
|
||||||
|
./build-apple.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script `build-apple.sh` will build the iOS/macOS xcframework and output it to `./tun2proxy.xcframework`
|
||||||
|
|
||||||
|
To save the build time, you can use the `build-aarch64-apple-ios-debug.sh` or `build-aarch64-apple-ios.sh` script
|
||||||
|
to build the `aarch64-apple-ios` target only.
|
26
build-aarch64-apple-ios-debug.sh
Executable file
26
build-aarch64-apple-ios-debug.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
echo "Setting up the rust environment..."
|
||||||
|
rustup target add aarch64-apple-ios
|
||||||
|
cargo install cbindgen
|
||||||
|
|
||||||
|
echo "Building target aarch64-apple-ios..."
|
||||||
|
cargo build --target aarch64-apple-ios --features mimalloc
|
||||||
|
|
||||||
|
echo "Generating includes..."
|
||||||
|
mkdir -p target/include/
|
||||||
|
rm -rf target/include/*
|
||||||
|
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||||
|
cat > target/include/tun2proxy.modulemap <<EOF
|
||||||
|
framework module tun2proxy {
|
||||||
|
umbrella header "tun2proxy.h"
|
||||||
|
export *
|
||||||
|
module * { export * }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Creating XCFramework"
|
||||||
|
rm -rf ./tun2proxy.xcframework
|
||||||
|
xcodebuild -create-xcframework \
|
||||||
|
-library ./target/aarch64-apple-ios/debug/libtun2proxy.a -headers ./target/include/ \
|
||||||
|
-output ./tun2proxy.xcframework
|
26
build-aarch64-apple-ios.sh
Executable file
26
build-aarch64-apple-ios.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
echo "Setting up the rust environment..."
|
||||||
|
rustup target add aarch64-apple-ios
|
||||||
|
cargo install cbindgen
|
||||||
|
|
||||||
|
echo "Building target aarch64-apple-ios..."
|
||||||
|
cargo build --release --target aarch64-apple-ios --features mimalloc
|
||||||
|
|
||||||
|
echo "Generating includes..."
|
||||||
|
mkdir -p target/include/
|
||||||
|
rm -rf target/include/*
|
||||||
|
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||||
|
cat > target/include/tun2proxy.modulemap <<EOF
|
||||||
|
framework module tun2proxy {
|
||||||
|
umbrella header "tun2proxy.h"
|
||||||
|
export *
|
||||||
|
module * { export * }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Creating XCFramework"
|
||||||
|
rm -rf ./tun2proxy.xcframework
|
||||||
|
xcodebuild -create-xcframework \
|
||||||
|
-library ./target/aarch64-apple-ios/release/libtun2proxy.a -headers ./target/include/ \
|
||||||
|
-output ./tun2proxy.xcframework
|
129
build-android.sh
Executable file
129
build-android.sh
Executable file
|
@ -0,0 +1,129 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
work_dir=$(pwd)
|
||||||
|
|
||||||
|
ANDROID_API_VERSION=21
|
||||||
|
# NDK homepage: https://developer.android.com/ndk/downloads#lts-downloads
|
||||||
|
ANDROID_NDK_VERSION=26.3.11579264
|
||||||
|
# Android commandline tools homepage: https://developer.android.com/studio/index.html#command-line-tools-only
|
||||||
|
CMDLINE_TOOLS_VERSION=6858069
|
||||||
|
|
||||||
|
export ANDROID_HOME=/tmp/Android/sdk
|
||||||
|
export NDK_HOME=${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}
|
||||||
|
export PATH=$ANDROID_HOME/cmdline-tools/bin:$PATH
|
||||||
|
mkdir -p $ANDROID_HOME
|
||||||
|
|
||||||
|
name=tun2proxy
|
||||||
|
BASE=`dirname "$0"`
|
||||||
|
android_libs=$BASE/${name}-android-libs
|
||||||
|
mkdir -p $android_libs
|
||||||
|
|
||||||
|
function setup_env() {
|
||||||
|
cargo install cbindgen
|
||||||
|
apt update && apt install -y make llvm-dev libclang-dev clang pkg-config zip unzip curl default-jdk build-essential
|
||||||
|
cd /tmp/
|
||||||
|
curl -OL https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip
|
||||||
|
rm -rf /tmp/cmdline-tools
|
||||||
|
unzip commandlinetools-linux-${CMDLINE_TOOLS_VERSION}_latest.zip
|
||||||
|
rm -rf $ANDROID_HOME/cmdline-tools
|
||||||
|
mv cmdline-tools $ANDROID_HOME
|
||||||
|
yes | sdkmanager --sdk_root=$ANDROID_HOME --licenses
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to accept the licenses"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sdkmanager --sdk_root=$ANDROID_HOME "ndk;${ANDROID_NDK_VERSION}" "platforms;android-${ANDROID_API_VERSION}"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to install NDK"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_android() {
|
||||||
|
local manifest=./Cargo.toml
|
||||||
|
local mode=--release
|
||||||
|
local mode2=release
|
||||||
|
local targets=
|
||||||
|
|
||||||
|
if [ ! -z "$2" ]; then
|
||||||
|
targets="$2"
|
||||||
|
else
|
||||||
|
targets="aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for target in $targets; do
|
||||||
|
rustup target add $target
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$1" = "debug" ]; then
|
||||||
|
mode=
|
||||||
|
mode2=debug
|
||||||
|
fi
|
||||||
|
|
||||||
|
local BASE=`dirname "$0"`
|
||||||
|
local HOST_OS=`uname -s | tr "[:upper:]" "[:lower:]"`
|
||||||
|
local HOST_ARCH=`uname -m | tr "[:upper:]" "[:lower:]"`
|
||||||
|
local android_tools="$NDK_HOME/toolchains/llvm/prebuilt/$HOST_OS-$HOST_ARCH/bin"
|
||||||
|
|
||||||
|
export PATH="${android_tools}/":$PATH
|
||||||
|
|
||||||
|
for target in $targets; do
|
||||||
|
local target_dir=
|
||||||
|
case $target in
|
||||||
|
'armv7-linux-androideabi')
|
||||||
|
export CC_armv7_linux_androideabi="$android_tools/armv7a-linux-androideabi${ANDROID_API_VERSION}-clang"
|
||||||
|
export AR_armv7_linux_androideabi="$android_tools/llvm-ar"
|
||||||
|
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$android_tools/armv7a-linux-androideabi${ANDROID_API_VERSION}-clang"
|
||||||
|
target_dir=armeabi-v7a
|
||||||
|
;;
|
||||||
|
'x86_64-linux-android')
|
||||||
|
export CC_x86_64_linux_android="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
export AR_x86_64_linux_android="$android_tools/llvm-ar"
|
||||||
|
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
target_dir=x86_64
|
||||||
|
;;
|
||||||
|
'aarch64-linux-android')
|
||||||
|
export CC_aarch64_linux_android="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
export AR_aarch64_linux_android="$android_tools/llvm-ar"
|
||||||
|
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
target_dir=arm64-v8a
|
||||||
|
;;
|
||||||
|
'i686-linux-android')
|
||||||
|
export CC_i686_linux_android="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
export AR_i686_linux_android="$android_tools/llvm-ar"
|
||||||
|
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="$android_tools/${target}${ANDROID_API_VERSION}-clang"
|
||||||
|
target_dir=x86
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown target $target"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
cargo build --target $target $mode
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to build for target $target"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p $android_libs/$target_dir
|
||||||
|
cp $BASE/target/$target/${mode2}/lib${name}.so $android_libs/${target_dir}/lib${name}.so
|
||||||
|
cp $BASE/target/$target/${mode2}/lib${name}.a $android_libs/${target_dir}/lib${name}.a
|
||||||
|
done
|
||||||
|
|
||||||
|
cbindgen -c $BASE/cbindgen.toml -o $android_libs/$name.h
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
echo "Setting up the build environment..."
|
||||||
|
setup_env
|
||||||
|
cd $work_dir
|
||||||
|
|
||||||
|
echo "build android target"
|
||||||
|
build_android "$@"
|
||||||
|
cd $work_dir
|
||||||
|
|
||||||
|
echo "Creating zip file"
|
||||||
|
rm -rf ${name}-android-libs.zip
|
||||||
|
zip -r ${name}-android-libs.zip ${name}-android-libs
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
56
build-apple.sh
Executable file
56
build-apple.sh
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
echo "Setting up the rust environment..."
|
||||||
|
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios x86_64-apple-darwin aarch64-apple-darwin
|
||||||
|
cargo install cbindgen
|
||||||
|
|
||||||
|
echo "Building..."
|
||||||
|
|
||||||
|
echo "cargo build --release --target x86_64-apple-darwin"
|
||||||
|
cargo build --release --target x86_64-apple-darwin
|
||||||
|
|
||||||
|
echo "cargo build --release --target aarch64-apple-darwin"
|
||||||
|
cargo build --release --target aarch64-apple-darwin
|
||||||
|
|
||||||
|
echo "cargo build --release --target aarch64-apple-ios"
|
||||||
|
cargo build --release --target aarch64-apple-ios --features mimalloc
|
||||||
|
|
||||||
|
echo "cargo build --release --target x86_64-apple-ios"
|
||||||
|
cargo build --release --target x86_64-apple-ios
|
||||||
|
|
||||||
|
echo "cargo build --release --target x86_64-apple-ios-sim"
|
||||||
|
cargo build --release --target aarch64-apple-ios-sim
|
||||||
|
|
||||||
|
echo "Generating includes..."
|
||||||
|
mkdir -p target/include/
|
||||||
|
rm -rf target/include/*
|
||||||
|
cbindgen --config cbindgen.toml -o target/include/tun2proxy.h
|
||||||
|
cat > target/include/tun2proxy.modulemap <<EOF
|
||||||
|
framework module tun2proxy {
|
||||||
|
umbrella header "tun2proxy.h"
|
||||||
|
export *
|
||||||
|
module * { export * }
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "lipo..."
|
||||||
|
|
||||||
|
echo "Simulator"
|
||||||
|
lipo -create \
|
||||||
|
target/aarch64-apple-ios-sim/release/libtun2proxy.a \
|
||||||
|
target/x86_64-apple-ios/release/libtun2proxy.a \
|
||||||
|
-output ./target/libtun2proxy-ios-sim.a
|
||||||
|
|
||||||
|
echo "MacOS"
|
||||||
|
lipo -create \
|
||||||
|
target/aarch64-apple-darwin/release/libtun2proxy.a \
|
||||||
|
target/x86_64-apple-darwin/release/libtun2proxy.a \
|
||||||
|
-output ./target/libtun2proxy-macos.a
|
||||||
|
|
||||||
|
echo "Creating XCFramework"
|
||||||
|
rm -rf ./tun2proxy.xcframework
|
||||||
|
xcodebuild -create-xcframework \
|
||||||
|
-library ./target/aarch64-apple-ios/release/libtun2proxy.a -headers ./target/include/ \
|
||||||
|
-library ./target/libtun2proxy-ios-sim.a -headers ./target/include/ \
|
||||||
|
-library ./target/libtun2proxy-macos.a -headers ./target/include/ \
|
||||||
|
-output ./tun2proxy.xcframework
|
103
build.rs
Normal file
103
build.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
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"))?;
|
||||||
|
use std::io::Write;
|
||||||
|
f.write_all(format!("CARGO_TARGET_DIR: '{}'\r\n", cargo_target_dir.display()).as_bytes())?;
|
||||||
|
|
||||||
|
// The wintun-bindings crate's root directory
|
||||||
|
let crate_dir = get_crate_dir("wintun-bindings")?;
|
||||||
|
|
||||||
|
// The path to the DLL file, relative to the crate root, depending on the target architecture
|
||||||
|
let dll_path = get_wintun_bin_relative_path()?;
|
||||||
|
let src_path = crate_dir.join(dll_path);
|
||||||
|
|
||||||
|
let dst_path = cargo_target_dir.join("wintun.dll");
|
||||||
|
|
||||||
|
f.write_all(format!("Source path: '{}'\r\n", src_path.display()).as_bytes())?;
|
||||||
|
f.write_all(format!("Target path: '{}'\r\n", dst_path.display()).as_bytes())?;
|
||||||
|
|
||||||
|
// Copy to the target directory
|
||||||
|
if let Err(e) = std::fs::copy(src_path, &dst_path) {
|
||||||
|
f.write_all(format!("Failed to copy 'wintun.dll': {e}\r\n").as_bytes())?;
|
||||||
|
} else {
|
||||||
|
f.write_all(format!("Copied 'wintun.dll' to '{}'\r\n", dst_path.display()).as_bytes())?;
|
||||||
|
|
||||||
|
// Set the modified time to the current time, or the publishing process will fail.
|
||||||
|
let file = std::fs::OpenOptions::new().write(true).open(&dst_path)?;
|
||||||
|
file.set_modified(std::time::SystemTime::now())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_cargo_target_dir() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?);
|
||||||
|
let profile = std::env::var("PROFILE")?;
|
||||||
|
let mut target_dir = None;
|
||||||
|
let mut sub_path = out_dir.as_path();
|
||||||
|
while let Some(parent) = sub_path.parent() {
|
||||||
|
if parent.ends_with(&profile) {
|
||||||
|
target_dir = Some(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sub_path = parent;
|
||||||
|
}
|
||||||
|
Ok(target_dir.ok_or("not found")?.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_wintun_bin_relative_path() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH")?;
|
||||||
|
|
||||||
|
let dll_path = match target_arch.as_str() {
|
||||||
|
"x86" => "wintun/bin/x86/wintun.dll",
|
||||||
|
"x86_64" => "wintun/bin/amd64/wintun.dll",
|
||||||
|
"arm" => "wintun/bin/arm/wintun.dll",
|
||||||
|
"aarch64" => "wintun/bin/arm64/wintun.dll",
|
||||||
|
_ => return Err("Unsupported architecture".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(dll_path.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn get_crate_dir(crate_name: &str) -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let output = std::process::Command::new("cargo")
|
||||||
|
.arg("metadata")
|
||||||
|
.arg("--format-version=1")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let metadata = serde_json::from_slice::<serde_json::Value>(&output.stdout)?;
|
||||||
|
let packages = metadata["packages"].as_array().ok_or("packages")?;
|
||||||
|
|
||||||
|
let mut crate_dir = None;
|
||||||
|
|
||||||
|
for package in packages {
|
||||||
|
let name = package["name"].as_str().ok_or("name")?;
|
||||||
|
if name == crate_name {
|
||||||
|
let path = package["manifest_path"].as_str().ok_or("manifest_path")?;
|
||||||
|
let path = std::path::PathBuf::from(path);
|
||||||
|
crate_dir = Some(path.parent().ok_or("parent")?.to_path_buf());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
25
cbindgen.toml
Normal file
25
cbindgen.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
language = "C"
|
||||||
|
cpp_compat = true
|
||||||
|
|
||||||
|
[export]
|
||||||
|
include = [
|
||||||
|
"tun2proxy_run_with_cli",
|
||||||
|
"tun2proxy_with_fd_run",
|
||||||
|
"tun2proxy_with_name_run",
|
||||||
|
"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]
|
||||||
|
"ArgVerbosity" = "Tun2proxyVerbosity"
|
||||||
|
"ArgDns" = "Tun2proxyDns"
|
||||||
|
"TrafficStatus" = "Tun2proxyTrafficStatus"
|
||||||
|
|
||||||
|
[enum]
|
||||||
|
prefix_with_name = true
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
max_width = 140
|
24
scripts/dante.conf
Normal file
24
scripts/dante.conf
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# logoutput: /var/log/socks.log
|
||||||
|
internal: 10.0.0.3 port = 10800
|
||||||
|
external: 10.0.0.3
|
||||||
|
clientmethod: none
|
||||||
|
socksmethod: none
|
||||||
|
user.privileged: root
|
||||||
|
user.notprivileged: nobody
|
||||||
|
|
||||||
|
client pass {
|
||||||
|
from: 0/0 to: 0/0
|
||||||
|
log: error connect disconnect
|
||||||
|
}
|
||||||
|
|
||||||
|
socks pass {
|
||||||
|
from: 0/0 to: 0/0
|
||||||
|
command: bind connect udpassociate
|
||||||
|
log: error connect disconnect
|
||||||
|
socksmethod: none
|
||||||
|
}
|
||||||
|
|
||||||
|
socks pass {
|
||||||
|
from: 0.0.0.0/0 to: 0.0.0.0/0
|
||||||
|
command: bindreply udpreply
|
||||||
|
}
|
54
scripts/iperf3.sh
Executable file
54
scripts/iperf3.sh
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# sudo apt install iperf3 dante-server
|
||||||
|
# sudo systemctl stop danted
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
echo $SCRIPT_DIR
|
||||||
|
|
||||||
|
netns="test"
|
||||||
|
dante="danted"
|
||||||
|
tun2proxy="${SCRIPT_DIR}/../target/release/tun2proxy-bin"
|
||||||
|
|
||||||
|
ip netns add "$netns"
|
||||||
|
|
||||||
|
ip link add veth0 type veth peer name veth0 netns "$netns"
|
||||||
|
|
||||||
|
# Configure veth0 in default ns
|
||||||
|
ip addr add 10.0.0.2/24 dev veth0
|
||||||
|
ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure veth0 in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.3/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.4/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure lo interface in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 127.0.0.1/8 dev lo
|
||||||
|
ip netns exec "$netns" ip link set dev lo up
|
||||||
|
|
||||||
|
echo "Starting Dante in background ..."
|
||||||
|
ip netns exec "$netns" "$dante" -f ${SCRIPT_DIR}/dante.conf &
|
||||||
|
|
||||||
|
# Start iperf3 server in netns
|
||||||
|
ip netns exec "$netns" iperf3 -s -B 10.0.0.4 &
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Prepare tun2proxy
|
||||||
|
ip tuntap add name tun0 mode tun
|
||||||
|
ip link set tun0 up
|
||||||
|
ip route add 10.0.0.4 dev tun0
|
||||||
|
"$tun2proxy" --tun tun0 --proxy socks5://10.0.0.3:10800 -v off &
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Run iperf client through tun2proxy
|
||||||
|
iperf3 -c 10.0.0.4 -P 10 -R
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
iperf3 -c 10.0.0.4 -P 10
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
# sudo sh -c "pkill tun2proxy-bin; pkill iperf3; pkill danted; ip link del tun0; ip netns del test"
|
66
scripts/linux.sh
Executable file
66
scripts/linux.sh
Executable file
|
@ -0,0 +1,66 @@
|
||||||
|
#! /usr/bin/bash -x
|
||||||
|
|
||||||
|
# Please set the following parameters according to your environment
|
||||||
|
# BYPASS_IP=123.45.67.89
|
||||||
|
PROXY_IP=127.0.0.1
|
||||||
|
PROXY_PORT=1080
|
||||||
|
PROXY_TYPE=SOCKS5
|
||||||
|
|
||||||
|
function core_function() {
|
||||||
|
local is_envonly="${1}"
|
||||||
|
local bypass_ip="${2}"
|
||||||
|
|
||||||
|
sudo ip tuntap add name tun0 mode tun
|
||||||
|
sudo ip link set tun0 up
|
||||||
|
|
||||||
|
sudo ip route add "${bypass_ip}" $(ip route | grep '^default' | cut -d ' ' -f 2-)
|
||||||
|
|
||||||
|
sudo ip route add 128.0.0.0/1 dev tun0
|
||||||
|
sudo ip route add 0.0.0.0/1 dev tun0
|
||||||
|
|
||||||
|
sudo ip route add ::/1 dev tun0
|
||||||
|
sudo ip route add 8000::/1 dev tun0
|
||||||
|
|
||||||
|
sudo sh -c "echo nameserver 198.18.0.1 > /etc/resolv.conf"
|
||||||
|
|
||||||
|
if [ "$is_envonly" = true ]; then
|
||||||
|
read -n 1 -s -r -p "Don't do anything. If you want to exit and clearup environment, press any key..."
|
||||||
|
echo ""
|
||||||
|
restore
|
||||||
|
else
|
||||||
|
trap 'echo "" && echo "tun2proxy exited with code: $?" && restore' EXIT
|
||||||
|
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
local APP_BIN_PATH="${SCRIPT_DIR}/../target/release/tun2proxy-bin"
|
||||||
|
"${APP_BIN_PATH}" --tun tun0 --proxy "${PROXY_TYPE}://${PROXY_IP}:${PROXY_PORT}" -v trace
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function restore() {
|
||||||
|
sudo ip link del tun0
|
||||||
|
sudo systemctl restart systemd-resolved.service
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
local action=${1}
|
||||||
|
# [ -z ${1} ] && action="envonly"
|
||||||
|
|
||||||
|
local bypass_ip=${2}
|
||||||
|
# [ -z ${2} ] && bypass_ip="123.45.67.89"
|
||||||
|
|
||||||
|
case "${action}" in
|
||||||
|
envonly)
|
||||||
|
core_function true "${bypass_ip}"
|
||||||
|
;;
|
||||||
|
tun2proxy)
|
||||||
|
core_function false "${bypass_ip}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Arguments error! [${action}]"
|
||||||
|
echo "Usage: `basename $0` [envonly|tun2proxy] [bypass_ip]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
83
scripts/rperf.sh
Executable file
83
scripts/rperf.sh
Executable file
|
@ -0,0 +1,83 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
function install_rperf_bin() {
|
||||||
|
local rperf_bin_url="https://github.com/ssrlive/rperf/releases/latest/download/rperf-x86_64-unknown-linux-musl.zip"
|
||||||
|
local rperf_bin_zip_file="rperf-x86_64-unknown-linux-musl.zip"
|
||||||
|
|
||||||
|
command -v rperf > /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Downloading rperf binary ..."
|
||||||
|
wget "$rperf_bin_url" >/dev/null 2>&1
|
||||||
|
unzip "$rperf_bin_zip_file" rperf -d /usr/local/bin/ >/dev/null 2>&1
|
||||||
|
rm "$rperf_bin_zip_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rperf -h >/dev/null 2>&1
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to install rperf binary"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_rperf_bin
|
||||||
|
|
||||||
|
sudo apt install dante-server -y >/dev/null 2>&1
|
||||||
|
sudo systemctl stop danted
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# echo $SCRIPT_DIR
|
||||||
|
|
||||||
|
netns="test"
|
||||||
|
dante="danted"
|
||||||
|
tun2proxy="${SCRIPT_DIR}/../target/release/tun2proxy-bin"
|
||||||
|
|
||||||
|
ip netns add "$netns"
|
||||||
|
|
||||||
|
ip link add veth0 type veth peer name veth0 netns "$netns"
|
||||||
|
|
||||||
|
# Configure veth0 in default ns
|
||||||
|
ip addr add 10.0.0.2/24 dev veth0
|
||||||
|
ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure veth0 in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.3/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.4/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure lo interface in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 127.0.0.1/8 dev lo
|
||||||
|
ip netns exec "$netns" ip link set dev lo up
|
||||||
|
|
||||||
|
echo "Starting Dante in background ..."
|
||||||
|
ip netns exec "$netns" "$dante" -f ${SCRIPT_DIR}/dante.conf &
|
||||||
|
|
||||||
|
# Start rperf server in netns
|
||||||
|
ip netns exec "$netns" rperf -s -B 10.0.0.4 &
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Prepare tun2proxy
|
||||||
|
ip tuntap add name tun0 mode tun
|
||||||
|
ip link set tun0 up
|
||||||
|
ip route add 10.0.0.4 dev tun0
|
||||||
|
"$tun2proxy" --tun tun0 --proxy socks5://10.0.0.3:10800 -v off &
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Run rperf client through tun2proxy
|
||||||
|
rperf -c 10.0.0.4 -v off -P 1 -r
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
rperf -c 10.0.0.4 -v off -P 1
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
rperf -c 10.0.0.4 -v off -P 1 -u
|
||||||
|
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
rperf -c 10.0.0.4 -v trace -P 1 -u -r
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
# sudo sh -c "pkill tun2proxy-bin; pkill rperf; pkill danted; ip link del tun0; ip netns del test"
|
68
src/android.rs
Normal file
68
src/android.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
#![cfg(target_os = "android")]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Args,
|
||||||
|
args::ArgProxy,
|
||||||
|
error::{Error, Result},
|
||||||
|
};
|
||||||
|
use jni::{
|
||||||
|
JNIEnv,
|
||||||
|
objects::{JClass, JString},
|
||||||
|
sys::{jboolean, jchar, jint},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Running tun2proxy 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
|
||||||
|
/// - tun_mtu: the tun mtu
|
||||||
|
/// - dns_strategy: the dns strategy, see ArgDns enum
|
||||||
|
/// - verbosity: the verbosity level, see ArgVerbosity enum
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Tun2proxy_run(
|
||||||
|
mut env: JNIEnv,
|
||||||
|
_clazz: JClass,
|
||||||
|
proxy_url: JString,
|
||||||
|
tun_fd: jint,
|
||||||
|
close_fd_on_drop: jboolean,
|
||||||
|
tun_mtu: jchar,
|
||||||
|
verbosity: jint,
|
||||||
|
dns_strategy: jint,
|
||||||
|
) -> jint {
|
||||||
|
let dns = dns_strategy.try_into().unwrap();
|
||||||
|
let verbosity = verbosity.try_into().unwrap();
|
||||||
|
let filter_str = &format!("off,tun2proxy={verbosity}");
|
||||||
|
let filter = android_logger::FilterBuilder::new().parse(filter_str).build();
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default()
|
||||||
|
.with_tag("tun2proxy")
|
||||||
|
.with_max_level(log::LevelFilter::Trace)
|
||||||
|
.with_filter(filter),
|
||||||
|
);
|
||||||
|
let proxy_url = get_java_string(&mut env, &proxy_url).unwrap();
|
||||||
|
let proxy = ArgProxy::try_from(proxy_url.as_str()).unwrap();
|
||||||
|
let close_fd_on_drop = close_fd_on_drop != 0;
|
||||||
|
|
||||||
|
let mut args = Args::default();
|
||||||
|
args.proxy(proxy)
|
||||||
|
.tun_fd(Some(tun_fd))
|
||||||
|
.close_fd_on_drop(close_fd_on_drop)
|
||||||
|
.dns(dns)
|
||||||
|
.verbosity(verbosity);
|
||||||
|
crate::general_api::general_run_for_api(args, tun_mtu, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Shutdown tun2proxy
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Tun2proxy_stop(_env: JNIEnv, _: JClass) -> jint {
|
||||||
|
crate::general_api::tun2proxy_stop_internal()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_java_string(env: &mut JNIEnv, string: &JString) -> Result<String, Error> {
|
||||||
|
Ok(env.get_string(string)?.into())
|
||||||
|
}
|
466
src/args.rs
Normal file
466
src/args.rs
Normal file
|
@ -0,0 +1,466 @@
|
||||||
|
use crate::{Error, Result};
|
||||||
|
use socks5_impl::protocol::UserKey;
|
||||||
|
use tproxy_config::IpCidr;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::ffi::OsString;
|
||||||
|
|
||||||
|
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! version_info {
|
||||||
|
() => {
|
||||||
|
concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), " ", env!("BUILD_TIME"), ")")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_info() -> &'static str {
|
||||||
|
concat!("Tunnel interface to proxy.\nVersion: ", version_info!())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, clap::Parser)]
|
||||||
|
#[command(author, version = version_info!(), about = about_info(), long_about = None)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Proxy URL in the form proto://[username[:password]@]host:port,
|
||||||
|
/// where proto is one of socks4, socks5, http.
|
||||||
|
/// Username and password are encoded in percent encoding. For example:
|
||||||
|
/// socks5://myname:pass%40word@127.0.0.1:1080
|
||||||
|
#[arg(short, long, value_parser = |s: &str| ArgProxy::try_from(s), value_name = "URL")]
|
||||||
|
pub proxy: ArgProxy,
|
||||||
|
|
||||||
|
/// Name of the tun interface, such as tun0, utun4, etc.
|
||||||
|
/// If this option is not provided, the OS will generate a random one.
|
||||||
|
#[arg(short, long, value_name = "name", value_parser = validate_tun)]
|
||||||
|
#[cfg_attr(unix, arg(conflicts_with = "tun_fd"))]
|
||||||
|
pub tun: Option<String>,
|
||||||
|
|
||||||
|
/// File descriptor of the tun interface
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[arg(long, value_name = "fd", conflicts_with = "tun")]
|
||||||
|
pub tun_fd: Option<i32>,
|
||||||
|
|
||||||
|
/// Set whether to close the received raw file descriptor on drop or not.
|
||||||
|
/// This setting is dependent on [tun_fd].
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[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
|
||||||
|
/// while maintaining proxy connectivity via the global network namespace.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[arg(long)]
|
||||||
|
pub unshare: bool,
|
||||||
|
|
||||||
|
/// Create a pidfile of `unshare` process when using `--unshare`.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[arg(long)]
|
||||||
|
pub unshare_pidfile: Option<String>,
|
||||||
|
|
||||||
|
/// File descriptor for UNIX datagram socket meant to transfer
|
||||||
|
/// network sockets from global namespace to the new one.
|
||||||
|
/// See `unshare(1)`, `namespaces(7)`, `sendmsg(2)`, `unix(7)`.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[arg(long, value_name = "fd", hide(true))]
|
||||||
|
pub socket_transfer_fd: Option<i32>,
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[arg(requires = "unshare")]
|
||||||
|
pub admin_command: Vec<OsString>,
|
||||||
|
|
||||||
|
/// IPv6 enabled
|
||||||
|
#[arg(short = '6', long)]
|
||||||
|
pub ipv6_enabled: bool,
|
||||||
|
|
||||||
|
/// Routing and system setup, which decides whether to setup the routing and system configuration.
|
||||||
|
/// This option requires root-like privileges on every platform.
|
||||||
|
/// It is very important on Linux, see `capabilities(7)`.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub setup: bool,
|
||||||
|
|
||||||
|
/// DNS handling strategy
|
||||||
|
#[arg(short, long, value_name = "strategy", value_enum, default_value = "direct")]
|
||||||
|
pub dns: ArgDns,
|
||||||
|
|
||||||
|
/// DNS resolver address
|
||||||
|
#[arg(long, value_name = "IP", default_value = "8.8.8.8")]
|
||||||
|
pub dns_addr: IpAddr,
|
||||||
|
|
||||||
|
/// IP address pool to be used by virtual DNS in CIDR notation.
|
||||||
|
#[arg(long, value_name = "CIDR", default_value = "198.18.0.0/15")]
|
||||||
|
pub virtual_dns_pool: IpCidr,
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
#[arg(short, long, value_name = "IP/CIDR")]
|
||||||
|
pub bypass: Vec<IpCidr>,
|
||||||
|
|
||||||
|
/// TCP timeout in seconds
|
||||||
|
#[arg(long, value_name = "seconds", default_value = "600")]
|
||||||
|
pub tcp_timeout: u64,
|
||||||
|
|
||||||
|
/// UDP timeout in seconds
|
||||||
|
#[arg(long, value_name = "seconds", default_value = "10")]
|
||||||
|
pub udp_timeout: u64,
|
||||||
|
|
||||||
|
/// Verbosity level
|
||||||
|
#[arg(short, long, value_name = "level", value_enum, default_value = "info")]
|
||||||
|
pub verbosity: ArgVerbosity,
|
||||||
|
|
||||||
|
/// Daemonize for unix family or run as Windows service
|
||||||
|
#[arg(long)]
|
||||||
|
pub daemonize: bool,
|
||||||
|
|
||||||
|
/// Exit immediately when fatal error occurs, useful for running as a service
|
||||||
|
#[arg(long)]
|
||||||
|
pub exit_on_fatal_error: bool,
|
||||||
|
|
||||||
|
/// 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 validate_tun(p: &str) -> Result<String> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if p.len() <= 4 || &p[..4] != "utun" {
|
||||||
|
return Err(Error::from("Invalid tun interface name, please use utunX"));
|
||||||
|
}
|
||||||
|
Ok(p.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Args {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let setup = false;
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
let setup = true;
|
||||||
|
Args {
|
||||||
|
proxy: ArgProxy::default(),
|
||||||
|
tun: None,
|
||||||
|
#[cfg(unix)]
|
||||||
|
tun_fd: None,
|
||||||
|
#[cfg(unix)]
|
||||||
|
close_fd_on_drop: None,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
unshare: false,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
unshare_pidfile: None,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
socket_transfer_fd: None,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
admin_command: Vec::new(),
|
||||||
|
ipv6_enabled: false,
|
||||||
|
setup,
|
||||||
|
dns: ArgDns::default(),
|
||||||
|
dns_addr: "8.8.8.8".parse().unwrap(),
|
||||||
|
bypass: vec![],
|
||||||
|
tcp_timeout: 600,
|
||||||
|
udp_timeout: 10,
|
||||||
|
verbosity: ArgVerbosity::Info,
|
||||||
|
virtual_dns_pool: IpCidr::from_str("198.18.0.0/15").unwrap(),
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
#[allow(clippy::let_and_return)]
|
||||||
|
pub fn parse_args() -> Self {
|
||||||
|
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.");
|
||||||
|
std::process::exit(-1);
|
||||||
|
}
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn proxy(&mut self, proxy: ArgProxy) -> &mut Self {
|
||||||
|
self.proxy = proxy;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dns(&mut self, dns: ArgDns) -> &mut Self {
|
||||||
|
self.dns = dns;
|
||||||
|
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;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn close_fd_on_drop(&mut self, close_fd_on_drop: bool) -> &mut Self {
|
||||||
|
self.close_fd_on_drop = Some(close_fd_on_drop);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verbosity(&mut self, verbosity: ArgVerbosity) -> &mut Self {
|
||||||
|
self.verbosity = verbosity;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tun(&mut self, tun: String) -> &mut Self {
|
||||||
|
self.tun = Some(tun);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dns_addr(&mut self, dns_addr: IpAddr) -> &mut Self {
|
||||||
|
self.dns_addr = dns_addr;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bypass(&mut self, bypass: IpCidr) -> &mut Self {
|
||||||
|
self.bypass.push(bypass);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ipv6_enabled(&mut self, ipv6_enabled: bool) -> &mut Self {
|
||||||
|
self.ipv6_enabled = ipv6_enabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup(&mut self, setup: bool) -> &mut Self {
|
||||||
|
self.setup = setup;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
|
||||||
|
pub enum ArgVerbosity {
|
||||||
|
Off = 0,
|
||||||
|
Error,
|
||||||
|
Warn,
|
||||||
|
#[default]
|
||||||
|
Info,
|
||||||
|
Debug,
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
impl TryFrom<jni::sys::jint> for ArgVerbosity {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: jni::sys::jint) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(ArgVerbosity::Off),
|
||||||
|
1 => Ok(ArgVerbosity::Error),
|
||||||
|
2 => Ok(ArgVerbosity::Warn),
|
||||||
|
3 => Ok(ArgVerbosity::Info),
|
||||||
|
4 => Ok(ArgVerbosity::Debug),
|
||||||
|
5 => Ok(ArgVerbosity::Trace),
|
||||||
|
_ => Err(Error::from("Invalid verbosity level")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ArgVerbosity> for log::LevelFilter {
|
||||||
|
fn from(verbosity: ArgVerbosity) -> Self {
|
||||||
|
match verbosity {
|
||||||
|
ArgVerbosity::Off => log::LevelFilter::Off,
|
||||||
|
ArgVerbosity::Error => log::LevelFilter::Error,
|
||||||
|
ArgVerbosity::Warn => log::LevelFilter::Warn,
|
||||||
|
ArgVerbosity::Info => log::LevelFilter::Info,
|
||||||
|
ArgVerbosity::Debug => log::LevelFilter::Debug,
|
||||||
|
ArgVerbosity::Trace => log::LevelFilter::Trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<log::Level> for ArgVerbosity {
|
||||||
|
fn from(level: log::Level) -> Self {
|
||||||
|
match level {
|
||||||
|
log::Level::Error => ArgVerbosity::Error,
|
||||||
|
log::Level::Warn => ArgVerbosity::Warn,
|
||||||
|
log::Level::Info => ArgVerbosity::Info,
|
||||||
|
log::Level::Debug => ArgVerbosity::Debug,
|
||||||
|
log::Level::Trace => ArgVerbosity::Trace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ArgVerbosity {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ArgVerbosity::Off => write!(f, "off"),
|
||||||
|
ArgVerbosity::Error => write!(f, "error"),
|
||||||
|
ArgVerbosity::Warn => write!(f, "warn"),
|
||||||
|
ArgVerbosity::Info => write!(f, "info"),
|
||||||
|
ArgVerbosity::Debug => write!(f, "debug"),
|
||||||
|
ArgVerbosity::Trace => write!(f, "trace"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DNS query handling strategy
|
||||||
|
/// - Virtual: Use a virtual DNS server to handle DNS queries, also known as Fake-IP mode
|
||||||
|
/// - OverTcp: Use TCP to send DNS queries to the DNS server
|
||||||
|
/// - Direct: Do not handle DNS by relying on DNS server bypassing
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
|
||||||
|
pub enum ArgDns {
|
||||||
|
Virtual = 0,
|
||||||
|
OverTcp,
|
||||||
|
#[default]
|
||||||
|
Direct,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
impl TryFrom<jni::sys::jint> for ArgDns {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: jni::sys::jint) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(ArgDns::Virtual),
|
||||||
|
1 => Ok(ArgDns::OverTcp),
|
||||||
|
2 => Ok(ArgDns::Direct),
|
||||||
|
_ => Err(Error::from("Invalid DNS strategy")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ArgProxy {
|
||||||
|
pub proxy_type: ProxyType,
|
||||||
|
pub addr: SocketAddr,
|
||||||
|
pub credentials: Option<UserKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ArgProxy {
|
||||||
|
fn default() -> Self {
|
||||||
|
ArgProxy {
|
||||||
|
proxy_type: ProxyType::Socks5,
|
||||||
|
addr: "127.0.0.1:1080".parse().unwrap(),
|
||||||
|
credentials: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ArgProxy {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let auth = match &self.credentials {
|
||||||
|
Some(creds) => format!("{creds}"),
|
||||||
|
None => "".to_owned(),
|
||||||
|
};
|
||||||
|
if auth.is_empty() {
|
||||||
|
write!(f, "{}://{}", &self.proxy_type, &self.addr)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}://{}@{}", &self.proxy_type, auth, &self.addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ArgProxy {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(s: &str) -> Result<Self> {
|
||||||
|
if s == "none" {
|
||||||
|
return Ok(ArgProxy {
|
||||||
|
proxy_type: ProxyType::None,
|
||||||
|
addr: "0.0.0.0:0".parse().unwrap(),
|
||||||
|
credentials: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let e = format!("`{s}` is not a valid proxy URL");
|
||||||
|
let url = url::Url::parse(s).map_err(|_| Error::from(&e))?;
|
||||||
|
let e = format!("`{s}` does not contain a host");
|
||||||
|
let host = url.host_str().ok_or(Error::from(e))?;
|
||||||
|
|
||||||
|
let e = format!("`{s}` does not contain a port");
|
||||||
|
let port = url.port_or_known_default().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
|
||||||
|
} else {
|
||||||
|
use percent_encoding::percent_decode;
|
||||||
|
let username = percent_decode(url.username().as_bytes()).decode_utf8()?;
|
||||||
|
let password = percent_decode(url.password().unwrap_or("").as_bytes()).decode_utf8()?;
|
||||||
|
Some(UserKey::new(username, password))
|
||||||
|
};
|
||||||
|
|
||||||
|
let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;
|
||||||
|
|
||||||
|
Ok(ArgProxy {
|
||||||
|
proxy_type,
|
||||||
|
addr,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
|
||||||
|
pub enum ProxyType {
|
||||||
|
Http = 0,
|
||||||
|
Socks4,
|
||||||
|
#[default]
|
||||||
|
Socks5,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ProxyType {
|
||||||
|
type Error = Error;
|
||||||
|
fn try_from(value: &str) -> Result<Self> {
|
||||||
|
match value {
|
||||||
|
"http" => Ok(ProxyType::Http),
|
||||||
|
"socks4" => Ok(ProxyType::Socks4),
|
||||||
|
"socks5" => Ok(ProxyType::Socks5),
|
||||||
|
"none" => Ok(ProxyType::None),
|
||||||
|
scheme => Err(Error::from(&format!("`{scheme}` is an invalid proxy type"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ProxyType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ProxyType::Socks4 => write!(f, "socks4"),
|
||||||
|
ProxyType::Socks5 => write!(f, "socks5"),
|
||||||
|
ProxyType::Http => write!(f, "http"),
|
||||||
|
ProxyType::None => write!(f, "none"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
145
src/bin/main.rs
Normal file
145
src/bin/main.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use tun2proxy::{ArgVerbosity, Args, BoxError};
|
||||||
|
|
||||||
|
fn main() -> Result<(), BoxError> {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
let args = Args::parse_args();
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if args.daemonize {
|
||||||
|
let stdout = std::fs::File::create("/tmp/tun2proxy.out")?;
|
||||||
|
let stderr = std::fs::File::create("/tmp/tun2proxy.err")?;
|
||||||
|
let daemonize = daemonize::Daemonize::new()
|
||||||
|
.working_directory("/tmp")
|
||||||
|
.umask(0o777)
|
||||||
|
.stdout(stdout)
|
||||||
|
.stderr(stderr)
|
||||||
|
.privileged_action(|| "Executed before drop privileges");
|
||||||
|
let _ = daemonize.start()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if args.daemonize {
|
||||||
|
tun2proxy::win_svc::start_service()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
|
||||||
|
rt.block_on(main_async(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_logging(args: &Args) {
|
||||||
|
let avoid_trace = match args.verbosity {
|
||||||
|
ArgVerbosity::Trace => ArgVerbosity::Debug,
|
||||||
|
_ => args.verbosity,
|
||||||
|
};
|
||||||
|
let default = format!(
|
||||||
|
"{:?},hickory_proto=warn,ipstack={:?},netlink_proto={:?},netlink_sys={:?}",
|
||||||
|
args.verbosity, avoid_trace, avoid_trace, avoid_trace
|
||||||
|
);
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn main_async(args: Args) -> Result<(), BoxError> {
|
||||||
|
setup_logging(&args);
|
||||||
|
|
||||||
|
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")]
|
||||||
|
if args.unshare && args.socket_transfer_fd.is_none() {
|
||||||
|
if let Err(err) = namespace_proxy_main(args, shutdown_token).await {
|
||||||
|
log::error!("namespace proxy error: {err}");
|
||||||
|
}
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C" fn traffic_cb(status: *const tun2proxy::TrafficStatus, _: *mut std::ffi::c_void) {
|
||||||
|
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()) };
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ctrlc_fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||||
|
let ctrlc_fired_clone = ctrlc_fired.clone();
|
||||||
|
let ctrlc_handel = ctrlc2::AsyncCtrlC::new(move || {
|
||||||
|
log::info!("Ctrl-C received, exiting...");
|
||||||
|
ctrlc_fired_clone.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
shutdown_token.cancel();
|
||||||
|
true
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn namespace_proxy_main(
|
||||||
|
_args: Args,
|
||||||
|
_shutdown_token: tokio_util::sync::CancellationToken,
|
||||||
|
) -> Result<std::process::ExitStatus, tun2proxy::Error> {
|
||||||
|
use nix::fcntl::{OFlag, open};
|
||||||
|
use nix::sys::stat::Mode;
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
let (socket, remote_fd) = tun2proxy::socket_transfer::create_transfer_socket_pair().await?;
|
||||||
|
|
||||||
|
let fd = open("/proc/self/exe", OFlag::O_PATH, Mode::empty())?;
|
||||||
|
|
||||||
|
let child = tokio::process::Command::new("unshare")
|
||||||
|
.args("--user --map-current-user --net --mount --keep-caps --kill-child --fork".split(' '))
|
||||||
|
.arg(format!("/proc/self/fd/{}", fd.as_raw_fd()))
|
||||||
|
.arg("--socket-transfer-fd")
|
||||||
|
.arg(remote_fd.as_raw_fd().to_string())
|
||||||
|
.args(std::env::args().skip(1))
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
let mut child = match child {
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
log::error!("`unshare(1)` executable wasn't located in PATH.");
|
||||||
|
log::error!("Consider installing linux utils package: `apt install util-linux`");
|
||||||
|
log::error!("Or similar for your distribution.");
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
child => child?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let unshare_pid = child.id().unwrap_or(0);
|
||||||
|
log::info!("The tun proxy is running in unprivileged mode. See `namespaces(7)`.");
|
||||||
|
log::info!("");
|
||||||
|
log::info!("If you need to run a process that relies on root-like capabilities (e.g. `openvpn`)");
|
||||||
|
log::info!("Use `tun2proxy-bin --unshare --setup [...] -- openvpn --config [...]`");
|
||||||
|
log::info!("");
|
||||||
|
log::info!("To run a new process in the created namespace (e.g. a flatpak app)");
|
||||||
|
log::info!("Use `nsenter --preserve-credentials --user --net --mount --target {unshare_pid} /bin/sh`");
|
||||||
|
log::info!("");
|
||||||
|
if let Some(pidfile) = _args.unshare_pidfile.as_ref() {
|
||||||
|
log::info!("Writing unshare pid to {pidfile}");
|
||||||
|
std::fs::write(pidfile, unshare_pid.to_string()).ok();
|
||||||
|
}
|
||||||
|
tokio::spawn(async move { tun2proxy::socket_transfer::process_socket_requests(&socket).await });
|
||||||
|
|
||||||
|
Ok(child.wait().await?)
|
||||||
|
}
|
271
src/bin/udpgw_server.rs
Normal file
271
src/bin/udpgw_server.rs
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_info() -> &'static str {
|
||||||
|
concat!("UDP Gateway Server for tun2proxy\nVersion: ", tun2proxy::version_info!())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, clap::Parser)]
|
||||||
|
#[command(author, version = tun2proxy::version_info!(), about = about_info(), 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 {masked_addr} retrieve_from_async_stream \"{e}\"");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if client.last_activity.elapsed() >= CLIENT_DISCONNECT_TIMEOUT {
|
||||||
|
log::debug!("client {masked_addr} last_activity elapsed \"{e}\"");
|
||||||
|
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 {masked_addr} send keepalive");
|
||||||
|
// 2. if keepalive packet, do nothing, send keepalive response to client
|
||||||
|
send_keepalive_response(tx.clone(), conn_id).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log::trace!("client {masked_addr} received udp data {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 {masked_addr} process udp function \"{e}\"");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 {masked_addr} with {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!(), tun2proxy::version_info!());
|
||||||
|
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::AsyncCtrlC::new(move || {
|
||||||
|
log::info!("Ctrl-C received, exiting...");
|
||||||
|
ctrlc_fired_clone.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
shutdown_token.cancel();
|
||||||
|
true
|
||||||
|
})?;
|
||||||
|
|
||||||
|
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?;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {masked_addr} connected");
|
||||||
|
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 {masked_addr} disconnected with {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))
|
||||||
|
}
|
28
src/directions.rs
Normal file
28
src/directions.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub(crate) enum IncomingDirection {
|
||||||
|
FromServer,
|
||||||
|
FromClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub(crate) enum OutgoingDirection {
|
||||||
|
ToServer,
|
||||||
|
ToClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub(crate) enum Direction {
|
||||||
|
Incoming(IncomingDirection),
|
||||||
|
Outgoing(OutgoingDirection),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||||
|
pub(crate) struct DataEvent<'a, T> {
|
||||||
|
pub(crate) direction: T,
|
||||||
|
pub(crate) buffer: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type IncomingDataEvent<'a> = DataEvent<'a, IncomingDirection>;
|
||||||
|
pub(crate) type OutgoingDataEvent<'a> = DataEvent<'a, OutgoingDirection>;
|
70
src/dns.rs
Normal file
70
src/dns.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
use hickory_proto::{
|
||||||
|
op::{Message, MessageType, ResponseCode},
|
||||||
|
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) => 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
|
||||||
|
// recognize it.
|
||||||
|
request.set_message_type(MessageType::Response);
|
||||||
|
|
||||||
|
request.add_answer(record);
|
||||||
|
Ok(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_ipv6_entries(message: &mut Message) {
|
||||||
|
message.answers_mut().retain(|answer| !matches!(answer.data(), RData::AAAA(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_ipaddr_from_dns_message(message: &Message) -> Result<IpAddr, String> {
|
||||||
|
if message.response_code() != ResponseCode::NoError {
|
||||||
|
return Err(format!("{:?}", message.response_code()));
|
||||||
|
}
|
||||||
|
let mut cname = None;
|
||||||
|
for answer in message.answers() {
|
||||||
|
match answer.data() {
|
||||||
|
RData::A(addr) => {
|
||||||
|
return Ok(IpAddr::V4((*addr).into()));
|
||||||
|
}
|
||||||
|
RData::AAAA(addr) => {
|
||||||
|
return Ok(IpAddr::V6((*addr).into()));
|
||||||
|
}
|
||||||
|
RData::CNAME(name) => {
|
||||||
|
cname = Some(name.to_utf8());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(cname) = cname {
|
||||||
|
return Err(cname);
|
||||||
|
}
|
||||||
|
Err(format!("{:?}", message.answers()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_domain_from_dns_message(message: &Message) -> Result<String, String> {
|
||||||
|
let query = message.queries().first().ok_or("DnsRequest no query body")?;
|
||||||
|
let name = query.name().to_string();
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_data_to_dns_message(data: &[u8], used_by_tcp: bool) -> Result<Message, String> {
|
||||||
|
if used_by_tcp {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err("invalid dns data".into());
|
||||||
|
}
|
||||||
|
let len = u16::from_be_bytes([data[0], data[1]]) as usize;
|
||||||
|
let data = data.get(2..len + 2).ok_or("invalid dns data")?;
|
||||||
|
return parse_data_to_dns_message(data, false);
|
||||||
|
}
|
||||||
|
let message = Message::from_vec(data).map_err(|e| e.to_string())?;
|
||||||
|
Ok(message)
|
||||||
|
}
|
72
src/dump_logger.rs
Normal file
72
src/dump_logger.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use crate::ArgVerbosity;
|
||||||
|
use std::{
|
||||||
|
os::raw::{c_char, c_void},
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) static DUMP_CALLBACK: Mutex<Option<DumpCallback>> = Mutex::new(None);
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// set dump log info callback.
|
||||||
|
#[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,
|
||||||
|
) {
|
||||||
|
*DUMP_CALLBACK.lock().unwrap() = Some(DumpCallback(callback, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DumpCallback(Option<unsafe extern "C" fn(ArgVerbosity, *const c_char, *mut c_void)>, *mut c_void);
|
||||||
|
|
||||||
|
impl DumpCallback {
|
||||||
|
unsafe fn call(self, dump_level: ArgVerbosity, info: *const c_char) {
|
||||||
|
if let Some(cb) = self.0 {
|
||||||
|
unsafe { cb(dump_level, info, self.1) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for DumpCallback {}
|
||||||
|
unsafe impl Sync for DumpCallback {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct DumpLogger {}
|
||||||
|
|
||||||
|
impl log::Log for DumpLogger {
|
||||||
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
|
metadata.level() <= log::Level::Trace
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &log::Record) {
|
||||||
|
if self.enabled(record.metadata()) {
|
||||||
|
let current_crate_name = env!("CARGO_CRATE_NAME");
|
||||||
|
if record.module_path().unwrap_or("").starts_with(current_crate_name) {
|
||||||
|
self.do_dump_log(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DumpLogger {
|
||||||
|
fn do_dump_log(&self, record: &log::Record) {
|
||||||
|
let timestamp: chrono::DateTime<chrono::Local> = chrono::Local::now();
|
||||||
|
let msg = format!(
|
||||||
|
"[{} {:<5} {}] - {}",
|
||||||
|
timestamp.format("%Y-%m-%d %H:%M:%S"),
|
||||||
|
record.level(),
|
||||||
|
record.module_path().unwrap_or(""),
|
||||||
|
record.args()
|
||||||
|
);
|
||||||
|
let c_msg = std::ffi::CString::new(msg).unwrap();
|
||||||
|
let ptr = c_msg.as_ptr();
|
||||||
|
if let Some(cb) = DUMP_CALLBACK.lock().unwrap().clone() {
|
||||||
|
unsafe {
|
||||||
|
cb.call(record.level().into(), ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
src/error.rs
71
src/error.rs
|
@ -3,49 +3,57 @@ pub enum Error {
|
||||||
#[error("std::ffi::NulError {0:?}")]
|
#[error("std::ffi::NulError {0:?}")]
|
||||||
Nul(#[from] std::ffi::NulError),
|
Nul(#[from] std::ffi::NulError),
|
||||||
|
|
||||||
#[error("ctrlc::Error {0:?}")]
|
#[error(transparent)]
|
||||||
InterruptHandler(#[from] ctrlc::Error),
|
|
||||||
|
|
||||||
#[error("std::io::Error {0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[error("nix::errno::Errno {0:?}")]
|
||||||
|
NixErrno(#[from] nix::errno::Errno),
|
||||||
|
|
||||||
|
#[error("TryFromIntError {0:?}")]
|
||||||
|
TryFromInt(#[from] std::num::TryFromIntError),
|
||||||
|
|
||||||
#[error("std::net::AddrParseError {0}")]
|
#[error("std::net::AddrParseError {0}")]
|
||||||
AddrParse(#[from] std::net::AddrParseError),
|
AddrParse(#[from] std::net::AddrParseError),
|
||||||
|
|
||||||
#[error("smoltcp::iface::RouteTableFull {0:?}")]
|
#[error("std::str::Utf8Error {0:?}")]
|
||||||
RouteTableFull(#[from] smoltcp::iface::RouteTableFull),
|
Utf8(#[from] std::str::Utf8Error),
|
||||||
|
|
||||||
#[error("smoltcp::socket::tcp::RecvError {0:?}")]
|
#[error("TryFromSliceError {0:?}")]
|
||||||
Recv(#[from] smoltcp::socket::tcp::RecvError),
|
TryFromSlice(#[from] std::array::TryFromSliceError),
|
||||||
|
|
||||||
#[error("smoltcp::socket::tcp::ListenError {0:?}")]
|
#[error("IpStackError {0:?}")]
|
||||||
Listen(#[from] smoltcp::socket::tcp::ListenError),
|
IpStack(#[from] Box<ipstack::IpStackError>),
|
||||||
|
|
||||||
#[error("smoltcp::socket::udp::BindError {0:?}")]
|
#[error("DnsProtoError {0:?}")]
|
||||||
Bind(#[from] smoltcp::socket::udp::BindError),
|
DnsProto(#[from] hickory_proto::ProtoError),
|
||||||
|
|
||||||
#[error("smoltcp::socket::tcp::SendError {0:?}")]
|
#[error("httparse::Error {0:?}")]
|
||||||
Send(#[from] smoltcp::socket::tcp::SendError),
|
Httparse(#[from] httparse::Error),
|
||||||
|
|
||||||
#[error("&str {0}")]
|
#[error("digest_auth::Error {0:?}")]
|
||||||
Str(String),
|
DigestAuth(#[from] digest_auth::Error),
|
||||||
|
|
||||||
#[error("String {0}")]
|
#[cfg(target_os = "android")]
|
||||||
|
#[error("jni::errors::Error {0:?}")]
|
||||||
|
Jni(#[from] jni::errors::Error),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
String(String),
|
String(String),
|
||||||
|
|
||||||
#[error("&String {0}")]
|
|
||||||
RefString(String),
|
|
||||||
|
|
||||||
#[error("nix::errno::Errno {0:?}")]
|
|
||||||
OSError(#[from] nix::errno::Errno),
|
|
||||||
|
|
||||||
#[error("std::num::ParseIntError {0:?}")]
|
#[error("std::num::ParseIntError {0:?}")]
|
||||||
IntParseError(#[from] std::num::ParseIntError),
|
IntParseError(#[from] std::num::ParseIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ipstack::IpStackError> for Error {
|
||||||
|
fn from(err: ipstack::IpStackError) -> Self {
|
||||||
|
Self::IpStack(Box::new(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<&str> for Error {
|
impl From<&str> for Error {
|
||||||
fn from(err: &str) -> Self {
|
fn from(err: &str) -> Self {
|
||||||
Self::Str(err.to_string())
|
Self::String(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +65,19 @@ impl From<String> for Error {
|
||||||
|
|
||||||
impl From<&String> for Error {
|
impl From<&String> for Error {
|
||||||
fn from(err: &String) -> Self {
|
fn from(err: &String) -> Self {
|
||||||
Self::RefString(err.to_string())
|
Self::String(err.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Error> for std::io::Error {
|
||||||
|
fn from(err: Error) -> Self {
|
||||||
|
match err {
|
||||||
|
Error::Io(err) => err,
|
||||||
|
_ => std::io::Error::other(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
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);
|
||||||
|
|
||||||
|
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(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||||
|
if args.setup {
|
||||||
|
restore = Some(tproxy_config::tproxy_setup(&tproxy_args).await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.clone()));
|
||||||
|
|
||||||
|
match join_handle.await? {
|
||||||
|
Ok(sessions) => {
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
|
||||||
|
tproxy_config::tproxy_remove(restore).await?;
|
||||||
|
Ok(sessions)
|
||||||
|
}
|
||||||
|
Err(err) => Err(std::io::Error::from(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # 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
|
||||||
|
}
|
431
src/http.rs
431
src/http.rs
|
@ -1,108 +1,316 @@
|
||||||
use crate::error::Error;
|
use crate::{
|
||||||
use crate::tun2proxy::{
|
directions::{IncomingDataEvent, IncomingDirection, OutgoingDataEvent, OutgoingDirection},
|
||||||
Connection, ConnectionManager, IncomingDataEvent, IncomingDirection, OutgoingDataEvent,
|
error::{Error, Result},
|
||||||
OutgoingDirection, TcpProxy,
|
proxy_handler::{ProxyHandler, ProxyHandlerManager},
|
||||||
|
session_info::{IpProtocol, SessionInfo},
|
||||||
};
|
};
|
||||||
use crate::Credentials;
|
use httparse::Response;
|
||||||
use base64::Engine;
|
use socks5_impl::protocol::UserKey;
|
||||||
use smoltcp::wire::IpProtocol;
|
use std::{
|
||||||
use std::collections::VecDeque;
|
collections::{HashMap, VecDeque, hash_map::RandomState},
|
||||||
use std::net::SocketAddr;
|
iter::FromIterator,
|
||||||
use std::rc::Rc;
|
net::SocketAddr,
|
||||||
|
str,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use unicase::UniCase;
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
enum AuthenticationScheme {
|
||||||
|
None,
|
||||||
|
Basic,
|
||||||
|
Digest,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
#[derive(Eq, PartialEq, Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
enum HttpState {
|
enum HttpState {
|
||||||
SendRequest,
|
SendRequest,
|
||||||
ExpectStatusCode,
|
ExpectResponseHeaders,
|
||||||
ExpectResponse,
|
ExpectResponse,
|
||||||
|
Reset,
|
||||||
Established,
|
Established,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) type DigestState = digest_auth::WwwAuthenticateHeader;
|
||||||
|
|
||||||
pub struct HttpConnection {
|
pub struct HttpConnection {
|
||||||
|
server_addr: SocketAddr,
|
||||||
state: HttpState,
|
state: HttpState,
|
||||||
client_inbuf: VecDeque<u8>,
|
client_inbuf: VecDeque<u8>,
|
||||||
server_inbuf: VecDeque<u8>,
|
server_inbuf: VecDeque<u8>,
|
||||||
client_outbuf: VecDeque<u8>,
|
client_outbuf: VecDeque<u8>,
|
||||||
server_outbuf: VecDeque<u8>,
|
server_outbuf: VecDeque<u8>,
|
||||||
data_buf: VecDeque<u8>,
|
|
||||||
crlf_state: u8,
|
crlf_state: u8,
|
||||||
|
counter: usize,
|
||||||
|
skip: usize,
|
||||||
|
digest_state: Arc<Mutex<Option<DigestState>>>,
|
||||||
|
before: bool,
|
||||||
|
credentials: Option<UserKey>,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static PROXY_AUTHENTICATE: &str = "Proxy-Authenticate";
|
||||||
|
static PROXY_AUTHORIZATION: &str = "Proxy-Authorization";
|
||||||
|
static CONNECTION: &str = "Connection";
|
||||||
|
static TRANSFER_ENCODING: &str = "Transfer-Encoding";
|
||||||
|
static CONTENT_LENGTH: &str = "Content-Length";
|
||||||
|
|
||||||
impl HttpConnection {
|
impl HttpConnection {
|
||||||
fn new(connection: &Connection, manager: Rc<dyn ConnectionManager>) -> Self {
|
async fn new(
|
||||||
let mut server_outbuf: VecDeque<u8> = VecDeque::new();
|
server_addr: SocketAddr,
|
||||||
{
|
info: SessionInfo,
|
||||||
let credentials = manager.get_credentials();
|
domain_name: Option<String>,
|
||||||
server_outbuf.extend(b"CONNECT ".iter());
|
credentials: Option<UserKey>,
|
||||||
server_outbuf.extend(connection.dst.to_string().as_bytes());
|
digest_state: Arc<Mutex<Option<DigestState>>>,
|
||||||
server_outbuf.extend(b" HTTP/1.1\r\nHost: ".iter());
|
) -> Result<Self> {
|
||||||
server_outbuf.extend(connection.dst.to_string().as_bytes());
|
let mut res = Self {
|
||||||
server_outbuf.extend(b"\r\n".iter());
|
server_addr,
|
||||||
if let Some(credentials) = credentials {
|
state: HttpState::ExpectResponseHeaders,
|
||||||
server_outbuf.extend(b"Proxy-Authorization: Basic ");
|
client_inbuf: VecDeque::default(),
|
||||||
let mut auth_plain = credentials.username.clone();
|
server_inbuf: VecDeque::default(),
|
||||||
auth_plain.extend(b":".iter());
|
client_outbuf: VecDeque::default(),
|
||||||
auth_plain.extend(&credentials.password);
|
server_outbuf: VecDeque::default(),
|
||||||
let auth_b64 = base64::engine::general_purpose::STANDARD.encode(auth_plain);
|
skip: 0,
|
||||||
server_outbuf.extend(auth_b64.as_bytes().iter());
|
counter: 0,
|
||||||
server_outbuf.extend(b"\r\n".iter());
|
crlf_state: 0,
|
||||||
}
|
digest_state,
|
||||||
server_outbuf.extend(b"\r\n".iter());
|
before: false,
|
||||||
|
credentials,
|
||||||
|
info,
|
||||||
|
domain_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.send_tunnel_request().await?;
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
async fn send_tunnel_request(&mut self) -> Result<(), Error> {
|
||||||
state: HttpState::ExpectStatusCode,
|
let host = if let Some(domain_name) = &self.domain_name {
|
||||||
client_inbuf: Default::default(),
|
format!("{}:{}", domain_name, self.info.dst.port())
|
||||||
server_inbuf: Default::default(),
|
} else {
|
||||||
client_outbuf: Default::default(),
|
self.info.dst.to_string()
|
||||||
server_outbuf,
|
};
|
||||||
data_buf: Default::default(),
|
|
||||||
crlf_state: Default::default(),
|
self.server_outbuf.extend(b"CONNECT ");
|
||||||
}
|
self.server_outbuf.extend(host.as_bytes());
|
||||||
|
self.server_outbuf.extend(b" HTTP/1.1\r\nHost: ");
|
||||||
|
self.server_outbuf.extend(host.as_bytes());
|
||||||
|
self.server_outbuf.extend(b"\r\n");
|
||||||
|
|
||||||
|
let scheme = if self.digest_state.lock().await.is_none() {
|
||||||
|
AuthenticationScheme::Basic
|
||||||
|
} else {
|
||||||
|
AuthenticationScheme::Digest
|
||||||
|
};
|
||||||
|
self.send_auth_data(scheme).await?;
|
||||||
|
|
||||||
|
self.server_outbuf.extend(b"\r\n");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn state_change(&mut self) -> Result<(), Error> {
|
async fn send_auth_data(&mut self, scheme: AuthenticationScheme) -> Result<()> {
|
||||||
let http_len = "HTTP/1.1 200".len();
|
let Some(credentials) = &self.credentials else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
match scheme {
|
||||||
|
AuthenticationScheme::Digest => {
|
||||||
|
let uri = if let Some(domain_name) = &self.domain_name {
|
||||||
|
format!("{}:{}", domain_name, self.info.dst.port())
|
||||||
|
} else {
|
||||||
|
self.info.dst.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let context = digest_auth::AuthContext::new_with_method(
|
||||||
|
&credentials.username,
|
||||||
|
&credentials.password,
|
||||||
|
&uri,
|
||||||
|
Option::<&'_ [u8]>::None,
|
||||||
|
digest_auth::HttpMethod::CONNECT,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut state = self.digest_state.lock().await;
|
||||||
|
let response = state.as_mut().unwrap().respond(&context).unwrap();
|
||||||
|
|
||||||
|
self.server_outbuf
|
||||||
|
.extend(format!("{}: {}\r\n", PROXY_AUTHORIZATION, response.to_header_string()).as_bytes());
|
||||||
|
}
|
||||||
|
AuthenticationScheme::Basic => {
|
||||||
|
let auth_b64 = base64easy::encode(credentials.to_string(), base64easy::EngineKind::Standard);
|
||||||
|
self.server_outbuf
|
||||||
|
.extend(format!("{PROXY_AUTHORIZATION}: Basic {auth_b64}\r\n").as_bytes());
|
||||||
|
}
|
||||||
|
AuthenticationScheme::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn state_change(&mut self) -> Result<()> {
|
||||||
match self.state {
|
match self.state {
|
||||||
HttpState::ExpectStatusCode if self.server_inbuf.len() > http_len => {
|
HttpState::ExpectResponseHeaders => {
|
||||||
let status_line: Vec<u8> =
|
while self.counter < self.server_inbuf.len() {
|
||||||
self.server_inbuf.range(0..http_len + 1).copied().collect();
|
let b = self.server_inbuf[self.counter];
|
||||||
let slice = &status_line.as_slice()[0.."HTTP/1.1 2".len()];
|
|
||||||
if slice != b"HTTP/1.1 2" && slice != b"HTTP/1.0 2"
|
|
||||||
|| self.server_inbuf[http_len] != b' '
|
|
||||||
{
|
|
||||||
let status_str = String::from_utf8_lossy(&status_line.as_slice()[0..http_len]);
|
|
||||||
let e =
|
|
||||||
format!("Expected success status code. Server replied with {status_str}.");
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
self.state = HttpState::ExpectResponse;
|
|
||||||
return self.state_change();
|
|
||||||
}
|
|
||||||
HttpState::ExpectResponse => {
|
|
||||||
let mut counter = 0usize;
|
|
||||||
for b_ref in self.server_inbuf.iter() {
|
|
||||||
let b = *b_ref;
|
|
||||||
if b == b'\n' {
|
if b == b'\n' {
|
||||||
self.crlf_state += 1;
|
self.crlf_state += 1;
|
||||||
} else if b != b'\r' {
|
} else if b != b'\r' {
|
||||||
self.crlf_state = 0;
|
self.crlf_state = 0;
|
||||||
}
|
}
|
||||||
counter += 1;
|
|
||||||
|
|
||||||
|
self.counter += 1;
|
||||||
if self.crlf_state == 2 {
|
if self.crlf_state == 2 {
|
||||||
self.server_inbuf.drain(0..counter);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.server_outbuf.append(&mut self.data_buf);
|
if self.crlf_state != 2 {
|
||||||
self.data_buf.clear();
|
// Waiting for the end of the headers yet
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_size = self.counter;
|
||||||
|
|
||||||
|
self.counter = 0;
|
||||||
|
self.crlf_state = 0;
|
||||||
|
|
||||||
|
let mut headers = [httparse::EMPTY_HEADER; 16];
|
||||||
|
let mut res = Response::new(&mut headers);
|
||||||
|
|
||||||
|
// First make the buffer contiguous
|
||||||
|
let slice = self.server_inbuf.make_contiguous();
|
||||||
|
let status = res.parse(slice)?;
|
||||||
|
if status.is_partial() {
|
||||||
|
// TODO: Optimize in order to detect 200
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let len = status.unwrap();
|
||||||
|
let status_code = res.code.unwrap();
|
||||||
|
let version = res.version.unwrap();
|
||||||
|
|
||||||
|
if status_code == 200 {
|
||||||
|
// Connection successful
|
||||||
self.state = HttpState::Established;
|
self.state = HttpState::Established;
|
||||||
return self.state_change();
|
// The server may have sent a banner already (SMTP, SSH, etc.).
|
||||||
}
|
// Therefore, server_inbuf must retain this data.
|
||||||
|
self.server_inbuf.drain(0..header_size);
|
||||||
|
return Box::pin(self.state_change()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.server_inbuf.drain(0..counter);
|
if status_code != 407 {
|
||||||
|
let e = format!(
|
||||||
|
"Expected success status code. Server replied with {} [Reason: {}].",
|
||||||
|
status_code,
|
||||||
|
res.reason.unwrap()
|
||||||
|
);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers_map: HashMap<UniCase<&str>, &[u8], RandomState> =
|
||||||
|
HashMap::from_iter(headers.map(|x| (UniCase::new(x.name), x.value)));
|
||||||
|
|
||||||
|
let Some(auth_data) = headers_map.get(&UniCase::new(PROXY_AUTHENTICATE)) else {
|
||||||
|
return Err("Proxy requires auth but doesn't send it datails".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if !auth_data[..6].eq_ignore_ascii_case(b"digest") {
|
||||||
|
// Fail to auth and the scheme isn't in the
|
||||||
|
// supported auth method schemes
|
||||||
|
return Err("Bad credentials".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analize challenge params
|
||||||
|
let data = str::from_utf8(auth_data)?;
|
||||||
|
let state = digest_auth::parse(data)?;
|
||||||
|
if self.before && !state.stale {
|
||||||
|
return Err("Bad credentials".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the digest state
|
||||||
|
self.digest_state.lock().await.replace(state);
|
||||||
|
self.before = true;
|
||||||
|
|
||||||
|
let closed = match headers_map.get(&UniCase::new(CONNECTION)) {
|
||||||
|
Some(conn_header) => conn_header.eq_ignore_ascii_case(b"close"),
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if closed || version == 0 {
|
||||||
|
// Close mio stream connection and reset it
|
||||||
|
// Reset all the buffers
|
||||||
|
self.server_inbuf.clear();
|
||||||
|
self.server_outbuf.clear();
|
||||||
|
self.send_tunnel_request().await?;
|
||||||
|
|
||||||
|
self.state = HttpState::Reset;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTTP/1.1 expected to be keep alive waiting for the next frame so, we must
|
||||||
|
// 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
|
||||||
|
if headers_map.contains_key(&UniCase::new(TRANSFER_ENCODING)) {
|
||||||
|
unimplemented!("Header Transfer-Encoding not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
let content_length = match headers_map.get(&UniCase::new(CONTENT_LENGTH)) {
|
||||||
|
Some(v) => {
|
||||||
|
let value = str::from_utf8(v)?;
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9110#section-5.6.1
|
||||||
|
match value.parse::<usize>() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
let mut it = value.split(',').map(|x| x.parse::<usize>());
|
||||||
|
let f = it.next().unwrap()?;
|
||||||
|
for k in it {
|
||||||
|
if k? != f {
|
||||||
|
return Err("Malformed response".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Close the connection by information miss
|
||||||
|
self.server_inbuf.clear();
|
||||||
|
self.server_outbuf.clear();
|
||||||
|
self.send_tunnel_request().await?;
|
||||||
|
|
||||||
|
self.state = HttpState::Reset;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handshake state
|
||||||
|
self.state = HttpState::ExpectResponse;
|
||||||
|
self.skip = content_length + len;
|
||||||
|
|
||||||
|
return Box::pin(self.state_change()).await;
|
||||||
|
}
|
||||||
|
HttpState::ExpectResponse => {
|
||||||
|
if self.skip > 0 {
|
||||||
|
let cnt = self.skip.min(self.server_inbuf.len());
|
||||||
|
self.server_inbuf.drain(..cnt);
|
||||||
|
self.skip -= cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.skip == 0 {
|
||||||
|
// Expected to the server_inbuff to be empty
|
||||||
|
|
||||||
|
// self.server_outbuf.append(&mut self.data_buf);
|
||||||
|
// self.data_buf.clear();
|
||||||
|
self.send_tunnel_request().await?;
|
||||||
|
self.state = HttpState::ExpectResponseHeaders;
|
||||||
|
|
||||||
|
return Box::pin(self.state_change()).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HttpState::Established => {
|
HttpState::Established => {
|
||||||
self.client_outbuf.extend(self.server_inbuf.iter());
|
self.client_outbuf.extend(self.server_inbuf.iter());
|
||||||
|
@ -110,14 +318,31 @@ impl HttpConnection {
|
||||||
self.server_inbuf.clear();
|
self.server_inbuf.clear();
|
||||||
self.client_inbuf.clear();
|
self.client_inbuf.clear();
|
||||||
}
|
}
|
||||||
|
HttpState::Reset => {
|
||||||
|
self.state = HttpState::ExpectResponseHeaders;
|
||||||
|
return Box::pin(self.state_change()).await;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TcpProxy for HttpConnection {
|
#[async_trait::async_trait]
|
||||||
fn push_data(&mut self, event: IncomingDataEvent<'_>) -> Result<(), Error> {
|
impl ProxyHandler for HttpConnection {
|
||||||
|
fn get_server_addr(&self) -> SocketAddr {
|
||||||
|
self.server_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session_info(&self) -> SessionInfo {
|
||||||
|
self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_domain_name(&self) -> Option<String> {
|
||||||
|
self.domain_name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_data(&mut self, event: IncomingDataEvent<'_>) -> std::io::Result<()> {
|
||||||
let direction = event.direction;
|
let direction = event.direction;
|
||||||
let buffer = event.buffer;
|
let buffer = event.buffer;
|
||||||
match direction {
|
match direction {
|
||||||
|
@ -125,15 +350,12 @@ impl TcpProxy for HttpConnection {
|
||||||
self.server_inbuf.extend(buffer.iter());
|
self.server_inbuf.extend(buffer.iter());
|
||||||
}
|
}
|
||||||
IncomingDirection::FromClient => {
|
IncomingDirection::FromClient => {
|
||||||
if self.state == HttpState::Established {
|
|
||||||
self.client_inbuf.extend(buffer.iter());
|
self.client_inbuf.extend(buffer.iter());
|
||||||
} else {
|
|
||||||
self.data_buf.extend(buffer.iter());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state_change()
|
self.state_change().await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn consume_data(&mut self, dir: OutgoingDirection, size: usize) {
|
fn consume_data(&mut self, dir: OutgoingDirection, size: usize) {
|
||||||
|
@ -160,45 +382,52 @@ impl TcpProxy for HttpConnection {
|
||||||
fn connection_established(&self) -> bool {
|
fn connection_established(&self) -> bool {
|
||||||
self.state == HttpState::Established
|
self.state == HttpState::Established
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn data_len(&self, dir: OutgoingDirection) -> usize {
|
||||||
|
match dir {
|
||||||
|
OutgoingDirection::ToServer => self.server_outbuf.len(),
|
||||||
|
OutgoingDirection::ToClient => self.client_outbuf.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_connection(&self) -> bool {
|
||||||
|
self.state == HttpState::Reset
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_udp_associate(&self) -> Option<SocketAddr> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct HttpManager {
|
pub(crate) struct HttpManager {
|
||||||
server: SocketAddr,
|
server: SocketAddr,
|
||||||
credentials: Option<Credentials>,
|
credentials: Option<UserKey>,
|
||||||
|
digest_state: Arc<Mutex<Option<DigestState>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectionManager for HttpManager {
|
#[async_trait::async_trait]
|
||||||
fn handles_connection(&self, connection: &Connection) -> bool {
|
impl ProxyHandlerManager for HttpManager {
|
||||||
connection.proto == IpProtocol::Tcp
|
async fn new_proxy_handler(
|
||||||
}
|
|
||||||
|
|
||||||
fn new_connection(
|
|
||||||
&self,
|
&self,
|
||||||
connection: &Connection,
|
info: SessionInfo,
|
||||||
manager: Rc<dyn ConnectionManager>,
|
domain_name: Option<String>,
|
||||||
) -> Result<Option<Box<dyn TcpProxy>>, Error> {
|
_udp_associate: bool,
|
||||||
if connection.proto != IpProtocol::Tcp {
|
) -> std::io::Result<Arc<Mutex<dyn ProxyHandler>>> {
|
||||||
return Ok(None);
|
if info.protocol != IpProtocol::Tcp {
|
||||||
|
return Err(Error::from("Protocol not supported by HTTP proxy").into());
|
||||||
}
|
}
|
||||||
Ok(Some(Box::new(HttpConnection::new(connection, manager))))
|
Ok(Arc::new(Mutex::new(
|
||||||
}
|
HttpConnection::new(self.server, info, domain_name, self.credentials.clone(), self.digest_state.clone()).await?,
|
||||||
|
)))
|
||||||
fn close_connection(&self, _: &Connection) {}
|
|
||||||
|
|
||||||
fn get_server(&self) -> SocketAddr {
|
|
||||||
self.server
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_credentials(&self) -> &Option<Credentials> {
|
|
||||||
&self.credentials
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpManager {
|
impl HttpManager {
|
||||||
pub fn new(server: SocketAddr, credentials: Option<Credentials>) -> Rc<Self> {
|
pub fn new(server: SocketAddr, credentials: Option<UserKey>) -> Self {
|
||||||
Rc::new(Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
credentials,
|
credentials,
|
||||||
})
|
digest_state: Arc::new(Mutex::new(None)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
961
src/lib.rs
961
src/lib.rs
|
@ -1,136 +1,851 @@
|
||||||
use crate::error::Error;
|
#[cfg(feature = "udpgw")]
|
||||||
use crate::socks5::SocksVersion;
|
use crate::udpgw::UdpGwClient;
|
||||||
use crate::{http::HttpManager, socks5::SocksManager, tun2proxy::TunToProxy};
|
use crate::{
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
directions::{IncomingDataEvent, IncomingDirection, OutgoingDirection},
|
||||||
|
http::HttpManager,
|
||||||
|
no_proxy::NoProxyManager,
|
||||||
|
session_info::{IpProtocol, SessionInfo},
|
||||||
|
virtual_dns::VirtualDns,
|
||||||
|
};
|
||||||
|
use ipstack::{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,
|
||||||
|
net::{IpAddr, SocketAddr},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
|
||||||
|
net::{TcpSocket, TcpStream, UdpSocket},
|
||||||
|
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 mod error;
|
pub use {
|
||||||
mod http;
|
args::{ArgDns, ArgProxy, ArgVerbosity, Args, ProxyType},
|
||||||
pub mod setup;
|
error::{BoxError, Error, Result},
|
||||||
mod socks5;
|
traffic_status::{TrafficStatus, tun2proxy_set_traffic_status_callback},
|
||||||
mod tun2proxy;
|
|
||||||
mod virtdevice;
|
|
||||||
mod virtdns;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Proxy {
|
|
||||||
pub proxy_type: ProxyType,
|
|
||||||
pub addr: SocketAddr,
|
|
||||||
pub credentials: Option<Credentials>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Proxy {
|
|
||||||
pub fn from_url(s: &str) -> Result<Proxy, Error> {
|
|
||||||
let e = format!("`{s}` is not a valid proxy URL");
|
|
||||||
let url = url::Url::parse(s).map_err(|_| Error::from(&e))?;
|
|
||||||
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 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 credentials = if url.username() == "" && url.password().is_none() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let username = String::from(url.username());
|
|
||||||
let password = String::from(url.password().unwrap_or(""));
|
|
||||||
Some(Credentials::new(&username, &password))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let scheme = url.scheme();
|
#[cfg(feature = "mimalloc")]
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
let proxy_type = match url.scheme().to_ascii_lowercase().as_str() {
|
pub use general_api::general_run_async;
|
||||||
"socks4" => Some(ProxyType::Socks4),
|
|
||||||
"socks5" => Some(ProxyType::Socks5),
|
mod android;
|
||||||
"http" => Some(ProxyType::Http),
|
mod args;
|
||||||
_ => None,
|
mod directions;
|
||||||
|
mod dns;
|
||||||
|
mod dump_logger;
|
||||||
|
mod error;
|
||||||
|
mod general_api;
|
||||||
|
mod http;
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
#[cfg_attr(
|
||||||
|
target_os = "linux",
|
||||||
|
derive(bincode::Encode, bincode::Decode, serde::Serialize, serde::Deserialize)
|
||||||
|
)]
|
||||||
|
pub enum SocketProtocol {
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
}
|
}
|
||||||
.ok_or(Error::from(&format!("`{scheme}` is an invalid proxy type")))?;
|
|
||||||
|
|
||||||
Ok(Proxy {
|
#[allow(unused)]
|
||||||
proxy_type,
|
#[derive(Hash, Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
#[cfg_attr(
|
||||||
|
target_os = "linux",
|
||||||
|
derive(bincode::Encode, bincode::Decode, serde::Serialize, serde::Deserialize)
|
||||||
|
)]
|
||||||
|
pub enum SocketDomain {
|
||||||
|
IpV4,
|
||||||
|
IpV6,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<IpAddr> for SocketDomain {
|
||||||
|
fn from(value: IpAddr) -> Self {
|
||||||
|
match value {
|
||||||
|
IpAddr::V4(_) => Self::IpV4,
|
||||||
|
IpAddr::V6(_) => Self::IpV6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SocketQueue {
|
||||||
|
tcp_v4: Mutex<Receiver<TcpSocket>>,
|
||||||
|
tcp_v6: Mutex<Receiver<TcpSocket>>,
|
||||||
|
udp_v4: Mutex<Receiver<UdpSocket>>,
|
||||||
|
udp_v6: Mutex<Receiver<UdpSocket>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocketQueue {
|
||||||
|
async fn recv_tcp(&self, domain: SocketDomain) -> Result<TcpSocket, std::io::Error> {
|
||||||
|
match domain {
|
||||||
|
SocketDomain::IpV4 => &self.tcp_v4,
|
||||||
|
SocketDomain::IpV6 => &self.tcp_v6,
|
||||||
|
}
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.ok_or(ErrorKind::Other.into())
|
||||||
|
}
|
||||||
|
async fn recv_udp(&self, domain: SocketDomain) -> Result<UdpSocket, std::io::Error> {
|
||||||
|
match domain {
|
||||||
|
SocketDomain::IpV4 => &self.udp_v4,
|
||||||
|
SocketDomain::IpV6 => &self.udp_v6,
|
||||||
|
}
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.ok_or(ErrorKind::Other.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_tcp_stream(socket_queue: &Option<Arc<SocketQueue>>, peer: SocketAddr) -> std::io::Result<TcpStream> {
|
||||||
|
match &socket_queue {
|
||||||
|
None => TcpStream::connect(peer).await,
|
||||||
|
Some(queue) => queue.recv_tcp(peer.ip().into()).await?.connect(peer).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_udp_stream(socket_queue: &Option<Arc<SocketQueue>>, peer: SocketAddr) -> std::io::Result<UdpStream> {
|
||||||
|
match &socket_queue {
|
||||||
|
None => UdpStream::connect(peer).await,
|
||||||
|
Some(queue) => {
|
||||||
|
let socket = queue.recv_udp(peer.ip().into()).await?;
|
||||||
|
socket.connect(peer).await?;
|
||||||
|
UdpStream::from_tokio(socket, peer).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the proxy server
|
||||||
|
/// # Arguments
|
||||||
|
/// * `device` - The network device to use
|
||||||
|
/// * `mtu` - The MTU of the network device
|
||||||
|
/// * `args` - The arguments to use
|
||||||
|
/// * `shutdown_token` - The token to exit the server
|
||||||
|
/// # 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,
|
||||||
|
{
|
||||||
|
log::info!("{} {} starting...", env!("CARGO_PKG_NAME"), version_info!());
|
||||||
|
log::info!("Proxy {} server: {}", args.proxy.proxy_type, args.proxy.addr);
|
||||||
|
|
||||||
|
let server_addr = args.proxy.addr;
|
||||||
|
let key = args.proxy.credentials.clone();
|
||||||
|
let dns_addr = args.dns_addr;
|
||||||
|
let ipv6_enabled = args.ipv6_enabled;
|
||||||
|
let virtual_dns = if args.dns == ArgDns::Virtual {
|
||||||
|
Some(Arc::new(Mutex::new(VirtualDns::new(args.virtual_dns_pool))))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let socket_queue = match args.socket_transfer_fd {
|
||||||
|
None => None,
|
||||||
|
Some(fd) => {
|
||||||
|
use crate::socket_transfer::{reconstruct_socket, reconstruct_transfer_socket, request_sockets};
|
||||||
|
use tokio::sync::mpsc::channel;
|
||||||
|
|
||||||
|
let fd = reconstruct_socket(fd)?;
|
||||||
|
let socket = reconstruct_transfer_socket(fd)?;
|
||||||
|
let socket = Arc::new(Mutex::new(socket));
|
||||||
|
|
||||||
|
macro_rules! create_socket_queue {
|
||||||
|
($domain:ident) => {{
|
||||||
|
const SOCKETS_PER_REQUEST: usize = 64;
|
||||||
|
|
||||||
|
let socket = socket.clone();
|
||||||
|
let (tx, rx) = channel(SOCKETS_PER_REQUEST);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let sockets =
|
||||||
|
match request_sockets(socket.lock().await, SocketDomain::$domain, SOCKETS_PER_REQUEST as u32).await {
|
||||||
|
Ok(sockets) => sockets,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("Socket allocation request failed: {err}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for s in sockets {
|
||||||
|
if let Err(_) = tx.send(s).await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mutex::new(rx)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Arc::new(SocketQueue {
|
||||||
|
tcp_v4: create_socket_queue!(IpV4),
|
||||||
|
tcp_v6: create_socket_queue!(IpV6),
|
||||||
|
udp_v4: create_socket_queue!(IpV4),
|
||||||
|
udp_v6: create_socket_queue!(IpV6),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
let socket_queue = None;
|
||||||
|
|
||||||
|
use socks5_impl::protocol::Version::{V4, V5};
|
||||||
|
let mgr: Arc<dyn ProxyHandlerManager> = match args.proxy.proxy_type {
|
||||||
|
ProxyType::Socks5 => Arc::new(SocksProxyManager::new(server_addr, V5, key)),
|
||||||
|
ProxyType::Socks4 => Arc::new(SocksProxyManager::new(server_addr, V4, key)),
|
||||||
|
ProxyType::Http => Arc::new(HttpManager::new(server_addr, key)),
|
||||||
|
ProxyType::None => Arc::new(NoProxyManager::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ipstack_config = ipstack::IpStackConfig::default();
|
||||||
|
ipstack_config.mtu(mtu);
|
||||||
|
ipstack_config.tcp_timeout(std::time::Duration::from_secs(args.tcp_timeout));
|
||||||
|
ipstack_config.udp_timeout(std::time::Duration::from_secs(args.udp_timeout));
|
||||||
|
|
||||||
|
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,
|
addr,
|
||||||
credentials,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
|
||||||
pub enum ProxyType {
|
|
||||||
Socks4,
|
|
||||||
Socks5,
|
|
||||||
Http,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ProxyType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ProxyType::Socks4 => write!(f, "socks4"),
|
|
||||||
ProxyType::Socks5 => write!(f, "socks5"),
|
|
||||||
ProxyType::Http => write!(f, "http"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Options {
|
|
||||||
virtdns: Option<virtdns::VirtualDns>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_virtual_dns(mut self) -> Self {
|
|
||||||
self.virtdns = Some(virtdns::VirtualDns::new());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
|
||||||
pub struct Credentials {
|
|
||||||
pub(crate) username: Vec<u8>,
|
|
||||||
pub(crate) password: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Credentials {
|
|
||||||
pub fn new(username: &str, password: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
username: username.as_bytes().to_vec(),
|
|
||||||
password: password.as_bytes().to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main_entry(tun: &str, proxy: &Proxy, options: Options) -> Result<(), Error> {
|
|
||||||
let mut ttp = TunToProxy::new(tun, options)?;
|
|
||||||
match proxy.proxy_type {
|
|
||||||
ProxyType::Socks4 => {
|
|
||||||
ttp.add_connection_manager(SocksManager::new(
|
|
||||||
proxy.addr,
|
|
||||||
SocksVersion::V4,
|
|
||||||
proxy.credentials.clone(),
|
|
||||||
));
|
));
|
||||||
|
let client_keepalive = client.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = client_keepalive.heartbeat_task().await;
|
||||||
|
});
|
||||||
|
client
|
||||||
|
});
|
||||||
|
|
||||||
|
let task_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||||
|
use std::sync::atomic::Ordering::Relaxed;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let task_count = task_count.clone();
|
||||||
|
let virtual_dns = virtual_dns.clone();
|
||||||
|
let ip_stack_stream = tokio::select! {
|
||||||
|
_ = shutdown_token.cancelled() => {
|
||||||
|
log::info!("Shutdown received");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
ProxyType::Socks5 => {
|
ip_stack_stream = ip_stack.accept() => {
|
||||||
ttp.add_connection_manager(SocksManager::new(
|
ip_stack_stream?
|
||||||
proxy.addr,
|
|
||||||
SocksVersion::V5,
|
|
||||||
proxy.credentials.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
ProxyType::Http => {
|
};
|
||||||
ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials.clone()));
|
let max_sessions = args.max_sessions;
|
||||||
|
match ip_stack_stream {
|
||||||
|
IpStackStream::Tcp(tcp) => {
|
||||||
|
if task_count.load(Relaxed) >= max_sessions {
|
||||||
|
if args.exit_on_fatal_error {
|
||||||
|
log::info!("Too many sessions that over {max_sessions}, exiting...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log::warn!("Too many sessions that over {max_sessions}, dropping new session");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
virtual_dns.touch_ip(&tcp.peer_addr().ip());
|
||||||
|
virtual_dns.resolve_ip(&tcp.peer_addr().ip()).cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let proxy_handler = mgr.new_proxy_handler(info, domain_name, false).await?;
|
||||||
|
let socket_queue = socket_queue.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = handle_tcp_session(tcp, proxy_handler, socket_queue).await {
|
||||||
|
log::error!("{info} error \"{err}\"");
|
||||||
|
}
|
||||||
|
log::trace!("Session count {}", task_count.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
IpStackStream::Udp(udp) => {
|
||||||
|
if task_count.load(Relaxed) >= max_sessions {
|
||||||
|
if args.exit_on_fatal_error {
|
||||||
|
log::info!("Too many sessions that over {max_sessions}, exiting...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log::warn!("Too many sessions that over {max_sessions}, dropping new session");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
|
info.dst.set_ip(dns_addr); // !!! Here we change the destination address to remote DNS server!!!
|
||||||
|
}
|
||||||
|
if args.dns == ArgDns::OverTcp {
|
||||||
|
info.protocol = IpProtocol::Tcp;
|
||||||
|
let proxy_handler = mgr.new_proxy_handler(info, None, false).await?;
|
||||||
|
let socket_queue = socket_queue.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = handle_dns_over_tcp_session(udp, proxy_handler, socket_queue, ipv6_enabled).await {
|
||||||
|
log::error!("{info} error \"{err}\"");
|
||||||
|
}
|
||||||
|
log::trace!("Session count {}", task_count.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if args.dns == ArgDns::Virtual {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Some(virtual_dns) = virtual_dns {
|
||||||
|
if let Err(err) = handle_virtual_dns_session(udp, virtual_dns).await {
|
||||||
|
log::error!("{info} error \"{err}\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ttp.run()
|
log::trace!("Session count {}", task_count.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert_eq!(args.dns, ArgDns::Direct);
|
||||||
|
}
|
||||||
|
let domain_name = if let Some(virtual_dns) = &virtual_dns {
|
||||||
|
let mut virtual_dns = virtual_dns.lock().await;
|
||||||
|
virtual_dns.touch_ip(&udp.peer_addr().ip());
|
||||||
|
virtual_dns.resolve_ip(&udp.peer_addr().ip()).cloned()
|
||||||
|
} 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 {info} with \"{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();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let ty = args.proxy.proxy_type;
|
||||||
|
if let Err(err) = handle_udp_associate_session(udp, ty, proxy_handler, socket_queue, ipv6_enabled).await {
|
||||||
|
log::info!("Ending {info} with \"{err}\"");
|
||||||
|
}
|
||||||
|
log::trace!("Session count {}", task_count.fetch_sub(1, Relaxed).saturating_sub(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to create UDP connection: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IpStackStream::UnknownTransport(u) => {
|
||||||
|
let len = u.payload().len();
|
||||||
|
log::info!("#0 unhandled transport - Ip Protocol {:?}, length {}", u.ip_protocol(), len);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IpStackStream::UnknownNetwork(pkt) => {
|
||||||
|
log::info!("#0 unknown transport - {} bytes", pkt.len());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(task_count.load(Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_virtual_dns_session(mut udp: IpStackUdpStream, dns: Arc<Mutex<VirtualDns>>) -> crate::Result<()> {
|
||||||
|
let mut buf = [0_u8; 4096];
|
||||||
|
loop {
|
||||||
|
let len = match udp.read(&mut buf).await {
|
||||||
|
Err(e) => {
|
||||||
|
// indicate UDP read fails not an error.
|
||||||
|
log::debug!("Virtual DNS session error: {e}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(len) => len,
|
||||||
|
};
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let (msg, qname, ip) = dns.lock().await.generate_query(&buf[..len])?;
|
||||||
|
udp.write_all(&msg).await?;
|
||||||
|
log::debug!("Virtual DNS query: {qname} -> {ip}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn copy_and_record_traffic<R, W>(reader: &mut R, writer: &mut W, is_tx: bool) -> tokio::io::Result<u64>
|
||||||
|
where
|
||||||
|
R: tokio::io::AsyncRead + Unpin + ?Sized,
|
||||||
|
W: tokio::io::AsyncWrite + Unpin + ?Sized,
|
||||||
|
{
|
||||||
|
let mut buf = vec![0; 8192];
|
||||||
|
let mut total = 0;
|
||||||
|
loop {
|
||||||
|
match reader.read(&mut buf).await? {
|
||||||
|
0 => break, // EOF
|
||||||
|
n => {
|
||||||
|
total += n as u64;
|
||||||
|
let (tx, rx) = if is_tx { (n, 0) } else { (0, n) };
|
||||||
|
if let Err(e) = crate::traffic_status::traffic_status_update(tx, rx) {
|
||||||
|
log::debug!("Record traffic status error: {e}");
|
||||||
|
}
|
||||||
|
writer.write_all(&buf[..n]).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_tcp_session(
|
||||||
|
mut tcp_stack: IpStackTcpStream,
|
||||||
|
proxy_handler: Arc<Mutex<dyn ProxyHandler>>,
|
||||||
|
socket_queue: Option<Arc<SocketQueue>>,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let (session_info, server_addr) = {
|
||||||
|
let handler = proxy_handler.lock().await;
|
||||||
|
|
||||||
|
(handler.get_session_info(), handler.get_server_addr())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut server = create_tcp_stream(&socket_queue, server_addr).await?;
|
||||||
|
|
||||||
|
log::info!("Beginning {session_info}");
|
||||||
|
|
||||||
|
if let Err(e) = handle_proxy_session(&mut server, proxy_handler).await {
|
||||||
|
tcp_stack.shutdown().await?;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut t_rx, mut t_tx) = tokio::io::split(tcp_stack);
|
||||||
|
let (mut s_rx, mut s_tx) = tokio::io::split(server);
|
||||||
|
|
||||||
|
let res = tokio::join!(
|
||||||
|
async move {
|
||||||
|
let r = copy_and_record_traffic(&mut t_rx, &mut s_tx, true).await;
|
||||||
|
if let Err(err) = s_tx.shutdown().await {
|
||||||
|
log::trace!("{session_info} s_tx shutdown error {err}");
|
||||||
|
}
|
||||||
|
r
|
||||||
|
},
|
||||||
|
async move {
|
||||||
|
let r = copy_and_record_traffic(&mut s_rx, &mut t_tx, false).await;
|
||||||
|
if let Err(err) = t_tx.shutdown().await {
|
||||||
|
log::trace!("{session_info} t_tx shutdown error {err}");
|
||||||
|
}
|
||||||
|
r
|
||||||
|
},
|
||||||
|
);
|
||||||
|
log::info!("Ending {session_info} with {res:?}");
|
||||||
|
|
||||||
|
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,
|
||||||
|
proxy_handler: Arc<Mutex<dyn ProxyHandler>>,
|
||||||
|
socket_queue: Option<Arc<SocketQueue>>,
|
||||||
|
ipv6_enabled: bool,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
use socks5_impl::protocol::{Address, StreamOperation, UdpHeader};
|
||||||
|
|
||||||
|
let (session_info, server_addr, domain_name, udp_addr) = {
|
||||||
|
let handler = proxy_handler.lock().await;
|
||||||
|
(
|
||||||
|
handler.get_session_info(),
|
||||||
|
handler.get_server_addr(),
|
||||||
|
handler.get_domain_name(),
|
||||||
|
handler.get_udp_associate(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("Beginning {session_info}");
|
||||||
|
|
||||||
|
// `_server` is meaningful here, it must be alive all the time
|
||||||
|
// to ensure that UDP transmission will not be interrupted accidentally.
|
||||||
|
let (_server, udp_addr) = match udp_addr {
|
||||||
|
Some(udp_addr) => (None, udp_addr),
|
||||||
|
None => {
|
||||||
|
let mut server = create_tcp_stream(&socket_queue, server_addr).await?;
|
||||||
|
let udp_addr = handle_proxy_session(&mut server, proxy_handler).await?;
|
||||||
|
(Some(server), udp_addr.ok_or("udp associate failed")?)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut udp_server = create_udp_stream(&socket_queue, udp_addr).await?;
|
||||||
|
|
||||||
|
let mut buf1 = [0_u8; 4096];
|
||||||
|
let mut buf2 = [0_u8; 4096];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
len = udp_stack.read(&mut buf1) => {
|
||||||
|
let len = len?;
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let buf1 = &buf1[..len];
|
||||||
|
|
||||||
|
crate::traffic_status::traffic_status_update(len, 0)?;
|
||||||
|
|
||||||
|
if let ProxyType::Socks4 | ProxyType::Socks5 = proxy_type {
|
||||||
|
let s5addr = if let Some(domain_name) = &domain_name {
|
||||||
|
Address::DomainAddress(domain_name.clone(), session_info.dst.port())
|
||||||
|
} else {
|
||||||
|
session_info.dst.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add SOCKS5 UDP header to the incoming data
|
||||||
|
let mut s5_udp_data = Vec::<u8>::new();
|
||||||
|
UdpHeader::new(0, s5addr).write_to_stream(&mut s5_udp_data)?;
|
||||||
|
s5_udp_data.extend_from_slice(buf1);
|
||||||
|
|
||||||
|
udp_server.write_all(&s5_udp_data).await?;
|
||||||
|
} else {
|
||||||
|
udp_server.write_all(buf1).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
len = udp_server.read(&mut buf2) => {
|
||||||
|
let len = len?;
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let buf2 = &buf2[..len];
|
||||||
|
|
||||||
|
crate::traffic_status::traffic_status_update(0, len)?;
|
||||||
|
|
||||||
|
if let ProxyType::Socks4 | ProxyType::Socks5 = proxy_type {
|
||||||
|
// Remove SOCKS5 UDP header from the server data
|
||||||
|
let header = UdpHeader::retrieve_from_stream(&mut &buf2[..])?;
|
||||||
|
let data = &buf2[header.len()..];
|
||||||
|
|
||||||
|
let buf = if session_info.dst.port() == DNS_PORT {
|
||||||
|
let mut message = dns::parse_data_to_dns_message(data, false)?;
|
||||||
|
if !ipv6_enabled {
|
||||||
|
dns::remove_ipv6_entries(&mut message);
|
||||||
|
}
|
||||||
|
message.to_vec()?
|
||||||
|
} else {
|
||||||
|
data.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
udp_stack.write_all(&buf).await?;
|
||||||
|
} else {
|
||||||
|
udp_stack.write_all(buf2).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Ending {session_info}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_dns_over_tcp_session(
|
||||||
|
mut udp_stack: IpStackUdpStream,
|
||||||
|
proxy_handler: Arc<Mutex<dyn ProxyHandler>>,
|
||||||
|
socket_queue: Option<Arc<SocketQueue>>,
|
||||||
|
ipv6_enabled: bool,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let (session_info, server_addr) = {
|
||||||
|
let handler = proxy_handler.lock().await;
|
||||||
|
|
||||||
|
(handler.get_session_info(), handler.get_server_addr())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut server = create_tcp_stream(&socket_queue, server_addr).await?;
|
||||||
|
|
||||||
|
log::info!("Beginning {session_info}");
|
||||||
|
|
||||||
|
let _ = handle_proxy_session(&mut server, proxy_handler).await?;
|
||||||
|
|
||||||
|
let mut buf1 = [0_u8; 4096];
|
||||||
|
let mut buf2 = [0_u8; 4096];
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
len = udp_stack.read(&mut buf1) => {
|
||||||
|
let len = len?;
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let buf1 = &buf1[..len];
|
||||||
|
|
||||||
|
_ = dns::parse_data_to_dns_message(buf1, false)?;
|
||||||
|
|
||||||
|
// Insert the DNS message length in front of the payload
|
||||||
|
let len = u16::try_from(buf1.len())?;
|
||||||
|
let mut buf = Vec::with_capacity(std::mem::size_of::<u16>() + usize::from(len));
|
||||||
|
buf.extend_from_slice(&len.to_be_bytes());
|
||||||
|
buf.extend_from_slice(buf1);
|
||||||
|
|
||||||
|
server.write_all(&buf).await?;
|
||||||
|
|
||||||
|
crate::traffic_status::traffic_status_update(buf.len(), 0)?;
|
||||||
|
}
|
||||||
|
len = server.read(&mut buf2) => {
|
||||||
|
let len = len?;
|
||||||
|
if len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut buf = buf2[..len].to_vec();
|
||||||
|
|
||||||
|
crate::traffic_status::traffic_status_update(0, len)?;
|
||||||
|
|
||||||
|
let mut to_send: VecDeque<Vec<u8>> = VecDeque::new();
|
||||||
|
loop {
|
||||||
|
if buf.len() < 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
|
||||||
|
if buf.len() < len + 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the length field
|
||||||
|
let data = buf[2..len + 2].to_vec();
|
||||||
|
|
||||||
|
let mut message = dns::parse_data_to_dns_message(&data, false)?;
|
||||||
|
|
||||||
|
let name = dns::extract_domain_from_dns_message(&message)?;
|
||||||
|
let ip = dns::extract_ipaddr_from_dns_message(&message);
|
||||||
|
log::trace!("DNS over TCP query result: {name} -> {ip:?}");
|
||||||
|
|
||||||
|
if !ipv6_enabled {
|
||||||
|
dns::remove_ipv6_entries(&mut message);
|
||||||
|
}
|
||||||
|
|
||||||
|
to_send.push_back(message.to_vec()?);
|
||||||
|
if len + 2 == buf.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
buf = buf[len + 2..].to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(packet) = to_send.pop_front() {
|
||||||
|
udp_stack.write_all(&packet).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Ending {session_info}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function is used to handle the business logic of tun2proxy and SOCKS5 server.
|
||||||
|
/// When handling UDP proxy, the return value UDP associate IP address is the result of this business logic.
|
||||||
|
/// However, when handling TCP business logic, the return value Ok(None) is meaningless, just indicating that the operation was successful.
|
||||||
|
async fn handle_proxy_session(server: &mut TcpStream, proxy_handler: Arc<Mutex<dyn ProxyHandler>>) -> crate::Result<Option<SocketAddr>> {
|
||||||
|
let mut launched = false;
|
||||||
|
let mut proxy_handler = proxy_handler.lock().await;
|
||||||
|
let dir = OutgoingDirection::ToServer;
|
||||||
|
let (mut tx, mut rx) = (0, 0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if proxy_handler.connection_established() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !launched {
|
||||||
|
let data = proxy_handler.peek_data(dir).buffer;
|
||||||
|
let len = data.len();
|
||||||
|
if len == 0 {
|
||||||
|
return Err("proxy_handler launched went wrong".into());
|
||||||
|
}
|
||||||
|
server.write_all(data).await?;
|
||||||
|
proxy_handler.consume_data(dir, len);
|
||||||
|
tx += len;
|
||||||
|
|
||||||
|
launched = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = [0_u8; 4096];
|
||||||
|
let len = server.read(&mut buf).await?;
|
||||||
|
if len == 0 {
|
||||||
|
return Err("server closed accidentially".into());
|
||||||
|
}
|
||||||
|
rx += len;
|
||||||
|
let event = IncomingDataEvent {
|
||||||
|
direction: IncomingDirection::FromServer,
|
||||||
|
buffer: &buf[..len],
|
||||||
|
};
|
||||||
|
proxy_handler.push_data(event).await?;
|
||||||
|
|
||||||
|
let data = proxy_handler.peek_data(dir).buffer;
|
||||||
|
let len = data.len();
|
||||||
|
if len > 0 {
|
||||||
|
server.write_all(data).await?;
|
||||||
|
proxy_handler.consume_data(dir, len);
|
||||||
|
tx += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::traffic_status::traffic_status_update(tx, rx)?;
|
||||||
|
Ok(proxy_handler.get_udp_associate())
|
||||||
}
|
}
|
||||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -1,96 +0,0 @@
|
||||||
use clap::Parser;
|
|
||||||
use env_logger::Env;
|
|
||||||
|
|
||||||
use std::net::IpAddr;
|
|
||||||
use std::process::ExitCode;
|
|
||||||
|
|
||||||
use tun2proxy::error::Error;
|
|
||||||
use tun2proxy::setup::{get_default_cidrs, Setup};
|
|
||||||
use tun2proxy::Options;
|
|
||||||
use tun2proxy::{main_entry, Proxy};
|
|
||||||
|
|
||||||
/// Tunnel interface to proxy
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(author, version, about = "Tunnel interface to proxy.", long_about = None)]
|
|
||||||
struct Args {
|
|
||||||
/// Name of the tun interface
|
|
||||||
#[arg(short, long, value_name = "name", default_value = "tun0")]
|
|
||||||
tun: String,
|
|
||||||
|
|
||||||
/// Proxy URL in the form proto://[username[:password]@]host:port
|
|
||||||
#[arg(short, long, value_parser = Proxy::from_url, value_name = "URL")]
|
|
||||||
proxy: Proxy,
|
|
||||||
|
|
||||||
/// DNS handling
|
|
||||||
#[arg(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
value_name = "method",
|
|
||||||
value_enum,
|
|
||||||
default_value = "virtual"
|
|
||||||
)]
|
|
||||||
dns: ArgDns,
|
|
||||||
|
|
||||||
/// Routing and system setup
|
|
||||||
#[arg(short, long, value_name = "method", value_enum)]
|
|
||||||
setup: Option<ArgSetup>,
|
|
||||||
|
|
||||||
/// Public proxy IP used in routing setup
|
|
||||||
#[arg(long, value_name = "IP")]
|
|
||||||
setup_ip: Option<IpAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
|
|
||||||
enum ArgDns {
|
|
||||||
Virtual,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
|
|
||||||
enum ArgSetup {
|
|
||||||
Auto,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
dotenvy::dotenv().ok();
|
|
||||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
let addr = args.proxy.addr;
|
|
||||||
let proxy_type = args.proxy.proxy_type;
|
|
||||||
log::info!("Proxy {proxy_type} server: {addr}");
|
|
||||||
|
|
||||||
let mut options = Options::new();
|
|
||||||
if args.dns == ArgDns::Virtual {
|
|
||||||
options = options.with_virtual_dns();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = (|| -> Result<(), Error> {
|
|
||||||
let mut setup: Setup;
|
|
||||||
if args.setup == Some(ArgSetup::Auto) {
|
|
||||||
let bypass_tun_ip = match args.setup_ip {
|
|
||||||
Some(addr) => addr,
|
|
||||||
None => args.proxy.addr.ip(),
|
|
||||||
};
|
|
||||||
setup = Setup::new(
|
|
||||||
&args.tun,
|
|
||||||
&bypass_tun_ip,
|
|
||||||
get_default_cidrs(),
|
|
||||||
args.setup_ip.is_some(),
|
|
||||||
);
|
|
||||||
|
|
||||||
setup.configure()?;
|
|
||||||
|
|
||||||
setup.drop_privileges()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
main_entry(&args.tun, &args.proxy, options)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})() {
|
|
||||||
log::error!("{e}");
|
|
||||||
std::process::exit(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
|
107
src/no_proxy.rs
Normal file
107
src/no_proxy.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use crate::{
|
||||||
|
directions::{IncomingDataEvent, IncomingDirection, OutgoingDataEvent, OutgoingDirection},
|
||||||
|
proxy_handler::{ProxyHandler, ProxyHandlerManager},
|
||||||
|
session_info::SessionInfo,
|
||||||
|
};
|
||||||
|
use std::{collections::VecDeque, net::SocketAddr, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
struct NoProxyHandler {
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
client_outbuf: VecDeque<u8>,
|
||||||
|
server_outbuf: VecDeque<u8>,
|
||||||
|
udp_associate: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ProxyHandler for NoProxyHandler {
|
||||||
|
fn get_server_addr(&self) -> SocketAddr {
|
||||||
|
self.info.dst
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session_info(&self) -> SessionInfo {
|
||||||
|
self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_domain_name(&self) -> Option<String> {
|
||||||
|
self.domain_name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_data(&mut self, event: IncomingDataEvent<'_>) -> std::io::Result<()> {
|
||||||
|
let IncomingDataEvent { direction, buffer } = event;
|
||||||
|
match direction {
|
||||||
|
IncomingDirection::FromServer => {
|
||||||
|
self.client_outbuf.extend(buffer.iter());
|
||||||
|
}
|
||||||
|
IncomingDirection::FromClient => {
|
||||||
|
self.server_outbuf.extend(buffer.iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_data(&mut self, dir: OutgoingDirection, size: usize) {
|
||||||
|
let buffer = match dir {
|
||||||
|
OutgoingDirection::ToServer => &mut self.server_outbuf,
|
||||||
|
OutgoingDirection::ToClient => &mut self.client_outbuf,
|
||||||
|
};
|
||||||
|
buffer.drain(0..size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent {
|
||||||
|
let buffer = match dir {
|
||||||
|
OutgoingDirection::ToServer => &mut self.server_outbuf,
|
||||||
|
OutgoingDirection::ToClient => &mut self.client_outbuf,
|
||||||
|
};
|
||||||
|
OutgoingDataEvent {
|
||||||
|
direction: dir,
|
||||||
|
buffer: buffer.make_contiguous(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_established(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_len(&self, dir: OutgoingDirection) -> usize {
|
||||||
|
match dir {
|
||||||
|
OutgoingDirection::ToServer => self.server_outbuf.len(),
|
||||||
|
OutgoingDirection::ToClient => self.client_outbuf.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_connection(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_udp_associate(&self) -> Option<SocketAddr> {
|
||||||
|
self.udp_associate.then_some(self.info.dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct NoProxyManager;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ProxyHandlerManager for NoProxyManager {
|
||||||
|
async fn new_proxy_handler(
|
||||||
|
&self,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
udp_associate: bool,
|
||||||
|
) -> std::io::Result<Arc<Mutex<dyn ProxyHandler>>> {
|
||||||
|
Ok(Arc::new(Mutex::new(NoProxyHandler {
|
||||||
|
info,
|
||||||
|
domain_name,
|
||||||
|
client_outbuf: VecDeque::default(),
|
||||||
|
server_outbuf: VecDeque::default(),
|
||||||
|
udp_associate,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoProxyManager {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
32
src/proxy_handler.rs
Normal file
32
src/proxy_handler.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use crate::{
|
||||||
|
directions::{IncomingDataEvent, OutgoingDataEvent, OutgoingDirection},
|
||||||
|
session_info::SessionInfo,
|
||||||
|
};
|
||||||
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub(crate) trait ProxyHandler: Send + Sync {
|
||||||
|
fn get_server_addr(&self) -> SocketAddr;
|
||||||
|
fn get_session_info(&self) -> SessionInfo;
|
||||||
|
fn get_domain_name(&self) -> Option<String>;
|
||||||
|
async fn push_data(&mut self, event: IncomingDataEvent<'_>) -> std::io::Result<()>;
|
||||||
|
fn consume_data(&mut self, dir: OutgoingDirection, size: usize);
|
||||||
|
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent;
|
||||||
|
fn connection_established(&self) -> bool;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn data_len(&self, dir: OutgoingDirection) -> usize;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn reset_connection(&self) -> bool;
|
||||||
|
fn get_udp_associate(&self) -> Option<SocketAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub(crate) trait ProxyHandlerManager: Send + Sync {
|
||||||
|
async fn new_proxy_handler(
|
||||||
|
&self,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
udp_associate: bool,
|
||||||
|
) -> std::io::Result<Arc<Mutex<dyn ProxyHandler>>>;
|
||||||
|
}
|
53
src/session_info.rs
Normal file
53
src/session_info.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Hash, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||||
|
pub(crate) enum IpProtocol {
|
||||||
|
#[default]
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
|
Icmp,
|
||||||
|
Other(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for IpProtocol {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IpProtocol::Tcp => write!(f, "TCP"),
|
||||||
|
IpProtocol::Udp => write!(f, "UDP"),
|
||||||
|
IpProtocol::Icmp => write!(f, "ICMP"),
|
||||||
|
IpProtocol::Other(v) => write!(f, "Other(0x{v:02X})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Hash, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
||||||
|
pub(crate) struct SessionInfo {
|
||||||
|
pub(crate) src: SocketAddr,
|
||||||
|
pub(crate) dst: SocketAddr,
|
||||||
|
pub(crate) protocol: IpProtocol,
|
||||||
|
id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
let src = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0);
|
||||||
|
let dst = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0);
|
||||||
|
Self::new(src, dst, IpProtocol::Tcp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SESSION_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||||
|
|
||||||
|
impl SessionInfo {
|
||||||
|
pub fn new(src: SocketAddr, dst: SocketAddr, protocol: IpProtocol) -> Self {
|
||||||
|
let id = SESSION_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
Self { src, dst, protocol, id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SessionInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "#{} {} {} -> {}", self.id, self.protocol, self.src, self.dst)
|
||||||
|
}
|
||||||
|
}
|
365
src/setup.rs
365
src/setup.rs
|
@ -1,365 +0,0 @@
|
||||||
use crate::error::Error;
|
|
||||||
use smoltcp::wire::IpCidr;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::io::BufRead;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use std::os::fd::RawFd;
|
|
||||||
|
|
||||||
use std::process::{Command, Output};
|
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use fork::Fork;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Setup {
|
|
||||||
routes: Vec<IpCidr>,
|
|
||||||
tunnel_bypass_addr: IpAddr,
|
|
||||||
allow_private: bool,
|
|
||||||
tun: String,
|
|
||||||
set_up: bool,
|
|
||||||
delete_proxy_route: bool,
|
|
||||||
child: libc::pid_t,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_default_cidrs() -> [IpCidr; 4] {
|
|
||||||
[
|
|
||||||
IpCidr::new(Ipv4Addr::from_str("0.0.0.0").unwrap().into(), 1),
|
|
||||||
IpCidr::new(Ipv4Addr::from_str("128.0.0.0").unwrap().into(), 1),
|
|
||||||
IpCidr::new(Ipv6Addr::from_str("::").unwrap().into(), 1),
|
|
||||||
IpCidr::new(Ipv6Addr::from_str("8000::").unwrap().into(), 1),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_iproute<I, S>(args: I, error: &str, require_success: bool) -> Result<Output, Error>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = S>,
|
|
||||||
S: AsRef<OsStr>,
|
|
||||||
{
|
|
||||||
let mut command = Command::new("");
|
|
||||||
for (i, arg) in args.into_iter().enumerate() {
|
|
||||||
if i == 0 {
|
|
||||||
command = Command::new(arg);
|
|
||||||
} else {
|
|
||||||
command.arg(arg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let e = Error::from(error);
|
|
||||||
let output = command.output().map_err(|_| e)?;
|
|
||||||
if !require_success || output.status.success() {
|
|
||||||
Ok(output)
|
|
||||||
} else {
|
|
||||||
let mut args: Vec<&str> = command.get_args().map(|x| x.to_str().unwrap()).collect();
|
|
||||||
let program = command.get_program().to_str().unwrap();
|
|
||||||
let mut cmdline = Vec::<&str>::new();
|
|
||||||
cmdline.push(program);
|
|
||||||
cmdline.append(&mut args);
|
|
||||||
let command = cmdline.as_slice().join(" ");
|
|
||||||
match String::from_utf8(output.stderr.clone()) {
|
|
||||||
Ok(output) => Err(format!(
|
|
||||||
"[{}] Command `{}` failed: {}",
|
|
||||||
nix::unistd::getpid(),
|
|
||||||
command,
|
|
||||||
output
|
|
||||||
)
|
|
||||||
.into()),
|
|
||||||
Err(_) => Err(format!(
|
|
||||||
"Command `{:?}` failed with exit code {}",
|
|
||||||
command,
|
|
||||||
output.status.code().unwrap()
|
|
||||||
)
|
|
||||||
.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Setup {
|
|
||||||
pub fn new(
|
|
||||||
tun: impl Into<String>,
|
|
||||||
tunnel_bypass_addr: &IpAddr,
|
|
||||||
routes: impl IntoIterator<Item = IpCidr>,
|
|
||||||
allow_private: bool,
|
|
||||||
) -> Self {
|
|
||||||
let routes_cidr = routes.into_iter().collect();
|
|
||||||
Self {
|
|
||||||
tun: tun.into(),
|
|
||||||
tunnel_bypass_addr: *tunnel_bypass_addr,
|
|
||||||
allow_private,
|
|
||||||
routes: routes_cidr,
|
|
||||||
set_up: false,
|
|
||||||
delete_proxy_route: false,
|
|
||||||
child: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route_proxy_address(&mut self) -> Result<bool, Error> {
|
|
||||||
let route_show_args = if self.tunnel_bypass_addr.is_ipv6() {
|
|
||||||
["ip", "-6", "route", "show"]
|
|
||||||
} else {
|
|
||||||
["ip", "-4", "route", "show"]
|
|
||||||
};
|
|
||||||
|
|
||||||
let routes = run_iproute(route_show_args, "failed to get routing table", true)?;
|
|
||||||
|
|
||||||
let mut route_info = Vec::<(IpCidr, Vec<String>)>::new();
|
|
||||||
|
|
||||||
for line in routes.stdout.lines() {
|
|
||||||
if line.is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let line = line.unwrap();
|
|
||||||
if line.starts_with([' ', '\t']) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut split = line.split_whitespace();
|
|
||||||
let mut dst_str = split.next().unwrap();
|
|
||||||
if dst_str == "default" {
|
|
||||||
dst_str = if self.tunnel_bypass_addr.is_ipv6() {
|
|
||||||
"::/0"
|
|
||||||
} else {
|
|
||||||
"0.0.0.0/0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (addr_str, prefix_len_str) = match dst_str.split_once(['/']) {
|
|
||||||
None => (
|
|
||||||
dst_str,
|
|
||||||
if self.tunnel_bypass_addr.is_ipv6() {
|
|
||||||
"128"
|
|
||||||
} else {
|
|
||||||
"32"
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Some((addr_str, prefix_len_str)) => (addr_str, prefix_len_str),
|
|
||||||
};
|
|
||||||
|
|
||||||
let cidr: IpCidr = IpCidr::new(
|
|
||||||
std::net::IpAddr::from_str(addr_str).unwrap().into(),
|
|
||||||
u8::from_str(prefix_len_str).unwrap(),
|
|
||||||
);
|
|
||||||
let route_components: Vec<String> = split.map(String::from).collect();
|
|
||||||
route_info.push((cidr, route_components))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort routes by prefix length, the most specific route comes first.
|
|
||||||
route_info.sort_by(|entry1, entry2| entry2.0.prefix_len().cmp(&entry1.0.prefix_len()));
|
|
||||||
|
|
||||||
for (cidr, route_components) in route_info {
|
|
||||||
if !cidr.contains_addr(&smoltcp::wire::IpAddress::from(self.tunnel_bypass_addr)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The IP address is routed through a more specific route than the default route.
|
|
||||||
// In this case, there is nothing to do.
|
|
||||||
if cidr.prefix_len() != 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut proxy_route = vec!["ip".into(), "route".into(), "add".into()];
|
|
||||||
proxy_route.push(self.tunnel_bypass_addr.to_string());
|
|
||||||
proxy_route.extend(route_components.into_iter());
|
|
||||||
run_iproute(proxy_route, "failed to clone route for proxy", false)?;
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_resolv_conf() -> Result<(), Error> {
|
|
||||||
let fd = nix::fcntl::open(
|
|
||||||
"/tmp/tun2proxy-resolv.conf",
|
|
||||||
nix::fcntl::OFlag::O_RDWR | nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_CREAT,
|
|
||||||
nix::sys::stat::Mode::from_bits(0o644_u32).unwrap(),
|
|
||||||
)?;
|
|
||||||
let data = "nameserver 198.18.0.1\n".as_bytes();
|
|
||||||
let mut written = 0;
|
|
||||||
loop {
|
|
||||||
if written >= data.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
written += nix::unistd::write(fd, &data[written..])?;
|
|
||||||
}
|
|
||||||
nix::sys::stat::fchmod(fd, nix::sys::stat::Mode::from_bits(0o444_u32).unwrap())?;
|
|
||||||
let source = format!("/proc/self/fd/{}", fd);
|
|
||||||
nix::mount::mount(
|
|
||||||
source.as_str().into(),
|
|
||||||
"/etc/resolv.conf",
|
|
||||||
"".into(),
|
|
||||||
nix::mount::MsFlags::MS_BIND,
|
|
||||||
"".into(),
|
|
||||||
)?;
|
|
||||||
nix::unistd::close(fd)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_tunnel_routes(&self) -> Result<(), Error> {
|
|
||||||
for route in &self.routes {
|
|
||||||
run_iproute(
|
|
||||||
[
|
|
||||||
"ip",
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
route.to_string().as_str(),
|
|
||||||
"dev",
|
|
||||||
self.tun.as_str(),
|
|
||||||
],
|
|
||||||
"failed to add route",
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown(&mut self) -> Result<(), Error> {
|
|
||||||
self.set_up = false;
|
|
||||||
log::info!(
|
|
||||||
"[{}] Restoring network configuration",
|
|
||||||
nix::unistd::getpid()
|
|
||||||
);
|
|
||||||
let _ = Command::new("ip")
|
|
||||||
.args(["link", "del", self.tun.as_str()])
|
|
||||||
.output();
|
|
||||||
if self.delete_proxy_route {
|
|
||||||
let _ = Command::new("ip")
|
|
||||||
.args(["route", "del", self.tunnel_bypass_addr.to_string().as_str()])
|
|
||||||
.output();
|
|
||||||
}
|
|
||||||
nix::mount::umount("/etc/resolv.conf")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_and_handle_signals(&mut self, read_from_child: RawFd, write_to_parent: RawFd) {
|
|
||||||
if let Err(e) = (|| -> Result<(), Error> {
|
|
||||||
nix::unistd::close(read_from_child)?;
|
|
||||||
run_iproute(
|
|
||||||
[
|
|
||||||
"ip",
|
|
||||||
"tuntap",
|
|
||||||
"add",
|
|
||||||
"name",
|
|
||||||
self.tun.as_str(),
|
|
||||||
"mode",
|
|
||||||
"tun",
|
|
||||||
],
|
|
||||||
"failed to create tunnel device",
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.set_up = true;
|
|
||||||
let _tun_name = self.tun.clone();
|
|
||||||
let _proxy_ip = self.tunnel_bypass_addr;
|
|
||||||
|
|
||||||
run_iproute(
|
|
||||||
["ip", "link", "set", self.tun.as_str(), "up"],
|
|
||||||
"failed to bring up tunnel device",
|
|
||||||
true,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let delete_proxy_route = self.route_proxy_address()?;
|
|
||||||
self.delete_proxy_route = delete_proxy_route;
|
|
||||||
Self::setup_resolv_conf()?;
|
|
||||||
self.add_tunnel_routes()?;
|
|
||||||
|
|
||||||
// Signal to child that we are done setting up everything.
|
|
||||||
if nix::unistd::write(write_to_parent, &[1])? != 1 {
|
|
||||||
return Err("Failed to write to pipe".into());
|
|
||||||
}
|
|
||||||
nix::unistd::close(write_to_parent)?;
|
|
||||||
|
|
||||||
// Now wait for the termination signals.
|
|
||||||
let mut mask = nix::sys::signal::SigSet::empty();
|
|
||||||
mask.add(nix::sys::signal::SIGINT);
|
|
||||||
mask.add(nix::sys::signal::SIGTERM);
|
|
||||||
mask.add(nix::sys::signal::SIGQUIT);
|
|
||||||
mask.thread_block().unwrap();
|
|
||||||
|
|
||||||
let mut fd = nix::sys::signalfd::SignalFd::new(&mask).unwrap();
|
|
||||||
loop {
|
|
||||||
let res = fd.read_signal().unwrap().unwrap();
|
|
||||||
let signo = nix::sys::signal::Signal::try_from(res.ssi_signo as i32).unwrap();
|
|
||||||
if signo == nix::sys::signal::SIGINT
|
|
||||||
|| signo == nix::sys::signal::SIGTERM
|
|
||||||
|| signo == nix::sys::signal::SIGQUIT
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.shutdown()?;
|
|
||||||
Ok(())
|
|
||||||
})() {
|
|
||||||
log::error!("{e}");
|
|
||||||
self.shutdown().unwrap();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn drop_privileges(&self) -> Result<(), Error> {
|
|
||||||
let gid_str = match std::env::var("SUDO_GID") {
|
|
||||||
Ok(uid_str) => uid_str,
|
|
||||||
_ => String::from("65535"),
|
|
||||||
};
|
|
||||||
let gid = gid_str.parse::<u32>()?;
|
|
||||||
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))?;
|
|
||||||
|
|
||||||
let uid_str = match std::env::var("SUDO_UID") {
|
|
||||||
Ok(uid_str) => uid_str,
|
|
||||||
_ => String::from("65535"),
|
|
||||||
};
|
|
||||||
let uid = uid_str.parse::<u32>()?;
|
|
||||||
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(&mut self) -> Result<(), Error> {
|
|
||||||
log::info!(
|
|
||||||
"[{}] Setting up network configuration",
|
|
||||||
nix::unistd::getpid()
|
|
||||||
);
|
|
||||||
if nix::unistd::getuid() != 0.into() {
|
|
||||||
return Err("Automatic setup requires root privileges".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.tunnel_bypass_addr.is_loopback() && !self.allow_private {
|
|
||||||
log::warn!(
|
|
||||||
"The proxy address {} is a loopback address. You may need to manually \
|
|
||||||
provide --setup-ip to specify the server IP bypassing the tunnel",
|
|
||||||
self.tunnel_bypass_addr
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let (read_from_child, write_to_parent) = nix::unistd::pipe()?;
|
|
||||||
match fork::fork() {
|
|
||||||
Ok(Fork::Child) => {
|
|
||||||
prctl::set_death_signal(nix::sys::signal::SIGINT as isize).unwrap();
|
|
||||||
self.setup_and_handle_signals(read_from_child, write_to_parent);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Ok(Fork::Parent(child)) => {
|
|
||||||
self.child = child;
|
|
||||||
nix::unistd::close(write_to_parent)?;
|
|
||||||
let mut buf = [0];
|
|
||||||
if nix::unistd::read(read_from_child, &mut buf)? != 1 {
|
|
||||||
return Err("Failed to read from pipe".into());
|
|
||||||
}
|
|
||||||
nix::unistd::close(read_from_child)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err("Failed to fork".into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore(&mut self) -> Result<(), Error> {
|
|
||||||
nix::sys::signal::kill(
|
|
||||||
nix::unistd::Pid::from_raw(self.child),
|
|
||||||
nix::sys::signal::SIGINT,
|
|
||||||
)?;
|
|
||||||
nix::sys::wait::waitpid(nix::unistd::Pid::from_raw(self.child), None)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
242
src/socket_transfer.rs
Normal file
242
src/socket_transfer.rs
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::{SocketDomain, SocketProtocol, error};
|
||||||
|
use nix::{
|
||||||
|
errno::Errno,
|
||||||
|
fcntl::{self, FdFlag},
|
||||||
|
sys::socket::{ControlMessage, ControlMessageOwned, MsgFlags, SockType, cmsg_space, getsockopt, recvmsg, sendmsg, sockopt},
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
io::{ErrorKind, IoSlice, IoSliceMut, Result},
|
||||||
|
ops::DerefMut,
|
||||||
|
os::fd::{AsFd, AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd},
|
||||||
|
};
|
||||||
|
use tokio::net::{TcpSocket, UdpSocket, UnixDatagram};
|
||||||
|
|
||||||
|
const REQUEST_BUFFER_SIZE: usize = 64;
|
||||||
|
|
||||||
|
#[derive(bincode::Encode, bincode::Decode, Hash, Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
|
||||||
|
struct Request {
|
||||||
|
protocol: SocketProtocol,
|
||||||
|
domain: SocketDomain,
|
||||||
|
number: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(bincode::Encode, bincode::Decode, PartialEq, Debug, Hash, Copy, Clone, Eq, Serialize, Deserialize)]
|
||||||
|
enum Response {
|
||||||
|
Ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct socket from raw `fd`
|
||||||
|
pub fn reconstruct_socket(fd: RawFd) -> Result<OwnedFd> {
|
||||||
|
// `fd` is confirmed to be valid so it should be closed
|
||||||
|
let socket = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||||
|
|
||||||
|
// Check if `fd` is valid
|
||||||
|
let fd_flags = fcntl::fcntl(socket.as_fd(), fcntl::F_GETFD)?;
|
||||||
|
|
||||||
|
// Insert CLOEXEC flag to the `fd` to prevent further propagation across `execve(2)` calls
|
||||||
|
let mut fd_flags = FdFlag::from_bits(fd_flags).ok_or(ErrorKind::Unsupported)?;
|
||||||
|
if !fd_flags.contains(FdFlag::FD_CLOEXEC) {
|
||||||
|
fd_flags.insert(FdFlag::FD_CLOEXEC);
|
||||||
|
fcntl::fcntl(socket.as_fd(), fcntl::F_SETFD(fd_flags))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct transfer socket from `fd`
|
||||||
|
///
|
||||||
|
/// Panics if called outside of tokio runtime
|
||||||
|
pub fn reconstruct_transfer_socket(fd: OwnedFd) -> Result<UnixDatagram> {
|
||||||
|
// Check if socket of type DATAGRAM
|
||||||
|
let sock_type = getsockopt(&fd, sockopt::SockType)?;
|
||||||
|
if !matches!(sock_type, SockType::Datagram) {
|
||||||
|
return Err(ErrorKind::InvalidInput.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let std_socket: std::os::unix::net::UnixDatagram = fd.into();
|
||||||
|
std_socket.set_nonblocking(true)?;
|
||||||
|
|
||||||
|
// Fails if tokio context is absent
|
||||||
|
Ok(UnixDatagram::from_std(std_socket).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create pair of interconnected sockets one of which is set to stay open across `execve(2)` calls.
|
||||||
|
pub async fn create_transfer_socket_pair() -> std::io::Result<(UnixDatagram, OwnedFd)> {
|
||||||
|
let (local, remote) = tokio::net::UnixDatagram::pair()?;
|
||||||
|
|
||||||
|
let remote_fd: OwnedFd = remote.into_std().unwrap().into();
|
||||||
|
|
||||||
|
// Get `remote_fd` flags
|
||||||
|
let fd_flags = fcntl::fcntl(remote_fd.as_fd(), fcntl::F_GETFD)?;
|
||||||
|
|
||||||
|
// Remove CLOEXEC flag from the `remote_fd` to allow propagating across `execve(2)`
|
||||||
|
let mut fd_flags = FdFlag::from_bits(fd_flags).ok_or(ErrorKind::Unsupported)?;
|
||||||
|
fd_flags.remove(FdFlag::FD_CLOEXEC);
|
||||||
|
fcntl::fcntl(remote_fd.as_fd(), fcntl::F_SETFD(fd_flags))?;
|
||||||
|
|
||||||
|
Ok((local, remote_fd))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TransferableSocket: Sized {
|
||||||
|
fn from_fd(fd: OwnedFd) -> Result<Self>;
|
||||||
|
fn domain() -> SocketProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferableSocket for TcpSocket {
|
||||||
|
fn from_fd(fd: OwnedFd) -> Result<Self> {
|
||||||
|
// Check if socket is of type STREAM
|
||||||
|
let sock_type = getsockopt(&fd, sockopt::SockType)?;
|
||||||
|
if !matches!(sock_type, SockType::Stream) {
|
||||||
|
return Err(ErrorKind::InvalidInput.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let std_stream: std::net::TcpStream = fd.into();
|
||||||
|
std_stream.set_nonblocking(true)?;
|
||||||
|
|
||||||
|
Ok(TcpSocket::from_std_stream(std_stream))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain() -> SocketProtocol {
|
||||||
|
SocketProtocol::Tcp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferableSocket for UdpSocket {
|
||||||
|
/// Panics if called outside of tokio runtime
|
||||||
|
fn from_fd(fd: OwnedFd) -> Result<Self> {
|
||||||
|
// Check if socket is of type DATAGRAM
|
||||||
|
let sock_type = getsockopt(&fd, sockopt::SockType)?;
|
||||||
|
if !matches!(sock_type, SockType::Datagram) {
|
||||||
|
return Err(ErrorKind::InvalidInput.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let std_socket: std::net::UdpSocket = fd.into();
|
||||||
|
std_socket.set_nonblocking(true)?;
|
||||||
|
|
||||||
|
Ok(UdpSocket::try_from(std_socket).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain() -> SocketProtocol {
|
||||||
|
SocketProtocol::Udp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send [`Request`] to `socket` and return received [`TransferableSocket`]s
|
||||||
|
///
|
||||||
|
/// Panics if called outside of tokio runtime
|
||||||
|
pub async fn request_sockets<S, T>(mut socket: S, domain: SocketDomain, number: u32) -> error::Result<Vec<T>>
|
||||||
|
where
|
||||||
|
S: DerefMut<Target = UnixDatagram>,
|
||||||
|
T: TransferableSocket,
|
||||||
|
{
|
||||||
|
// Borrow socket as mut to prevent multiple simultaneous requests
|
||||||
|
let socket = socket.deref_mut();
|
||||||
|
|
||||||
|
let mut request = [0u8; 1000];
|
||||||
|
|
||||||
|
// Send 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[..size]).await?;
|
||||||
|
|
||||||
|
// Receive response
|
||||||
|
loop {
|
||||||
|
socket.readable().await?;
|
||||||
|
|
||||||
|
let mut buf = [0_u8; REQUEST_BUFFER_SIZE];
|
||||||
|
let mut iov = [IoSliceMut::new(&mut buf[..])];
|
||||||
|
let mut cmsg = vec![0; cmsg_space::<RawFd>() * number as usize];
|
||||||
|
let msg = recvmsg::<()>(socket.as_fd().as_raw_fd(), &mut iov, Some(&mut cmsg), MsgFlags::empty());
|
||||||
|
|
||||||
|
let msg = match msg {
|
||||||
|
Err(Errno::EAGAIN) => continue,
|
||||||
|
msg => msg?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
let response = &msg.iovs().next().unwrap()[..msg.bytes];
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process received file descriptors
|
||||||
|
let mut sockets = Vec::<T>::with_capacity(number as usize);
|
||||||
|
for cmsg in msg.cmsgs()? {
|
||||||
|
if let ControlMessageOwned::ScmRights(fds) = cmsg {
|
||||||
|
for fd in fds {
|
||||||
|
if fd < 0 {
|
||||||
|
return Err("Received socket is invalid".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let owned_fd = reconstruct_socket(fd)?;
|
||||||
|
sockets.push(T::from_fd(owned_fd)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(sockets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process [`Request`]s received from `socket`
|
||||||
|
///
|
||||||
|
/// Panics if called outside of tokio runtime
|
||||||
|
pub async fn process_socket_requests(socket: &UnixDatagram) -> error::Result<()> {
|
||||||
|
loop {
|
||||||
|
let mut buf = [0_u8; REQUEST_BUFFER_SIZE];
|
||||||
|
|
||||||
|
let len = socket.recv(&mut buf[..]).await?;
|
||||||
|
|
||||||
|
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 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 {
|
||||||
|
let fd = match request.protocol {
|
||||||
|
SocketProtocol::Tcp => match request.domain {
|
||||||
|
SocketDomain::IpV4 => tokio::net::TcpSocket::new_v4(),
|
||||||
|
SocketDomain::IpV6 => tokio::net::TcpSocket::new_v6(),
|
||||||
|
}
|
||||||
|
.map(|s| unsafe { OwnedFd::from_raw_fd(s.into_raw_fd()) }),
|
||||||
|
SocketProtocol::Udp => match request.domain {
|
||||||
|
SocketDomain::IpV4 => tokio::net::UdpSocket::bind("0.0.0.0:0").await,
|
||||||
|
SocketDomain::IpV6 => tokio::net::UdpSocket::bind("[::]:0").await,
|
||||||
|
}
|
||||||
|
.map(|s| s.into_std().unwrap().into()),
|
||||||
|
};
|
||||||
|
match fd {
|
||||||
|
Err(err) => log::warn!("Failed to allocate socket: {err}"),
|
||||||
|
Ok(fd) => owned_fd_buf.push(fd),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.writable().await?;
|
||||||
|
|
||||||
|
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[..size])];
|
||||||
|
|
||||||
|
sendmsg::<()>(socket.as_raw_fd(), &iov, &[cmsg], MsgFlags::empty(), None)?;
|
||||||
|
}
|
||||||
|
}
|
367
src/socks.rs
Normal file
367
src/socks.rs
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
use crate::{
|
||||||
|
directions::{IncomingDataEvent, IncomingDirection, OutgoingDataEvent, OutgoingDirection},
|
||||||
|
error::{Error, Result},
|
||||||
|
proxy_handler::{ProxyHandler, ProxyHandlerManager},
|
||||||
|
session_info::SessionInfo,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Debug)]
|
||||||
|
enum SocksState {
|
||||||
|
ClientHello,
|
||||||
|
ServerHello,
|
||||||
|
SendAuthData,
|
||||||
|
ReceiveAuthResponse,
|
||||||
|
SendRequest,
|
||||||
|
ReceiveResponse,
|
||||||
|
Established,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SocksProxyImpl {
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
state: SocksState,
|
||||||
|
client_inbuf: VecDeque<u8>,
|
||||||
|
server_inbuf: VecDeque<u8>,
|
||||||
|
client_outbuf: VecDeque<u8>,
|
||||||
|
server_outbuf: VecDeque<u8>,
|
||||||
|
version: Version,
|
||||||
|
credentials: Option<UserKey>,
|
||||||
|
command: protocol::Command,
|
||||||
|
udp_associate: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocksProxyImpl {
|
||||||
|
fn new(
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
credentials: Option<UserKey>,
|
||||||
|
version: Version,
|
||||||
|
command: protocol::Command,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut result = Self {
|
||||||
|
server_addr,
|
||||||
|
info,
|
||||||
|
domain_name,
|
||||||
|
state: SocksState::ClientHello,
|
||||||
|
client_inbuf: VecDeque::default(),
|
||||||
|
server_inbuf: VecDeque::default(),
|
||||||
|
client_outbuf: VecDeque::default(),
|
||||||
|
server_outbuf: VecDeque::default(),
|
||||||
|
version,
|
||||||
|
credentials,
|
||||||
|
command,
|
||||||
|
udp_associate: None,
|
||||||
|
};
|
||||||
|
result.send_client_hello()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_client_hello_socks4(&mut self) -> Result<(), Error> {
|
||||||
|
let credentials = &self.credentials;
|
||||||
|
self.server_outbuf.extend(&[self.version as u8, protocol::Command::Connect.into()]);
|
||||||
|
self.server_outbuf.extend(self.info.dst.port().to_be_bytes());
|
||||||
|
let mut ip_vec = Vec::<u8>::new();
|
||||||
|
let mut name_vec = Vec::<u8>::new();
|
||||||
|
match &self.info.dst {
|
||||||
|
SocketAddr::V4(addr) => {
|
||||||
|
if let Some(host) = &self.domain_name {
|
||||||
|
ip_vec.extend(&[0, 0, 0, host.len() as u8]);
|
||||||
|
name_vec.extend(host.as_bytes());
|
||||||
|
name_vec.push(0);
|
||||||
|
} else {
|
||||||
|
ip_vec.extend(addr.ip().octets().as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SocketAddr::V6(addr) => {
|
||||||
|
return Err(format!("SOCKS4 does not support IPv6: {addr}").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.server_outbuf.extend(ip_vec);
|
||||||
|
if let Some(credentials) = credentials {
|
||||||
|
self.server_outbuf.extend(credentials.username.as_bytes());
|
||||||
|
if !credentials.password.is_empty() {
|
||||||
|
self.server_outbuf.push_back(b':');
|
||||||
|
self.server_outbuf.extend(credentials.password.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.server_outbuf.push_back(0);
|
||||||
|
self.server_outbuf.extend(name_vec);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_client_hello_socks5(&mut self) -> Result<(), Error> {
|
||||||
|
let credentials = &self.credentials;
|
||||||
|
let mut methods = vec![AuthMethod::NoAuth, AuthMethod::from(4_u8), AuthMethod::from(100_u8)];
|
||||||
|
if credentials.is_some() {
|
||||||
|
methods.push(AuthMethod::UserPass);
|
||||||
|
}
|
||||||
|
handshake::Request::new(methods).write_to_stream(&mut self.server_outbuf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_client_hello(&mut self) -> Result<(), Error> {
|
||||||
|
match self.version {
|
||||||
|
Version::V4 => {
|
||||||
|
self.send_client_hello_socks4()?;
|
||||||
|
}
|
||||||
|
Version::V5 => {
|
||||||
|
self.send_client_hello_socks5()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.state = SocksState::ServerHello;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_server_hello_socks4(&mut self) -> std::io::Result<()> {
|
||||||
|
if self.server_inbuf.len() < 8 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.server_inbuf[1] != 0x5a {
|
||||||
|
return Err(crate::Error::from("SOCKS4 server replied with an unexpected reply code.").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.server_inbuf.drain(0..8);
|
||||||
|
|
||||||
|
self.state = SocksState::Established;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_server_hello_socks5(&mut self) -> std::io::Result<()> {
|
||||||
|
let response = handshake::Response::retrieve_from_stream(&mut self.server_inbuf.clone());
|
||||||
|
if let Err(e) = response {
|
||||||
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
log::trace!("receive_server_hello_socks5 needs more data \"{e}\"...");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let respones = response?;
|
||||||
|
self.server_inbuf.drain(0..respones.len());
|
||||||
|
let auth_method = respones.method;
|
||||||
|
|
||||||
|
if auth_method != AuthMethod::NoAuth && self.credentials.is_none()
|
||||||
|
|| (auth_method != AuthMethod::NoAuth && auth_method != AuthMethod::UserPass) && self.credentials.is_some()
|
||||||
|
{
|
||||||
|
return Err(crate::Error::from("SOCKS5 server requires an unsupported authentication method.").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = if auth_method == AuthMethod::UserPass {
|
||||||
|
SocksState::SendAuthData
|
||||||
|
} else {
|
||||||
|
SocksState::SendRequest
|
||||||
|
};
|
||||||
|
self.state_change()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_server_hello(&mut self) -> std::io::Result<()> {
|
||||||
|
match self.version {
|
||||||
|
Version::V4 => self.receive_server_hello_socks4(),
|
||||||
|
Version::V5 => self.receive_server_hello_socks5(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_auth_data(&mut self) -> std::io::Result<()> {
|
||||||
|
let tmp = UserKey::default();
|
||||||
|
let credentials = self.credentials.as_ref().unwrap_or(&tmp);
|
||||||
|
let request = password_method::Request::new(&credentials.username, &credentials.password);
|
||||||
|
request.write_to_stream(&mut self.server_outbuf)?;
|
||||||
|
self.state = SocksState::ReceiveAuthResponse;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_auth_data(&mut self) -> std::io::Result<()> {
|
||||||
|
use password_method::Response;
|
||||||
|
let response = Response::retrieve_from_stream(&mut self.server_inbuf.clone());
|
||||||
|
if let Err(e) = response {
|
||||||
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
log::trace!("receive_auth_data needs more data \"{e}\"...");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = response?;
|
||||||
|
self.server_inbuf.drain(0..response.len());
|
||||||
|
if response.status != password_method::Status::Succeeded {
|
||||||
|
return Err(crate::Error::from(format!("SOCKS authentication failed: {:?}", response.status)).into());
|
||||||
|
}
|
||||||
|
self.state = SocksState::SendRequest;
|
||||||
|
self.state_change()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_request_socks5(&mut self) -> std::io::Result<()> {
|
||||||
|
let addr = if self.command == protocol::Command::UdpAssociate {
|
||||||
|
Address::unspecified()
|
||||||
|
} else if let Some(domain_name) = &self.domain_name {
|
||||||
|
Address::DomainAddress(domain_name.clone(), self.info.dst.port())
|
||||||
|
} else {
|
||||||
|
self.info.dst.into()
|
||||||
|
};
|
||||||
|
protocol::Request::new(self.command, addr).write_to_stream(&mut self.server_outbuf)?;
|
||||||
|
self.state = SocksState::ReceiveResponse;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_connection_status(&mut self) -> std::io::Result<()> {
|
||||||
|
let response = protocol::Response::retrieve_from_stream(&mut self.server_inbuf.clone());
|
||||||
|
if let Err(e) = response {
|
||||||
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
log::trace!("receive_connection_status needs more data \"{e}\"...");
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response = response?;
|
||||||
|
self.server_inbuf.drain(0..response.len());
|
||||||
|
if response.reply != protocol::Reply::Succeeded {
|
||||||
|
return Err(crate::Error::from(format!("SOCKS connection failed: {}", response.reply)).into());
|
||||||
|
}
|
||||||
|
if self.command == protocol::Command::UdpAssociate {
|
||||||
|
self.udp_associate = Some(SocketAddr::try_from(&response.address)?);
|
||||||
|
// log::trace!("UDP associate recieved address {}", response.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = SocksState::Established;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relay_traffic(&mut self) -> Result<(), Error> {
|
||||||
|
self.client_outbuf.extend(self.server_inbuf.iter());
|
||||||
|
self.server_outbuf.extend(self.client_inbuf.iter());
|
||||||
|
self.server_inbuf.clear();
|
||||||
|
self.client_inbuf.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_change(&mut self) -> std::io::Result<()> {
|
||||||
|
match self.state {
|
||||||
|
SocksState::ServerHello => self.receive_server_hello()?,
|
||||||
|
|
||||||
|
SocksState::SendAuthData => self.send_auth_data()?,
|
||||||
|
|
||||||
|
SocksState::ReceiveAuthResponse => self.receive_auth_data()?,
|
||||||
|
|
||||||
|
SocksState::SendRequest => self.send_request_socks5()?,
|
||||||
|
|
||||||
|
SocksState::ReceiveResponse => self.receive_connection_status()?,
|
||||||
|
|
||||||
|
SocksState::Established => self.relay_traffic()?,
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ProxyHandler for SocksProxyImpl {
|
||||||
|
fn get_server_addr(&self) -> SocketAddr {
|
||||||
|
self.server_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_session_info(&self) -> SessionInfo {
|
||||||
|
self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_domain_name(&self) -> Option<String> {
|
||||||
|
self.domain_name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_data(&mut self, event: IncomingDataEvent<'_>) -> std::io::Result<()> {
|
||||||
|
let IncomingDataEvent { direction, buffer } = event;
|
||||||
|
match direction {
|
||||||
|
IncomingDirection::FromServer => {
|
||||||
|
self.server_inbuf.extend(buffer.iter());
|
||||||
|
}
|
||||||
|
IncomingDirection::FromClient => {
|
||||||
|
self.client_inbuf.extend(buffer.iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state_change()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consume_data(&mut self, dir: OutgoingDirection, size: usize) {
|
||||||
|
let buffer = match dir {
|
||||||
|
OutgoingDirection::ToServer => &mut self.server_outbuf,
|
||||||
|
OutgoingDirection::ToClient => &mut self.client_outbuf,
|
||||||
|
};
|
||||||
|
buffer.drain(0..size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent {
|
||||||
|
let buffer = match dir {
|
||||||
|
OutgoingDirection::ToServer => &mut self.server_outbuf,
|
||||||
|
OutgoingDirection::ToClient => &mut self.client_outbuf,
|
||||||
|
};
|
||||||
|
OutgoingDataEvent {
|
||||||
|
direction: dir,
|
||||||
|
buffer: buffer.make_contiguous(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection_established(&self) -> bool {
|
||||||
|
self.state == SocksState::Established
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_len(&self, dir: OutgoingDirection) -> usize {
|
||||||
|
match dir {
|
||||||
|
OutgoingDirection::ToServer => self.server_outbuf.len(),
|
||||||
|
OutgoingDirection::ToClient => self.client_outbuf.len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_connection(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_udp_associate(&self) -> Option<SocketAddr> {
|
||||||
|
self.udp_associate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SocksProxyManager {
|
||||||
|
server: SocketAddr,
|
||||||
|
credentials: Option<UserKey>,
|
||||||
|
version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ProxyHandlerManager for SocksProxyManager {
|
||||||
|
async fn new_proxy_handler(
|
||||||
|
&self,
|
||||||
|
info: SessionInfo,
|
||||||
|
domain_name: Option<String>,
|
||||||
|
udp_associate: bool,
|
||||||
|
) -> std::io::Result<Arc<Mutex<dyn ProxyHandler>>> {
|
||||||
|
use socks5_impl::protocol::Command::{Connect, UdpAssociate};
|
||||||
|
let command = if udp_associate { UdpAssociate } else { Connect };
|
||||||
|
let credentials = self.credentials.clone();
|
||||||
|
Ok(Arc::new(Mutex::new(SocksProxyImpl::new(
|
||||||
|
self.server,
|
||||||
|
info,
|
||||||
|
domain_name,
|
||||||
|
credentials,
|
||||||
|
self.version,
|
||||||
|
command,
|
||||||
|
)?)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocksProxyManager {
|
||||||
|
pub(crate) fn new(server: SocketAddr, version: Version, credentials: Option<UserKey>) -> Self {
|
||||||
|
Self {
|
||||||
|
server,
|
||||||
|
credentials,
|
||||||
|
version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
422
src/socks5.rs
422
src/socks5.rs
|
@ -1,422 +0,0 @@
|
||||||
use crate::error::Error;
|
|
||||||
use crate::tun2proxy::{
|
|
||||||
Connection, ConnectionManager, DestinationHost, IncomingDataEvent, IncomingDirection,
|
|
||||||
OutgoingDataEvent, OutgoingDirection, TcpProxy,
|
|
||||||
};
|
|
||||||
use crate::Credentials;
|
|
||||||
use smoltcp::wire::IpProtocol;
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
use std::net::{IpAddr, SocketAddr};
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
enum SocksState {
|
|
||||||
ClientHello,
|
|
||||||
ServerHello,
|
|
||||||
SendAuthData,
|
|
||||||
ReceiveAuthResponse,
|
|
||||||
SendRequest,
|
|
||||||
ReceiveResponse,
|
|
||||||
Established,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
enum SocksAddressType {
|
|
||||||
Ipv4 = 1,
|
|
||||||
DomainName = 3,
|
|
||||||
Ipv6 = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub enum SocksVersion {
|
|
||||||
V4 = 4,
|
|
||||||
V5 = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
enum SocksAuthentication {
|
|
||||||
None = 0,
|
|
||||||
Password = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
enum SocksReplies {
|
|
||||||
Succeeded,
|
|
||||||
GeneralFailure,
|
|
||||||
ConnectionDisallowed,
|
|
||||||
NetworkUnreachable,
|
|
||||||
ConnectionRefused,
|
|
||||||
TtlExpired,
|
|
||||||
CommandUnsupported,
|
|
||||||
AddressUnsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for SocksReplies {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "{:?}", self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct SocksConnection {
|
|
||||||
connection: Connection,
|
|
||||||
state: SocksState,
|
|
||||||
client_inbuf: VecDeque<u8>,
|
|
||||||
server_inbuf: VecDeque<u8>,
|
|
||||||
client_outbuf: VecDeque<u8>,
|
|
||||||
server_outbuf: VecDeque<u8>,
|
|
||||||
data_buf: VecDeque<u8>,
|
|
||||||
manager: Rc<dyn ConnectionManager>,
|
|
||||||
version: SocksVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SocksConnection {
|
|
||||||
pub fn new(
|
|
||||||
connection: &Connection,
|
|
||||||
manager: Rc<dyn ConnectionManager>,
|
|
||||||
version: SocksVersion,
|
|
||||||
) -> Result<Self, Error> {
|
|
||||||
let mut result = Self {
|
|
||||||
connection: connection.clone(),
|
|
||||||
state: SocksState::ServerHello,
|
|
||||||
client_inbuf: Default::default(),
|
|
||||||
server_inbuf: Default::default(),
|
|
||||||
client_outbuf: Default::default(),
|
|
||||||
server_outbuf: Default::default(),
|
|
||||||
data_buf: Default::default(),
|
|
||||||
manager,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
result.send_client_hello()?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_client_hello(&mut self) -> Result<(), Error> {
|
|
||||||
let credentials = self.manager.get_credentials();
|
|
||||||
match self.version {
|
|
||||||
SocksVersion::V4 => {
|
|
||||||
self.server_outbuf.extend(&[
|
|
||||||
4u8,
|
|
||||||
1,
|
|
||||||
(self.connection.dst.port >> 8) as u8,
|
|
||||||
(self.connection.dst.port & 0xff) as u8,
|
|
||||||
]);
|
|
||||||
let mut ip_vec = Vec::<u8>::new();
|
|
||||||
let mut name_vec = Vec::<u8>::new();
|
|
||||||
match &self.connection.dst.host {
|
|
||||||
DestinationHost::Address(dst_ip) => {
|
|
||||||
match dst_ip {
|
|
||||||
IpAddr::V4(ip) => ip_vec.extend(ip.octets().as_ref()),
|
|
||||||
IpAddr::V6(_) => return Err("SOCKS4 does not support IPv6".into()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
DestinationHost::Hostname(host) => {
|
|
||||||
ip_vec.extend(&[0, 0, 0, host.len() as u8]);
|
|
||||||
name_vec.extend(host.as_bytes());
|
|
||||||
name_vec.push(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.server_outbuf.extend(ip_vec);
|
|
||||||
if let Some(credentials) = credentials {
|
|
||||||
self.server_outbuf.extend(&credentials.username);
|
|
||||||
if !credentials.password.is_empty() {
|
|
||||||
self.server_outbuf.push_back(b':');
|
|
||||||
self.server_outbuf.extend(&credentials.password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.server_outbuf.push_back(0);
|
|
||||||
self.server_outbuf.extend(name_vec);
|
|
||||||
}
|
|
||||||
|
|
||||||
SocksVersion::V5 => {
|
|
||||||
if credentials.is_some() {
|
|
||||||
self.server_outbuf
|
|
||||||
.extend(&[5u8, 1, SocksAuthentication::Password as u8]);
|
|
||||||
} else {
|
|
||||||
self.server_outbuf
|
|
||||||
.extend(&[5u8, 1, SocksAuthentication::None as u8]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.state = SocksState::ServerHello;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_server_hello_socks4(&mut self) -> Result<(), Error> {
|
|
||||||
if self.server_inbuf.len() < 8 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.server_inbuf[1] != 0x5a {
|
|
||||||
return Err("SOCKS4 server replied with an unexpected reply code.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.server_inbuf.drain(0..8);
|
|
||||||
self.server_outbuf.append(&mut self.data_buf);
|
|
||||||
self.data_buf.clear();
|
|
||||||
|
|
||||||
self.state = SocksState::Established;
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_server_hello_socks5(&mut self) -> Result<(), Error> {
|
|
||||||
if self.server_inbuf.len() < 2 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if self.server_inbuf[0] != 5 {
|
|
||||||
return Err("SOCKS5 server replied with an unexpected version.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.server_inbuf[1] != 0 && self.manager.get_credentials().is_none()
|
|
||||||
|| self.server_inbuf[1] != 2 && self.manager.get_credentials().is_some()
|
|
||||||
{
|
|
||||||
return Err("SOCKS5 server requires an unsupported authentication method.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.server_inbuf.drain(0..2);
|
|
||||||
|
|
||||||
if self.manager.get_credentials().is_some() {
|
|
||||||
self.state = SocksState::SendAuthData;
|
|
||||||
} else {
|
|
||||||
self.state = SocksState::SendRequest;
|
|
||||||
}
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_server_hello(&mut self) -> Result<(), Error> {
|
|
||||||
match self.version {
|
|
||||||
SocksVersion::V4 => self.receive_server_hello_socks4(),
|
|
||||||
SocksVersion::V5 => self.receive_server_hello_socks5(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_auth_data(&mut self) -> Result<(), Error> {
|
|
||||||
let tmp = Credentials::default();
|
|
||||||
let credentials = self.manager.get_credentials().as_ref().unwrap_or(&tmp);
|
|
||||||
self.server_outbuf
|
|
||||||
.extend(&[1u8, credentials.username.len() as u8]);
|
|
||||||
self.server_outbuf.extend(&credentials.username);
|
|
||||||
self.server_outbuf
|
|
||||||
.extend(&[credentials.password.len() as u8]);
|
|
||||||
self.server_outbuf.extend(&credentials.password);
|
|
||||||
self.state = SocksState::ReceiveAuthResponse;
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_auth_data(&mut self) -> Result<(), Error> {
|
|
||||||
if self.server_inbuf.len() < 2 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if self.server_inbuf[0] != 1 || self.server_inbuf[1] != 0 {
|
|
||||||
return Err("SOCKS authentication failed.".into());
|
|
||||||
}
|
|
||||||
self.server_inbuf.drain(0..2);
|
|
||||||
self.state = SocksState::SendRequest;
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_connection_status(&mut self) -> Result<(), Error> {
|
|
||||||
if self.server_inbuf.len() < 4 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let ver = self.server_inbuf[0];
|
|
||||||
let rep = self.server_inbuf[1];
|
|
||||||
let _rsv = self.server_inbuf[2];
|
|
||||||
let atyp = self.server_inbuf[3];
|
|
||||||
|
|
||||||
if ver != 5 {
|
|
||||||
return Err("SOCKS5 server replied with an unexpected version.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if rep != 0 {
|
|
||||||
return Err("SOCKS5 connection unsuccessful.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if atyp != SocksAddressType::Ipv4 as u8
|
|
||||||
&& atyp != SocksAddressType::Ipv6 as u8
|
|
||||||
&& atyp != SocksAddressType::DomainName as u8
|
|
||||||
{
|
|
||||||
return Err("SOCKS5 server replied with unrecognized address type.".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if atyp == SocksAddressType::DomainName as u8 && self.server_inbuf.len() < 5 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if atyp == SocksAddressType::DomainName as u8
|
|
||||||
&& self.server_inbuf.len() < 7 + (self.server_inbuf[4] as usize)
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let message_length = if atyp == SocksAddressType::Ipv4 as u8 {
|
|
||||||
10
|
|
||||||
} else if atyp == SocksAddressType::Ipv6 as u8 {
|
|
||||||
22
|
|
||||||
} else {
|
|
||||||
7 + (self.server_inbuf[4] as usize)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.server_inbuf.drain(0..message_length);
|
|
||||||
self.server_outbuf.append(&mut self.data_buf);
|
|
||||||
self.data_buf.clear();
|
|
||||||
|
|
||||||
self.state = SocksState::Established;
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_request(&mut self) -> Result<(), Error> {
|
|
||||||
self.server_outbuf.extend(&[5u8, 1, 0]);
|
|
||||||
match &self.connection.dst.host {
|
|
||||||
DestinationHost::Address(dst_ip) => {
|
|
||||||
let cmd = if dst_ip.is_ipv4() {
|
|
||||||
SocksAddressType::Ipv4
|
|
||||||
} else {
|
|
||||||
SocksAddressType::Ipv6
|
|
||||||
};
|
|
||||||
self.server_outbuf.extend(&[cmd as u8]);
|
|
||||||
match dst_ip {
|
|
||||||
IpAddr::V4(ip) => self.server_outbuf.extend(ip.octets().as_ref()),
|
|
||||||
IpAddr::V6(ip) => self.server_outbuf.extend(ip.octets().as_ref()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
DestinationHost::Hostname(host) => {
|
|
||||||
self.server_outbuf
|
|
||||||
.extend(&[SocksAddressType::DomainName as u8, host.len() as u8]);
|
|
||||||
self.server_outbuf.extend(host.as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.server_outbuf.extend(&[
|
|
||||||
(self.connection.dst.port >> 8) as u8,
|
|
||||||
(self.connection.dst.port & 0xff) as u8,
|
|
||||||
]);
|
|
||||||
self.state = SocksState::ReceiveResponse;
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn relay_traffic(&mut self) -> Result<(), Error> {
|
|
||||||
self.client_outbuf.extend(self.server_inbuf.iter());
|
|
||||||
self.server_outbuf.extend(self.client_inbuf.iter());
|
|
||||||
self.server_inbuf.clear();
|
|
||||||
self.client_inbuf.clear();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state_change(&mut self) -> Result<(), Error> {
|
|
||||||
match self.state {
|
|
||||||
SocksState::ServerHello => self.receive_server_hello(),
|
|
||||||
|
|
||||||
SocksState::SendAuthData => self.send_auth_data(),
|
|
||||||
|
|
||||||
SocksState::ReceiveAuthResponse => self.receive_auth_data(),
|
|
||||||
|
|
||||||
SocksState::SendRequest => self.send_request(),
|
|
||||||
|
|
||||||
SocksState::ReceiveResponse => self.receive_connection_status(),
|
|
||||||
|
|
||||||
SocksState::Established => self.relay_traffic(),
|
|
||||||
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TcpProxy for SocksConnection {
|
|
||||||
fn push_data(&mut self, event: IncomingDataEvent<'_>) -> Result<(), Error> {
|
|
||||||
let direction = event.direction;
|
|
||||||
let buffer = event.buffer;
|
|
||||||
match direction {
|
|
||||||
IncomingDirection::FromServer => {
|
|
||||||
self.server_inbuf.extend(buffer.iter());
|
|
||||||
}
|
|
||||||
IncomingDirection::FromClient => {
|
|
||||||
if self.state == SocksState::Established {
|
|
||||||
self.client_inbuf.extend(buffer.iter());
|
|
||||||
} else {
|
|
||||||
self.data_buf.extend(buffer.iter());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state_change()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn consume_data(&mut self, dir: OutgoingDirection, size: usize) {
|
|
||||||
let buffer = if dir == OutgoingDirection::ToServer {
|
|
||||||
&mut self.server_outbuf
|
|
||||||
} else {
|
|
||||||
&mut self.client_outbuf
|
|
||||||
};
|
|
||||||
buffer.drain(0..size);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent {
|
|
||||||
let buffer = if dir == OutgoingDirection::ToServer {
|
|
||||||
&mut self.server_outbuf
|
|
||||||
} else {
|
|
||||||
&mut self.client_outbuf
|
|
||||||
};
|
|
||||||
OutgoingDataEvent {
|
|
||||||
direction: dir,
|
|
||||||
buffer: buffer.make_contiguous(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connection_established(&self) -> bool {
|
|
||||||
self.state == SocksState::Established
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SocksManager {
|
|
||||||
server: SocketAddr,
|
|
||||||
credentials: Option<Credentials>,
|
|
||||||
version: SocksVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionManager for SocksManager {
|
|
||||||
fn handles_connection(&self, connection: &Connection) -> bool {
|
|
||||||
connection.proto == IpProtocol::Tcp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_connection(
|
|
||||||
&self,
|
|
||||||
connection: &Connection,
|
|
||||||
manager: Rc<dyn ConnectionManager>,
|
|
||||||
) -> Result<Option<Box<dyn TcpProxy>>, Error> {
|
|
||||||
if connection.proto != IpProtocol::Tcp {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Some(Box::new(SocksConnection::new(
|
|
||||||
connection,
|
|
||||||
manager,
|
|
||||||
self.version,
|
|
||||||
)?)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_connection(&self, _: &Connection) {}
|
|
||||||
|
|
||||||
fn get_server(&self) -> SocketAddr {
|
|
||||||
self.server
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_credentials(&self) -> &Option<Credentials> {
|
|
||||||
&self.credentials
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SocksManager {
|
|
||||||
pub fn new(
|
|
||||||
server: SocketAddr,
|
|
||||||
version: SocksVersion,
|
|
||||||
credentials: Option<Credentials>,
|
|
||||||
) -> Rc<Self> {
|
|
||||||
Rc::new(Self {
|
|
||||||
server,
|
|
||||||
credentials,
|
|
||||||
version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
86
src/traffic_status.rs
Normal file
86
src/traffic_status.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// set traffic status callback.
|
||||||
|
#[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)>,
|
||||||
|
ctx: *mut c_void,
|
||||||
|
) {
|
||||||
|
if let Ok(mut cb) = TRAFFIC_STATUS_CALLBACK.lock() {
|
||||||
|
*cb = Some(TrafficStatusCallback(callback, ctx));
|
||||||
|
} else {
|
||||||
|
log::error!("set traffic status callback failed");
|
||||||
|
}
|
||||||
|
if send_interval_secs > 0 {
|
||||||
|
SEND_INTERVAL_SECS.store(send_interval_secs as u64, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Default, Copy, Clone)]
|
||||||
|
pub struct TrafficStatus {
|
||||||
|
pub tx: u64,
|
||||||
|
pub rx: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TrafficStatusCallback(Option<unsafe extern "C" fn(*const TrafficStatus, *mut c_void)>, *mut c_void);
|
||||||
|
|
||||||
|
impl TrafficStatusCallback {
|
||||||
|
unsafe fn call(self, info: &TrafficStatus) {
|
||||||
|
if let Some(cb) = self.0 {
|
||||||
|
unsafe { cb(info, self.1) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for TrafficStatusCallback {}
|
||||||
|
unsafe impl Sync for TrafficStatusCallback {}
|
||||||
|
|
||||||
|
static TRAFFIC_STATUS_CALLBACK: std::sync::Mutex<Option<TrafficStatusCallback>> = std::sync::Mutex::new(None);
|
||||||
|
static SEND_INTERVAL_SECS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||||
|
|
||||||
|
static TRAFFIC_STATUS: LazyLock<Mutex<TrafficStatus>> = LazyLock::new(|| Mutex::new(TrafficStatus::default()));
|
||||||
|
static TIME_STAMP: LazyLock<Mutex<std::time::Instant>> = LazyLock::new(|| Mutex::new(std::time::Instant::now()));
|
||||||
|
|
||||||
|
pub(crate) fn traffic_status_update(delta_tx: usize, delta_rx: usize) -> Result<()> {
|
||||||
|
{
|
||||||
|
let is_none_or_error = TRAFFIC_STATUS_CALLBACK.lock().map(|guard| guard.is_none()).unwrap_or_else(|e| {
|
||||||
|
log::error!("Failed to acquire lock: {e}");
|
||||||
|
true
|
||||||
|
});
|
||||||
|
if is_none_or_error {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let traffic_status = {
|
||||||
|
let mut traffic_status = TRAFFIC_STATUS.lock().map_err(|e| Error::from(e.to_string()))?;
|
||||||
|
traffic_status.tx += delta_tx as u64;
|
||||||
|
traffic_status.rx += delta_rx as u64;
|
||||||
|
*traffic_status
|
||||||
|
};
|
||||||
|
let old_time = { *TIME_STAMP.lock().map_err(|e| Error::from(e.to_string()))? };
|
||||||
|
let interval_secs = SEND_INTERVAL_SECS.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if std::time::Instant::now().duration_since(old_time).as_secs() >= interval_secs {
|
||||||
|
send_traffic_stat(&traffic_status)?;
|
||||||
|
{
|
||||||
|
let mut time_stamp = TIME_STAMP.lock().map_err(|e| Error::from(e.to_string()))?;
|
||||||
|
*time_stamp = std::time::Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_traffic_stat(traffic_status: &TrafficStatus) -> Result<()> {
|
||||||
|
if let Ok(cb) = TRAFFIC_STATUS_CALLBACK.lock() {
|
||||||
|
if let Some(cb) = cb.clone() {
|
||||||
|
unsafe { cb.call(traffic_status) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
691
src/tun2proxy.rs
691
src/tun2proxy.rs
|
@ -1,691 +0,0 @@
|
||||||
use crate::error::Error;
|
|
||||||
use crate::virtdevice::VirtualTunDevice;
|
|
||||||
use crate::{Credentials, Options};
|
|
||||||
use log::{error, info};
|
|
||||||
use mio::event::Event;
|
|
||||||
use mio::net::TcpStream;
|
|
||||||
use mio::unix::SourceFd;
|
|
||||||
use mio::{Events, Interest, Poll, Token};
|
|
||||||
use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet};
|
|
||||||
use smoltcp::phy::{Device, Medium, RxToken, TunTapInterface, TxToken};
|
|
||||||
use smoltcp::socket::{tcp, udp};
|
|
||||||
use smoltcp::time::Instant;
|
|
||||||
use smoltcp::wire::{IpCidr, IpProtocol, Ipv4Packet, Ipv6Packet, TcpPacket, UdpPacket};
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::convert::{From, TryFrom};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::net::Shutdown::Both;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Hash, Clone, Eq, PartialEq)]
|
|
||||||
pub(crate) enum DestinationHost {
|
|
||||||
Address(IpAddr),
|
|
||||||
Hostname(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for DestinationHost {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
DestinationHost::Address(addr) => addr.fmt(f),
|
|
||||||
DestinationHost::Hostname(name) => name.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Hash, Clone, Eq, PartialEq)]
|
|
||||||
pub(crate) struct Destination {
|
|
||||||
pub(crate) host: DestinationHost,
|
|
||||||
pub(crate) port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<Destination> for SocketAddr {
|
|
||||||
type Error = Error;
|
|
||||||
fn try_from(value: Destination) -> Result<Self, Self::Error> {
|
|
||||||
let ip = match value.host {
|
|
||||||
DestinationHost::Address(addr) => addr,
|
|
||||||
DestinationHost::Hostname(e) => {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(SocketAddr::new(ip, value.port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SocketAddr> for Destination {
|
|
||||||
fn from(addr: SocketAddr) -> Self {
|
|
||||||
Self {
|
|
||||||
host: DestinationHost::Address(addr.ip()),
|
|
||||||
port: addr.port(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Destination {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if let DestinationHost::Address(IpAddr::V6(addr)) = self.host {
|
|
||||||
write!(f, "[{}]:{}", addr, self.port)
|
|
||||||
} else {
|
|
||||||
write!(f, "{}:{}", self.host, self.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Hash, Clone, Eq, PartialEq)]
|
|
||||||
pub(crate) struct Connection {
|
|
||||||
pub(crate) src: SocketAddr,
|
|
||||||
pub(crate) dst: Destination,
|
|
||||||
pub(crate) proto: IpProtocol,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connection {
|
|
||||||
fn to_named(&self, name: String) -> Self {
|
|
||||||
let mut result = self.clone();
|
|
||||||
result.dst.host = DestinationHost::Hostname(name);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Connection {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(f, "{} -> {}", self.src, self.dst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
pub(crate) enum IncomingDirection {
|
|
||||||
FromServer,
|
|
||||||
FromClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
pub(crate) enum OutgoingDirection {
|
|
||||||
ToServer,
|
|
||||||
ToClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) enum ConnectionEvent<'a> {
|
|
||||||
NewConnection(&'a Connection),
|
|
||||||
ConnectionClosed(&'a Connection),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct DataEvent<'a, T> {
|
|
||||||
pub(crate) direction: T,
|
|
||||||
pub(crate) buffer: &'a [u8],
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type IncomingDataEvent<'a> = DataEvent<'a, IncomingDirection>;
|
|
||||||
pub(crate) type OutgoingDataEvent<'a> = DataEvent<'a, OutgoingDirection>;
|
|
||||||
|
|
||||||
fn get_transport_info(
|
|
||||||
proto: IpProtocol,
|
|
||||||
transport_offset: usize,
|
|
||||||
packet: &[u8],
|
|
||||||
) -> Option<((u16, u16), bool, usize, usize)> {
|
|
||||||
if proto == IpProtocol::Udp {
|
|
||||||
match UdpPacket::new_checked(packet) {
|
|
||||||
Ok(result) => Some((
|
|
||||||
(result.src_port(), result.dst_port()),
|
|
||||||
false,
|
|
||||||
transport_offset + 8,
|
|
||||||
packet.len() - 8,
|
|
||||||
)),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
} else if proto == IpProtocol::Tcp {
|
|
||||||
match TcpPacket::new_checked(packet) {
|
|
||||||
Ok(result) => Some((
|
|
||||||
(result.src_port(), result.dst_port()),
|
|
||||||
result.syn() && !result.ack(),
|
|
||||||
transport_offset + result.header_len() as usize,
|
|
||||||
packet.len(),
|
|
||||||
)),
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connection_tuple(frame: &[u8]) -> Option<(Connection, bool, usize, usize)> {
|
|
||||||
if let Ok(packet) = Ipv4Packet::new_checked(frame) {
|
|
||||||
let proto = packet.next_header();
|
|
||||||
|
|
||||||
let mut a: [u8; 4] = Default::default();
|
|
||||||
a.copy_from_slice(packet.src_addr().as_bytes());
|
|
||||||
let src_addr = IpAddr::from(a);
|
|
||||||
a.copy_from_slice(packet.dst_addr().as_bytes());
|
|
||||||
let dst_addr = IpAddr::from(a);
|
|
||||||
|
|
||||||
return if let Some((ports, first_packet, payload_offset, payload_size)) = get_transport_info(
|
|
||||||
proto,
|
|
||||||
packet.header_len().into(),
|
|
||||||
&frame[packet.header_len().into()..],
|
|
||||||
) {
|
|
||||||
let connection = Connection {
|
|
||||||
src: SocketAddr::new(src_addr, ports.0),
|
|
||||||
dst: SocketAddr::new(dst_addr, ports.1).into(),
|
|
||||||
proto,
|
|
||||||
};
|
|
||||||
Some((connection, first_packet, payload_offset, payload_size))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
match Ipv6Packet::new_checked(frame) {
|
|
||||||
Ok(packet) => {
|
|
||||||
// TODO: Support extension headers.
|
|
||||||
let proto = packet.next_header();
|
|
||||||
|
|
||||||
let mut a: [u8; 16] = Default::default();
|
|
||||||
a.copy_from_slice(packet.src_addr().as_bytes());
|
|
||||||
let src_addr = IpAddr::from(a);
|
|
||||||
a.copy_from_slice(packet.dst_addr().as_bytes());
|
|
||||||
let dst_addr = IpAddr::from(a);
|
|
||||||
|
|
||||||
if let Some((ports, first_packet, payload_offset, payload_size)) =
|
|
||||||
get_transport_info(proto, packet.header_len(), &frame[packet.header_len()..])
|
|
||||||
{
|
|
||||||
let connection = Connection {
|
|
||||||
src: SocketAddr::new(src_addr, ports.0),
|
|
||||||
dst: SocketAddr::new(dst_addr, ports.1).into(),
|
|
||||||
proto,
|
|
||||||
};
|
|
||||||
Some((connection, first_packet, payload_offset, payload_size))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WRITE_CLOSED: u8 = 1;
|
|
||||||
|
|
||||||
struct ConnectionState {
|
|
||||||
smoltcp_handle: SocketHandle,
|
|
||||||
mio_stream: TcpStream,
|
|
||||||
token: Token,
|
|
||||||
handler: Box<dyn TcpProxy>,
|
|
||||||
smoltcp_socket_state: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait TcpProxy {
|
|
||||||
fn push_data(&mut self, event: IncomingDataEvent<'_>) -> Result<(), Error>;
|
|
||||||
fn consume_data(&mut self, dir: OutgoingDirection, size: usize);
|
|
||||||
fn peek_data(&mut self, dir: OutgoingDirection) -> OutgoingDataEvent;
|
|
||||||
fn connection_established(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait ConnectionManager {
|
|
||||||
fn handles_connection(&self, connection: &Connection) -> bool;
|
|
||||||
fn new_connection(
|
|
||||||
&self,
|
|
||||||
connection: &Connection,
|
|
||||||
manager: Rc<dyn ConnectionManager>,
|
|
||||||
) -> Result<Option<Box<dyn TcpProxy>>, Error>;
|
|
||||||
fn close_connection(&self, connection: &Connection);
|
|
||||||
fn get_server(&self) -> SocketAddr;
|
|
||||||
fn get_credentials(&self) -> &Option<Credentials>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TCP_TOKEN: Token = Token(0);
|
|
||||||
const UDP_TOKEN: Token = Token(1);
|
|
||||||
|
|
||||||
pub(crate) struct TunToProxy<'a> {
|
|
||||||
tun: TunTapInterface,
|
|
||||||
poll: Poll,
|
|
||||||
iface: Interface,
|
|
||||||
connections: HashMap<Connection, ConnectionState>,
|
|
||||||
connection_managers: Vec<Rc<dyn ConnectionManager>>,
|
|
||||||
next_token: usize,
|
|
||||||
token_to_connection: HashMap<Token, Connection>,
|
|
||||||
sockets: SocketSet<'a>,
|
|
||||||
device: VirtualTunDevice,
|
|
||||||
options: Options,
|
|
||||||
write_sockets: HashSet<Token>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TunToProxy<'a> {
|
|
||||||
pub(crate) fn new(interface: &str, options: Options) -> Result<Self, Error> {
|
|
||||||
let tun = TunTapInterface::new(interface, Medium::Ip)?;
|
|
||||||
let poll = Poll::new()?;
|
|
||||||
poll.registry().register(
|
|
||||||
&mut SourceFd(&tun.as_raw_fd()),
|
|
||||||
TCP_TOKEN,
|
|
||||||
Interest::READABLE,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let config = Config::new();
|
|
||||||
let mut virt = VirtualTunDevice::new(tun.capabilities());
|
|
||||||
let gateway4: Ipv4Addr = Ipv4Addr::from_str("0.0.0.1")?;
|
|
||||||
let gateway6: Ipv6Addr = Ipv6Addr::from_str("::1")?;
|
|
||||||
let mut iface = Interface::new(config, &mut virt);
|
|
||||||
iface.update_ip_addrs(|ip_addrs| {
|
|
||||||
ip_addrs.push(IpCidr::new(gateway4.into(), 0)).unwrap();
|
|
||||||
ip_addrs.push(IpCidr::new(gateway6.into(), 0)).unwrap()
|
|
||||||
});
|
|
||||||
iface.routes_mut().add_default_ipv4_route(gateway4.into())?;
|
|
||||||
iface.routes_mut().add_default_ipv6_route(gateway6.into())?;
|
|
||||||
iface.set_any_ip(true);
|
|
||||||
|
|
||||||
let tun = Self {
|
|
||||||
tun,
|
|
||||||
poll,
|
|
||||||
iface,
|
|
||||||
connections: HashMap::default(),
|
|
||||||
next_token: 2,
|
|
||||||
token_to_connection: HashMap::default(),
|
|
||||||
connection_managers: Vec::default(),
|
|
||||||
sockets: SocketSet::new([]),
|
|
||||||
device: virt,
|
|
||||||
options,
|
|
||||||
write_sockets: HashSet::default(),
|
|
||||||
};
|
|
||||||
Ok(tun)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_connection_manager(&mut self, manager: Rc<dyn ConnectionManager>) {
|
|
||||||
self.connection_managers.push(manager);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_smoltcp_send(&mut self) -> Result<(), Error> {
|
|
||||||
self.iface
|
|
||||||
.poll(Instant::now(), &mut self.device, &mut self.sockets);
|
|
||||||
|
|
||||||
while let Some(vec) = self.device.exfiltrate_packet() {
|
|
||||||
let slice = vec.as_slice();
|
|
||||||
|
|
||||||
// TODO: Actual write. Replace.
|
|
||||||
self.tun
|
|
||||||
.transmit(Instant::now())
|
|
||||||
.ok_or("tx token not available")?
|
|
||||||
.consume(slice.len(), |buf| {
|
|
||||||
buf[..].clone_from_slice(slice);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_connection(&mut self, connection: &Connection) -> Result<(), Error> {
|
|
||||||
let e = "connection not exist";
|
|
||||||
let mut conn = self.connections.remove(connection).ok_or(e)?;
|
|
||||||
let token = &conn.token;
|
|
||||||
self.token_to_connection.remove(token);
|
|
||||||
self.poll.registry().deregister(&mut conn.mio_stream)?;
|
|
||||||
info!("CLOSE {}", connection);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_connection_manager(&self, connection: &Connection) -> Option<Rc<dyn ConnectionManager>> {
|
|
||||||
for manager in self.connection_managers.iter() {
|
|
||||||
if manager.handles_connection(connection) {
|
|
||||||
return Some(manager.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tunsocket_read_and_forward(&mut self, connection: &Connection) -> Result<(), Error> {
|
|
||||||
if let Some(state) = self.connections.get_mut(connection) {
|
|
||||||
let closed = {
|
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(state.smoltcp_handle);
|
|
||||||
let mut error = Ok(());
|
|
||||||
while socket.can_recv() && error.is_ok() {
|
|
||||||
socket.recv(|data| {
|
|
||||||
let event = IncomingDataEvent {
|
|
||||||
direction: IncomingDirection::FromClient,
|
|
||||||
buffer: data,
|
|
||||||
};
|
|
||||||
error = state.handler.push_data(event);
|
|
||||||
|
|
||||||
(data.len(), ())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match error {
|
|
||||||
Ok(_) => socket.state() == tcp::State::CloseWait,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("{e}");
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expect ACKs etc. from smoltcp sockets.
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
|
|
||||||
if closed {
|
|
||||||
let e = "connection not exist";
|
|
||||||
let connection_state = self.connections.get_mut(connection).ok_or(e)?;
|
|
||||||
connection_state.mio_stream.shutdown(Both)?;
|
|
||||||
self.remove_connection(connection)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn receive_tun(&mut self, frame: &mut [u8]) -> Result<(), Error> {
|
|
||||||
if let Some((connection, first_packet, _payload_offset, _payload_size)) =
|
|
||||||
connection_tuple(frame)
|
|
||||||
{
|
|
||||||
let resolved_conn = match &mut self.options.virtdns {
|
|
||||||
None => connection.clone(),
|
|
||||||
Some(virt_dns) => {
|
|
||||||
let ip = SocketAddr::try_from(connection.dst.clone())?.ip();
|
|
||||||
virt_dns.touch_ip(&ip);
|
|
||||||
match virt_dns.resolve_ip(&ip) {
|
|
||||||
None => connection.clone(),
|
|
||||||
Some(name) => connection.to_named(name.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let dst = connection.dst;
|
|
||||||
(|| -> Result<(), Error> {
|
|
||||||
if resolved_conn.proto == IpProtocol::Tcp {
|
|
||||||
let cm = self.get_connection_manager(&resolved_conn);
|
|
||||||
if cm.is_none() {
|
|
||||||
log::trace!("no connect manager");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let server = cm.unwrap().get_server();
|
|
||||||
if first_packet {
|
|
||||||
for manager in self.connection_managers.iter_mut() {
|
|
||||||
if let Some(handler) =
|
|
||||||
manager.new_connection(&resolved_conn, manager.clone())?
|
|
||||||
{
|
|
||||||
let mut socket = tcp::Socket::new(
|
|
||||||
tcp::SocketBuffer::new(vec![0; 4096]),
|
|
||||||
tcp::SocketBuffer::new(vec![0; 4096]),
|
|
||||||
);
|
|
||||||
socket.set_ack_delay(None);
|
|
||||||
let dst = SocketAddr::try_from(dst)?;
|
|
||||||
socket.listen(dst)?;
|
|
||||||
let handle = self.sockets.add(socket);
|
|
||||||
|
|
||||||
let client = TcpStream::connect(server)?;
|
|
||||||
|
|
||||||
let token = Token(self.next_token);
|
|
||||||
self.next_token += 1;
|
|
||||||
|
|
||||||
let mut state = ConnectionState {
|
|
||||||
smoltcp_handle: handle,
|
|
||||||
mio_stream: client,
|
|
||||||
token,
|
|
||||||
handler,
|
|
||||||
smoltcp_socket_state: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.token_to_connection
|
|
||||||
.insert(token, resolved_conn.clone());
|
|
||||||
self.poll.registry().register(
|
|
||||||
&mut state.mio_stream,
|
|
||||||
token,
|
|
||||||
Interest::READABLE | Interest::WRITABLE,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.connections.insert(resolved_conn.clone(), state);
|
|
||||||
|
|
||||||
info!("CONNECT {}", resolved_conn,);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !self.connections.contains_key(&resolved_conn) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject the packet to advance the smoltcp socket state
|
|
||||||
self.device.inject_packet(frame);
|
|
||||||
|
|
||||||
// Having advanced the socket state, we expect the socket to ACK
|
|
||||||
// Exfiltrate the response packets generated by the socket and inject them
|
|
||||||
// into the tunnel interface.
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
|
|
||||||
// Read from the smoltcp socket and push the data to the connection handler.
|
|
||||||
self.tunsocket_read_and_forward(&resolved_conn)?;
|
|
||||||
|
|
||||||
// The connection handler builds up the connection or encapsulates the data.
|
|
||||||
// Therefore, we now expect it to write data to the server.
|
|
||||||
self.write_to_server(&resolved_conn)?;
|
|
||||||
} else if resolved_conn.proto == IpProtocol::Udp && resolved_conn.dst.port == 53 {
|
|
||||||
if let Some(virtual_dns) = &mut self.options.virtdns {
|
|
||||||
let payload = &frame[_payload_offset.._payload_offset + _payload_size];
|
|
||||||
if let Some(response) = virtual_dns.receive_query(payload) {
|
|
||||||
let rx_buffer = udp::PacketBuffer::new(
|
|
||||||
vec![udp::PacketMetadata::EMPTY],
|
|
||||||
vec![0; 4096],
|
|
||||||
);
|
|
||||||
let tx_buffer = udp::PacketBuffer::new(
|
|
||||||
vec![udp::PacketMetadata::EMPTY],
|
|
||||||
vec![0; 4096],
|
|
||||||
);
|
|
||||||
let mut socket = udp::Socket::new(rx_buffer, tx_buffer);
|
|
||||||
let dst = SocketAddr::try_from(dst)?;
|
|
||||||
socket.bind(dst)?;
|
|
||||||
socket
|
|
||||||
.send_slice(response.as_slice(), resolved_conn.src.into())
|
|
||||||
.expect("failed to send DNS response");
|
|
||||||
let handle = self.sockets.add(socket);
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
self.sockets.remove(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, UDP is not yet supported.
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})()
|
|
||||||
.or_else(|error| {
|
|
||||||
log::error! {"{error}"}
|
|
||||||
Ok::<(), Error>(())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_server(&mut self, connection: &Connection) -> Result<(), Error> {
|
|
||||||
if let Some(state) = self.connections.get_mut(connection) {
|
|
||||||
let event = state.handler.peek_data(OutgoingDirection::ToServer);
|
|
||||||
if event.buffer.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let result = state.mio_stream.write(event.buffer);
|
|
||||||
match result {
|
|
||||||
Ok(consumed) => {
|
|
||||||
state
|
|
||||||
.handler
|
|
||||||
.consume_data(OutgoingDirection::ToServer, consumed);
|
|
||||||
}
|
|
||||||
Err(error) if error.kind() != std::io::ErrorKind::WouldBlock => {
|
|
||||||
return Err(error.into());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_to_client(&mut self, token: Token, connection: &Connection) -> Result<(), Error> {
|
|
||||||
loop {
|
|
||||||
if let Some(state) = self.connections.get_mut(connection) {
|
|
||||||
let socket_state = state.smoltcp_socket_state;
|
|
||||||
let socket_handle = state.smoltcp_handle;
|
|
||||||
let event = state.handler.peek_data(OutgoingDirection::ToClient);
|
|
||||||
let buflen = event.buffer.len();
|
|
||||||
let consumed;
|
|
||||||
{
|
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(socket_handle);
|
|
||||||
if socket.may_send() {
|
|
||||||
if let Some(virtdns) = &mut self.options.virtdns {
|
|
||||||
// Unwrapping is fine because every smoltcp socket is bound to an.
|
|
||||||
virtdns.touch_ip(&IpAddr::from(socket.local_endpoint().unwrap().addr));
|
|
||||||
}
|
|
||||||
consumed = socket.send_slice(event.buffer)?;
|
|
||||||
state
|
|
||||||
.handler
|
|
||||||
.consume_data(OutgoingDirection::ToClient, consumed);
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
if consumed < buflen {
|
|
||||||
self.write_sockets.insert(token);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
self.write_sockets.remove(&token);
|
|
||||||
if consumed == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(socket_handle);
|
|
||||||
// Closing and removing the connection here may work in practice but is actually not
|
|
||||||
// correct. Only the write end was closed but we could still read from it!
|
|
||||||
// TODO: Fix and test half-open connection scenarios as mentioned in the README.
|
|
||||||
// TODO: Investigate how half-closed connections from the other end are handled.
|
|
||||||
if socket_state & WRITE_CLOSED != 0 && consumed == buflen {
|
|
||||||
socket.close();
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
self.write_sockets.remove(&token);
|
|
||||||
self.remove_connection(connection)?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tun_event(&mut self, event: &Event) -> Result<(), Error> {
|
|
||||||
if event.is_readable() {
|
|
||||||
while let Some((rx_token, _)) = self.tun.receive(Instant::now()) {
|
|
||||||
rx_token.consume(|frame| self.receive_tun(frame))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_to_smoltcp(&mut self) -> Result<(), Error> {
|
|
||||||
let cloned = self.write_sockets.clone();
|
|
||||||
for token in cloned.iter() {
|
|
||||||
if let Some(connection) = self.token_to_connection.get(token) {
|
|
||||||
let connection = connection.clone();
|
|
||||||
if let Err(error) = self.write_to_client(*token, &connection) {
|
|
||||||
self.remove_connection(&connection)?;
|
|
||||||
log::error!("Write to client: {}: ", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mio_socket_event(&mut self, event: &Event) -> Result<(), Error> {
|
|
||||||
let e = "connection not found";
|
|
||||||
let conn_ref = self.token_to_connection.get(&event.token());
|
|
||||||
// We may have closed the connection in an earlier iteration over the poll
|
|
||||||
// events, e.g. because an event through the tunnel interface indicated that the connection
|
|
||||||
// should be closed.
|
|
||||||
if conn_ref.is_none() {
|
|
||||||
log::trace!("{e}");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let connection = conn_ref.unwrap().clone();
|
|
||||||
|
|
||||||
(|| -> Result<(), Error> {
|
|
||||||
if event.is_readable() || event.is_read_closed() {
|
|
||||||
{
|
|
||||||
let state = self.connections.get_mut(&connection).ok_or(e)?;
|
|
||||||
|
|
||||||
// TODO: Move this reading process to its own function.
|
|
||||||
let mut vecbuf = Vec::<u8>::new();
|
|
||||||
let read_result = state.mio_stream.read_to_end(&mut vecbuf);
|
|
||||||
let read = match read_result {
|
|
||||||
Ok(read_result) => read_result,
|
|
||||||
Err(error) => {
|
|
||||||
if error.kind() != std::io::ErrorKind::WouldBlock {
|
|
||||||
error!("Read from proxy: {}", error);
|
|
||||||
}
|
|
||||||
vecbuf.len()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if read == 0 {
|
|
||||||
{
|
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(
|
|
||||||
self.connections.get(&connection).ok_or(e)?.smoltcp_handle,
|
|
||||||
);
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
self.remove_connection(&connection.clone())?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = vecbuf.as_slice();
|
|
||||||
let data_event = IncomingDataEvent {
|
|
||||||
direction: IncomingDirection::FromServer,
|
|
||||||
buffer: &data[0..read],
|
|
||||||
};
|
|
||||||
if let Err(error) = state.handler.push_data(data_event) {
|
|
||||||
state.mio_stream.shutdown(Both)?;
|
|
||||||
{
|
|
||||||
let socket = self.sockets.get_mut::<tcp::Socket>(
|
|
||||||
self.connections.get(&connection).ok_or(e)?.smoltcp_handle,
|
|
||||||
);
|
|
||||||
socket.close();
|
|
||||||
}
|
|
||||||
self.expect_smoltcp_send()?;
|
|
||||||
log::error! {"{error}"}
|
|
||||||
self.remove_connection(&connection.clone())?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if event.is_read_closed() {
|
|
||||||
state.smoltcp_socket_state |= WRITE_CLOSED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have read from the proxy server and pushed the data to the connection handler.
|
|
||||||
// Thus, expect data to be processed (e.g. decapsulated) and forwarded to the client.
|
|
||||||
self.write_to_client(event.token(), &connection)?;
|
|
||||||
}
|
|
||||||
if event.is_writable() {
|
|
||||||
self.write_to_server(&connection)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})()
|
|
||||||
.or_else(|error| {
|
|
||||||
self.remove_connection(&connection)?;
|
|
||||||
log::error! {"{error}"}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn udp_event(&mut self, _event: &Event) {}
|
|
||||||
|
|
||||||
pub(crate) fn run(&mut self) -> Result<(), Error> {
|
|
||||||
let mut events = Events::with_capacity(1024);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match self.poll.poll(&mut events, None) {
|
|
||||||
Ok(()) => {
|
|
||||||
for event in events.iter() {
|
|
||||||
match event.token() {
|
|
||||||
TCP_TOKEN => self.tun_event(event)?,
|
|
||||||
UDP_TOKEN => self.udp_event(event),
|
|
||||||
_ => self.mio_socket_event(event)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.send_to_smoltcp()?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
if e.kind() != std::io::ErrorKind::Interrupted {
|
|
||||||
return Err(e.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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{n:02X})"),
|
||||||
|
};
|
||||||
|
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 {sn} {local_addr:?} send keepalive failed: {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} {local_addr:?} send keepalive and recieve it successfully");
|
||||||
|
rx += len;
|
||||||
|
}
|
||||||
|
Ok((len, v)) => {
|
||||||
|
log::debug!("stream {sn} {local_addr:?} keepalive unexpected response: {v}");
|
||||||
|
rx += len;
|
||||||
|
}
|
||||||
|
Err(e) => log::debug!("stream {sn} {local_addr:?} keepalive no response, error \"{e}\""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,78 +0,0 @@
|
||||||
use smoltcp::phy;
|
|
||||||
use smoltcp::phy::{Device, DeviceCapabilities};
|
|
||||||
use smoltcp::time::Instant;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct VirtualTunDevice {
|
|
||||||
capabilities: DeviceCapabilities,
|
|
||||||
inbuf: Vec<Vec<u8>>,
|
|
||||||
outbuf: Vec<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualTunDevice {
|
|
||||||
pub fn inject_packet(&mut self, buffer: &[u8]) {
|
|
||||||
self.inbuf.push(buffer.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exfiltrate_packet(&mut self) -> Option<Vec<u8>> {
|
|
||||||
self.outbuf.pop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct VirtRxToken {
|
|
||||||
buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl phy::RxToken for VirtRxToken {
|
|
||||||
fn consume<R, F>(mut self, f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut [u8]) -> R,
|
|
||||||
{
|
|
||||||
f(&mut self.buffer[..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct VirtTxToken<'a>(&'a mut VirtualTunDevice);
|
|
||||||
|
|
||||||
impl<'a> phy::TxToken for VirtTxToken<'a> {
|
|
||||||
fn consume<R, F>(self, len: usize, f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut [u8]) -> R,
|
|
||||||
{
|
|
||||||
let mut buffer = vec![0; len];
|
|
||||||
let result = f(&mut buffer);
|
|
||||||
self.0.outbuf.push(buffer);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device for VirtualTunDevice {
|
|
||||||
type RxToken<'a> = VirtRxToken;
|
|
||||||
type TxToken<'a> = VirtTxToken<'a>;
|
|
||||||
|
|
||||||
fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
|
|
||||||
if let Some(buffer) = self.inbuf.pop() {
|
|
||||||
let rx = Self::RxToken { buffer };
|
|
||||||
let tx = VirtTxToken(self);
|
|
||||||
return Some((rx, tx));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transmit(&mut self, _timestamp: Instant) -> Option<Self::TxToken<'_>> {
|
|
||||||
return Some(VirtTxToken(self));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capabilities(&self) -> DeviceCapabilities {
|
|
||||||
self.capabilities.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualTunDevice {
|
|
||||||
pub fn new(capabilities: DeviceCapabilities) -> Self {
|
|
||||||
Self {
|
|
||||||
capabilities,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
281
src/virtdns.rs
281
src/virtdns.rs
|
@ -1,281 +0,0 @@
|
||||||
use hashlink::linked_hash_map::RawEntryMut;
|
|
||||||
use hashlink::LruCache;
|
|
||||||
use smoltcp::wire::Ipv4Cidr;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
const DNS_TTL: u8 = 30; // TTL in DNS replies in seconds
|
|
||||||
const MAPPING_TIMEOUT: u64 = 60; // Mapping timeout in seconds
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
#[allow(dead_code, clippy::upper_case_acronyms)]
|
|
||||||
enum DnsRecordType {
|
|
||||||
A = 1,
|
|
||||||
AAAA = 28,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Debug)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
enum DnsClass {
|
|
||||||
IN = 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NameCacheEntry {
|
|
||||||
name: String,
|
|
||||||
expiry: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct VirtualDns {
|
|
||||||
lru_cache: LruCache<IpAddr, NameCacheEntry>,
|
|
||||||
name_to_ip: HashMap<String, IpAddr>,
|
|
||||||
network_addr: IpAddr,
|
|
||||||
broadcast_addr: IpAddr,
|
|
||||||
next_addr: IpAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VirtualDns {
|
|
||||||
fn default() -> Self {
|
|
||||||
let start_addr = Ipv4Addr::from_str("198.18.0.0").unwrap();
|
|
||||||
let cidr = Ipv4Cidr::new(start_addr.into(), 15);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
next_addr: start_addr.into(),
|
|
||||||
name_to_ip: Default::default(),
|
|
||||||
network_addr: IpAddr::try_from(cidr.network().address().into_address()).unwrap(),
|
|
||||||
broadcast_addr: IpAddr::try_from(cidr.broadcast().unwrap().into_address()).unwrap(),
|
|
||||||
lru_cache: LruCache::new_unbounded(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VirtualDns {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receive_query(&mut self, data: &[u8]) -> Option<Vec<u8>> {
|
|
||||||
if data.len() < 17 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// bit 1: Message is a query (0)
|
|
||||||
// bits 2 - 5: Standard query opcode (0)
|
|
||||||
// bit 6: Unused
|
|
||||||
// bit 7: Message is not truncated (0)
|
|
||||||
// bit 8: Recursion desired (1)
|
|
||||||
let is_supported_query = (data[2] & 0b11111011) == 0b00000001;
|
|
||||||
let num_queries = (data[4] as u16) << 8 | data[5] as u16;
|
|
||||||
if !is_supported_query || num_queries != 1 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = VirtualDns::parse_qname(data, 12);
|
|
||||||
let (qname, offset) = result?;
|
|
||||||
if offset + 3 >= data.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let qtype = (data[offset] as u16) << 8 | data[offset + 1] as u16;
|
|
||||||
let qclass = (data[offset + 2] as u16) << 8 | data[offset + 3] as u16;
|
|
||||||
|
|
||||||
if qtype != DnsRecordType::A as u16 && qtype != DnsRecordType::AAAA as u16
|
|
||||||
|| qclass != DnsClass::IN as u16
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if qtype == DnsRecordType::A as u16 {
|
|
||||||
log::info!("DNS query: {}", qname);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response = Vec::<u8>::new();
|
|
||||||
response.extend(&data[0..offset + 4]);
|
|
||||||
response[2] |= 0x80; // Message is a response
|
|
||||||
response[3] |= 0x80; // Recursion available
|
|
||||||
|
|
||||||
// Record count of the answer section:
|
|
||||||
// We only send an answer record for A queries, assuming that IPv4 is supported everywhere.
|
|
||||||
// This way, we do not have to handle two IP spaces for the virtual DNS feature.
|
|
||||||
response[6] = 0;
|
|
||||||
response[7] = if qtype == DnsRecordType::A as u16 {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Zero count of other sections:
|
|
||||||
// authority section
|
|
||||||
response[8] = 0;
|
|
||||||
response[9] = 0;
|
|
||||||
|
|
||||||
// additional section
|
|
||||||
response[10] = 0;
|
|
||||||
response[11] = 0;
|
|
||||||
if qtype == DnsRecordType::A as u16 {
|
|
||||||
if let Some(ip) = self.allocate_ip(qname) {
|
|
||||||
response.extend(&[
|
|
||||||
0xc0, 0x0c, // Question name pointer
|
|
||||||
0, 1, // Record type: A
|
|
||||||
0, 1, // Class: IN
|
|
||||||
0, 0, 0, DNS_TTL, // TTL
|
|
||||||
0, 4, // Data length: 4 bytes
|
|
||||||
]);
|
|
||||||
match ip as IpAddr {
|
|
||||||
IpAddr::V4(ip) => response.extend(ip.octets().as_ref()),
|
|
||||||
IpAddr::V6(ip) => response.extend(ip.octets().as_ref()),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
log::error!("Virtual IP space for DNS exhausted");
|
|
||||||
response[7] = 0; // No answers
|
|
||||||
|
|
||||||
// Set rcode to SERVFAIL
|
|
||||||
response[3] &= 0xf0;
|
|
||||||
response[3] |= 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response[7] = 0; // No answers
|
|
||||||
}
|
|
||||||
Some(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment_ip(addr: IpAddr) -> Option<IpAddr> {
|
|
||||||
let mut ip_bytes = match addr as IpAddr {
|
|
||||||
IpAddr::V4(ip) => Vec::<u8>::from(ip.octets()),
|
|
||||||
IpAddr::V6(ip) => Vec::<u8>::from(ip.octets()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Traverse bytes from right to left and stop when we can add one.
|
|
||||||
for j in 0..ip_bytes.len() {
|
|
||||||
let i = ip_bytes.len() - 1 - j;
|
|
||||||
if ip_bytes[i] != 255 {
|
|
||||||
// We can add 1 without carry and are done.
|
|
||||||
ip_bytes[i] += 1;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// Zero this byte and carry over to the next one.
|
|
||||||
ip_bytes[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let addr = if addr.is_ipv4() {
|
|
||||||
let bytes: [u8; 4] = ip_bytes.as_slice().try_into().ok()?;
|
|
||||||
IpAddr::V4(Ipv4Addr::from(bytes))
|
|
||||||
} else {
|
|
||||||
let bytes: [u8; 16] = ip_bytes.as_slice().try_into().ok()?;
|
|
||||||
IpAddr::V6(Ipv6Addr::from(bytes))
|
|
||||||
};
|
|
||||||
Some(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is to be called whenever we receive or send a packet on the socket
|
|
||||||
// which connects the tun interface to the client, so existing IP address to name
|
|
||||||
// mappings to not expire as long as the connection is active.
|
|
||||||
pub fn touch_ip(&mut self, addr: &IpAddr) -> bool {
|
|
||||||
match self.lru_cache.get_mut(addr) {
|
|
||||||
None => false,
|
|
||||||
Some(entry) => {
|
|
||||||
entry.expiry = Instant::now() + Duration::from_secs(MAPPING_TIMEOUT);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resolve_ip(&mut self, addr: &IpAddr) -> Option<&String> {
|
|
||||||
match self.lru_cache.get(addr) {
|
|
||||||
None => None,
|
|
||||||
Some(entry) => Some(&entry.name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allocate_ip(&mut self, name: String) -> Option<IpAddr> {
|
|
||||||
let now = Instant::now();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let p = self.lru_cache.iter().next();
|
|
||||||
if p.is_none() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let (ip, entry) = p.unwrap();
|
|
||||||
if now > entry.expiry {
|
|
||||||
let name = entry.name.clone();
|
|
||||||
self.lru_cache.remove(&ip.clone());
|
|
||||||
self.name_to_ip.remove(&name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ip) = self.name_to_ip.get(&name) {
|
|
||||||
let result = Some(*ip);
|
|
||||||
self.touch_ip(&ip.clone());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
let started_at = self.next_addr;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if let RawEntryMut::Vacant(vacant) =
|
|
||||||
self.lru_cache.raw_entry_mut().from_key(&self.next_addr)
|
|
||||||
{
|
|
||||||
let expiry = Instant::now() + Duration::from_secs(MAPPING_TIMEOUT);
|
|
||||||
vacant.insert(
|
|
||||||
self.next_addr,
|
|
||||||
NameCacheEntry {
|
|
||||||
name: name.clone(),
|
|
||||||
expiry,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// e.insert(name.clone());
|
|
||||||
self.name_to_ip.insert(name, self.next_addr);
|
|
||||||
return Some(self.next_addr);
|
|
||||||
}
|
|
||||||
self.next_addr = Self::increment_ip(self.next_addr)?;
|
|
||||||
if self.next_addr == self.broadcast_addr {
|
|
||||||
// Wrap around.
|
|
||||||
self.next_addr = self.network_addr;
|
|
||||||
}
|
|
||||||
if self.next_addr == started_at {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a non-root DNS qname at a specific offset and return the name along with its size.
|
|
||||||
/// DNS packet parsing should be continued after the name.
|
|
||||||
fn parse_qname(data: &[u8], mut offset: usize) -> Option<(String, usize)> {
|
|
||||||
// Since we only parse qnames and qnames can't point anywhere,
|
|
||||||
// we do not support pointers. (0xC0 is a bitmask for pointer detection.)
|
|
||||||
let label_type = data[offset] & 0xC0;
|
|
||||||
if label_type != 0x00 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut qname = String::from("");
|
|
||||||
loop {
|
|
||||||
if offset >= data.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let label_len = data[offset];
|
|
||||||
if label_len == 0 {
|
|
||||||
if qname.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
offset += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if !qname.is_empty() {
|
|
||||||
qname.push('.');
|
|
||||||
}
|
|
||||||
for _ in 0..label_len {
|
|
||||||
offset += 1;
|
|
||||||
if offset >= data.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
qname.push(data[offset] as char);
|
|
||||||
}
|
|
||||||
offset += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((qname, offset))
|
|
||||||
}
|
|
||||||
}
|
|
150
src/virtual_dns.rs
Normal file
150
src/virtual_dns.rs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
use crate::error::Result;
|
||||||
|
use hashlink::{LruCache, linked_hash_map::RawEntryMut};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
convert::TryInto,
|
||||||
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tproxy_config::IpCidr;
|
||||||
|
|
||||||
|
const MAPPING_TIMEOUT: u64 = 60; // Mapping timeout in seconds
|
||||||
|
|
||||||
|
struct NameCacheEntry {
|
||||||
|
name: String,
|
||||||
|
expiry: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A virtual DNS server which allocates IP addresses to clients.
|
||||||
|
/// The IP addresses are in the range of private IP addresses.
|
||||||
|
/// The DNS server is implemented as a LRU cache.
|
||||||
|
pub struct VirtualDns {
|
||||||
|
trailing_dot: bool,
|
||||||
|
lru_cache: LruCache<IpAddr, NameCacheEntry>,
|
||||||
|
name_to_ip: HashMap<String, IpAddr>,
|
||||||
|
network_addr: IpAddr,
|
||||||
|
broadcast_addr: IpAddr,
|
||||||
|
next_addr: IpAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VirtualDns {
|
||||||
|
pub fn new(ip_pool: IpCidr) -> Self {
|
||||||
|
Self {
|
||||||
|
trailing_dot: false,
|
||||||
|
next_addr: ip_pool.first_address(),
|
||||||
|
name_to_ip: HashMap::default(),
|
||||||
|
network_addr: ip_pool.first_address(),
|
||||||
|
broadcast_addr: ip_pool.last_address(),
|
||||||
|
lru_cache: LruCache::new_unbounded(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the DNS response to send back to the client.
|
||||||
|
pub fn generate_query(&mut self, data: &[u8]) -> Result<(Vec<u8>, String, IpAddr)> {
|
||||||
|
use crate::dns;
|
||||||
|
let message = dns::parse_data_to_dns_message(data, false)?;
|
||||||
|
let qname = dns::extract_domain_from_dns_message(&message)?;
|
||||||
|
let ip = self.find_or_allocate_ip(qname.clone())?;
|
||||||
|
let message = dns::build_dns_response(message, &qname, ip, 5)?;
|
||||||
|
Ok((message.to_vec()?, qname, ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_ip(addr: IpAddr) -> Result<IpAddr> {
|
||||||
|
let mut ip_bytes = match addr as IpAddr {
|
||||||
|
IpAddr::V4(ip) => Vec::<u8>::from(ip.octets()),
|
||||||
|
IpAddr::V6(ip) => Vec::<u8>::from(ip.octets()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Traverse bytes from right to left and stop when we can add one.
|
||||||
|
for j in 0..ip_bytes.len() {
|
||||||
|
let i = ip_bytes.len() - 1 - j;
|
||||||
|
if ip_bytes[i] != 255 {
|
||||||
|
// We can add 1 without carry and are done.
|
||||||
|
ip_bytes[i] += 1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// Zero this byte and carry over to the next one.
|
||||||
|
ip_bytes[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let addr = if addr.is_ipv4() {
|
||||||
|
let bytes: [u8; 4] = ip_bytes.as_slice().try_into()?;
|
||||||
|
IpAddr::V4(Ipv4Addr::from(bytes))
|
||||||
|
} else {
|
||||||
|
let bytes: [u8; 16] = ip_bytes.as_slice().try_into()?;
|
||||||
|
IpAddr::V6(Ipv6Addr::from(bytes))
|
||||||
|
};
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is to be called whenever we receive or send a packet on the socket
|
||||||
|
// which connects the tun interface to the client, so existing IP address to name
|
||||||
|
// mappings to not expire as long as the connection is active.
|
||||||
|
pub fn touch_ip(&mut self, addr: &IpAddr) {
|
||||||
|
_ = self.lru_cache.get_mut(addr).map(|entry| {
|
||||||
|
entry.expiry = Instant::now() + Duration::from_secs(MAPPING_TIMEOUT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_ip(&mut self, addr: &IpAddr) -> Option<&String> {
|
||||||
|
self.lru_cache.get(addr).map(|entry| &entry.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_or_allocate_ip(&mut self, name: String) -> Result<IpAddr> {
|
||||||
|
// This function is a search and creation function.
|
||||||
|
// Thus, it is sufficient to canonicalize the name here.
|
||||||
|
let insert_name = if name.ends_with('.') && !self.trailing_dot {
|
||||||
|
String::from(name.trim_end_matches('.'))
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
// Iterate through all entries of the LRU cache and remove those that have expired.
|
||||||
|
loop {
|
||||||
|
let (ip, entry) = match self.lru_cache.iter().next() {
|
||||||
|
None => break,
|
||||||
|
Some((ip, entry)) => (ip, entry),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The entry has expired.
|
||||||
|
if now > entry.expiry {
|
||||||
|
let name = entry.name.clone();
|
||||||
|
self.lru_cache.remove(&ip.clone());
|
||||||
|
self.name_to_ip.remove(&name);
|
||||||
|
continue; // There might be another expired entry after this one.
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // The entry has not expired and all following entries are newer.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the IP if it is stored inside our LRU cache.
|
||||||
|
if let Some(ip) = self.name_to_ip.get(&insert_name) {
|
||||||
|
let ip = *ip;
|
||||||
|
self.touch_ip(&ip);
|
||||||
|
return Ok(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, store name and IP pair inside the LRU cache.
|
||||||
|
let started_at = self.next_addr;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let RawEntryMut::Vacant(vacant) = self.lru_cache.raw_entry_mut().from_key(&self.next_addr) {
|
||||||
|
let expiry = Instant::now() + Duration::from_secs(MAPPING_TIMEOUT);
|
||||||
|
let name0 = insert_name.clone();
|
||||||
|
vacant.insert(self.next_addr, NameCacheEntry { name: insert_name, expiry });
|
||||||
|
self.name_to_ip.insert(name0, self.next_addr);
|
||||||
|
return Ok(self.next_addr);
|
||||||
|
}
|
||||||
|
self.next_addr = Self::increment_ip(self.next_addr)?;
|
||||||
|
if self.next_addr == self.broadcast_addr {
|
||||||
|
// Wrap around.
|
||||||
|
self.next_addr = self.network_addr;
|
||||||
|
}
|
||||||
|
if self.next_addr == started_at {
|
||||||
|
return Err("Virtual IP space for DNS exhausted".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/win_svc.rs
Normal file
101
src/win_svc.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
#![cfg(windows)]
|
||||||
|
|
||||||
|
const SERVICE_NAME: &str = "tun2proxy";
|
||||||
|
|
||||||
|
windows_service::define_windows_service!(ffi_service_main, my_service_main);
|
||||||
|
|
||||||
|
pub fn start_service() -> Result<(), windows_service::Error> {
|
||||||
|
// Register generated `ffi_service_main` with the system and start the service,
|
||||||
|
// blocking this thread until the service is stopped.
|
||||||
|
windows_service::service_dispatcher::start(SERVICE_NAME, ffi_service_main)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn my_service_main(arguments: Vec<std::ffi::OsString>) {
|
||||||
|
// The entry point where execution will start on a background thread after a call to
|
||||||
|
// `service_dispatcher::start` from `main`.
|
||||||
|
|
||||||
|
if let Err(_e) = run_service(arguments) {
|
||||||
|
log::error!("Error: {_e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_service(_arguments: Vec<std::ffi::OsString>) -> Result<(), crate::BoxError> {
|
||||||
|
use windows_service::service::ServiceControl;
|
||||||
|
use windows_service::service_control_handler::{self, ServiceControlHandlerResult};
|
||||||
|
|
||||||
|
let shutdown_token = crate::CancellationToken::new();
|
||||||
|
let shutdown_token_clone = shutdown_token.clone();
|
||||||
|
|
||||||
|
let event_handler = move |control_event| -> ServiceControlHandlerResult {
|
||||||
|
match control_event {
|
||||||
|
ServiceControl::Stop => {
|
||||||
|
// Handle stop event and return control back to the system.
|
||||||
|
shutdown_token_clone.cancel();
|
||||||
|
ServiceControlHandlerResult::NoError
|
||||||
|
}
|
||||||
|
// All services must accept Interrogate even if it's a no-op.
|
||||||
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||||
|
_ => ServiceControlHandlerResult::NotImplemented,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register system service event handler
|
||||||
|
let status_handle = service_control_handler::register(SERVICE_NAME, event_handler)?;
|
||||||
|
|
||||||
|
let mut next_status = windows_service::service::ServiceStatus {
|
||||||
|
// Should match the one from system service registry
|
||||||
|
service_type: windows_service::service::ServiceType::OWN_PROCESS,
|
||||||
|
// The new state
|
||||||
|
current_state: windows_service::service::ServiceState::Running,
|
||||||
|
// Accept stop events when running
|
||||||
|
controls_accepted: windows_service::service::ServiceControlAccept::STOP,
|
||||||
|
// Used to report an error when starting or stopping only, otherwise must be zero
|
||||||
|
exit_code: windows_service::service::ServiceExitCode::Win32(0),
|
||||||
|
// Only used for pending states, otherwise must be zero
|
||||||
|
checkpoint: 0,
|
||||||
|
// Only used for pending states, otherwise must be zero
|
||||||
|
wait_hint: std::time::Duration::default(),
|
||||||
|
// Unused for setting status
|
||||||
|
process_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tell the system that the service is running now
|
||||||
|
status_handle.set_service_status(next_status.clone())?;
|
||||||
|
|
||||||
|
// main logic here
|
||||||
|
{
|
||||||
|
let args = crate::Args::parse_args();
|
||||||
|
|
||||||
|
let default = format!("{:?},trust_dns_proto=warn", args.verbosity);
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init();
|
||||||
|
|
||||||
|
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 = unsafe { &*status };
|
||||||
|
log::debug!("Traffic: ▲ {} : ▼ {}", status.tx, status.rx);
|
||||||
|
}
|
||||||
|
unsafe { crate::tun2proxy_set_traffic_status_callback(1, Some(traffic_cb), std::ptr::null_mut()) };
|
||||||
|
|
||||||
|
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>(())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the system that the service is stopped now
|
||||||
|
next_status.current_state = windows_service::service::ServiceState::Stopped;
|
||||||
|
status_handle.set_service_status(next_status)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
24
tests/iperf/dante.conf
Normal file
24
tests/iperf/dante.conf
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# logoutput: /var/log/socks.log
|
||||||
|
internal: 10.0.0.3 port = 10800
|
||||||
|
external: 10.0.0.3
|
||||||
|
clientmethod: none
|
||||||
|
socksmethod: none
|
||||||
|
user.privileged: root
|
||||||
|
user.notprivileged: nobody
|
||||||
|
|
||||||
|
client pass {
|
||||||
|
from: 0/0 to: 0/0
|
||||||
|
log: error connect disconnect
|
||||||
|
}
|
||||||
|
|
||||||
|
socks pass {
|
||||||
|
from: 0/0 to: 0/0
|
||||||
|
command: bind connect udpassociate
|
||||||
|
log: error connect disconnect
|
||||||
|
socksmethod: none
|
||||||
|
}
|
||||||
|
|
||||||
|
socks pass {
|
||||||
|
from: 0.0.0.0/0 to: 0.0.0.0/0
|
||||||
|
command: bindreply udpreply
|
||||||
|
}
|
50
tests/iperf/test.sh
Executable file
50
tests/iperf/test.sh
Executable file
|
@ -0,0 +1,50 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# sudo apt install iperf3 dante-server
|
||||||
|
# sudo systemctl stop danted
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
echo $SCRIPT_DIR
|
||||||
|
|
||||||
|
netns="test"
|
||||||
|
dante="danted"
|
||||||
|
tun2proxy="${SCRIPT_DIR}/../../target/release/tun2proxy-bin"
|
||||||
|
|
||||||
|
ip netns add "$netns"
|
||||||
|
|
||||||
|
ip link add veth0 type veth peer name veth0 netns "$netns"
|
||||||
|
|
||||||
|
# Configure veth0 in default ns
|
||||||
|
ip addr add 10.0.0.2/24 dev veth0
|
||||||
|
ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure veth0 in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.3/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip addr add 10.0.0.4/24 dev veth0
|
||||||
|
ip netns exec "$netns" ip link set dev veth0 up
|
||||||
|
|
||||||
|
# Configure lo interface in child ns
|
||||||
|
ip netns exec "$netns" ip addr add 127.0.0.1/8 dev lo
|
||||||
|
ip netns exec "$netns" ip link set dev lo up
|
||||||
|
|
||||||
|
echo "Starting Dante in background ..."
|
||||||
|
ip netns exec "$netns" "$dante" -f ${SCRIPT_DIR}/dante.conf &
|
||||||
|
|
||||||
|
# Start iperf3 server in netns
|
||||||
|
ip netns exec "$netns" iperf3 -s -B 10.0.0.4 &
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Prepare tun2proxy
|
||||||
|
ip tuntap add name tun0 mode tun
|
||||||
|
ip link set tun0 up
|
||||||
|
ip route add 10.0.0.4 dev tun0
|
||||||
|
"$tun2proxy" --proxy socks5://10.0.0.3:10800 &
|
||||||
|
|
||||||
|
# Run iperf client through tun2proxy
|
||||||
|
iperf3 -c 10.0.0.4
|
||||||
|
|
||||||
|
iperf3 -c 10.0.0.4 -R -P 10
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
# sudo sh -c "pkill tun2proxy-bin; pkill iperf3; pkill danted; ip link del tun0; ip netns del test"
|
15
tests/manual-tests/half-open-close-client/client.py
Normal file
15
tests/manual-tests/half-open-close-client/client.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
ip, port = sys.argv[1].split(':', 1)
|
||||||
|
port = int(port)
|
||||||
|
s.connect((ip, port))
|
||||||
|
s.sendall('I am closing the write end, but I can still receive data'.encode())
|
||||||
|
s.shutdown(socket.SHUT_WR)
|
||||||
|
while True:
|
||||||
|
data = s.recv(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
print(data.decode())
|
16
tests/manual-tests/half-open-close-client/server.py
Normal file
16
tests/manual-tests/half-open-close-client/server.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(('0.0.0.0', 1337))
|
||||||
|
s.listen()
|
||||||
|
conn, addr = s.accept()
|
||||||
|
with conn:
|
||||||
|
while True:
|
||||||
|
data = conn.recv(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
print(data.decode())
|
||||||
|
time.sleep(3)
|
||||||
|
conn.sendall('This will still be received by the client that has closed its write end'.encode())
|
15
tests/manual-tests/half-open-close-server/client.py
Normal file
15
tests/manual-tests/half-open-close-server/client.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
ip, port = sys.argv[1].split(':', 1)
|
||||||
|
port = int(port)
|
||||||
|
s.connect((ip, port))
|
||||||
|
while True:
|
||||||
|
data = s.recv(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
print(data.decode())
|
||||||
|
time.sleep(3)
|
||||||
|
s.sendall('Message after server write end close'.encode())
|
15
tests/manual-tests/half-open-close-server/server.py
Normal file
15
tests/manual-tests/half-open-close-server/server.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import socket
|
||||||
|
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(('0.0.0.0', 1337))
|
||||||
|
s.listen()
|
||||||
|
conn, addr = s.accept()
|
||||||
|
with conn:
|
||||||
|
conn.sendall('I am closing the write end, but I can still receive data'.encode())
|
||||||
|
conn.shutdown(socket.SHUT_WR)
|
||||||
|
while True:
|
||||||
|
data = conn.recv(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
print(data.decode())
|
13
tests/manual-tests/test-ds-dns-lookup.py
Normal file
13
tests/manual-tests/test-ds-dns-lookup.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import dns.message
|
||||||
|
import socket
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.bind(('0.0.0.0', 0))
|
||||||
|
|
||||||
|
s.sendto(dns.message.make_query('example.org', 'A').to_wire(), ('8.8.8.8', 53))
|
||||||
|
s.sendto(dns.message.make_query('example.org', 'AAAA').to_wire(), ('8.8.8.8', 53))
|
||||||
|
|
||||||
|
data, _ = s.recvfrom(0xffff)
|
||||||
|
print(dns.message.from_wire(data))
|
||||||
|
data, _ = s.recvfrom(0xffff)
|
||||||
|
print(dns.message.from_wire(data))
|
164
tests/proxy.rs
164
tests/proxy.rs
|
@ -1,164 +0,0 @@
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
extern crate reqwest;
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use fork::Fork;
|
|
||||||
use nix::sys::signal;
|
|
||||||
use nix::unistd::Pid;
|
|
||||||
use serial_test::serial;
|
|
||||||
|
|
||||||
use tun2proxy::setup::{get_default_cidrs, Setup};
|
|
||||||
use tun2proxy::{main_entry, Options, Proxy, ProxyType};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct Test {
|
|
||||||
proxy: Proxy,
|
|
||||||
}
|
|
||||||
|
|
||||||
static TUN_TEST_DEVICE: &str = "tun0";
|
|
||||||
|
|
||||||
fn proxy_from_env(env_var: &str) -> Result<Proxy, String> {
|
|
||||||
let url =
|
|
||||||
env::var(env_var).map_err(|_| format!("{env_var} environment variable not found"))?;
|
|
||||||
Proxy::from_url(url.as_str()).map_err(|_| format!("{env_var} URL cannot be parsed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_from_env(env_var: &str) -> Result<Test, String> {
|
|
||||||
let proxy = proxy_from_env(env_var)?;
|
|
||||||
Ok(Test { proxy })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tests() -> [Result<Test, String>; 3] {
|
|
||||||
[
|
|
||||||
test_from_env("SOCKS4_SERVER"),
|
|
||||||
test_from_env("SOCKS5_SERVER"),
|
|
||||||
test_from_env("HTTP_SERVER"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[ctor::ctor]
|
|
||||||
fn init() {
|
|
||||||
dotenvy::dotenv().ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_ip_host_http() {
|
|
||||||
reqwest::blocking::get("http://1.1.1.1").expect("failed to issue HTTP request");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_example_https() {
|
|
||||||
reqwest::blocking::get("https://example.org").expect("failed to issue HTTPs request");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_test<F, T>(filter: F, test_function: T)
|
|
||||||
where
|
|
||||||
F: Fn(&Test) -> bool,
|
|
||||||
T: Fn(),
|
|
||||||
{
|
|
||||||
for potential_test in tests() {
|
|
||||||
match potential_test {
|
|
||||||
Ok(test) => {
|
|
||||||
if !filter(&test) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut setup = Setup::new(
|
|
||||||
TUN_TEST_DEVICE,
|
|
||||||
&test.proxy.addr.ip(),
|
|
||||||
get_default_cidrs(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
setup.configure().unwrap();
|
|
||||||
|
|
||||||
match fork::fork() {
|
|
||||||
Ok(Fork::Parent(child)) => {
|
|
||||||
test_function();
|
|
||||||
signal::kill(Pid::from_raw(child), signal::SIGINT)
|
|
||||||
.expect("failed to kill child");
|
|
||||||
setup.restore().unwrap();
|
|
||||||
}
|
|
||||||
Ok(Fork::Child) => {
|
|
||||||
prctl::set_death_signal(signal::SIGINT as isize).unwrap();
|
|
||||||
let _ = main_entry(
|
|
||||||
TUN_TEST_DEVICE,
|
|
||||||
&test.proxy,
|
|
||||||
Options::new().with_virtual_dns(),
|
|
||||||
);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Err(_) => panic!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_var(var: &str) {
|
|
||||||
env::var(var).unwrap_or_else(|_| panic!("{} environment variable required", var));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_socks4() {
|
|
||||||
require_var("SOCKS4_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Socks4,
|
|
||||||
request_ip_host_http,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_socks5() {
|
|
||||||
require_var("SOCKS5_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Socks5,
|
|
||||||
request_ip_host_http,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_http() {
|
|
||||||
require_var("HTTP_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Http,
|
|
||||||
request_ip_host_http,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_socks4_dns() {
|
|
||||||
require_var("SOCKS4_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Socks4,
|
|
||||||
request_example_https,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_socks5_dns() {
|
|
||||||
require_var("SOCKS5_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Socks5,
|
|
||||||
request_example_https,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[serial]
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_http_dns() {
|
|
||||||
require_var("HTTP_SERVER");
|
|
||||||
run_test(
|
|
||||||
|test| test.proxy.proxy_type == ProxyType::Http,
|
|
||||||
request_example_https,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
3
tests/requirements.txt
Normal file
3
tests/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
requests
|
||||||
|
python-dotenv
|
||||||
|
psutil
|
78
tests/tests.py
Normal file
78
tests/tests.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import glob
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
import dotenv
|
||||||
|
import requests
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def get_ip(version=None):
|
||||||
|
"""provider = 'https://%swtfismyip.com/text'
|
||||||
|
prefix = {
|
||||||
|
None: '',
|
||||||
|
4: 'ipv4.',
|
||||||
|
6: 'ipv6.'
|
||||||
|
}[version]"""
|
||||||
|
provider = 'https://%sipify.org'
|
||||||
|
prefix = {
|
||||||
|
None: 'api64.',
|
||||||
|
4: 'api4.',
|
||||||
|
6: 'api6.'
|
||||||
|
}[version]
|
||||||
|
result = requests.Session().get(provider % prefix).text.strip()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool_path():
|
||||||
|
default = glob.glob(os.path.join(os.path.dirname(__file__), '..', 'target', '*', 'tun2proxy-bin'))
|
||||||
|
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
|
||||||
|
def _test(ip_version, dns, proxy_var):
|
||||||
|
ip_noproxy = get_ip(ip_version)
|
||||||
|
additional = ['-6'] if ip_version == 6 else []
|
||||||
|
p = subprocess.Popen(
|
||||||
|
['sudo', get_tool_path(), "--proxy", os.getenv(proxy_var), '--setup', '-v', 'trace', '--dns', dns, *additional])
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
ip_withproxy = get_ip(ip_version)
|
||||||
|
|
||||||
|
assert ip_noproxy != ip_withproxy
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
finally:
|
||||||
|
sudo_kill_process_and_children(p)
|
||||||
|
p.terminate()
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_tests(cls):
|
||||||
|
ip_options = [None, 4]
|
||||||
|
if bool(int(os.environ.get('IPV6', 1))):
|
||||||
|
ip_options.append(6)
|
||||||
|
for ip_version, dns, proxy_var in itertools.product(ip_options, ['virtual', 'over-tcp'],
|
||||||
|
['SOCKS5_PROXY', 'HTTP_PROXY']):
|
||||||
|
setattr(cls, 'test_ipv%s_dns%s_proxy%s' % (ip_version, dns, proxy_var),
|
||||||
|
lambda self: cls._test(ip_version, dns, proxy_var))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
Tun2ProxyTest.add_tests()
|
||||||
|
unittest.main()
|
Loading…
Add table
Add a link
Reference in a new issue