Compare commits

...

415 commits

Author SHA1 Message Date
Koi to Coco
a8ebe0b9be
fix: use vec! to allocate buffer #213 (#214)
Some checks failed
Push or PR / build_n_test (macos-latest) (push) Has been cancelled
Push or PR / build_n_test (ubuntu-latest) (push) Has been cancelled
Push or PR / build_n_test (windows-latest) (push) Has been cancelled
Push or PR / build_n_test_android (push) Has been cancelled
Push or PR / build_n_test_ios (push) Has been cancelled
Push or PR / Check semver (push) Has been cancelled
Integration Tests / Proxy Tests (push) Has been cancelled
2025-06-29 16:18:25 +08:00
ssrlive
31b0972801 make rustc happy 2025-06-29 16:11:14 +08:00
B. Blechschmidt
9a6c96cf8b chore: publish v0.7.11 2025-06-19 19:16:24 +02:00
B. Blechschmidt
b5fbaa2d19 doc: fix Docker URL in README 2025-06-19 16:08:01 +02:00
B. Blechschmidt
3baa41a1fb fix: support multi-arch musl builds in Dockerfile 2025-06-19 16:05:54 +02:00
ssrlive
0cf4427ef6 setup_logging function 2025-06-19 20:15:47 +08:00
B. Blechschmidt
bc00dcc5ae fix: docker publish workflow 2025-06-19 13:47:48 +02:00
B. Blechschmidt
d87562b8d3 style: add pre-commit-config 2025-06-19 13:43:14 +02:00
B. Blechschmidt
fa09daabac fix: variable ref in publish-docker action 2025-06-19 13:42:57 +02:00
B. Blechschmidt
b36473ced9 feat(Docker): multi-stage Dockerfile with OS-less container 2025-06-19 13:32:01 +02:00
B. Blechschmidt
584bdc17ed feat(Linux): phase out reliance on iproute2 2025-06-17 01:10:25 +02:00
ssrlive
1880396822 use ctrlc2 async feature 2025-06-14 13:05:54 +08:00
Paper-Dragon
8b4ecabd8f
build image based on alpine/musl (#212) 2025-06-11 14:12:02 +08:00
B. Blechschmidt
fbc47a3001 fix(ci): account for change in load_dotenv 2025-06-11 00:21:03 +02:00
ssrlive
88d31ce168 Bump version 0.7.10 2025-06-03 14:05:28 +08:00
dependabot[bot]
ddebf5ee50
Update tun requirement from 0.7 to 0.8 (#209) 2025-06-03 12:02:31 +08:00
ssrlive
8cdb4f535d
Significant change in --setup parameter (#207) 2025-06-02 10:31:44 +08:00
ssrlive
6a5692cea0 refine code 2025-05-21 15:45:48 +08:00
ssrlive
3dc8f222cb Bump version 0.7.9 2025-05-08 10:26:00 +08:00
ssrlive
7c32b62727 Exclude dependabot[bot] in Integration Tests 2025-05-02 16:49:34 +08:00
dependabot[bot]
cf4a565f93
Update socks5-impl requirement from 0.6 to 0.7 (#201) 2025-05-01 08:30:43 +08:00
ssrlive
54f7dbc81b update nix deps 2025-04-30 10:59:48 +08:00
ssrlive
b71f479bf3 close-stale-issues.yml 2025-04-23 13:58:13 +08:00
ssrlive
2ead13a3f4 version_info & about_info 2025-04-22 14:58:52 +08:00
ssrlive
88423039c6 make TASK_COUNT as local task_count variable 2025-04-20 19:56:36 +08:00
ssrlive
7121a80300 Bump version 0.7.8 2025-04-19 17:50:56 +08:00
ssrlive
9e75475a23 force exit process while fatal error 2025-04-18 16:09:35 +08:00
ssrlive
7657f1603f Bump version 0.7.7 2025-03-28 20:23:47 +08:00
ssrlive
a380817951 update hickory-proto (DNS parser) 2025-03-19 08:36:29 +08:00
ssrlive
a2399c8b28 log ipstack info adjusted 2025-03-12 11:18:47 +08:00
ssrlive
61bbafcf82 version_info method 2025-03-11 12:41:57 +08:00
ssrlive
ca7cd25c4e Bump version 0.7.6 2025-03-07 14:15:14 +08:00
ssrlive
68716bdc9f update deps 2025-03-07 14:07:55 +08:00
ssrlive
e556f7657b Bump version 0.7.5 2025-02-27 14:56:14 +08:00
ssrlive
fd7dca9988 unsafe_in_unsafe issues 2025-02-27 14:40:09 +08:00
ssrlive
9a018f2393 update ipstack 2025-02-19 21:45:23 +08:00
ssrlive
c5d907551b ubuntu-20.04 used in publish script 2025-02-12 20:54:53 +08:00
ssrlive
6b038c2a80 Bump version 0.7.4 2025-02-12 18:09:37 +08:00
ssrlive
5287bef3c0 PI issues for macOS 2025-01-10 18:48:32 +08:00
ssrlive
04db15f553 Bump version 0.7.3 2025-01-07 21:15:44 +08:00
Ahmed Elsayed
f8c902b61c
use shlex instead of split whitespaces. (#179) 2025-01-07 21:03:25 +08:00
ssrlive
8ba2c1a2b7 Bump version 0.7.2 2025-01-03 15:30:41 +08:00
ssrlive
e939f5f3dc remove mod mobile 2025-01-03 15:22:14 +08:00
ssrlive
ecd1ab80bf base64 removed 2025-01-03 12:32:28 +08:00
Mostafa Kazemi
51de01854b
Fix typo in comment (#178) 2025-01-03 11:00:19 +08:00
ssrlive
bac54ec56c Bump version 0.7.1 2025-01-03 02:26:51 +08:00
ssrlive
6034870264 rename desktop_run_async to general_run_async 2025-01-03 02:05:50 +08:00
ssrlive
e933e5d4c0 iOS & Android testing suits 2025-01-03 00:41:29 +08:00
ssrlive
7136e2a20c refactor desktop_run_async 2025-01-02 23:44:57 +08:00
ssrlive
2a8e31225c refine clap::Parser 2025-01-02 17:08:19 +08:00
ssrlive
ea5ee834db Bump version 0.6.7 2024-12-27 17:15:48 +08:00
ssrlive
4d4a0ce85c minor changes 2024-12-26 20:38:13 +08:00
ssrlive
258637a52e upgrade dependencies 2024-12-17 11:35:58 +08:00
ssrlive
a01de17b36 minor changes 2024-11-26 13:32:39 +08:00
Paper-Dragon
724557b30e
docker-compose.yaml support (#166) 2024-11-26 13:12:04 +08:00
ssrlive
7a7293effd Refine code 2024-11-26 12:58:26 +08:00
ssrlive
46bf4434ef Bump version 0.6.6 2024-11-26 12:28:03 +08:00
ssrlive
d37cb44b62 Fix #165 2024-11-26 12:17:16 +08:00
ssrlive
987635d3dc Contributors in README 2024-11-24 15:49:10 +08:00
ssrlive
ebd3128778 Bump version 0.6.5 2024-11-21 14:15:37 +08:00
ssrlive
ee4df8f97b cbindgen issues 2024-11-17 17:54:30 +08:00
ssrlive
7314906841 mask_socket_addr function 2024-11-11 15:11:06 +08:00
ssrlive
23d4e59367 minor changes 2024-11-11 11:51:28 +08:00
ssrlive
28d54be638 Bump version 0.6.4 2024-11-09 18:30:56 +08:00
dependabot[bot]
8c98d1dc74
Update thiserror requirement from 1 to 2 (#162) 2024-11-07 10:37:46 +08:00
ssrlive
1a508918a2
Auto merge script 2024-11-05 17:31:33 +08:00
ssrlive
c2382ee29b minor changes 2024-11-04 22:08:21 +08:00
ssrlive
21355e37da Bump version 0.6.3 2024-11-03 10:36:07 +08:00
ssrlive
e8143a691b remove useless is_in_heartbeat in udpgw 2024-11-02 17:16:54 +08:00
ssrlive
53f60ffda6 readme on udpgw 2024-11-02 13:55:47 +08:00
ssrlive
9088cf6fe5 minor changes 2024-11-02 07:25:46 +08:00
ssrlive
d7e3913450 Bump version 0.6.2 2024-11-01 15:08:49 +08:00
ssrlive
52d814ce79 refine udpgw 2024-11-01 15:02:18 +08:00
ssrlive
b4142453fd Bump version 0.6.1 2024-10-30 19:10:04 +08:00
ssrlive
0aad0d1709 refactor udpgw 2024-10-30 19:00:28 +08:00
ssrlive
3fb02f0fc7 switch to tun crate instead of tun2 2024-10-28 14:03:35 +08:00
ssrlive
b9cf06da33 refine code 2024-10-27 15:27:50 +08:00
ssrlive
2ade72e79d publish version 0.6.0 2024-10-26 11:04:26 +08:00
ssrlive
e3cc5ea1ce fix daemonize issues 2024-10-26 09:51:00 +08:00
sujiacong
b6bb9bedfc
support udp gateway mode (#155) 2024-10-26 02:15:52 +08:00
ssrlive
f823202b33 Bump version 0.5.4 2024-10-10 00:35:30 +08:00
ssrlive
9aa2afb0fd Fix daemonize issues 2024-10-09 23:54:25 +08:00
ssrlive
918e6137ab Bump version 0.5.3 2024-10-09 17:00:40 +08:00
ssrlive
d093973160 refine ctrl-c logic 2024-10-09 16:58:37 +08:00
ssrlive
4ef71a5b4c --max-sessions option 2024-10-09 16:57:14 +08:00
ssrlive
b03032b8cd Bump version 0.5.2 2024-10-07 13:34:48 +08:00
ssrlive
c991006f4c --exit-on-fatal-error option 2024-10-07 13:29:32 +08:00
ssrlive
fe32a65291 Bump version 0.5.1 2024-10-03 08:50:51 +08:00
ssrlive
93e15e0a8b build x86_64-win7-windows-msvc target 2024-10-03 08:50:23 +08:00
ssrlive
b74aeab182 target armv7-unknown-linux-musleabi 2024-09-30 17:12:46 +08:00
ssrlive
c9b24a865c minor changes 2024-09-29 18:17:45 +08:00
ssrlive
2396d769d2 Bump version 0.5.0 2024-09-26 12:29:56 +08:00
ssrlive
b24d48a042 testing python script issues 2024-09-26 11:51:52 +08:00
ssrlive
6c8ae7a33f rename target from 'tun2proxy' to 'tun2proxy-bin'
make rust compiler happy
2024-09-26 10:54:54 +08:00
ssrlive
77d651dc70 minor changes 2024-09-26 10:06:52 +08:00
ssrlive
febd654f35 CI testing scripts 2024-09-23 16:01:34 +08:00
ssrlive
143f203fde
Bump version 0.4.5 2024-09-16 17:18:14 +08:00
Paper-Dragon
a5bc8f49b4
multi-arch build docker images (#141) 2024-09-16 17:10:56 +08:00
ssrlive
1ccba18273 Bump version 0.4.4 2024-09-14 22:12:10 +08:00
ssrlive
607d709c03 Apply daemonize for unix 2024-09-14 22:02:05 +08:00
ssrlive
e817257866 refine code 2024-09-14 21:38:15 +08:00
ssrlive
c583e884b5 Bump version 0.4.3 2024-09-14 16:10:33 +08:00
ssrlive
1e6c6f4f66 Fix #144 2024-09-14 16:08:54 +08:00
ssrlive
c167f45a5e Bump version 0.4.2 2024-09-14 10:11:25 +08:00
ssrlive
02b15951b6 update Semver checking script 2024-09-14 10:05:47 +08:00
ssrlive
6dadc1504a Support windows service, fix #143 2024-09-14 09:55:27 +08:00
ssrlive
187e251142 Bump version 0.4.1 2024-09-02 23:22:11 +08:00
ssrlive
15646925a7 issues of parameter constraint for 'tun' 2024-08-29 10:01:58 +08:00
ssrlive
beb3d364a8 fix windows issues 2024-08-29 09:23:33 +08:00
B. Blechschmidt
8334acd085 Update version 2024-08-28 23:17:59 +02:00
B. Blechschmidt
1e7f649192 Remove unused import with updated dependencies 2024-08-28 23:12:21 +02:00
B. Blechschmidt
8c28f2e000 Implement --virtual-dns-pool 2024-08-28 23:06:37 +02:00
ssrlive
3f76ccec97 Apply mimalloc to iOS only 2024-08-03 18:21:02 +08:00
ssrlive
f787ff6d23 rust toolchain version issues 2024-08-02 10:35:05 +08:00
ssrlive
1dd6746bbc mimalloc usage 2024-08-02 10:18:11 +08:00
ssrlive
6567b6bc00 LazyLock usage 2024-07-27 00:08:07 +08:00
ssrlive
016aaa6128 Bump version 0.3.1 2024-07-24 21:11:23 +08:00
ssrlive
824b443d2b Bump version 0.3.0 2024-07-22 09:32:02 +08:00
ssrlive
06ed994655 Check semver script 2024-07-20 12:55:01 +08:00
ssrlive
e879599e6b close_fd_on_drop issues 2024-07-20 12:28:52 +08:00
ssrlive
0ca92dcdc2 minor changes 2024-07-18 19:35:14 +08:00
BlackbirdBop
635c7e557f
Make close_fd_on_drop configurable (#132) 2024-07-18 19:01:11 +08:00
ssrlive
15fe95a2c6 Bump version 0.2.24 2024-07-16 11:50:58 +08:00
ssrlive
d5a404fda7 update deps 2024-07-08 18:35:18 +08:00
ssrlive
3b2adf92cb update deps 2024-06-30 16:06:43 +08:00
ssrlive
1ba8f8b167 Update deps 2024-06-28 13:46:25 +08:00
James Brown
48f527ad81
Update lib.rs (#130)
Fix #129
2024-06-28 01:35:49 +08:00
B. Blechschmidt
060ca5740f Format build provenance attestation section 2024-06-23 21:30:43 +02:00
B. Blechschmidt
bb1a1fe286 Add build provenance attestation hint 2024-06-23 21:27:59 +02:00
B. Blechschmidt
d8d40b09de Bump version 0.2.23 2024-06-23 21:07:33 +02:00
B. Blechschmidt
ea0c10a5c1 Add more comments to virtual DNS implementation 2024-06-23 20:37:26 +02:00
B. Blechschmidt
01ba8f382f Virtual DNS: Move name canonicalization into find_or_allocate_ip 2024-06-23 20:31:08 +02:00
B. Blechschmidt
b525d3f99e Virtual DNS: Do not add trailing dot 2024-06-22 16:39:25 +02:00
ssrlive
b8c22db037 build-android.sh 2024-06-18 17:44:06 +08:00
dependabot[bot]
dbf960884d
Update android_logger requirement from 0.13 to 0.14 (#122) 2024-06-17 11:45:39 +08:00
B. Blechschmidt
b0432c7659 Re-add tested build provenance attestations 2024-06-15 16:30:46 +02:00
ssrlive
628e6cba84 Bump version 0.2.22 2024-06-14 15:11:28 +08:00
ssrlive
203cfba302 update deps 2024-06-14 14:59:08 +08:00
ssrlive
9d9c152b54
Bump version 0.2.21 2024-06-11 19:08:51 +08:00
ssrlive
3b5f526728 traffic status logic 2024-06-11 19:03:07 +08:00
ssrlive
1789259f6f Implementation of traffic status callback 2024-06-11 17:25:13 +08:00
Birk Blechschmidt
4243057fbf
Merge pull request #119 from koitococo/master
Write unshare pid into file for scripting purposes
2024-06-05 18:11:18 +02:00
Koi to Coco
07ffbe057c
Write unshare pid into file for scripting purposes 2024-06-05 04:22:53 +00:00
B. Blechschmidt
4554d3bc55 Build provenance attestation 2024-06-01 02:00:45 +02:00
B. Blechschmidt
a082a6f45b Enable build verification 2024-05-31 23:14:55 +02:00
ssrlive
4b0ca087eb Don't play with fire 2024-05-30 20:06:15 +08:00
ssrlive
1023f00d12 Bump version 0.2.20 2024-05-26 10:56:40 +08:00
ssrlive
30a54329e4 Fix #114 2024-05-26 10:52:29 +08:00
ssrlive
e604dec01c fix nix error cos upgrade 2024-05-26 09:34:49 +08:00
ssrlive
d062b1b66a Fix #115 2024-05-23 21:27:42 +08:00
ssrlive
e6360d83a7 refine code 2024-05-16 13:30:10 +08:00
ssrlive
588364d060
Add files via upload 2024-05-16 13:18:02 +08:00
ssrlive
3202e7bbd2
Bump version 0.2.19 2024-05-09 18:26:53 +08:00
ssrlive
3980b985f2 warnings removed 2024-05-05 17:02:58 +08:00
ssrlive
64dd43c6f3 cbindgen issues in publish script 2024-04-29 11:40:39 +08:00
ssrlive
0f241325ad Bump version 0.2.18 2024-04-29 11:13:45 +08:00
ssrlive
5e32994f91 unhandled transport 2024-04-23 19:54:19 +08:00
ssrlive
04a0555101 test scripts issues 2024-04-23 13:22:27 +08:00
ssrlive
a9ef8f658b Android build script 2024-04-21 04:35:37 +00:00
ssrlive
8438eddc95 The bypass value is IP/CIDR now 2024-04-19 22:17:36 +08:00
B. Blechschmidt
c36c4ecf1b Add CI workflow for functional tests 2024-04-18 23:24:24 +02:00
B. Blechschmidt
03f98a0741 Update tproxy-config dependency 2024-04-18 22:39:07 +02:00
B. Blechschmidt
8aa2a66942 Add functional tests 2024-04-18 22:20:05 +02:00
B. Blechschmidt
f418ca4fe7 Fix over-tcp DNS for HTTP proxies 2024-04-18 21:45:09 +02:00
ssrlive
d5d847fa92 Bump version 0.2.17 2024-04-14 19:18:18 +08:00
B. Blechschmidt
09994d43cc Fix routing issues described in #104 2024-04-13 17:16:30 +02:00
ssrlive
2df59ae596 UDP read time out case 2024-04-10 11:26:34 +08:00
ssrlive
7bee2e0968 TryFrom for ArgProxy 2024-04-09 14:24:22 +08:00
ssrlive
58364580f5 TryFrom for ProxyType 2024-04-09 12:57:18 +08:00
ssrlive
18f4689d21 refine code 2024-04-08 20:49:44 +08:00
ssrlive
ba1615fcd1 minor changes 2024-04-08 19:23:13 +08:00
ssrlive
92011edd43 use percent-encoding instead of url-escape 2024-04-08 18:37:56 +08:00
ssrlive
84c03426f2
Bump version 0.2.16 2024-04-08 15:39:20 +08:00
ssrlive
e582d6cbec Fix #107 2024-04-08 15:28:56 +08:00
ssrlive
c1d93942cc async-recursion removed 2024-04-08 14:24:41 +08:00
ssrlive
18044a8056 change Apple building description 2024-04-08 13:45:42 +08:00
B. Blechschmidt
ebbe939f85 Use destructor to restore network config 2024-04-07 23:08:32 +02:00
Birk Blechschmidt
0239a225a1
Merge support for unprivileged namespaces on Linux
Add support for unprivileged namespaces on Linux
(pull request #104 from one-d-wide/namespaces)
2024-04-07 21:52:21 +02:00
B. Blechschmidt
40368dd232 Increase security and portability through the use of /proc/self/exe 2024-04-07 21:47:15 +02:00
B. Blechschmidt
4f5a128972 Update README 2024-04-07 21:33:04 +02:00
B. Blechschmidt
e8469f0aee Restrict namespace arguments to Linux 2024-04-07 21:33:04 +02:00
ssrlive
af6a8a3cb0 minor changes 2024-04-07 21:33:04 +02:00
Remy D. Farley
f9f5401ba4 fix socks5 udp connectivity 2024-04-07 21:33:04 +02:00
ssrlive
56be614334 Args class 2024-04-07 21:33:04 +02:00
ssrlive
181497e709 remove useless get_server_addr 2024-04-07 21:33:04 +02:00
B. Blechschmidt
a08b3338c3 Apply clippy suggestion 2024-04-07 21:33:04 +02:00
Remy D. Farley
d351b5031c add support for unprivileged namespaces 2024-04-07 21:32:58 +02:00
ssrlive
050f8c0e65 minor changes 2024-04-04 20:55:50 +08:00
Remy D. Farley
5e99c9f874 add no-proxy mode 2024-04-03 20:58:15 +00:00
Remy D. Farley
361cf95f4e add udp timeout option 2024-04-03 20:58:15 +00:00
Remy D. Farley
74e5220d08 ci: don't abort checks immediately if error is encountered 2024-04-03 20:58:07 +00:00
Remy D. Farley
b7e59b130e ci: don't abort checks immediately if error is encountered 2024-04-03 22:51:58 +02:00
ssrlive
ce0c02b3bf Bump version 0.2.15 2024-03-30 12:30:01 +08:00
ssrlive
4adc38c726 Bump version 0.2.14 2024-03-28 17:03:36 +08:00
ssrlive
eab795e61c build-apple-debug.sh 2024-03-24 17:20:51 +08:00
ssrlive
715a85920c update deps 2024-03-19 18:31:04 +08:00
ssrlive
c430d76534 tcp timeout option 2024-03-18 13:12:30 +08:00
ssrlive
3fe47d92ec update deps 2024-03-15 16:52:55 +08:00
ssrlive
c9272609b8 building script 2024-03-14 20:28:33 +08:00
ssrlive
3a156f5837
Bump version 0.2.13 2024-03-13 14:17:27 +08:00
ssrlive
9841987031 fix #101 2024-03-13 13:07:32 +08:00
ssrlive
bd96807bf8 minor changes 2024-03-11 08:57:18 +08:00
ssrlive
7cb251c190 refine TUN_QUIT 2024-03-06 18:01:02 +08:00
ssrlive
989406d00c script issues 2024-03-06 17:08:28 +08:00
ssrlive
d3e77e6c17
Bump version 0.2.12 2024-03-06 12:22:41 +08:00
ssrlive
fb7b6862e5
tst (#99) 2024-03-05 12:33:47 +08:00
ssrlive
7e7aadb04b
tun shutdown issues. (#97) 2024-03-03 19:49:10 +08:00
Ebrahim Tahernejad
4ab6f1a9bc
XCFramework build for apple (#93) 2024-02-29 11:38:44 +08:00
B. Blechschmidt
01a0d9164d Fix banner bug with HTTP proxies 2024-02-26 01:24:22 +01:00
B. Blechschmidt
b3314f5abc Properly close streams 2024-02-26 00:50:23 +01:00
ssrlive
ee63dc1559 Bump version 0.2.11 2024-02-25 22:24:55 +08:00
ssrlive
3628533c8b update deps 2024-02-25 22:20:54 +08:00
B. Blechschmidt
a52dccd827 Update to v0.2.10 2024-02-25 13:49:15 +01:00
B. Blechschmidt
444e72689c Add .idea to gitignore 2024-02-25 13:47:38 +01:00
B. Blechschmidt
12efc5f392 Update tproxy-config to 2.0 2024-02-25 13:34:56 +01:00
B. Blechschmidt
1d49ec87ad Update Desktop API to use returned state 2024-02-25 13:13:23 +01:00
ssrlive
5c228ca07e tun_name issues 2024-02-25 16:59:18 +08:00
B. Blechschmidt
1b859a5374 Re-add Dockerfile and Docker build workflow 2024-02-25 01:09:22 +01:00
B. Blechschmidt
a5db99b03b Change order of operations to support auto setup 2024-02-25 00:45:42 +01:00
B. Blechschmidt
498a43b471 Do not swallow error when not tracing 2024-02-24 23:38:52 +01:00
B. Blechschmidt
d03e3c268d Improve error message 2024-02-24 22:25:22 +01:00
B. Blechschmidt
91fcd07733 Do not terminate when UDP is not supported by proxy
When an HTTP proxy is used, `new_proxy_handler` can result in an error
when a UDP packet is processed. Without this commit, this results in the
termination of tun2proxy.
2024-02-24 22:09:14 +01:00
ssrlive
bd27833c29 print error info 2024-02-24 20:40:39 +08:00
ssrlive
cfbc5fabb1 print error info 2024-02-24 19:24:51 +08:00
ssrlive
b11e49b455 wiki link 2024-02-24 14:22:37 +08:00
ssrlive
129450a9db setup parameter issues 2024-02-22 18:23:48 +08:00
ssrlive
977c3ce518
Bump version 0.2.9 2024-02-13 10:51:13 +08:00
ssrlive
a1083273ee
Ffi2 (#90) 2024-02-13 10:46:13 +08:00
ssrlive
a26621bbcd
Ffi (#89) 2024-02-12 21:36:18 +08:00
ssrlive
e9c378099e
Bump version 0.2.8 2024-02-11 18:41:11 +08:00
ssrlive
9f60eee2e1 ArgProxy issues 2024-02-11 12:36:36 +08:00
ssrlive
5514da71f9 refine TUN_QUIT 2024-02-11 01:53:20 +08:00
ssrlive
a317a3fc9e
Bump version 0.2.7 2024-02-11 01:07:03 +08:00
Andrej Mihajlov
2a9775ce2e
Introduce cancellation token and reduce amount of code (#88)
Test passed on Android. Thanks a lot.
2024-02-11 00:36:54 +08:00
ssrlive
2434c62524 Bump version 0.2.6 2024-02-10 20:21:28 +08:00
Andrej Mihajlov
9a4bd9f800
Re-export the missing types that are used within Args (#87) 2024-02-10 19:59:48 +08:00
ssrlive
ea5ffff82c Bump version 0.2.5 2024-02-10 14:45:44 +08:00
ssrlive
8a67915388 refactor FFI 2024-02-07 23:32:51 +08:00
ssrlive
8067394003
Delete publishdir/tun2proxy-x86_64-pc-windows-msvc.zip 2024-02-01 22:18:21 +08:00
ssrlive
4454ccc811
release script issues (#85) 2024-02-01 22:12:31 +08:00
ssrlive
3e373677bc release script issues 2024-02-01 19:48:19 +08:00
ssrlive
9c4fa4260a
beginning async version (#84) 2024-02-01 19:15:32 +08:00
ssrlive
337619169e upgrade dependencies 2024-01-12 23:54:17 +08:00
ssrlive
61ed6d62c4 clippy issues 2024-01-01 14:40:50 +08:00
ssrlive
0edd07479d upgrade dependencies 2023-12-25 23:10:13 +08:00
ssrlive
2b3463c55c android issues 2023-11-18 01:24:41 +08:00
ssrlive
97c4aa5137 rustfmt max_width = 140 2023-11-13 20:30:24 +08:00
ssrlive
ebec547ccb
Bump version 0.1.12 2023-11-13 12:57:29 +08:00
ssrlive
e5041e6d9e
Memory leak fixing (#77)
* incoming packet with FIN or RST

* read_server_n_write_proxy_handler

* testing script

* Interest::WRITABLE and continue_read

* read_data_from_tcp_stream

* logging hide

* test

* script iperf
2023-11-13 12:02:19 +08:00
B. Blechschmidt
4016e401b2 Only publish on semver tag 2023-11-09 20:34:56 +01:00
B. Blechschmidt
07ec58532d Add Docker publish action 2023-11-09 20:27:54 +01:00
B. Blechschmidt
67c2aa1a22 Remove unnecessary buffer 2023-11-08 21:14:22 +01:00
ssrlive
3879e04327 minor reading issues 2023-11-08 13:35:44 +08:00
ssrlive
0e654eb4bd
Ctrlc issues (#75) 2023-11-06 20:03:40 +08:00
ssrlive
9396db4a52 test code 2023-11-04 14:34:47 +08:00
ssrlive
fe85ecd15c iperf3 testing script 2023-11-04 12:28:38 +08:00
B. Blechschmidt
c4ed29b234 Remove unnecessary SOCKS buffer 2023-11-03 22:45:27 +01:00
B. Blechschmidt
286ce0ca6d Add very basic and dirty iperf test 2023-11-03 20:28:31 +01:00
B. Blechschmidt
980ae0172e Bump version 0.1.10 2023-10-30 22:57:25 +01:00
B. Blechschmidt
e3494d921c Add comment for DNS fix 2023-10-30 20:48:01 +01:00
B. Blechschmidt
0ab52c623b Fix virtual DNS 2023-10-30 20:44:28 +01:00
B. Blechschmidt
e08a0f683d Allow multiple bypass IP addresses/CIDRs in routing setup
See issue #73.
2023-10-29 23:48:38 +01:00
ssrlive
9b27dd2df2 refine code 2023-10-23 14:49:31 +08:00
ssrlive
c6f9610eb3
Bump version 0.1.9 2023-10-23 10:03:35 +08:00
ssrlive
989c42ee61
Windows support (#72) 2023-10-23 09:44:27 +08:00
B. Blechschmidt
a9a562029f Update LICENSE 2023-10-10 21:04:11 +02:00
PaperDragon-SH
5d722fc2a3 optimize docker 2023-10-10 19:27:37 +02:00
ssrlive
b50cac82c0
Memory exhaustion (#69) 2023-10-10 14:22:33 +08:00
B. Blechschmidt
299b51667d Restore /etc/resolv.conf if it was written directly 2023-10-08 13:09:37 +02:00
B. Blechschmidt
cea0e0fa27 Resort to writing to /etc/resolv.conf directly if mount permissions are missing 2023-10-08 12:27:32 +02:00
ssrlive
6169014564
Bump Version 0.1.8 2023-10-03 14:20:16 +08:00
ssrlive
c1ea5f1af2
rename bypass_ip to bypass (#66) 2023-10-03 11:39:06 +08:00
B. Blechschmidt
d75488f1d8 Improve Docker support description 2023-10-01 19:37:33 +02:00
B. Blechschmidt
fc4d29dd2e Make Docker entrypoint executable 2023-10-01 19:24:50 +02:00
PaperDragon-SH
c0c7fda891 docker support 2023-10-01 19:24:50 +02:00
ssrlive
05cb35fabb read_data_from_tcp_stream for callback 2023-09-28 11:48:54 +08:00
ssrlive
03aa70f3c2 minor changes 2023-09-27 23:24:22 +08:00
ssrlive
a54e6ae23e minor changes 2023-09-27 19:32:28 +08:00
ssrlive
d4568c4676 read_data_from_tcp_stream 2023-09-27 19:27:19 +08:00
ssrlive
b5d8f0ee48 EXIT_TRIGGER_TOKEN 2023-09-26 18:25:59 +08:00
ssrlive
cc46526af0 process_incoming_tcp_packets 2023-09-03 23:04:54 +08:00
ssrlive
c723adce4f reading code 2023-09-03 22:27:37 +08:00
ssrlive
c1b322a01e log some errors 2023-09-03 19:08:20 +08:00
ssrlive
f175813cc8 remove_connection refactor 2023-09-03 18:39:11 +08:00
ssrlive
ef6f67b975 remove_connection refactor 2023-09-03 18:20:02 +08:00
ssrlive
8b014322fc
Bump version 0.1.7 2023-09-03 11:31:41 +08:00
ssrlive
b8dab403e9 reading code 2023-09-03 10:40:40 +08:00
ssrlive
59fa5b155e get_credentials removed 2023-09-02 21:26:58 +08:00
ssrlive
2122cc0ba8 useless close_connection removed 2023-09-02 20:39:58 +08:00
ssrlive
538e40d05b reading code 2023-09-02 20:23:32 +08:00
ssrlive
5bd62d3101 log::info adjustment 2023-09-02 17:36:30 +08:00
ssrlive
e5a645638a rename TcpProxy to ProxyHandler 2023-09-01 11:28:06 +08:00
ssrlive
11995d525b dns_over_tcp_expiry 2023-09-01 11:17:12 +08:00
ssrlive
0e3b45be4a Publish script 2023-08-31 11:54:28 -04:00
ssrlive
a17d9587d6 dependencies issues 2023-08-31 16:35:37 +08:00
ssrlive
abcff395d8 Bump version 0.1.6 2023-08-31 16:11:35 +08:00
ssrlive
0044756f78 --dns-addr option 2023-08-31 15:59:07 +08:00
ssrlive
bbb8d3b244
port to windows or macos (#61) 2023-08-31 14:31:02 +08:00
ssrlive
4b42413ab0 refine code 2023-08-23 23:33:20 +08:00
ssrlive
c41f3c46a0 minor changes 2023-08-23 23:13:16 +08:00
ssrlive
eac0ee90eb clippy fix 2023-08-23 22:59:52 +08:00
ssrlive
dc7fc3990c cached origin dst address 2023-08-23 22:57:27 +08:00
ssrlive
02b85739cb restore preprocess_origin_connection_info
or --dns none can not work
2023-08-23 22:28:15 +08:00
ssrlive
d04344238a update dependencies 2023-08-23 14:06:02 +08:00
ssrlive
4014c9891c
Bump version 0.1.5 2023-08-23 10:58:27 +08:00
ssrlive
11d4e4a0dc minor changes 2023-08-23 10:45:37 +08:00
ssrlive
d7861128f4 IPv6 enabled 2023-08-23 10:35:21 +08:00
ssrlive
e518355756
Merge pull request #58 from blechschmidt/v8
DNS over TCP
2023-08-23 09:50:30 +08:00
ssrlive
72a00af0ed re-format code 2023-08-23 09:28:11 +08:00
B. Blechschmidt
cdbed3ed9b Do not allow errors in printing function to screw up DNS lookups 2023-08-22 22:39:00 +02:00
B. Blechschmidt
edb775941e Support multiple DNS queries with DNS over TCP 2023-08-22 22:18:15 +02:00
B. Blechschmidt
3b5f803da8 Get first version of DNS over TCP to work 2023-08-22 18:36:51 +02:00
ssrlive
d7d69ce927 receive_dns_over_tcp_packet_and_write_to_client 2023-08-22 23:59:12 +08:00
ssrlive
9880741dc1 consume_cached_dns_over_tcp_packets 2023-08-22 18:55:44 +08:00
ssrlive
2211ec6d7a renaming 2023-08-22 18:43:45 +08:00
ssrlive
49dca1b535 process_incoming_udp_packets_dns_over_tcp 2023-08-22 18:38:20 +08:00
ssrlive
1f5586b880 udp_over_tcp_data_cache 2023-08-22 18:21:38 +08:00
ssrlive
df7ecfd6a9 minor changes 2023-08-22 18:00:24 +08:00
ssrlive
641363e0bc Merge branch 'master' into v8 2023-08-22 17:21:16 +08:00
ssrlive
b2505dcfd7 udp_acco_expiry 2023-08-22 17:20:35 +08:00
ssrlive
8b566b66d7 merge master 2023-08-22 15:40:02 +08:00
ssrlive
fb86172ecc refine code 2023-08-22 12:59:31 +08:00
ssrlive
40f8870033 preprocess_origin_connection_info 2023-08-22 12:14:14 +08:00
ssrlive
0f3903f455 deal_with_incoming_udp_packets 2023-08-22 11:19:58 +08:00
ssrlive
d42d3a8287 extract dns logic to separate functions 2023-08-22 10:44:46 +08:00
ssrlive
0d1677fb73 Merge branch 'master' into v8 2023-08-21 20:00:54 +08:00
ssrlive
89aeffe195 dns over tcp 2023-08-21 19:58:13 +08:00
ssrlive
10ade80488
Bump 0.1.4 2023-08-21 17:14:33 +08:00
ssrlive
17566451cf remove trait UdpProxy 2023-08-21 17:01:07 +08:00
ssrlive
3c09c2699d refine code 2023-08-21 16:08:48 +08:00
B. Blechschmidt
0f67dd6981 Remove error robustness todo
Excessive expect and unwrap usage has been dealt with.
2023-08-20 20:01:02 +02:00
B. Blechschmidt
3543472c38 Update README with UDP info 2023-08-20 19:58:30 +02:00
B. Blechschmidt
b244286e4d Fix handling of multiple packets per event 2023-08-20 19:36:59 +02:00
B. Blechschmidt
aa059e0dd5 Format correctly 2023-08-20 18:54:02 +02:00
B. Blechschmidt
b0e275ec08 Use LinkedList as UDP packet cache 2023-08-20 18:51:15 +02:00
B. Blechschmidt
5301cf8d37 Add dual stack DNS lookup test 2023-08-20 18:20:47 +02:00
B. Blechschmidt
d5b76c18cc Fix UDP associate address 2023-08-20 17:27:22 +02:00
ssrlive
9ad22fc419
Merge pull request #57 from blechschmidt/v7
DNS proxy
2023-08-20 14:06:22 +08:00
ssrlive
6439cc7b43 dns::remove_ipv6_entries 2023-08-20 13:32:47 +08:00
ssrlive
60b9683fac dns query from remote server 2023-08-20 12:13:28 +08:00
ssrlive
04d4faff68
Merge pull request #56 from blechschmidt/v7
UDP proxy completed
2023-08-20 11:13:07 +08:00
ssrlive
01157915b3 UDP proxy completed 2023-08-20 10:27:24 +08:00
ssrlive
b019ace2e1 minor changes 2023-08-18 09:43:18 +08:00
ssrlive
334514cfc1 clearup_expired_udp_associate 2023-08-18 09:40:16 +08:00
ssrlive
1bea9ba9ea
Merge pull request #54 from blechschmidt/v7
Prepare for UDP
2023-08-16 12:39:21 +08:00
ssrlive
119c9fef99 deal with Unsupported protocol 2023-08-16 12:18:42 +08:00
ssrlive
30420059cc send_udp_packet 2023-08-16 12:13:38 +08:00
ssrlive
da87fa8d5a UDP associate 2023-08-11 19:54:18 +08:00
ssrlive
46ca342aba connection_managers renamed to connection_manager 2023-08-11 15:49:39 +08:00
ssrlive
d00a18c865 create_new_tcp_connection_state 2023-08-11 15:06:21 +08:00
ssrlive
57851f029e token_to_info removed 2023-08-11 09:42:33 +08:00
ssrlive
489d5fec00 ConnectionInfo 2023-08-10 16:03:17 +08:00
ssrlive
94835c41a4 UDP Associate 2023-08-10 12:57:35 +08:00
ssrlive
382c2ac6e3
Merge pull request #52 from ssrlive/v6 2023-08-10 12:31:31 +08:00
ssrlive
507def8f29 create_new_tcp_proxy_connection 2023-08-10 11:22:07 +08:00
ssrlive
855aaa04fa read code 2023-08-10 10:45:03 +08:00
ssrlive
64ab4b503c minor changes 2023-08-09 14:31:33 +08:00
ssrlive
ca5b550e44 reformatting code 2023-08-09 00:38:32 +08:00
ssrlive
3720c41a6b minor changes 2023-08-09 00:02:33 +08:00
ssrlive
ff9c258fbd re-formatting with max_width = 120 2023-08-08 23:45:16 +08:00
ssrlive
4d9b10fd1c verbosity parameter 2023-08-08 11:37:24 +08:00
ssrlive
b92f2efd81 remove .expect call 2023-08-08 09:20:43 +08:00
ssrlive
3b9207fb7a fixing get_transport_info 2023-08-08 09:16:57 +08:00
ssrlive
c8b13fc404 receive_tun 2023-08-07 12:29:36 +08:00
ssrlive
da665b3825
Merge pull request #51 from blechschmidt/v6
publish script
2023-08-06 22:46:34 +08:00
ssrlive
41feb84c29 publish script 2023-08-06 22:39:00 +08:00
ssrlive
5fbce82032
Merge pull request #50 from blechschmidt/v5
refine Virtual DNS code.
2023-08-06 21:40:34 +08:00
ssrlive
5bb4bbf022 remove raw dns parse code 2023-08-06 21:36:00 +08:00
ssrlive
30d7217374 refine VirtualDns 2023-08-06 18:31:49 +08:00
ssrlive
5ce2e85919 trust-dns-proto import 2023-08-06 12:22:58 +08:00
ssrlive
4ebd019cb5 minor changes 2023-08-05 22:32:57 +08:00
ssrlive
1031f586f7
Refine code logic 2023-08-05 15:52:32 +08:00
ssrlive
8d835dc96d
Unexpected comsuming (#48) 2023-07-26 07:01:48 +08:00
ssrlive
a00f4b1a8b socks5 stuff 2023-07-24 20:48:56 +02:00
ssrlive
6e81e78dfb socks5 respones 2023-07-24 20:48:56 +02:00
ssrlive
c61b6c74cd swith socks5-impl 2023-07-24 20:48:56 +02:00
ssrlive
ab9f8011f0 Update dependencies 2023-07-22 15:06:43 +02:00
B. Blechschmidt
3e26675919 Support seamless digest auth with Connection: close (see PR #44) 2023-07-02 23:02:08 +02:00
B. Blechschmidt
a292be4bd8 Update README 2023-07-01 22:17:25 +02:00
B. Blechschmidt
1dc827e84c Apply clippy suggestions 2023-06-30 21:21:40 +02:00
B. Blechschmidt
e6b1e93cd0 Merge branch 'master' into digestauth 2023-06-30 21:16:57 +02:00
B. Blechschmidt
45dae79263 Update smoltcp to version 0.10.0 2023-06-30 21:14:28 +02:00
Jorge Alejandro Jimenez Luna
86429ee8eb
Initial support digest auth scheme 2023-06-22 13:09:36 -04:00
B. Blechschmidt
6767076a6b Implement GFW bypass (see issue #35) 2023-05-31 18:25:24 +02:00
B. Blechschmidt
75bfdcc95a Support authentication without credentials if credentials are provided 2023-05-27 10:28:35 +02:00
ssrlive
e5d1cfbef1
Reqwest without openssl (#8) 2023-05-18 12:34:15 +02:00
B. Blechschmidt
fb28783598 Remove file that should not have been committed 2023-04-27 23:02:13 +02:00
B. Blechschmidt
ad72147ff4 Free memory of closed connections 2023-04-27 22:42:55 +02:00
B. Blechschmidt
5e218c2130 Use pipe for exiting 2023-04-27 22:42:55 +02:00
Antonio
034417f525 Fix #29 2023-04-23 22:45:38 +02:00
B. Blechschmidt
0c45714a45 Merge Android support branch 2023-04-17 22:37:55 +02:00
B. Blechschmidt
0027c5ac4e Use smoltcp origin after feature merge 2023-04-17 22:37:39 +02:00
B. Blechschmidt
b838583bf1 Add sudo to automatic setup example in README 2023-04-16 10:28:52 +02:00
Antonio Cheong
d94cc90663 Fix #20
There was regression of b8a08871d0
2023-04-16 10:11:58 +02:00
B. Blechschmidt
42878c29fd Rename SOCKS module 2023-04-15 12:10:48 +02:00
B. Blechschmidt
f67d8b23a8 Beautify SOCKS implementation 2023-04-15 12:08:20 +02:00
ssrlive
cba6ba7318 new_token function 2023-04-15 10:07:40 +02:00
ssrlive
7442abece5 integrate to android 2023-04-14 20:17:10 +02:00
ssrlive
62a04229db shutdown function 2023-04-14 20:17:10 +02:00
B. Blechschmidt
fb3ad33b53 Add file descriptor and MTU to CLI arguments 2023-04-13 21:54:02 +02:00
B. Blechschmidt
500f6ef21f Add file descriptor support 2023-04-10 23:24:53 +02:00
B. Blechschmidt
9437308283 Support building for Android 2023-04-10 21:37:20 +02:00
B. Blechschmidt
cb1babebd4 Only include setup feature on Linux 2023-04-10 20:59:54 +02:00
B. Blechschmidt
b669b9de22 Remove unnecessary integer suffix 2023-04-10 20:48:42 +02:00
ssrlive
c0cff1da58 refine SocksAddressType 2023-04-10 12:30:45 +02:00
B. Blechschmidt
fd48be5feb Parameterize IP addresses in manual tests 2023-04-10 11:05:06 +02:00
B. Blechschmidt
70cea8e11f Add manual tests for half open connections 2023-04-10 10:57:41 +02:00
B. Blechschmidt
2cf7c9cdea Add label for testing pull requests in CI 2023-04-10 10:34:30 +02:00
B. Blechschmidt
1a53e2bb52 Fix sparse index in cargo config 2023-04-10 10:31:41 +02:00
ssrlive
14279a482c
Turn off target type 2023-04-10 12:09:30 +08:00
ssrlive
3fc112fc2c update for smoltcp 2023-04-10 09:58:17 +08:00
B. Blechschmidt
44122f3c68 Improve performance by increasing smoltcp socket buffer size 2023-04-04 10:17:13 +02:00
B. Blechschmidt
7818829760 Apply clippy fixes 2023-04-04 00:19:41 +02:00
B. Blechschmidt
10a674d1c9 Fix CPU spikes due to always-writable event and improve half-open connection handling 2023-04-04 00:18:50 +02:00
B. Blechschmidt
0be39345a8 Improve handling of half-open connections 2023-04-03 20:31:31 +02:00
66 changed files with 5904 additions and 2495 deletions

9
.cargo/config.toml Normal file
View 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
View file

@ -0,0 +1 @@
.gitignore

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"

20
.github/workflows/auto-merge.yaml vendored Normal file
View 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 }}

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

View file

@ -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
View 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
View 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 }}

View file

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

View file

@ -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,33 +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:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-run
- name: Populate .env - name: Populate .env
env: env:
DOTENV: ${{ secrets.DOTENV }} DOTENV: ${{ secrets.DOTENV }}
run: echo "$DOTENV" > .env run: |
- name: Set up runner SSH key echo "$DOTENV" > tests/.env
run: >- ln -s tests/.env
set -o allexport &&
source .env && - name: Set up Python
set +o allexport && uses: actions/setup-python@v2
mkdir ~/.ssh && with:
echo "$TEST_SERVER_PRIVATE_SSH_KEY" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa 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 - name: Run tests
run: >- run: |
set -o allexport && source venv/bin/activate
source .env && python tests/tests.py
set +o allexport &&
ssh -N -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -D 1080 "$TEST_SERVER_SSH_DST" &
while ! nc -z 127.0.0.1 1080; do sleep 1; done &&
sudo -E /home/runner/.cargo/bin/cargo test

17
.gitignore vendored
View file

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

View file

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

View file

@ -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
View 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"]

View file

@ -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
View file

@ -1,5 +1,26 @@
[![tun2proxy](https://socialify.git.ci/tun2proxy/tun2proxy/image?description=1&language=1&name=1&stargazers=1&theme=Light)](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.
[![Crates.io](https://img.shields.io/crates/v/tun2proxy.svg)](https://crates.io/crates/tun2proxy)
[![tun2proxy](https://docs.rs/tun2proxy/badge.svg)](https://docs.rs/tun2proxy)
[![Documentation](https://img.shields.io/badge/docs-release-brightgreen.svg?style=flat)](https://docs.rs/tun2proxy)
[![Download](https://img.shields.io/crates/d/tun2proxy.svg)](https://crates.io/crates/tun2proxy)
[![License](https://img.shields.io/crates/l/tun2proxy.svg?style=flat)](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
View 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.

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

@ -0,0 +1 @@
max_width = 140

24
scripts/dante.conf Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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(&params, 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
View 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
View 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
View 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);
}
}
}
}

View file

@ -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
View 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
}

View file

@ -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)),
}
} }
} }

View file

@ -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())
} }

View file

@ -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}");
return ExitCode::FAILURE;
};
ExitCode::SUCCESS
}

107
src/no_proxy.rs Normal file
View 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
View 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
View 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)
}
}

View file

@ -1,356 +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> {
// 65534 is usually the nobody user. Even in cases it is not, it is safer to use this ID
// than running with UID and GID 0.
nix::unistd::setgid(nix::unistd::Gid::from_raw(65534))?;
nix::unistd::setuid(nix::unistd::Uid::from_raw(65534))?;
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
View 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
View 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,
}
}
}

View file

@ -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
View 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(())
}

View file

@ -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
View 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(())
}
}

View file

@ -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()
}
}
}

View file

@ -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
View 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
View 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
View 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
View 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"

View 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())

View 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())

View 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())

View 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())

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

View file

@ -1,167 +0,0 @@
#[cfg(test)]
mod tests {
extern crate reqwest;
use std::env;
use std::net::IpAddr;
use std::str::FromStr;
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 bypass_ip = match env::var("BYPASS_IP") {
Err(_) => test.proxy.addr.ip(),
Ok(ip_str) => IpAddr::from_str(ip_str.as_str()).unwrap(),
};
let mut setup =
Setup::new(TUN_TEST_DEVICE, &bypass_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
View file

@ -0,0 +1,3 @@
requests
python-dotenv
psutil

78
tests/tests.py Normal file
View 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()