Compare commits

...
60 Commits
Author SHA1 Message Date
Jason LyuandGitHub ec85410144 Chore(stale): add exempt-pr-labels (#438) 2024-12-21 04:12:05 +08:00
43f0ba892f Chore(deps): Bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#434)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.29.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.29.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 21:53:43 -05:00
寻觅andGitHub e5009398d5 Feature: add Loong64 arch (#427) 2024-11-12 07:29:06 -05:00
Jason LyuandGitHub 54ef0500d3 Chore: bump go mods (#426) 2024-11-11 04:15:24 +08:00
b65d23180c Feature: set open FD offset for iOS (#418)
Co-authored-by: Jason Lyu <xjasonlyu@gmail.com>
2024-10-13 03:51:27 +08:00
Jason LyuandGitHub a821cc483c Chore: bump go mods (#413) 2024-09-30 04:44:55 +08:00
Jason LyuandGitHub 56786517dc Refactor: bufferpool new function (#407) 2024-09-23 02:39:36 +08:00
Jason LyuandGitHub 428f82694a Chore: add README credits (#408) 2024-09-23 02:39:24 +08:00
Jason LyuandGitHub bf745d0e0e Chore: update .dockerignore (#401) 2024-09-02 06:06:38 +08:00
Jason LyuandGitHub 391d3d9f89 Refactor(pool): use generic buffer/pool (#399) 2024-09-02 05:57:22 +08:00
Jason LyuandGitHub fc4c5c4c55 Refactor(pool): move to internal (#398) 2024-09-01 04:23:25 +08:00
Jason LyuandGitHub 978803cdf8 Refactor(socks): replace net.IP with netip.Addr (#397) 2024-09-01 04:04:01 +08:00
Jason LyuandGitHub bd37a1a4c6 Refactor(metadata): replace net.IP with netip.Addr (#396) 2024-09-01 02:57:09 +08:00
Jason LyuandGitHub 1f09b4d42d Refactor(core): replace net.IP with netip.Addr (#395) 2024-08-31 12:00:38 +08:00
Jason LyuandGitHub fd98f65994 Refactor(tunnel): modularize tunnel pkg (#393) 2024-08-31 11:31:18 +08:00
Jason LyuandGitHub 71c45ef87e Chore(mod): bump to go1.23 (#394) 2024-08-31 11:21:07 +08:00
Jason LyuandGitHub 601601a1dc Refactor(log): use go.uber.org/zap (#389) 2024-08-29 06:56:35 +08:00
Jason LyuandGitHub c8c08cfeea Chore: adjust markdown layout (#388) 2024-08-21 05:27:56 +08:00
Jason LyuandGitHub 776e6470d3 Feature(actions): add testing (#378) 2024-08-21 05:27:20 +08:00
Jason LyuandGitHub 66fafd224e Chore: update README (#387)
* remove Chinese README
2024-08-21 02:14:22 +08:00
Jason LyuandGitHub e083dafcf5 Chore(.gitignore): use github template (#381) 2024-07-22 08:53:29 +08:00
Jason LyuandGitHub 24b8cdd96b Refactor(debug): simplify version string (#380) 2024-07-22 08:34:24 +08:00
Jason LyuandGitHub dd791e50c1 Feature: bump gVisor to latest version (#376) 2024-07-20 11:31:44 +08:00
Jason LyuandGitHub 7555425ab8 Chore: update golangci-lint settings (#377)
* remove staticcheck config
2024-07-16 10:32:59 +08:00
Jason LyuandGitHub 592517a00d Chore(actions): update versions (#375) 2024-07-16 06:22:25 +08:00
63f71e0b84 Chore(deps): Bump github.com/gorilla/schema from 1.3.0 to 1.4.1 (#370)
Bumps [github.com/gorilla/schema](https://github.com/gorilla/schema) from 1.3.0 to 1.4.1.
- [Release notes](https://github.com/gorilla/schema/releases)
- [Commits](https://github.com/gorilla/schema/compare/v1.3.0...v1.4.1)

---
updated-dependencies:
- dependency-name: github.com/gorilla/schema
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-12 05:19:58 +08:00
Jason LyuandGitHub 488e5b223c Chore(actions): update versions (#362) 2024-05-13 02:34:37 +08:00
Jason LyuandGitHub 0d819e1aec Fix: codeql autobuild error (#355) 2024-04-07 02:53:17 +08:00
Jason LyuandGitHub 60a63db500 Chore: bump dependency (#354)
* gVisor: `v0.0.0-20240215211334-a66ecfdd829a` => `v0.0.0-20240405191320-0878b34101b5`
2024-04-07 02:33:43 +08:00
Yawning AngelandGitHub 8c7c9085c2 Fix: socks5 usernames and passwords can BOTH be up to 255 bytes (#343) 2024-02-28 10:27:14 +08:00
Jason LyuandGitHub 8653c18875 Fix: safely split command strings (#340) 2024-02-20 05:41:58 +08:00
Jason LyuandGitHub 7b1d73d86e Chore: bump to go1.22 (#337)
* Chore: update go mods
* Chore: bump to go1.22
* Chore: fix lint trigger
2024-02-16 06:47:17 +08:00
Nicolai MooreandGitHub a49ce339b7 Fix: call SetSendBufferSize when setting send buffer size option (#336) 2024-02-07 10:18:53 +08:00
Jason LyuandGitHub 2334083cf9 Chore: update CodeQL (#332) 2024-01-28 07:21:33 +08:00
Jason LyuandGitHub 01d4ac4864 Chore: update go mods (#331) 2024-01-28 07:10:26 +08:00
Jason LyuandGitHub c8f8cb5caf Chore(release/go): use version from go.mod (#317) 2023-11-14 06:21:26 +08:00
Jason LyuandGitHub e5bfa13a3f Chore: update go mod (#316)
* bump gvisor to `v0.0.0-20231113203814-cdee0abd0280`
2023-11-14 06:00:15 +08:00
ctyandGitHub e86b3b7dc5 Perf(SOCKS5): optimize memory footprint with authentication (#315) 2023-11-13 06:28:42 +08:00
Jason LyuandGitHub 010765138c Chore: update README (#314)
* Remove deleted repositories
2023-11-03 23:09:12 +08:00
Jason LyuandGitHub f8bddb162e Improve(proxy/ss): allow explicit none cipher mode (#312) 2023-10-25 09:36:02 +08:00
Jason LyuandGitHub 2d80a4ba3b Chore(actions/docker): use github variables (#311) 2023-10-25 00:21:43 +08:00
Jason LyuandGitHub 00a5f18ebd Feature(proxy): support gost relay protocol (#310) 2023-10-24 10:55:33 +08:00
Jason LyuandGitHub 47913b549f Fix: defer func for safeConnClose (#309) 2023-10-24 09:13:36 +08:00
Jason LyuandGitHub 2283f82bbc Refactor: direct proxy parsing (#308) 2023-10-24 04:44:17 +08:00
Jason LyuandGitHub cbf620b2f8 Improve: use embedded go-shadowsocks2 (#306)
* Improve: use embedded go-shadowsocks2
* Chore: update go mod
2023-10-23 02:52:26 +08:00
Jason LyuandGitHub 68da4d9997 Chore: update go mod (#303) 2023-09-29 04:56:49 +08:00
Jason LyuandGitHub 78086193fb Fix: bump versions for actions (#302) 2023-09-29 04:46:13 +08:00
Jason LyuandGitHub 631fa59182 Feature: persistent wintun with GUID option (#301)
Fixes: #300
2023-09-29 04:36:29 +08:00
Jason LyuandGitHub f448baa2ae Chore: use errors.ErrUnsupported (#294) 2023-09-05 01:51:03 +08:00
Jason LyuandGitHub b470006fb6 Chore: bump to go1.21 (#292) 2023-08-29 01:30:11 +08:00
Jason LyuandGitHub 73ea4358cb Docker: add options for multicast groups (#290) 2023-08-28 07:04:22 +08:00
Jason LyuandGitHub 19ba20ffc0 Improve(actions): add concurrency groups (#289) 2023-08-28 03:57:16 +08:00
Jason LyuandGitHub f1b7b4745b Chore: bump go mods (#288) 2023-08-28 03:48:38 +08:00
90f77548ed Feature: add support for multicast (#245)
* add support for multicast (#243)

* adjust setup

---------

Co-authored-by: xjasonlyu <xjasonlyu@gmail.com>
2023-08-28 03:40:49 +08:00
xjasonlyu fffcbbea10 Chore: set up sponsor button 2023-08-27 14:14:27 -04:00
xjasonlyu f588b4c731 Chore: star chart with dark theme supported 2023-08-06 13:44:24 +08:00
Jason LyuandGitHub 71efe6c7cf Chore: do not mark stale for help wanted issues (#280) 2023-07-27 10:21:20 +08:00
xjasonlyu 2067a3129e Chore: update badge links 2023-07-20 15:01:18 +08:00
yushengandGitHub 8309fddef3 Feature: fdbased open fd with offset (#272) 2023-06-30 13:18:53 +08:00
xjasonlyu 2b494a7517 Fix: HTTP proxy basic auth error (#266) 2023-06-20 14:40:54 +08:00
92 changed files with 2519 additions and 1018 deletions
+5 -3
View File
@@ -1,6 +1,8 @@
.github
.github/
.gitignore
.golangci.yaml
# Other
build/*
docs/*
docs/
build/
tests/
+1 -1
View File
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [ xjasonlyu ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+13 -12
View File
@@ -2,27 +2,28 @@
## Supported Versions
| Version | Supported |
| :-----: | :----------------: |
| Version | Supported |
|:-------:|:------------------:|
| 2.x | :white_check_mark: |
| 1.x | :x: |
| 1.x | :x: |
## Reporting a Vulnerability
If you believe you have found a security vulnerability in this repository, please report it to me through coordinated disclosure.
If you believe you have found a security vulnerability in this repository, please report it to me through coordinated
disclosure.
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please send an email to xjasonlyu[@]gmail.com.
Instead, please email to xjasonlyu[@]gmail.com.
Please include as much of the information listed below as you can to help me better understand and resolve the issue:
* The type of issue (e.g., buffer overflow, payload attack)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
* The type of issue (e.g., buffer overflow, payload attack)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help me triage your report more quickly.
+15 -4
View File
@@ -1,8 +1,13 @@
name: "CodeQL"
concurrency:
group: codeql-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [ main ]
pull_request:
jobs:
analyze:
@@ -16,15 +21,21 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
check-latest: true
go-version-file: 'go.mod'
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
+15 -13
View File
@@ -1,5 +1,9 @@
name: Publish Docker Image
concurrency:
group: docker-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
@@ -8,38 +12,36 @@ on:
- '*'
jobs:
build:
name: Build
docker:
name: Docker
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: xjasonlyu
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
@@ -49,7 +51,7 @@ jobs:
- name: Build and Push (dev)
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: .
push: true
@@ -62,7 +64,7 @@ jobs:
- name: Build and Push (latest)
if: startsWith(github.ref, 'refs/tags/')
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: .
push: true
+18 -6
View File
@@ -1,18 +1,30 @@
name: Linter
on: [ push, pull_request ]
concurrency:
group: linter-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
pull_request:
jobs:
lint:
linter:
name: Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
check-latest: true
go-version: '1.20'
go-version-file: 'go.mod'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: latest
+15 -13
View File
@@ -1,44 +1,46 @@
name: Publish Go Releases
concurrency:
group: release-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
tags:
- '*'
jobs:
build:
name: Build
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
check-latest: true
go-version: '1.20'
go-version-file: 'go.mod'
- name: Cache go module
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run test
run: |
go test ./...
- name: Build
if: startsWith(github.ref, 'refs/tags/')
run: make -j releases
- name: Upload Releases
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: build/*
+8 -2
View File
@@ -1,5 +1,10 @@
name: Mark stale issues and pull requests
permissions:
contents: write
issues: write
pull-requests: write
on:
schedule:
- cron: "0 10 * * *"
@@ -8,9 +13,10 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v7
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
exempt-issue-labels: 'question,bug,enhancement'
exempt-issue-labels: 'question,bug,enhancement,help wanted'
exempt-pr-labels: 'pending,WIP,help wanted'
days-before-stale: 60
days-before-close: 7
+31
View File
@@ -0,0 +1,31 @@
name: Test
concurrency:
group: test-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- 'main'
pull_request:
jobs:
build-test:
name: Build Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
check-latest: true
go-version-file: 'go.mod'
- name: Run test
run: |
go test ./...
+19 -1
View File
@@ -1,9 +1,27 @@
# Binaries
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# Build directory
build/
# IDE
-2
View File
@@ -19,5 +19,3 @@ linters-settings:
- standard
- default
- prefix(github.com/xjasonlyu/tun2socks)
staticcheck:
go: '1.20'
+1
View File
@@ -25,6 +25,7 @@ ENV UDP_TIMEOUT=
ENV TCP_SNDBUF=
ENV TCP_RCVBUF=
ENV TCP_AUTO_TUNING=
ENV MULTICAST_GROUPS=
ENV EXTRA_COMMANDS=
ENV TUN_INCLUDED_ROUTES=
ENV TUN_EXCLUDED_ROUTES=
+6 -2
View File
@@ -41,6 +41,7 @@ UNIX_ARCH_LIST = \
linux-ppc64 \
linux-ppc64le \
linux-s390x \
linux-loong64 \
openbsd-amd64 \
openbsd-amd64-v3 \
openbsd-arm64
@@ -129,6 +130,9 @@ linux-ppc64le:
linux-s390x:
GOARCH=s390x GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
linux-loong64:
GOARCH=loong64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
openbsd-amd64:
GOARCH=amd64 GOOS=openbsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
@@ -167,9 +171,9 @@ all-arch: $(UNIX_ARCH_LIST) $(WINDOWS_ARCH_LIST)
releases: $(unix_releases) $(windows_releases)
lint:
GOOS=darwin golangci-lint run ./...
GOOS=darwin golangci-lint run ./...
GOOS=windows golangci-lint run ./...
GOOS=linux golangci-lint run ./...
GOOS=linux golangci-lint run ./...
GOOS=freebsd golangci-lint run ./...
GOOS=openbsd golangci-lint run ./...
+13 -8
View File
@@ -8,7 +8,7 @@
[![Docker Pulls][6]](https://hub.docker.com/r/xjasonlyu/tun2socks)
[![Releases][7]](https://github.com/xjasonlyu/tun2socks/releases)
[1]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/release.yml?branch=main&logo=github
[1]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/release.yml?logo=github
[2]: https://img.shields.io/github/go-mod/go-version/xjasonlyu/tun2socks?logo=go
[3]: https://goreportcard.com/badge/github.com/xjasonlyu/tun2socks
[4]: https://api.codeclimate.com/v1/badges/b5b30239174fc6603aca/maintainability
@@ -16,20 +16,19 @@
[6]: https://img.shields.io/docker/pulls/xjasonlyu/tun2socks?logo=docker
[7]: https://img.shields.io/github/v/release/xjasonlyu/tun2socks?logo=smartthings
English | [简体中文](README_ZH.md)
## Features
- Proxy Everything: Handle all network traffic of any internet programs sent by the device through a proxy.
- Proxy Protocols: HTTP/Socks4/Socks5/Shadowsocks with authentication support for remote connections.
- Run Everywhere: Linux/macOS/Windows/FreeBSD/OpenBSD multi-platform support with specific optimization.
- Gateway Mode: Act as a layer three gateway to handle network traffic from other devices in the same network.
- Gateway Mode: Act as a Layer 3 gateway to handle network traffic from other devices on the same network.
- Full IPv6 Support: All functions work in IPv6, tunnel IPv4 connections through IPv6 proxy and vice versa.
- Network Stack: Powered by user-space TCP/IP stack from Google container application kernel **[gVisor](https://github.com/google/gvisor)**.
- Network Stack: Powered by the user-space TCP/IP stack from Google container application kernel **[gVisor](https://github.com/google/gvisor)**.
## Benchmarks
For all scenarios of usage, tun2socks performs best. See [here](https://github.com/xjasonlyu/tun2socks/wiki/Benchmarks) for more details.
For all scenarios of usage, tun2socks performs best.
See [benchmarks](https://github.com/xjasonlyu/tun2socks/wiki/Benchmarks) for more details.
![benchmark](docs/benchmark.png)
@@ -47,9 +46,9 @@ Welcome and feel free to ask any questions at [Discussions](https://github.com/x
## Credits
- [Dreamacro/clash](https://github.com/Dreamacro/clash) - A rule-based tunnel in Go
- [google/gvisor](https://github.com/google/gvisor) - Application Kernel for Containers
- [wireguard-go](https://git.zx2c4.com/wireguard-go) - Go Implementation of WireGuard
- [wintun](https://git.zx2c4.com/wintun/) - Layer 3 TUN Driver for Windows
## License
@@ -59,4 +58,10 @@ Welcome and feel free to ask any questions at [Discussions](https://github.com/x
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=xjasonlyu/tun2socks&type=Date)](https://star-history.com/#xjasonlyu/tun2socks&Date)
<a href="https://star-history.com/#xjasonlyu/tun2socks&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xjasonlyu/tun2socks&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xjasonlyu/tun2socks&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=xjasonlyu/tun2socks&type=Date" />
</picture>
</a>
-62
View File
@@ -1,62 +0,0 @@
![tun2socks](docs/wordmark.png)
[![GitHub Workflow][1]](https://github.com/xjasonlyu/tun2socks/actions)
[![Go Version][2]](https://github.com/xjasonlyu/tun2socks/blob/main/go.mod)
[![Go Report][3]](https://goreportcard.com/badge/github.com/xjasonlyu/tun2socks)
[![Maintainability][4]](https://codeclimate.com/github/xjasonlyu/tun2socks/maintainability)
[![GitHub License][5]](https://github.com/xjasonlyu/tun2socks/blob/main/LICENSE)
[![Docker Pulls][6]](https://hub.docker.com/r/xjasonlyu/tun2socks)
[![Releases][7]](https://github.com/xjasonlyu/tun2socks/releases)
[1]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/release.yml?branch=main&logo=github
[2]: https://img.shields.io/github/go-mod/go-version/xjasonlyu/tun2socks?logo=go
[3]: https://goreportcard.com/badge/github.com/xjasonlyu/tun2socks
[4]: https://api.codeclimate.com/v1/badges/b5b30239174fc6603aca/maintainability
[5]: https://img.shields.io/github/license/xjasonlyu/tun2socks
[6]: https://img.shields.io/docker/pulls/xjasonlyu/tun2socks?logo=docker
[7]: https://img.shields.io/github/v/release/xjasonlyu/tun2socks?logo=smartthings
[English](README.md) | 简体中文
## 特性介绍
- 全局代理: 处理来自本设备的任意网络应用的所有网络流量并通过代理转发。
- 代理协议: 通过 HTTP/Socks4/Socks5/Shadowsocks 远程连接且支持鉴权。
- 跨平台性: 具有 Linux/macOS/Windows/FreeBSD/OpenBSD 特定优化的多平台支持。
- 网关模式: 作为第三层网关处理来自同一网络中其他设备的所有网络流量。
- IPv6 支持: 所有功能都可以在 IPv6 中工作,允许通过 IPv6 代理转发 IPv4 连接,反之亦然。
- TCP/IP 栈: 由来自 Google 容器应用程序内核 **[gVisor](https://github.com/google/gvisor)** 的用户空间 TCP/IP 网络栈强力驱动。
## 性能测试
对于任意的使用场景,tun2socks 表现最佳。更多细节看[这里](https://github.com/xjasonlyu/tun2socks/wiki/Benchmarks)。
![benchmark](docs/benchmark.png)
## 使用文档
- [源码安装](https://github.com/xjasonlyu/tun2socks/wiki/Install-from-Source)
- [使用例子](https://github.com/xjasonlyu/tun2socks/wiki/Examples)
- [内存优化](https://github.com/xjasonlyu/tun2socks/wiki/Memory-Optimization)
文档以及使用方式可以在 [Wiki](https://github.com/xjasonlyu/tun2socks/wiki) 里找到。
## 交流讨论
欢迎来讨论区 [Discussions](https://github.com/xjasonlyu/tun2socks/discussions) 交流提问。
## 特别感谢
- [Dreamacro/clash](https://github.com/Dreamacro/clash) - A rule-based tunnel in Go
- [google/gvisor](https://github.com/google/gvisor) - Application Kernel for Containers
- [wireguard-go](https://git.zx2c4.com/wireguard-go) - Go Implementation of WireGuard
## 许可协议
[GPL-3.0](https://github.com/xjasonlyu/tun2socks/blob/main/LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks?ref=badge_large)
## 星星历史
[![Star History Chart](https://api.star-history.com/svg?repos=xjasonlyu/tun2socks&type=Date)](https://star-history.com/#xjasonlyu/tun2socks&Date)
@@ -1,30 +1,29 @@
package pool
package allocator
import (
"errors"
"math/bits"
"sync"
)
var _allocator = NewAllocator()
"github.com/xjasonlyu/tun2socks/v2/internal/pool"
)
// Allocator for incoming frames, optimized to prevent overwriting
// after zeroing.
type Allocator struct {
buffers []sync.Pool
buffers []*pool.Pool[[]byte]
}
// NewAllocator initiates a []byte allocator for frames less than
// 65536 bytes, the waste(memory fragmentation) of space allocation
// is guaranteed to be no more than 50%.
func NewAllocator() *Allocator {
// New initiates a []byte allocator for frames less than 65536 bytes,
// the waste(memory fragmentation) of space allocation is guaranteed
// to be no more than 50%.
func New() *Allocator {
alloc := &Allocator{}
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
alloc.buffers = make([]*pool.Pool[[]byte], 17) // 1B -> 64K
for k := range alloc.buffers {
i := k
alloc.buffers[k].New = func() any {
alloc.buffers[k] = pool.New(func() []byte {
return make([]byte, 1<<uint32(i))
}
})
}
return alloc
}
@@ -37,10 +36,10 @@ func (alloc *Allocator) Get(size int) []byte {
b := msb(size)
if size == 1<<b {
return alloc.buffers[b].Get().([]byte)[:size]
return alloc.buffers[b].Get()[:size]
}
return alloc.buffers[b+1].Get().([]byte)[:size]
return alloc.buffers[b+1].Get()[:size]
}
// Put returns a []byte to pool for future use,
@@ -51,8 +50,6 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size")
}
//lint:ignore SA6002 ignore temporarily
//nolint
alloc.buffers[b].Put(buf)
return nil
}
@@ -1,4 +1,4 @@
package pool
package allocator
import (
"math/rand"
@@ -8,7 +8,7 @@ import (
)
func TestAllocGet(t *testing.T) {
alloc := NewAllocator()
alloc := New()
assert.Nil(t, alloc.Get(0))
assert.Equal(t, 1, len(alloc.Get(1)))
assert.Equal(t, 2, len(alloc.Get(2)))
@@ -23,7 +23,7 @@ func TestAllocGet(t *testing.T) {
}
func TestAllocPut(t *testing.T) {
alloc := NewAllocator()
alloc := New()
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
@@ -33,7 +33,7 @@ func TestAllocPut(t *testing.T) {
}
func TestAllocPutThenGet(t *testing.T) {
alloc := NewAllocator()
alloc := New()
data := alloc.Get(4)
_ = alloc.Put(data)
newData := alloc.Get(4)
+29
View File
@@ -0,0 +1,29 @@
// Package buffer provides a pool of []byte.
package buffer
import (
"github.com/xjasonlyu/tun2socks/v2/buffer/allocator"
)
const (
// MaxSegmentSize is the largest possible UDP datagram size.
MaxSegmentSize = (1 << 16) - 1
// RelayBufferSize is the default buffer size for TCP relays.
// io.Copy default buffer size is 32 KiB, but the maximum packet
// size of vmess/shadowsocks is about 16 KiB, so define a buffer
// of 20 KiB to reduce the memory of each TCP relay.
RelayBufferSize = 20 << 10
)
var _allocator = allocator.New()
// Get gets a []byte from default allocator with most appropriate cap.
func Get(size int) []byte {
return _allocator.Get(size)
}
// Put returns a []byte to default allocator for future use.
func Put(buf []byte) error {
return _allocator.Put(buf)
}
-3
View File
@@ -1,3 +0,0 @@
package observable
type Iterable <-chan any
-65
View File
@@ -1,65 +0,0 @@
package observable
import (
"errors"
"sync"
)
type Observable struct {
iterable Iterable
listener map[Subscription]*Subscriber
mux sync.Mutex
done bool
}
func (o *Observable) process() {
for item := range o.iterable {
o.mux.Lock()
for _, sub := range o.listener {
sub.Emit(item)
}
o.mux.Unlock()
}
o.close()
}
func (o *Observable) close() {
o.mux.Lock()
defer o.mux.Unlock()
o.done = true
for _, sub := range o.listener {
sub.Close()
}
}
func (o *Observable) Subscribe() (Subscription, error) {
o.mux.Lock()
defer o.mux.Unlock()
if o.done {
return nil, errors.New("observable is closed")
}
subscriber := newSubscriber()
o.listener[subscriber.Out()] = subscriber
return subscriber.Out(), nil
}
func (o *Observable) UnSubscribe(sub Subscription) {
o.mux.Lock()
defer o.mux.Unlock()
subscriber, exist := o.listener[sub]
if !exist {
return
}
delete(o.listener, sub)
subscriber.Close()
}
func NewObservable(any Iterable) *Observable {
observable := &Observable{
iterable: any,
listener: map[Subscription]*Subscriber{},
}
go observable.process()
return observable
}
-148
View File
@@ -1,148 +0,0 @@
package observable
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/atomic"
)
func iterator(item []any) chan any {
ch := make(chan any)
go func() {
time.Sleep(100 * time.Millisecond)
for _, elm := range item {
ch <- elm
}
close(ch)
}()
return ch
}
func TestObservable(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
assert.Nil(t, err)
count := 0
for range data {
count++
}
assert.Equal(t, count, 5)
}
func TestObservable_MultiSubscribe(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
count := atomic.NewInt32(0)
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan any) {
for range ch {
count.Inc()
}
wg.Done()
}
go waitCh(ch1)
go waitCh(ch2)
wg.Wait()
assert.Equal(t, int32(10), count.Load())
}
func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
assert.Nil(t, err)
src.UnSubscribe(data)
_, open := <-data
assert.False(t, open)
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]any{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
_, closed := src.Subscribe()
assert.NotNil(t, closed)
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan any))
iter := iterator([]any{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
max := 100
var list []Subscription
for i := 0; i < max; i++ {
ch, _ := src.Subscribe()
list = append(list, ch)
}
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan any) {
for range ch {
}
wg.Done()
}
for _, ch := range list {
go waitCh(ch)
}
wg.Wait()
for _, sub := range list {
_, more := <-sub
assert.False(t, more)
}
if len(list) > 0 {
_, more := <-list[0]
assert.False(t, more)
}
}
func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan any)
o := NewObservable(ch)
num := 1000
var subs []Subscription
for i := 0; i < num; i++ {
sub, _ := o.Subscribe()
subs = append(subs, sub)
}
wg := sync.WaitGroup{}
wg.Add(num)
b.ResetTimer()
for _, sub := range subs {
go func(s Subscription) {
for range s {
}
wg.Done()
}(sub)
}
for i := 0; i < b.N; i++ {
ch <- i
}
close(ch)
wg.Wait()
}
-33
View File
@@ -1,33 +0,0 @@
package observable
import (
"sync"
)
type Subscription <-chan any
type Subscriber struct {
buffer chan any
once sync.Once
}
func (s *Subscriber) Emit(item any) {
s.buffer <- item
}
func (s *Subscriber) Out() Subscription {
return s.buffer
}
func (s *Subscriber) Close() {
s.once.Do(func() {
close(s.buffer)
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: make(chan any, 200),
}
return sub
}
-17
View File
@@ -1,17 +0,0 @@
package pool
import (
"bytes"
"sync"
)
var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
-23
View File
@@ -1,23 +0,0 @@
// Package pool provides a pool of []byte.
package pool
const (
// MaxSegmentSize is the largest possible UDP datagram size.
MaxSegmentSize = (1 << 16) - 1
// RelayBufferSize is a buffer of 20 KiB to reduce the memory
// of each TCP relay as io.Copy default buffer size is 32 KiB,
// but the maximum packet size of vmess/shadowsocks is about
// 16 KiB, so define .
RelayBufferSize = 20 << 10
)
// Get gets a []byte from default allocator with most appropriate cap.
func Get(size int) []byte {
return _allocator.Get(size)
}
// Put returns a []byte to default allocator for future use.
func Put(buf []byte) error {
return _allocator.Put(buf)
}
-3
View File
@@ -9,9 +9,6 @@ import (
type Device interface {
stack.LinkEndpoint
// Close stops and closes the device.
Close() error
// Name returns the current name of the device.
Name() string
+6 -5
View File
@@ -1,4 +1,4 @@
//go:build !windows
//go:build unix
package fdbased
@@ -21,7 +21,7 @@ type FD struct {
mtu uint32
}
func Open(name string, mtu uint32) (device.Device, error) {
func Open(name string, mtu uint32, offset int) (device.Device, error) {
fd, err := strconv.Atoi(name)
if err != nil {
return nil, fmt.Errorf("cannot open fd: %s", name)
@@ -29,7 +29,7 @@ func Open(name string, mtu uint32) (device.Device, error) {
if mtu == 0 {
mtu = defaultMTU
}
return open(fd, mtu)
return open(fd, mtu, offset)
}
func (f *FD) Type() string {
@@ -40,8 +40,9 @@ func (f *FD) Name() string {
return strconv.Itoa(f.fd)
}
func (f *FD) Close() error {
return unix.Close(f.fd)
func (f *FD) Close() {
defer f.LinkEndpoint.Close()
_ = unix.Close(f.fd)
}
var _ device.Device = (*FD)(nil)
+2 -2
View File
@@ -6,6 +6,6 @@ import (
"github.com/xjasonlyu/tun2socks/v2/core/device"
)
func Open(name string, mtu uint32) (device.Device, error) {
return nil, errors.New("not supported")
func Open(name string, mtu uint32, offset int) (device.Device, error) {
return nil, errors.ErrUnsupported
}
+1 -3
View File
@@ -1,5 +1,3 @@
//go:build (linux && amd64) || (linux && arm64)
package fdbased
import (
@@ -10,7 +8,7 @@ import (
"github.com/xjasonlyu/tun2socks/v2/core/device"
)
func open(fd int, mtu uint32) (device.Device, error) {
func open(fd int, mtu uint32, offset int) (device.Device, error) {
f := &FD{fd: fd, mtu: mtu}
ep, err := fdbased.New(&fdbased.Options{
@@ -1,4 +1,4 @@
//go:build !(linux && amd64) && !(linux && arm64) && !windows
//go:build unix && !linux
package fdbased
@@ -10,10 +10,9 @@ import (
"github.com/xjasonlyu/tun2socks/v2/core/device/iobased"
)
func open(fd int, mtu uint32) (device.Device, error) {
func open(fd int, mtu uint32, offset int) (device.Device, error) {
f := &FD{fd: fd, mtu: mtu}
ep, err := iobased.New(os.NewFile(uintptr(fd), f.Name()), mtu, 0)
ep, err := iobased.New(os.NewFile(uintptr(fd), f.Name()), mtu, offset)
if err != nil {
return nil, fmt.Errorf("create endpoint: %w", err)
}
+2 -2
View File
@@ -128,7 +128,7 @@ func (e *Endpoint) dispatchLoop(cancel context.CancelFunc) {
func (e *Endpoint) outboundLoop(ctx context.Context) {
for {
pkt := e.ReadContext(ctx)
if pkt.IsNil() {
if pkt == nil {
break
}
e.writePacket(pkt)
@@ -136,7 +136,7 @@ func (e *Endpoint) outboundLoop(ctx context.Context) {
}
// writePacket writes outbound packets to the io.Writer.
func (e *Endpoint) writePacket(pkt stack.PacketBufferPtr) tcpip.Error {
func (e *Endpoint) writePacket(pkt *stack.PacketBuffer) tcpip.Error {
defer pkt.DecRef()
buf := pkt.ToBuffer()
+5 -4
View File
@@ -1,4 +1,4 @@
//go:build (linux && amd64) || (linux && arm64)
//go:build linux
package tun
@@ -6,8 +6,8 @@ import (
"fmt"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/rawfile"
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
"gvisor.dev/gvisor/pkg/tcpip/link/rawfile"
"gvisor.dev/gvisor/pkg/tcpip/link/tun"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -76,8 +76,9 @@ func (t *TUN) Name() string {
return t.name
}
func (t *TUN) Close() error {
return unix.Close(t.fd)
func (t *TUN) Close() {
defer t.LinkEndpoint.Close()
_ = unix.Close(t.fd)
}
func setMTU(name string, n uint32) error {
+3 -3
View File
@@ -1,4 +1,4 @@
//go:build !(linux && amd64) && !(linux && arm64)
//go:build !linux
package tun
@@ -89,7 +89,7 @@ func (t *TUN) Name() string {
return name
}
func (t *TUN) Close() error {
func (t *TUN) Close() {
defer t.Endpoint.Close()
return t.nt.Close()
_ = t.nt.Close()
}
-26
View File
@@ -1,26 +0,0 @@
//go:build linux && !(amd64 || arm64)
package tun
import (
"fmt"
"os"
"golang.zx2c4.com/wireguard/tun"
gun "gvisor.dev/gvisor/pkg/tcpip/link/tun"
)
const (
offset = 0 /* IFF_NO_PI */
defaultMTU = 1500
)
func createTUN(name string, mtu int) (tun.Device, error) {
nfd, err := gun.Open(name)
if err != nil {
return nil, fmt.Errorf("create tun: %w", err)
}
fd := os.NewFile(uintptr(nfd), "/dev/net/tun")
return tun.CreateTUNFromFile(fd, mtu)
}
+61
View File
@@ -2,8 +2,11 @@ package core
import (
"fmt"
"net/netip"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"github.com/xjasonlyu/tun2socks/v2/core/option"
@@ -56,3 +59,61 @@ func withSpoofing(nicID tcpip.NICID, v bool) option.Option {
return nil
}
}
// withMulticastGroups adds a NIC to the given multicast groups.
func withMulticastGroups(nicID tcpip.NICID, multicastGroups []netip.Addr) option.Option {
return func(s *stack.Stack) error {
if len(multicastGroups) == 0 {
return nil
}
// The default NIC of tun2socks is working on Spoofing mode. When the UDP Endpoint
// tries to use a non-local address to connect, the network stack will
// generate a temporary addressState to build the route, which can be primary
// but is ephemeral. Nevertheless, when the UDP Endpoint tries to use a
// multicast address to connect, the network stack will select an available
// primary addressState to build the route. However, when tun2socks is in the
// just-initialized or idle state, there will be no available primary addressState,
// and the connect operation will fail. Therefore, we need to add permanent addresses,
// e.g. 10.0.0.1/8 and fd00:1/8, to the default NIC, which are only used to build
// routes for multicast response and do not affect other connections.
//
// In fact, for multicast, the sender normally does not expect a response.
// So, the ep.net.Connect is unnecessary. If we implement a custom UDP Forwarder
// and ForwarderRequest in the future, we can remove these code.
s.AddProtocolAddress(
nicID,
tcpip.ProtocolAddress{
Protocol: ipv4.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFrom4([4]byte{0x0a, 0, 0, 0x01}),
PrefixLen: 8,
},
},
stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
)
s.AddProtocolAddress(
nicID,
tcpip.ProtocolAddress{
Protocol: ipv6.ProtocolNumber,
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}),
PrefixLen: 8,
},
},
stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
)
for _, multicastGroup := range multicastGroups {
var err tcpip.Error
switch {
case multicastGroup.Is4():
err = s.JoinGroup(ipv4.ProtocolNumber, nicID, tcpip.AddrFrom4(multicastGroup.As4()))
case multicastGroup.Is6():
err = s.JoinGroup(ipv6.ProtocolNumber, nicID, tcpip.AddrFrom16(multicastGroup.As16()))
}
if err != nil {
return fmt.Errorf("join multicast group: %s", err)
}
}
return nil
}
}
+10 -2
View File
@@ -1,7 +1,8 @@
package core
import (
"gvisor.dev/gvisor/pkg/tcpip"
"net/netip"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -23,6 +24,10 @@ type Config struct {
// stack to set transport handlers.
TransportHandler adapter.TransportHandler
// MulticastGroups is used by internal stack to add
// nic to given groups.
MulticastGroups []netip.Addr
// Options are supplement options to apply settings
// for the internal stack.
Options []option.Option
@@ -49,7 +54,7 @@ func CreateStack(cfg *Config) (*stack.Stack, error) {
})
// Generate unique NIC id.
nicID := tcpip.NICID(s.UniqueID())
nicID := s.NextNICID()
opts = append(opts,
// Important: We must initiate transport protocol handlers
@@ -88,6 +93,9 @@ func CreateStack(cfg *Config) (*stack.Stack, error) {
// Add default route table for IPv4 and IPv6. This will handle
// all incoming ICMP packets.
withRouteTable(nicID),
// Add default NIC to the given multicast groups.
withMulticastGroups(nicID, cfg.MulticastGroups),
)
for _, opt := range opts {
+1 -1
View File
@@ -101,7 +101,7 @@ func setSocketOptions(s *stack.Stack, ep tcpip.Endpoint) tcpip.Error {
{ /* TCP recv/send buffer size */
var ss tcpip.TCPSendBufferSizeRangeOption
if err := s.TransportProtocolOption(header.TCPProtocolNumber, &ss); err == nil {
ep.SocketOptions().SetReceiveBufferSize(int64(ss.Default), false)
ep.SocketOptions().SetSendBufferSize(int64(ss.Default), false)
}
var rs tcpip.TCPReceiveBufferSizeRangeOption
+1 -1
View File
@@ -26,7 +26,7 @@ func withUDPHandler(handle func(adapter.UDPConn)) option.Option {
}
conn := &udpConn{
UDPConn: gonet.NewUDPConn(s, &wq, ep),
UDPConn: gonet.NewUDPConn(&wq, ep),
id: id,
}
handle(conn)
+4
View File
@@ -81,6 +81,10 @@ run() {
ARGS="$ARGS --tcp-auto-tuning"
fi
if [ -n "$MULTICAST_GROUPS" ]; then
ARGS="$ARGS --multicast-groups $MULTICAST_GROUPS"
fi
exec tun2socks \
--loglevel "$LOGLEVEL" \
--fwmark "$FWMARK" \
+24 -13
View File
@@ -3,12 +3,13 @@ package engine
import (
"errors"
"net"
"net/netip"
"os/exec"
"strings"
"sync"
"time"
"github.com/docker/go-units"
"github.com/google/shlex"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/stack"
@@ -16,7 +17,6 @@ import (
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/option"
"github.com/xjasonlyu/tun2socks/v2/dialer"
"github.com/xjasonlyu/tun2socks/v2/engine/mirror"
"github.com/xjasonlyu/tun2socks/v2/log"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/restapi"
@@ -82,22 +82,25 @@ func start() error {
func stop() (err error) {
_engineMu.Lock()
if _defaultDevice != nil {
err = _defaultDevice.Close()
_defaultDevice.Close()
}
if _defaultStack != nil {
_defaultStack.Close()
_defaultStack.Wait()
}
_engineMu.Unlock()
return err
return nil
}
func execCommand(cmd string) error {
parts := strings.Fields(cmd)
parts, err := shlex.Split(cmd)
if err != nil {
return err
}
if len(parts) == 0 {
return errors.New("empty command")
}
_, err := exec.Command(parts[0], parts[1:]...).Output()
_, err = exec.Command(parts[0], parts[1:]...).Output()
return err
}
@@ -106,7 +109,7 @@ func general(k *Key) error {
if err != nil {
return err
}
log.SetLevel(level)
log.SetLogger(log.Must(log.NewLeveled(level)))
if k.Interface != "" {
iface, err := net.InterfaceByName(k.Interface)
@@ -127,7 +130,7 @@ func general(k *Key) error {
if k.UDPTimeout < time.Second {
return errors.New("invalid udp timeout value")
}
tunnel.SetUDPTimeout(k.UDPTimeout)
tunnel.T().SetUDPTimeout(k.UDPTimeout)
}
return nil
}
@@ -153,7 +156,7 @@ func restAPI(k *Key) error {
go func() {
if err := restapi.Start(host, token); err != nil {
log.Warnf("[RESTAPI] failed to start: %v", err)
log.Errorf("[RESTAPI] failed to start: %v", err)
}
}()
log.Infof("[RESTAPI] serve at: %s", u)
@@ -170,8 +173,9 @@ func netstack(k *Key) (err error) {
}
if k.TUNPreUp != "" {
log.Infof("[TUN] pre-execute command: `%s`", k.TUNPreUp)
if preUpErr := execCommand(k.TUNPreUp); preUpErr != nil {
log.Warnf("[TUN] failed to pre-execute: %s: %v", k.TUNPreUp, preUpErr)
log.Errorf("[TUN] failed to pre-execute: %s: %v", k.TUNPreUp, preUpErr)
}
}
@@ -179,20 +183,26 @@ func netstack(k *Key) (err error) {
if k.TUNPostUp == "" || err != nil {
return
}
log.Infof("[TUN] post-execute command: `%s`", k.TUNPostUp)
if postUpErr := execCommand(k.TUNPostUp); postUpErr != nil {
log.Warnf("[TUN] failed to post-execute: %s: %v", k.TUNPostUp, postUpErr)
log.Errorf("[TUN] failed to post-execute: %s: %v", k.TUNPostUp, postUpErr)
}
}()
if _defaultProxy, err = parseProxy(k.Proxy); err != nil {
return
}
proxy.SetDialer(_defaultProxy)
tunnel.T().SetDialer(_defaultProxy)
if _defaultDevice, err = parseDevice(k.Device, uint32(k.MTU)); err != nil {
return
}
var multicastGroups []netip.Addr
if multicastGroups, err = parseMulticastGroups(k.MulticastGroups); err != nil {
return err
}
var opts []option.Option
if k.TCPModerateReceiveBuffer {
opts = append(opts, option.WithTCPModerateReceiveBuffer(true))
@@ -216,7 +226,8 @@ func netstack(k *Key) (err error) {
if _defaultStack, err = core.CreateStack(&core.Config{
LinkEndpoint: _defaultDevice,
TransportHandler: &mirror.Tunnel{},
TransportHandler: tunnel.T(),
MulticastGroups: multicastGroups,
Options: opts,
}); err != nil {
return
+1
View File
@@ -13,6 +13,7 @@ type Key struct {
TCPModerateReceiveBuffer bool `yaml:"tcp-moderate-receive-buffer"`
TCPSendBufferSize string `yaml:"tcp-send-buffer-size"`
TCPReceiveBufferSize string `yaml:"tcp-receive-buffer-size"`
MulticastGroups string `yaml:"multicast-groups"`
TUNPreUp string `yaml:"tun-pre-up"`
TUNPostUp string `yaml:"tun-post-up"`
UDPTimeout time.Duration `yaml:"udp-timeout"`
-18
View File
@@ -1,18 +0,0 @@
package mirror
import (
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/tunnel"
)
var _ adapter.TransportHandler = (*Tunnel)(nil)
type Tunnel struct{}
func (*Tunnel) HandleTCP(conn adapter.TCPConn) {
tunnel.TCPIn() <- conn
}
func (*Tunnel) HandleUDP(conn adapter.UDPConn) {
tunnel.UDPIn() <- conn
}
+74 -22
View File
@@ -4,9 +4,13 @@ import (
"encoding/base64"
"fmt"
"net"
"net/netip"
"net/url"
"runtime"
"strings"
"github.com/gorilla/schema"
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/device/fdbased"
"github.com/xjasonlyu/tun2socks/v2/core/device/tun"
@@ -51,19 +55,28 @@ func parseDevice(s string, mtu uint32) (device.Device, error) {
return nil, err
}
name := u.Host
driver := strings.ToLower(u.Scheme)
switch driver {
case fdbased.Driver:
return fdbased.Open(name, mtu)
return parseFD(u, mtu)
case tun.Driver:
return tun.Open(name, mtu)
return parseTUN(u, mtu)
default:
return nil, fmt.Errorf("unsupported driver: %s", driver)
}
}
func parseFD(u *url.URL, mtu uint32) (device.Device, error) {
offset := 0
// fd offset in ios
// https://stackoverflow.com/questions/69260852/ios-network-extension-packet-parsing/69487795#69487795
if runtime.GOOS == "ios" {
offset = 4
}
return fdbased.Open(u.Host, mtu, offset)
}
func parseProxy(s string) (proxy.Proxy, error) {
if !strings.Contains(s, "://") {
s = fmt.Sprintf("%s://%s", proto.Socks5 /* default protocol */, s)
@@ -82,48 +95,56 @@ func parseProxy(s string) (proxy.Proxy, error) {
case proto.Reject.String():
return proxy.NewReject(), nil
case proto.HTTP.String():
return proxy.NewHTTP(parseHTTP(u))
return parseHTTP(u)
case proto.Socks4.String():
return proxy.NewSocks4(parseSocks4(u))
return parseSocks4(u)
case proto.Socks5.String():
return proxy.NewSocks5(parseSocks5(u))
return parseSocks5(u)
case proto.Shadowsocks.String():
return proxy.NewShadowsocks(parseShadowsocks(u))
return parseShadowsocks(u)
case proto.Relay.String():
return parseRelay(u)
default:
return nil, fmt.Errorf("unsupported protocol: %s", protocol)
}
}
func parseHTTP(u *url.URL) (address, username, password string) {
address, username = u.Host, u.User.Username()
password, _ = u.User.Password()
return
func parseHTTP(u *url.URL) (proxy.Proxy, error) {
address, username := u.Host, u.User.Username()
password, _ := u.User.Password()
return proxy.NewHTTP(address, username, password)
}
func parseSocks4(u *url.URL) (address, username string) {
address, username = u.Host, u.User.Username()
return
func parseSocks4(u *url.URL) (proxy.Proxy, error) {
address, userID := u.Host, u.User.Username()
return proxy.NewSocks4(address, userID)
}
func parseSocks5(u *url.URL) (address, username, password string) {
address, username = u.Host, u.User.Username()
password, _ = u.User.Password()
func parseSocks5(u *url.URL) (proxy.Proxy, error) {
address, username := u.Host, u.User.Username()
password, _ := u.User.Password()
// Socks5 over UDS
if address == "" {
address = u.Path
}
return
return proxy.NewSocks5(address, username, password)
}
func parseShadowsocks(u *url.URL) (address, method, password, obfsMode, obfsHost string) {
address = u.Host
func parseShadowsocks(u *url.URL) (proxy.Proxy, error) {
var (
address = u.Host
method, password string
obfsMode, obfsHost string
)
if pass, set := u.User.Password(); set {
if ss := u.User.String(); ss == "" {
method = "dummy" // none cipher mode
} else if pass, set := u.User.Password(); set {
method = u.User.Username()
password = pass
} else {
data, _ := base64.RawURLEncoding.DecodeString(u.User.String())
data, _ := base64.RawURLEncoding.DecodeString(ss)
userInfo := strings.SplitN(string(data), ":", 2)
if len(userInfo) == 2 {
method = userInfo[0]
@@ -148,5 +169,36 @@ func parseShadowsocks(u *url.URL) (address, method, password, obfsMode, obfsHost
}
}
return proxy.NewShadowsocks(address, method, password, obfsMode, obfsHost)
}
func parseRelay(u *url.URL) (proxy.Proxy, error) {
address, username := u.Host, u.User.Username()
password, _ := u.User.Password()
opts := struct {
NoDelay bool
}{}
if err := schema.NewDecoder().Decode(&opts, u.Query()); err != nil {
return nil, err
}
return proxy.NewRelay(address, username, password, opts.NoDelay)
}
func parseMulticastGroups(s string) (multicastGroups []netip.Addr, _ error) {
for _, ip := range strings.Split(s, ",") {
if ip = strings.TrimSpace(ip); ip == "" {
continue
}
addr, err := netip.ParseAddr(ip)
if err != nil {
return nil, err
}
if !addr.IsMulticast() {
return nil, fmt.Errorf("invalid multicast IP: %s", addr)
}
multicastGroups = append(multicastGroups, addr)
}
return
}
+14
View File
@@ -0,0 +1,14 @@
//go:build unix
package engine
import (
"net/url"
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/device/tun"
)
func parseTUN(u *url.URL, mtu uint32) (device.Device, error) {
return tun.Open(u.Host, mtu)
}
+34
View File
@@ -0,0 +1,34 @@
package engine
import (
"net/url"
"github.com/gorilla/schema"
"golang.org/x/sys/windows"
wun "golang.zx2c4.com/wireguard/tun"
"github.com/xjasonlyu/tun2socks/v2/core/device"
"github.com/xjasonlyu/tun2socks/v2/core/device/tun"
"github.com/xjasonlyu/tun2socks/v2/internal/version"
)
func init() {
wun.WintunTunnelType = version.Name
}
func parseTUN(u *url.URL, mtu uint32) (device.Device, error) {
opts := struct {
GUID string
}{}
if err := schema.NewDecoder().Decode(&opts, u.Query()); err != nil {
return nil, err
}
if opts.GUID != "" {
guid, err := windows.GUIDFromString(opts.GUID)
if err != nil {
return nil, err
}
wun.WintunStaticRequestedGUID = &guid
}
return tun.Open(u.Host, mtu)
}
+19 -17
View File
@@ -1,33 +1,35 @@
module github.com/xjasonlyu/tun2socks/v2
go 1.20
go 1.23.1
require (
github.com/Dreamacro/go-shadowsocks2 v0.1.8
github.com/docker/go-units v0.5.0
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.7.1
github.com/go-chi/render v1.0.3
github.com/go-gost/relay v0.5.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/gorilla/schema v1.4.1
github.com/gorilla/websocket v1.5.3
github.com/stretchr/testify v1.9.0
go.uber.org/atomic v1.11.0
go.uber.org/automaxprocs v1.5.2
golang.org/x/sys v0.8.0
golang.org/x/time v0.3.0
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.31.0
golang.org/x/sys v0.28.0
golang.org/x/time v0.8.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20230603040744-5c9219dedd33
gvisor.dev/gvisor v0.0.0-20241108012608-5901531b85ba
)
require (
github.com/ajg/form v1.5.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
)
+41 -36
View File
@@ -1,58 +1,63 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.8 h1:Ixejp5JscEc866gAvm/l6TFd7BOBvDviKgwb1quWw3g=
github.com/Dreamacro/go-shadowsocks2 v0.1.8/go.mod h1:51y4Q6tJoCE7e8TmYXcQRqfoxPfE9Cvn79V6pB6Df7Y=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230603040744-5c9219dedd33 h1:64QentohifmKGeTgJCHilDgfmQVuYE45fsaS9psJ3zY=
gvisor.dev/gvisor v0.0.0-20230603040744-5c9219dedd33/go.mod h1:sQuqOkxbfJq/GS2uSnqHphtXclHyk/ZrAGhZBxxsq6g=
gvisor.dev/gvisor v0.0.0-20241108012608-5901531b85ba h1:ZXzGZ6Md+Vspe9x45C6QlpCdpwYC6nHFtWIfZzTqB0k=
gvisor.dev/gvisor v0.0.0-20241108012608-5901531b85ba/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
+38
View File
@@ -0,0 +1,38 @@
// Package pool provides internal pool utilities.
package pool
import (
"sync"
)
// A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed
// object pooling.
//
// Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will
// not be detected, so all internal pool use must take care to only store
// pointer types.
type Pool[T any] struct {
pool sync.Pool
}
// New returns a new [Pool] for T, and will use fn to construct new Ts when
// the pool is empty.
func New[T any](fn func() T) *Pool[T] {
return &Pool[T]{
pool: sync.Pool{
New: func() any {
return fn()
},
},
}
}
// Get gets a T from the pool, or creates a new one if the pool is empty.
func (p *Pool[T]) Get() T {
return p.pool.Get().(T)
}
// Put returns x into the pool.
func (p *Pool[T]) Put(x T) {
p.pool.Put(x)
}
+85
View File
@@ -0,0 +1,85 @@
package pool
import (
"runtime/debug"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
type pooledValue[T any] struct {
value T
}
func TestNew(t *testing.T) {
// Disable GC to avoid the victim cache during the test.
defer debug.SetGCPercent(debug.SetGCPercent(-1))
p := New(func() *pooledValue[string] {
return &pooledValue[string]{
value: "new",
}
})
// Probabilistically, 75% of sync.Pool.Put calls will succeed when -race
// is enabled (see ref below); attempt to make this quasi-deterministic by
// brute force (i.e., put significantly more objects in the pool than we
// will need for the test) in order to avoid testing without race enabled.
//
// ref: https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/sync/pool.go;l=100-103
for i := 0; i < 1_000; i++ {
p.Put(&pooledValue[string]{
value: t.Name(),
})
}
// Ensure that we always get the expected value. Note that this must only
// run a fraction of the number of times that Put is called above.
for i := 0; i < 10; i++ {
func() {
x := p.Get()
defer p.Put(x)
require.Equal(t, t.Name(), x.value)
}()
}
// Depool all objects that might be in the pool to ensure that it's empty.
for i := 0; i < 1_000; i++ {
p.Get()
}
// Now that the pool is empty, it should use the value specified in the
// underlying sync.Pool.New func.
require.Equal(t, "new", p.Get().value)
}
func TestNew_Race(t *testing.T) {
p := New(func() *pooledValue[int] {
return &pooledValue[int]{
value: -1,
}
})
var wg sync.WaitGroup
defer wg.Wait()
// Run a number of goroutines that read and write pool object fields to
// tease out races.
for i := 0; i < 1_000; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
x := p.Get()
defer p.Put(x)
// Must both read and write the field.
if n := x.value; n >= -1 {
x.value = i
}
}()
}
}
-7
View File
@@ -1,7 +0,0 @@
//go:build debug
package version
func init() {
_debug = true
}
+1 -14
View File
@@ -9,8 +9,6 @@ import (
const Name = "tun2socks"
var (
_debug = false
// Version can be set at link time by executing
// the command: `git describe --abbrev=0 --tags HEAD`
Version string
@@ -20,19 +18,8 @@ var (
GitCommit string
)
func versionize(s string) string {
return strings.TrimPrefix(s, "v")
}
func Debug() bool {
return _debug
}
func String() string {
if !Debug() {
return fmt.Sprintf("%s-%s", Name, versionize(Version))
}
return fmt.Sprintf("%s-%s (debug)", Name, versionize(Version))
return fmt.Sprintf("%s-%s", Name, strings.TrimPrefix(Version, "v"))
}
func BuildString() string {
+2
View File
@@ -0,0 +1,2 @@
// Package log is a thin wrapper based on "go.uber.org/zap".
package log
+16 -13
View File
@@ -1,7 +1,6 @@
package log
import (
"io"
"runtime"
"strings"
"time"
@@ -9,26 +8,30 @@ import (
glog "gvisor.dev/gvisor/pkg/log"
)
var _globalE = &emitter{}
func init() {
EnableStackLog(true)
glog.SetTarget(_globalE)
}
func EnableStackLog(v bool) {
if v {
glog.SetTarget(&emitter{}) // built-in logger
} else {
glog.SetTarget(&glog.Writer{Next: io.Discard})
}
type emitter struct {
logger *SugaredLogger
}
type emitter struct{}
func (e *emitter) setLogger(logger *SugaredLogger) {
e.logger = logger.WithOptions(pkgCallerSkip)
}
func (emitter) Emit(depth int, level glog.Level, _ time.Time, format string, args ...any) {
func (e *emitter) logf(level glog.Level, format string, args ...any) {
e.logger.Logf(1-Level(level), "[STACK] "+format, args...)
}
func (e *emitter) Emit(depth int, level glog.Level, _ time.Time, format string, args ...any) {
if _, file, line, ok := runtime.Caller(depth + 1); ok {
// Ignore (*gonet.TCPConn).RemoteAddr() warning: `ep.GetRemoteAddress() failed`.
if line == 457 && strings.HasSuffix(file, "/pkg/tcpip/adapters/gonet/gonet.go") {
// Ignore: gvisor.dev/gvisor/pkg/tcpip/adapters/gonet/gonet.go:457
if line == 457 && strings.HasSuffix(file, "gonet/gonet.go") {
return
}
}
logf(Level(level)+2, "[STACK] "+format, args...)
e.logf(level, format, args...)
}
-39
View File
@@ -1,39 +0,0 @@
package log
import (
"fmt"
"time"
"github.com/xjasonlyu/tun2socks/v2/common/observable"
)
var (
_logCh = make(chan any)
_source = observable.NewObservable(_logCh)
)
type Event struct {
Level Level `json:"level"`
Message string `json:"msg"`
Time time.Time `json:"time"`
}
func newEvent(level Level, format string, args ...any) *Event {
event := &Event{
Level: level,
Time: time.Now(),
Message: fmt.Sprintf(format, args...),
}
_logCh <- event /* send all events to logCh */
return event
}
func Subscribe() observable.Subscription {
sub, _ := _source.Subscribe()
return sub
}
func UnSubscribe(sub observable.Subscription) {
_source.UnSubscribe(sub)
}
+18 -59
View File
@@ -1,72 +1,31 @@
package log
import (
"encoding/json"
"fmt"
"strings"
"go.uber.org/zap/zapcore"
)
type Level uint32
// Level is an alias for zapcore.Level.
type Level = zapcore.Level
// Levels are aliases for Level.
const (
SilentLevel Level = iota
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
DebugLevel = zapcore.DebugLevel
InfoLevel = zapcore.InfoLevel
WarnLevel = zapcore.WarnLevel
ErrorLevel = zapcore.ErrorLevel
DPanicLevel = zapcore.DPanicLevel
PanicLevel = zapcore.PanicLevel
FatalLevel = zapcore.FatalLevel
InvalidLevel = zapcore.InvalidLevel
SilentLevel = InvalidLevel + 1
)
// UnmarshalJSON deserialize Level with json
func (level *Level) UnmarshalJSON(data []byte) error {
var lvl string
if err := json.Unmarshal(data, &lvl); err != nil {
return err
}
l, err := ParseLevel(lvl)
if err != nil {
return err
}
*level = l
return nil
}
// MarshalJSON serialize Level with json
func (level Level) MarshalJSON() ([]byte, error) {
return json.Marshal(level.String())
}
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case SilentLevel:
return "silent"
default:
return fmt.Sprintf("not a valid level %d", level)
}
}
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "silent":
// ParseLevel is a thin wrapper for zapcore.ParseLevel.
func ParseLevel(text string) (Level, error) {
switch text {
case "silent", "SILENT":
return SilentLevel, nil
case "error":
return ErrorLevel, nil
case "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
default:
return Level(0), fmt.Errorf("not a valid logrus Level: %q", lvl)
return zapcore.ParseLevel(text)
}
}
+58 -50
View File
@@ -1,63 +1,71 @@
package log
import (
"io"
"os"
"fmt"
"sync"
"github.com/sirupsen/logrus"
"go.uber.org/atomic"
"go.uber.org/zap"
)
// _defaultLevel is package default logging level.
var _defaultLevel = atomic.NewUint32(uint32(InfoLevel))
// global Logger and SugaredLogger.
var (
_globalMu sync.RWMutex
_globalL *Logger
_globalS *SugaredLogger
)
func init() {
logrus.SetOutput(os.Stdout)
logrus.SetLevel(logrus.DebugLevel)
SetLogger(zap.Must(zap.NewProduction()))
}
func SetOutput(out io.Writer) {
logrus.SetOutput(out)
}
func SetLevel(level Level) {
_defaultLevel.Store(uint32(level))
}
func Debugf(format string, args ...any) {
logf(DebugLevel, format, args...)
}
func Infof(format string, args ...any) {
logf(InfoLevel, format, args...)
}
func Warnf(format string, args ...any) {
logf(WarnLevel, format, args...)
}
func Errorf(format string, args ...any) {
logf(ErrorLevel, format, args...)
}
func Fatalf(format string, args ...any) {
logrus.Fatalf(format, args...)
}
func logf(level Level, format string, args ...any) {
event := newEvent(level, format, args...)
if uint32(event.Level) > _defaultLevel.Load() {
return
}
switch level {
func NewLeveled(l Level, options ...Option) (*Logger, error) {
switch l {
case SilentLevel:
return zap.NewNop(), nil
case DebugLevel:
logrus.WithTime(event.Time).Debugln(event.Message)
case InfoLevel:
logrus.WithTime(event.Time).Infoln(event.Message)
case WarnLevel:
logrus.WithTime(event.Time).Warnln(event.Message)
case ErrorLevel:
logrus.WithTime(event.Time).Errorln(event.Message)
return zap.NewDevelopment(options...)
case InfoLevel, WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, FatalLevel:
cfg := zap.NewProductionConfig()
cfg.Level.SetLevel(l)
return cfg.Build(options...)
default:
return nil, fmt.Errorf("invalid level: %s", l)
}
}
// SetLogger sets the global Logger and SugaredLogger.
func SetLogger(logger *Logger) {
_globalMu.Lock()
defer _globalMu.Unlock()
// apply pkgCallerSkip to global loggers.
_globalL = logger.WithOptions(pkgCallerSkip)
_globalS = _globalL.Sugar()
_globalE.setLogger(_globalS)
}
func logf(lvl Level, template string, args ...any) {
_globalMu.RLock()
s := _globalS
_globalMu.RUnlock()
s.Logf(lvl, template, args...)
}
func Debugf(template string, args ...any) {
logf(DebugLevel, template, args...)
}
func Infof(template string, args ...any) {
logf(InfoLevel, template, args...)
}
func Warnf(template string, args ...any) {
logf(WarnLevel, template, args...)
}
func Errorf(template string, args ...any) {
logf(ErrorLevel, template, args...)
}
func Fatalf(template string, args ...any) {
logf(FatalLevel, template, args...)
}
+22
View File
@@ -0,0 +1,22 @@
package log
import (
"go.uber.org/zap"
)
// Must is an alias for zap.Must.
var Must = zap.Must
// logger aliases for zap.Logger and zap.SugaredLogger.
type (
Logger = zap.Logger
SugaredLogger = zap.SugaredLogger
)
type (
// Option is an alias for zap.Option.
Option = zap.Option
)
// pkgCallerSkip skips the pkg wrapper code as the caller.
var pkgCallerSkip = zap.AddCallerSkip(2)
+2 -1
View File
@@ -30,12 +30,13 @@ func init() {
flag.StringVar(&configFile, "config", "", "YAML format configuration file")
flag.StringVar(&key.Device, "device", "", "Use this device [driver://]name")
flag.StringVar(&key.Interface, "interface", "", "Use network INTERFACE (Linux/MacOS only)")
flag.StringVar(&key.LogLevel, "loglevel", "info", "Log level [debug|info|warning|error|silent]")
flag.StringVar(&key.LogLevel, "loglevel", "info", "Log level [debug|info|warn|error|silent]")
flag.StringVar(&key.Proxy, "proxy", "", "Use this proxy [protocol://]host[:port]")
flag.StringVar(&key.RestAPI, "restapi", "", "HTTP statistic server listen address")
flag.StringVar(&key.TCPSendBufferSize, "tcp-sndbuf", "", "Set TCP send buffer size for netstack")
flag.StringVar(&key.TCPReceiveBufferSize, "tcp-rcvbuf", "", "Set TCP receive buffer size for netstack")
flag.BoolVar(&key.TCPModerateReceiveBuffer, "tcp-auto-tuning", false, "Enable TCP receive buffer auto-tuning")
flag.StringVar(&key.MulticastGroups, "multicast-groups", "", "Set multicast groups, separated by commas")
flag.StringVar(&key.TUNPreUp, "tun-pre-up", "", "Execute a command before TUN device setup")
flag.StringVar(&key.TUNPostUp, "tun-post-up", "", "Execute a command after TUN device setup")
flag.BoolVar(&versionFlag, "version", false, "Show version and then quit")
+22 -20
View File
@@ -2,26 +2,34 @@ package metadata
import (
"net"
"strconv"
"net/netip"
)
// Metadata contains metadata of transport protocol sessions.
type Metadata struct {
Network Network `json:"network"`
SrcIP net.IP `json:"sourceIP"`
MidIP net.IP `json:"dialerIP"`
DstIP net.IP `json:"destinationIP"`
SrcPort uint16 `json:"sourcePort"`
MidPort uint16 `json:"dialerPort"`
DstPort uint16 `json:"destinationPort"`
Network Network `json:"network"`
SrcIP netip.Addr `json:"sourceIP"`
MidIP netip.Addr `json:"dialerIP"`
DstIP netip.Addr `json:"destinationIP"`
SrcPort uint16 `json:"sourcePort"`
MidPort uint16 `json:"dialerPort"`
DstPort uint16 `json:"destinationPort"`
}
func (m *Metadata) DestinationAddrPort() netip.AddrPort {
return netip.AddrPortFrom(m.DstIP, m.DstPort)
}
func (m *Metadata) DestinationAddress() string {
return net.JoinHostPort(m.DstIP.String(), strconv.FormatUint(uint64(m.DstPort), 10))
return m.DestinationAddrPort().String()
}
func (m *Metadata) SourceAddrPort() netip.AddrPort {
return netip.AddrPortFrom(m.SrcIP, m.SrcPort)
}
func (m *Metadata) SourceAddress() string {
return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10))
return m.SourceAddrPort().String()
}
func (m *Metadata) Addr() net.Addr {
@@ -29,23 +37,17 @@ func (m *Metadata) Addr() net.Addr {
}
func (m *Metadata) TCPAddr() *net.TCPAddr {
if m.Network != TCP || m.DstIP == nil {
if m.Network != TCP || !m.DstIP.IsValid() {
return nil
}
return &net.TCPAddr{
IP: m.DstIP,
Port: int(m.DstPort),
}
return net.TCPAddrFromAddrPort(m.DestinationAddrPort())
}
func (m *Metadata) UDPAddr() *net.UDPAddr {
if m.Network != UDP || m.DstIP == nil {
if m.Network != UDP || !m.DstIP.IsValid() {
return nil
}
return &net.UDPAddr{
IP: m.DstIP,
Port: int(m.DstPort),
}
return net.UDPAddrFromAddrPort(m.DestinationAddrPort())
}
// Addr implements the net.Addr interface.
+2 -2
View File
@@ -25,9 +25,9 @@ func (b *Base) Proto() proto.Proto {
}
func (b *Base) DialContext(context.Context, *M.Metadata) (net.Conn, error) {
return nil, errors.New("not supported")
return nil, errors.ErrUnsupported
}
func (b *Base) DialUDP(*M.Metadata) (net.PacketConn, error) {
return nil, errors.New("not supported")
return nil, errors.ErrUnsupported
}
+4 -2
View File
@@ -41,7 +41,9 @@ func (h *HTTP) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Con
}
setKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
err = h.shakeHand(metadata, c)
return
@@ -61,7 +63,7 @@ func (h *HTTP) shakeHand(metadata *M.Metadata, rw io.ReadWriter) error {
}
if h.user != "" && h.pass != "" {
req.Header.Set("Proxy-Authorization", basicAuth(h.user, h.pass))
req.Header.Set("Proxy-Authorization", fmt.Sprintf("Basic %s", basicAuth(h.user, h.pass)))
}
if err := req.Write(rw); err != nil {
+3
View File
@@ -9,6 +9,7 @@ const (
Socks4
Socks5
Shadowsocks
Relay
)
type Proto uint8
@@ -27,6 +28,8 @@ func (proto Proto) String() string {
return "socks5"
case Shadowsocks:
return "ss"
case Relay:
return "relay"
default:
return fmt.Sprintf("proto(%d)", proto)
}
+252
View File
@@ -0,0 +1,252 @@
package proxy
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"net"
"sync"
"github.com/go-gost/relay"
"github.com/xjasonlyu/tun2socks/v2/buffer"
"github.com/xjasonlyu/tun2socks/v2/dialer"
M "github.com/xjasonlyu/tun2socks/v2/metadata"
"github.com/xjasonlyu/tun2socks/v2/proxy/proto"
)
var _ Proxy = (*Relay)(nil)
type Relay struct {
*Base
user string
pass string
noDelay bool
}
func NewRelay(addr, user, pass string, noDelay bool) (*Relay, error) {
return &Relay{
Base: &Base{
addr: addr,
proto: proto.Relay,
},
user: user,
pass: pass,
noDelay: noDelay,
}, nil
}
func (rl *Relay) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
return rl.dialContext(ctx, metadata)
}
func (rl *Relay) DialUDP(metadata *M.Metadata) (net.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
defer cancel()
return rl.dialContext(ctx, metadata)
}
func (rl *Relay) dialContext(ctx context.Context, metadata *M.Metadata) (rc *relayConn, err error) {
var c net.Conn
c, err = dialer.DialContext(ctx, "tcp", rl.Addr())
if err != nil {
return nil, fmt.Errorf("connect to %s: %w", rl.Addr(), err)
}
setKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
req := relay.Request{
Version: relay.Version1,
Cmd: relay.CmdConnect,
}
if metadata.Network == M.UDP {
req.Cmd |= relay.FUDP
req.Features = append(req.Features, &relay.NetworkFeature{
Network: relay.NetworkUDP,
})
}
if rl.user != "" {
req.Features = append(req.Features, &relay.UserAuthFeature{
Username: rl.user,
Password: rl.pass,
})
}
req.Features = append(req.Features, serializeRelayAddr(metadata))
if rl.noDelay {
if _, err = req.WriteTo(c); err != nil {
return
}
if err = readRelayResponse(c); err != nil {
return
}
}
switch metadata.Network {
case M.TCP:
rc = newRelayConn(c, metadata.Addr(), rl.noDelay, false)
if !rl.noDelay {
if _, err = req.WriteTo(rc.wbuf); err != nil {
return
}
}
case M.UDP:
rc = newRelayConn(c, metadata.Addr(), rl.noDelay, true)
if !rl.noDelay {
if _, err = req.WriteTo(rc.wbuf); err != nil {
return
}
}
default:
err = fmt.Errorf("network %s is unsupported", metadata.Network)
return
}
return
}
type relayConn struct {
net.Conn
udp bool
addr net.Addr
once sync.Once
wbuf *bytes.Buffer
}
func newRelayConn(c net.Conn, addr net.Addr, noDelay, udp bool) *relayConn {
rc := &relayConn{
Conn: c,
addr: addr,
udp: udp,
}
if !noDelay {
rc.wbuf = &bytes.Buffer{}
}
return rc
}
func (rc *relayConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, err := rc.Read(b)
return n, rc.addr, err
}
func (rc *relayConn) Read(b []byte) (n int, err error) {
rc.once.Do(func() {
if rc.wbuf != nil {
err = readRelayResponse(rc.Conn)
}
})
if err != nil {
return
}
if !rc.udp {
return rc.Conn.Read(b)
}
var bb [2]byte
_, err = io.ReadFull(rc.Conn, bb[:])
if err != nil {
return
}
dLen := int(binary.BigEndian.Uint16(bb[:]))
if len(b) >= dLen {
return io.ReadFull(rc.Conn, b[:dLen])
}
buf := buffer.Get(dLen)
defer buffer.Put(buf)
_, err = io.ReadFull(rc.Conn, buf)
n = copy(b, buf)
return
}
func (rc *relayConn) WriteTo(b []byte, _ net.Addr) (int, error) {
return rc.Write(b)
}
func (rc *relayConn) Write(b []byte) (int, error) {
if rc.udp {
return rc.udpWrite(b)
}
return rc.tcpWrite(b)
}
func (rc *relayConn) tcpWrite(b []byte) (n int, err error) {
if rc.wbuf != nil && rc.wbuf.Len() > 0 {
n = len(b)
rc.wbuf.Write(b)
_, err = rc.Conn.Write(rc.wbuf.Bytes())
rc.wbuf.Reset()
return
}
return rc.Conn.Write(b)
}
func (rc *relayConn) udpWrite(b []byte) (n int, err error) {
if len(b) > math.MaxUint16 {
err = errors.New("write: data maximum exceeded")
return
}
n = len(b)
if rc.wbuf != nil && rc.wbuf.Len() > 0 {
var bb [2]byte
binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
rc.wbuf.Write(bb[:])
rc.wbuf.Write(b)
_, err = rc.wbuf.WriteTo(rc.Conn)
return
}
var bb [2]byte
binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
_, err = rc.Conn.Write(bb[:])
if err != nil {
return
}
return rc.Conn.Write(b)
}
func readRelayResponse(r io.Reader) error {
resp := relay.Response{}
if _, err := resp.ReadFrom(r); err != nil {
return err
}
if resp.Version != relay.Version1 {
return relay.ErrBadVersion
}
if resp.Status != relay.StatusOK {
return fmt.Errorf("status %d", resp.Status)
}
return nil
}
func serializeRelayAddr(m *M.Metadata) *relay.AddrFeature {
af := &relay.AddrFeature{
Host: m.DstIP.String(),
Port: m.DstPort,
}
if m.DstIP.Is4() {
af.AType = relay.AddrIPv4
} else {
af.AType = relay.AddrIPv6
}
return af
}
+4 -3
View File
@@ -6,11 +6,10 @@ import (
"fmt"
"net"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/xjasonlyu/tun2socks/v2/dialer"
M "github.com/xjasonlyu/tun2socks/v2/metadata"
"github.com/xjasonlyu/tun2socks/v2/proxy/proto"
"github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/core"
obfs "github.com/xjasonlyu/tun2socks/v2/transport/simple-obfs"
"github.com/xjasonlyu/tun2socks/v2/transport/socks5"
)
@@ -50,7 +49,9 @@ func (ss *Shadowsocks) DialContext(ctx context.Context, metadata *M.Metadata) (c
}
setKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
switch ss.obfsMode {
case "tls":
+3 -1
View File
@@ -36,7 +36,9 @@ func (ss *Socks4) DialContext(ctx context.Context, metadata *M.Metadata) (c net.
}
setKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
err = socks4.ClientHandshake(c, metadata.DestinationAddress(), socks4.CmdConnect, ss.userID)
return
+4 -2
View File
@@ -49,7 +49,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *M.Metadata) (c net.
}
setKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
var user *socks5.User
if ss.user != "" {
@@ -65,7 +67,7 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *M.Metadata) (c net.
func (ss *Socks5) DialUDP(*M.Metadata) (_ net.PacketConn, err error) {
if ss.unix {
return nil, errors.New("not supported when unix domain socket is enabled")
return nil, fmt.Errorf("%w when unix domain socket is enabled", errors.ErrUnsupported)
}
ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
+1 -1
View File
@@ -17,7 +17,7 @@ import (
const defaultInterval = 1000
func init() {
registerMountPoint("/connections", connectionRouter())
registerEndpoint("/connections", connectionRouter())
}
func connectionRouter() http.Handler {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
func init() {
registerMountPoint("/debug/pprof/", pprofRouter())
registerEndpoint("/debug/pprof/", pprofRouter())
}
func pprofRouter() http.Handler {
+1 -1
View File
@@ -19,7 +19,7 @@ func SetStatsFunc(s func() tcpip.Stats) {
}
func init() {
registerMountPoint("/netstats", http.HandlerFunc(getNetStats))
registerEndpoint("/netstats", http.HandlerFunc(getNetStats))
}
func getNetStats(w http.ResponseWriter, r *http.Request) {
+4 -62
View File
@@ -14,7 +14,6 @@ import (
"github.com/gorilla/websocket"
V "github.com/xjasonlyu/tun2socks/v2/internal/version"
"github.com/xjasonlyu/tun2socks/v2/log"
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
)
@@ -25,11 +24,11 @@ var (
},
}
_mountPoints = make(map[string]http.Handler)
_endpoints = make(map[string]http.Handler)
)
func registerMountPoint(pattern string, handler http.Handler) {
_mountPoints[pattern] = handler
func registerEndpoint(pattern string, handler http.Handler) {
_endpoints[pattern] = handler
}
func Start(addr, token string) error {
@@ -46,11 +45,10 @@ func Start(addr, token string) error {
r.Group(func(r chi.Router) {
r.Use(authenticator(token))
r.Get("/", hello)
r.Get("/logs", getLogs)
r.Get("/traffic", traffic)
r.Get("/version", version)
// attach HTTP handlers
for pattern, handler := range _mountPoints {
for pattern, handler := range _endpoints {
r.Mount(pattern, handler)
}
})
@@ -103,61 +101,6 @@ func authenticator(token string) func(http.Handler) http.Handler {
}
}
func getLogs(w http.ResponseWriter, r *http.Request) {
lvl := r.URL.Query().Get("level")
if lvl == "" {
lvl = "info" /* default */
}
level, err := log.ParseLevel(lvl)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
wsConn, err = _upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
sub := log.Subscribe()
defer log.UnSubscribe(sub)
buf := &bytes.Buffer{}
for elm := range sub {
buf.Reset()
e := elm.(*log.Event)
if e.Level > level {
continue
}
if err = json.NewEncoder(buf).Encode(e); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
func traffic(w http.ResponseWriter, r *http.Request) {
var (
err error
@@ -210,7 +153,6 @@ func version(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{
"version": V.Version,
"commit": V.GitCommit,
"debug": V.Debug(),
"modules": V.Info(),
})
}
@@ -0,0 +1,23 @@
package bufferpool
import (
"bytes"
"github.com/xjasonlyu/tun2socks/v2/internal/pool"
)
const _size = 1024 // by default, create 1 KiB buffers
var _pool = pool.New(func() *bytes.Buffer {
return bytes.NewBuffer(make([]byte, 0, _size))
})
func Get() *bytes.Buffer {
buf := _pool.Get()
buf.Reset()
return buf
}
func Put(b *bytes.Buffer) {
_pool.Put(b)
}
+3
View File
@@ -0,0 +1,3 @@
## Embedded go-shadowsocks2
origin https://github.com/riobard/go-shadowsocks2
+164
View File
@@ -0,0 +1,164 @@
package core
import (
"crypto/md5"
"errors"
"net"
"sort"
"strings"
"github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/shadowaead"
"github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/shadowstream"
)
type Cipher interface {
StreamConnCipher
PacketConnCipher
}
type StreamConnCipher interface {
StreamConn(net.Conn) net.Conn
}
type PacketConnCipher interface {
PacketConn(net.PacketConn) net.PacketConn
}
// ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns).
var ErrCipherNotSupported = errors.New("cipher not supported")
const (
aeadAes128Gcm = "AEAD_AES_128_GCM"
aeadAes192Gcm = "AEAD_AES_192_GCM"
aeadAes256Gcm = "AEAD_AES_256_GCM"
aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305"
aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305"
)
// List of AEAD ciphers: key size in bytes and constructor
var aeadList = map[string]struct {
KeySize int
New func([]byte) (shadowaead.Cipher, error)
}{
aeadAes128Gcm: {16, shadowaead.AESGCM},
aeadAes192Gcm: {24, shadowaead.AESGCM},
aeadAes256Gcm: {32, shadowaead.AESGCM},
aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305},
aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305},
}
// List of stream ciphers: key size in bytes and constructor
var streamList = map[string]struct {
KeySize int
New func(key []byte) (shadowstream.Cipher, error)
}{
"RC4-MD5": {16, shadowstream.RC4MD5},
"AES-128-CTR": {16, shadowstream.AESCTR},
"AES-192-CTR": {24, shadowstream.AESCTR},
"AES-256-CTR": {32, shadowstream.AESCTR},
"AES-128-CFB": {16, shadowstream.AESCFB},
"AES-192-CFB": {24, shadowstream.AESCFB},
"AES-256-CFB": {32, shadowstream.AESCFB},
"CHACHA20-IETF": {32, shadowstream.Chacha20IETF},
"XCHACHA20": {32, shadowstream.Xchacha20},
}
// ListCipher returns a list of available cipher names sorted alphabetically.
func ListCipher() []string {
var l []string
for k := range aeadList {
l = append(l, k)
}
for k := range streamList {
l = append(l, k)
}
sort.Strings(l)
return l
}
// PickCipher returns a Cipher of the given name. Derive key from password if given key is empty.
func PickCipher(name string, key []byte, password string) (Cipher, error) {
name = strings.ToUpper(name)
switch name {
case "DUMMY":
return &dummy{}, nil
case "CHACHA20-IETF-POLY1305":
name = aeadChacha20Poly1305
case "XCHACHA20-IETF-POLY1305":
name = aeadXChacha20Poly1305
case "AES-128-GCM":
name = aeadAes128Gcm
case "AES-192-GCM":
name = aeadAes192Gcm
case "AES-256-GCM":
name = aeadAes256Gcm
}
if choice, ok := aeadList[name]; ok {
if len(key) == 0 {
key = Kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowaead.KeySizeError(choice.KeySize)
}
aead, err := choice.New(key)
return &AeadCipher{Cipher: aead, Key: key}, err
}
if choice, ok := streamList[name]; ok {
if len(key) == 0 {
key = Kdf(password, choice.KeySize)
}
if len(key) != choice.KeySize {
return nil, shadowstream.KeySizeError(choice.KeySize)
}
ciph, err := choice.New(key)
return &StreamCipher{Cipher: ciph, Key: key}, err
}
return nil, ErrCipherNotSupported
}
type AeadCipher struct {
shadowaead.Cipher
Key []byte
}
func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) }
func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowaead.NewPacketConn(c, aead)
}
type StreamCipher struct {
shadowstream.Cipher
Key []byte
}
func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) }
func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn {
return shadowstream.NewPacketConn(c, ciph)
}
// dummy cipher does not encrypt
type dummy struct{}
func (dummy) StreamConn(c net.Conn) net.Conn { return c }
func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c }
// key-derivation function from original Shadowsocks
func Kdf(password string, keyLen int) []byte {
var b, prev []byte
h := md5.New()
for len(b) < keyLen {
h.Write(prev)
h.Write([]byte(password))
b = h.Sum(b)
prev = b[len(b)-h.Size():]
h.Reset()
}
return b[:keyLen]
}
+94
View File
@@ -0,0 +1,94 @@
package shadowaead
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"io"
"strconv"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
type Cipher interface {
KeySize() int
SaltSize() int
Encrypter(salt []byte) (cipher.AEAD, error)
Decrypter(salt []byte) (cipher.AEAD, error)
}
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
func hkdfSHA1(secret, salt, info, outkey []byte) {
r := hkdf.New(sha1.New, secret, salt, info)
if _, err := io.ReadFull(r, outkey); err != nil {
panic(err) // should never happen
}
}
type metaCipher struct {
psk []byte
makeAEAD func(key []byte) (cipher.AEAD, error)
}
func (a *metaCipher) KeySize() int { return len(a.psk) }
func (a *metaCipher) SaltSize() int {
if ks := a.KeySize(); ks > 16 {
return ks
}
return 16
}
func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
subkey := make([]byte, a.KeySize())
hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
return a.makeAEAD(subkey)
}
func aesGCM(key []byte) (cipher.AEAD, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(blk)
}
// AESGCM creates a new Cipher with a pre-shared key. len(psk) must be
// one of 16, 24, or 32 to select AES-128/196/256-GCM.
func AESGCM(psk []byte) (Cipher, error) {
switch l := len(psk); l {
case 16, 24, 32: // AES 128/196/256
default:
return nil, aes.KeySizeError(l)
}
return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil
}
// Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func Chacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil
}
// XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
// must be 32.
func XChacha20Poly1305(psk []byte) (Cipher, error) {
if len(psk) != chacha20poly1305.KeySize {
return nil, KeySizeError(chacha20poly1305.KeySize)
}
return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil
}
+95
View File
@@ -0,0 +1,95 @@
package shadowaead
import (
"crypto/rand"
"errors"
"io"
"net"
"github.com/xjasonlyu/tun2socks/v2/buffer"
)
// ErrShortPacket means that the packet is too short for a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
var _zerononce [128]byte // read-only. 128 bytes is more than enough.
// Pack encrypts plaintext using Cipher with a randomly generated salt and
// returns a slice of dst containing the encrypted packet and any error occurred.
// Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead().
func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
salt := dst[:saltSize]
if _, err := rand.Read(salt); err != nil {
return nil, err
}
aead, err := ciph.Encrypter(salt)
if err != nil {
return nil, err
}
if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
return nil, io.ErrShortBuffer
}
b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil)
return dst[:saltSize+len(b)], nil
}
// Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred.
// Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead().
func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
saltSize := ciph.SaltSize()
if len(pkt) < saltSize {
return nil, ErrShortPacket
}
salt := pkt[:saltSize]
aead, err := ciph.Decrypter(salt)
if err != nil {
return nil, err
}
if len(pkt) < saltSize+aead.Overhead() {
return nil, ErrShortPacket
}
if saltSize+len(dst)+aead.Overhead() < len(pkt) {
return nil, io.ErrShortBuffer
}
b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil)
return b, err
}
type PacketConn struct {
net.PacketConn
Cipher
}
const maxPacketSize = 64 * 1024
// NewPacketConn wraps a net.PacketConn with cipher
func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
return &PacketConn{PacketConn: c, Cipher: ciph}
}
// WriteTo encrypts b and write to addr using the embedded PacketConn.
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
buf := buffer.Get(maxPacketSize)
defer buffer.Put(buf)
buf, err := Pack(buf, b, c)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
// ReadFrom reads from the embedded PacketConn and decrypts into b.
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, addr, err
}
bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}
+285
View File
@@ -0,0 +1,285 @@
package shadowaead
import (
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"net"
"github.com/xjasonlyu/tun2socks/v2/buffer"
)
const (
// payloadSizeMask is the maximum size of payload in bytes.
payloadSizeMask = 0x3FFF // 16*1024 - 1
bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead()
)
var ErrZeroChunk = errors.New("zero chunk")
type Writer struct {
io.Writer
cipher.AEAD
nonce [32]byte // should be sufficient for most nonce sizes
}
// NewWriter wraps an io.Writer with authenticated encryption.
func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} }
// Write encrypts p and writes to the embedded io.Writer.
func (w *Writer) Write(p []byte) (n int, err error) {
buf := buffer.Get(bufSize)
defer buffer.Put(buf)
nonce := w.nonce[:w.NonceSize()]
tag := w.Overhead()
off := 2 + tag
// compatible with snell
if len(p) == 0 {
buf = buf[:off]
buf[0], buf[1] = byte(0), byte(0)
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
_, err = w.Writer.Write(buf)
return
}
for nr := 0; n < len(p) && err == nil; n += nr {
nr = payloadSizeMask
if n+nr > len(p) {
nr = len(p) - n
}
buf = buf[:off+nr+tag]
buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
w.Seal(buf[:off], nonce, p[n:n+nr], nil)
increment(nonce)
_, err = w.Writer.Write(buf)
}
return
}
// ReadFrom reads from the given io.Reader until EOF or error, encrypts and
// writes to the embedded io.Writer. Returns number of bytes read from r and
// any error encountered.
func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
buf := buffer.Get(bufSize)
defer buffer.Put(buf)
nonce := w.nonce[:w.NonceSize()]
tag := w.Overhead()
off := 2 + tag
for {
nr, er := r.Read(buf[off : off+payloadSizeMask])
n += int64(nr)
buf[0], buf[1] = byte(nr>>8), byte(nr)
w.Seal(buf[:0], nonce, buf[:2], nil)
increment(nonce)
w.Seal(buf[:off], nonce, buf[off:off+nr], nil)
increment(nonce)
if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil {
err = ew
return
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
err = er
}
return
}
}
}
type Reader struct {
io.Reader
cipher.AEAD
nonce [32]byte // should be sufficient for most nonce sizes
buf []byte // to be put back into bufPool
off int // offset to unconsumed part of buf
}
// NewReader wraps an io.Reader with authenticated decryption.
func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} }
// Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead.
func (r *Reader) read(p []byte) (int, error) {
nonce := r.nonce[:r.NonceSize()]
tag := r.Overhead()
// decrypt payload size
p = p[:2+tag]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
_, err := r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
// decrypt payload
size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask
if size == 0 {
return 0, ErrZeroChunk
}
p = p[:size+tag]
if _, err := io.ReadFull(r.Reader, p); err != nil {
return 0, err
}
_, err = r.Open(p[:0], nonce, p, nil)
increment(nonce)
if err != nil {
return 0, err
}
return size, nil
}
// Read reads from the embedded io.Reader, decrypts and writes to p.
func (r *Reader) Read(p []byte) (int, error) {
if r.buf == nil {
if len(p) >= payloadSizeMask+r.Overhead() {
return r.read(p)
}
b := buffer.Get(bufSize)
n, err := r.read(b)
if err != nil {
return 0, err
}
r.buf = b[:n]
r.off = 0
}
n := copy(p, r.buf[r.off:])
r.off += n
if r.off == len(r.buf) {
buffer.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
}
// WriteTo reads from the embedded io.Reader, decrypts and writes to w until
// there's no more data to write or when an error occurs. Return number of
// bytes written to w and any error encountered.
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
if r.buf == nil {
r.buf = buffer.Get(bufSize)
r.off = len(r.buf)
}
for {
for r.off < len(r.buf) {
nw, ew := w.Write(r.buf[r.off:])
r.off += nw
n += int64(nw)
if ew != nil {
if r.off == len(r.buf) {
buffer.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
err = ew
return
}
}
nr, er := r.read(r.buf)
if er != nil {
if er != io.EOF {
err = er
}
return
}
r.buf = r.buf[:nr]
r.off = 0
}
}
// increment little-endian encoded unsigned integer b. Wrap around on overflow.
func increment(b []byte) {
for i := range b {
b[i]++
if b[i] != 0 {
return
}
}
}
type Conn struct {
net.Conn
Cipher
r *Reader
w *Writer
}
// NewConn wraps a stream-oriented net.Conn with cipher.
func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
func (c *Conn) initReader() error {
salt := make([]byte, c.SaltSize())
if _, err := io.ReadFull(c.Conn, salt); err != nil {
return err
}
aead, err := c.Decrypter(salt)
if err != nil {
return err
}
c.r = NewReader(c.Conn, aead)
return nil
}
func (c *Conn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.WriteTo(w)
}
func (c *Conn) initWriter() error {
salt := make([]byte, c.SaltSize())
if _, err := rand.Read(salt); err != nil {
return err
}
aead, err := c.Encrypter(salt)
if err != nil {
return err
}
_, err = c.Conn.Write(salt)
if err != nil {
return err
}
c.w = NewWriter(c.Conn, aead)
return nil
}
func (c *Conn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}
func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.ReadFrom(r)
}
+116
View File
@@ -0,0 +1,116 @@
package shadowstream
import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rc4"
"strconv"
"golang.org/x/crypto/chacha20"
)
// Cipher generates a pair of stream ciphers for encryption and decryption.
type Cipher interface {
IVSize() int
Encrypter(iv []byte) cipher.Stream
Decrypter(iv []byte) cipher.Stream
}
type KeySizeError int
func (e KeySizeError) Error() string {
return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
}
// CTR mode
type ctrStream struct{ cipher.Block }
func (b *ctrStream) IVSize() int { return b.BlockSize() }
func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) }
func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) }
func AESCTR(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return &ctrStream{blk}, nil
}
// CFB mode
type cfbStream struct{ cipher.Block }
func (b *cfbStream) IVSize() int { return b.BlockSize() }
func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) }
func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) }
func AESCFB(key []byte) (Cipher, error) {
blk, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return &cfbStream{blk}, nil
}
// IETF-variant of chacha20
type chacha20ietfkey []byte
func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize }
func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
if err != nil {
panic(err) // should never happen
}
return ciph
}
func Chacha20IETF(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
}
return chacha20ietfkey(key), nil
}
type xchacha20key []byte
func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX }
func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
func (k xchacha20key) Encrypter(iv []byte) cipher.Stream {
ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
if err != nil {
panic(err) // should never happen
}
return ciph
}
func Xchacha20(key []byte) (Cipher, error) {
if len(key) != chacha20.KeySize {
return nil, KeySizeError(chacha20.KeySize)
}
return xchacha20key(key), nil
}
type rc4Md5Key []byte
func (k rc4Md5Key) IVSize() int {
return 16
}
func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream {
h := md5.New()
h.Write([]byte(k))
h.Write(iv)
rc4key := h.Sum(nil)
c, _ := rc4.NewCipher(rc4key)
return c
}
func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream {
return k.Encrypter(iv)
}
func RC4MD5(key []byte) (Cipher, error) {
return rc4Md5Key(key), nil
}
+79
View File
@@ -0,0 +1,79 @@
package shadowstream
import (
"crypto/rand"
"errors"
"io"
"net"
"github.com/xjasonlyu/tun2socks/v2/buffer"
)
// ErrShortPacket means the packet is too short to be a valid encrypted packet.
var ErrShortPacket = errors.New("short packet")
// Pack encrypts plaintext using stream cipher s and a random IV.
// Returns a slice of dst containing random IV and ciphertext.
// Ensure len(dst) >= s.IVSize() + len(plaintext).
func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) {
if len(dst) < s.IVSize()+len(plaintext) {
return nil, io.ErrShortBuffer
}
iv := dst[:s.IVSize()]
_, err := rand.Read(iv)
if err != nil {
return nil, err
}
s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext)
return dst[:len(iv)+len(plaintext)], nil
}
// Unpack decrypts pkt using stream cipher s.
// Returns a slice of dst containing decrypted plaintext.
func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) {
if len(pkt) < s.IVSize() {
return nil, ErrShortPacket
}
if len(dst) < len(pkt)-s.IVSize() {
return nil, io.ErrShortBuffer
}
iv := pkt[:s.IVSize()]
s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):])
return dst[:len(pkt)-len(iv)], nil
}
type PacketConn struct {
net.PacketConn
Cipher
}
// NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption.
func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
return &PacketConn{PacketConn: c, Cipher: ciph}
}
const maxPacketSize = 64 * 1024
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
buf := buffer.Get(maxPacketSize)
defer buffer.Put(buf)
buf, err := Pack(buf, b, c.Cipher)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf, addr)
return len(b), err
}
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, addr, err
}
bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher)
if err != nil {
return n, addr, err
}
copy(b, bb)
return len(bb), addr, err
}
+197
View File
@@ -0,0 +1,197 @@
package shadowstream
import (
"crypto/cipher"
"crypto/rand"
"io"
"net"
)
const bufSize = 2048
type Writer struct {
io.Writer
cipher.Stream
buf [bufSize]byte
}
// NewWriter wraps an io.Writer with stream cipher encryption.
func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} }
func (w *Writer) Write(p []byte) (n int, err error) {
buf := w.buf[:]
for nw := 0; n < len(p) && err == nil; n += nw {
end := n + len(buf)
if end > len(p) {
end = len(p)
}
w.XORKeyStream(buf, p[n:end])
nw, err = w.Writer.Write(buf[:end-n])
}
return
}
func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
buf := w.buf[:]
for {
nr, er := r.Read(buf)
n += int64(nr)
b := buf[:nr]
w.XORKeyStream(b, b)
if _, err = w.Writer.Write(b); err != nil {
return
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
err = er
}
return
}
}
}
type Reader struct {
io.Reader
cipher.Stream
buf [bufSize]byte
}
// NewReader wraps an io.Reader with stream cipher decryption.
func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} }
func (r *Reader) Read(p []byte) (n int, err error) {
n, err = r.Reader.Read(p)
if err != nil {
return 0, err
}
r.XORKeyStream(p, p[:n])
return
}
func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
buf := r.buf[:]
for {
nr, er := r.Reader.Read(buf)
if nr > 0 {
r.XORKeyStream(buf, buf[:nr])
nw, ew := w.Write(buf[:nr])
n += int64(nw)
if ew != nil {
err = ew
return
}
}
if er != nil {
if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut)
err = er
}
return
}
}
}
// A Conn represents a Shadowsocks connection. It implements the net.Conn interface.
type Conn struct {
net.Conn
Cipher
r *Reader
w *Writer
readIV []byte
writeIV []byte
}
// NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption.
func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
func (c *Conn) initReader() error {
if c.r == nil {
iv, err := c.ObtainReadIV()
if err != nil {
return err
}
c.r = NewReader(c.Conn, c.Decrypter(iv))
}
return nil
}
func (c *Conn) Read(b []byte) (int, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.Read(b)
}
func (c *Conn) WriteTo(w io.Writer) (int64, error) {
if c.r == nil {
if err := c.initReader(); err != nil {
return 0, err
}
}
return c.r.WriteTo(w)
}
func (c *Conn) initWriter() error {
if c.w == nil {
iv, err := c.ObtainWriteIV()
if err != nil {
return err
}
if _, err := c.Conn.Write(iv); err != nil {
return err
}
c.w = NewWriter(c.Conn, c.Encrypter(iv))
}
return nil
}
func (c *Conn) Write(b []byte) (int, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.Write(b)
}
func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
if c.w == nil {
if err := c.initWriter(); err != nil {
return 0, err
}
}
return c.w.ReadFrom(r)
}
func (c *Conn) ObtainWriteIV() ([]byte, error) {
if len(c.writeIV) == c.IVSize() {
return c.writeIV, nil
}
iv := make([]byte, c.IVSize())
if _, err := rand.Read(iv); err != nil {
return nil, err
}
c.writeIV = iv
return iv, nil
}
func (c *Conn) ObtainReadIV() ([]byte, error) {
if len(c.readIV) == c.IVSize() {
return c.readIV, nil
}
iv := make([]byte, c.IVSize())
if _, err := io.ReadFull(c.Conn, iv); err != nil {
return nil, err
}
c.readIV = iv
return iv, nil
}
+6 -6
View File
@@ -10,7 +10,7 @@ import (
"net"
"net/http"
"github.com/xjasonlyu/tun2socks/v2/common/pool"
"github.com/xjasonlyu/tun2socks/v2/buffer"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
@@ -29,22 +29,22 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
pool.Put(ho.buf)
buffer.Put(ho.buf)
ho.buf = nil
}
return n, nil
}
if ho.firstResponse {
buf := pool.Get(pool.RelayBufferSize)
buf := buffer.Get(buffer.RelayBufferSize)
n, err := ho.Conn.Read(buf)
if err != nil {
pool.Put(buf)
buffer.Put(buf)
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
pool.Put(buf)
buffer.Put(buf)
return 0, io.EOF
}
ho.firstResponse = false
@@ -54,7 +54,7 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
pool.Put(buf)
buffer.Put(buf)
}
return n, nil
}
+8 -7
View File
@@ -1,14 +1,14 @@
package obfs
import (
"bytes"
"crypto/rand"
"encoding/binary"
"io"
"net"
"time"
"github.com/xjasonlyu/tun2socks/v2/common/pool"
"github.com/xjasonlyu/tun2socks/v2/buffer"
"github.com/xjasonlyu/tun2socks/v2/transport/internal/bufferpool"
)
const (
@@ -25,12 +25,12 @@ type TLSObfs struct {
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := pool.Get(discardN)
buf := buffer.Get(discardN)
_, err := io.ReadFull(to.Conn, buf)
if err != nil {
return 0, err
}
pool.Put(buf)
buffer.Put(buf)
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
@@ -99,8 +99,8 @@ func (to *TLSObfs) write(b []byte) (int, error) {
return len(b), err
}
buf := pool.GetBuffer()
defer pool.PutBuffer(buf)
buf := bufferpool.Get()
defer bufferpool.Put(buf)
buf.Write([]byte{0x17, 0x03, 0x03})
binary.Write(buf, binary.BigEndian, uint16(len(b)))
buf.Write(b)
@@ -124,7 +124,8 @@ func makeClientHelloMsg(data []byte, server string) []byte {
rand.Read(random)
rand.Read(sessionID)
buf := &bytes.Buffer{}
buf := bufferpool.Get()
defer bufferpool.Put(buf)
// handshake, TLS 1.0 version, length
buf.WriteByte(22)
+25 -19
View File
@@ -2,12 +2,14 @@
package socks4
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net"
"net/netip"
"strconv"
"github.com/xjasonlyu/tun2socks/v2/transport/internal/bufferpool"
)
const Version = 0x04
@@ -31,6 +33,7 @@ const (
var (
errVersionMismatched = errors.New("version code mismatched")
errIPv6NotSupported = errors.New("IPv6 not supported")
errCmdNotSupported = errors.New("command not supported")
ErrRequestRejected = errors.New("request rejected or failed")
ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client")
@@ -39,6 +42,10 @@ var (
)
func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) {
if command == CmdBind {
return errCmdNotSupported
}
var (
host string
port uint16
@@ -47,24 +54,27 @@ func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID stri
return err
}
ip := net.ParseIP(host)
if ip == nil /* HOST */ {
ip = net.IPv4(0, 0, 0, 1)
} else if ip.To4() == nil /* IPv6 */ {
ip, _ := netip.ParseAddr(host)
switch {
case !ip.IsValid(): /* HOST */
ip = netip.AddrFrom4([4]byte{0, 0, 0, 1})
case ip.Is4In6(): /* IPv4-mapped IPv6 */
ip = netip.AddrFrom4(ip.As4())
case ip.Is4(): /* IPv4 */
case ip.Is6(): /* IPv6 */
return errIPv6NotSupported
}
dstIP := /* [4]byte */ ip.To4()
req := &bytes.Buffer{}
req := bufferpool.Get()
defer bufferpool.Put(req)
req.WriteByte(Version)
req.WriteByte(command)
binary.Write(req, binary.BigEndian, port)
req.Write(dstIP)
_ = binary.Write(req, binary.BigEndian, port)
req.Write(ip.AsSlice())
req.WriteString(userID)
req.WriteByte(0) /* NULL */
req.WriteByte(0x00) /* NULL */
if isReservedIP(dstIP) /* SOCKS4A */ {
if isReservedIP(ip) /* SOCKS4A */ {
req.WriteString(host)
req.WriteByte(0) /* NULL */
}
@@ -103,13 +113,9 @@ func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID stri
// Internet Assigned Numbers Authority -- such an address is inadmissible
// as a destination IP address and thus should never occur if the client
// can resolve the domain name.)
func isReservedIP(ip net.IP) bool {
subnet := net.IPNet{
IP: net.IPv4zero,
Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0x00),
}
return !ip.IsUnspecified() && subnet.Contains(ip)
func isReservedIP(ip netip.Addr) bool {
prefix := netip.PrefixFrom(netip.IPv4Unspecified(), 24)
return !ip.IsUnspecified() && prefix.Contains(ip)
}
func splitHostPort(addr string) (string, uint16, error) {
+84
View File
@@ -0,0 +1,84 @@
package socks4
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsReservedIP(t *testing.T) {
reservedIPs := []string{
"0.0.0.1",
"0.0.0.2",
"0.0.0.50",
"0.0.0.100",
"0.0.0.255",
}
for _, ip := range reservedIPs {
assert.True(t, isReservedIP(netip.MustParseAddr(ip)))
}
unReservedIPs := []string{
"0.0.0.0",
"0.0.1.0",
"1.1.1.1",
"10.0.0.0",
"255.255.255.255",
}
for _, ip := range unReservedIPs {
assert.False(t, isReservedIP(netip.MustParseAddr(ip)))
}
}
func TestSplitHostPort(t *testing.T) {
tests := []struct {
addr string
host string
port uint16
}{
{
"1.1.1.1:80",
"1.1.1.1",
80,
},
{
"1.1.1.1:0",
"1.1.1.1",
0,
},
{
"0.0.0.0:0",
"0.0.0.0",
0,
},
{
"[::1]:443",
"::1",
443,
},
{
"example.com:80",
"example.com",
80,
},
}
for _, tt := range tests {
host, port, err := splitHostPort(tt.addr)
assert.NoError(t, err)
assert.Equal(t, tt.host, host)
assert.Equal(t, tt.port, port)
}
addrs := []string{
"1.1.1.1:-80",
"1.1.1.1:abcd",
"::1:80",
"[::1]",
"example.com",
}
for _, addr := range addrs {
_, _, err := splitHostPort(addr)
assert.Error(t, err)
}
}
+53 -27
View File
@@ -8,7 +8,19 @@ import (
"fmt"
"io"
"net"
"net/netip"
"strconv"
"github.com/xjasonlyu/tun2socks/v2/transport/internal/bufferpool"
)
// AuthMethod is the authentication method as defined in RFC 1928 section 3.
type AuthMethod = uint8
// SOCKS authentication methods as defined in RFC 1928 section 3.
const (
MethodNoAuth AuthMethod = 0x00
MethodUserPass AuthMethod = 0x02
)
// Version is the protocol version as defined in RFC 1928 section 4.
@@ -162,9 +174,9 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
var method uint8
if user != nil {
method = 0x02 /* USERNAME/PASSWORD */
method = MethodUserPass /* USERNAME/PASSWORD */
} else {
method = 0x00 /* NO AUTHENTICATION REQUIRED */
method = MethodNoAuth /* NO AUTHENTICATION REQUIRED */
}
// VER, NMETHODS, METHODS
@@ -181,23 +193,30 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
return nil, errors.New("socks version mismatched")
}
if buf[1] == 0x02 /* USERNAME/PASSWORD */ {
if buf[1] == MethodUserPass /* USERNAME/PASSWORD */ {
if user == nil {
return nil, errors.New("auth required")
}
// password protocol version
authMsg := &bytes.Buffer{}
authMsg.WriteByte(0x01 /* VER */)
authMsg.WriteByte(byte(len(user.Username)) /* ULEN */)
authMsg.WriteString(user.Username /* UNAME */)
authMsg.WriteByte(byte(len(user.Password)) /* PLEN */)
authMsg.WriteString(user.Password /* PASSWD */)
uLen := len(user.Username)
pLen := len(user.Password)
if len(authMsg.Bytes()) > MaxAuthLen {
return nil, errors.New("auth message too long")
// Both ULEN and PLEN are limited to the range [1, 255].
if uLen == 0 || pLen == 0 {
return nil, errors.New("auth username/password empty")
} else if uLen > MaxAuthLen || pLen > MaxAuthLen {
return nil, errors.New("auth username/password too long")
}
// password protocol version
authMsg := bufferpool.Get()
defer bufferpool.Put(authMsg)
authMsg.WriteByte(0x01 /* VER */)
authMsg.WriteByte(byte(uLen) /* ULEN */)
authMsg.WriteString(user.Username /* UNAME */)
authMsg.WriteByte(byte(pLen) /* PLEN */)
authMsg.WriteString(user.Password /* PASSWD */)
if _, err := rw.Write(authMsg.Bytes()); err != nil {
return nil, err
}
@@ -210,12 +229,20 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
return nil, errors.New("rejected username/password")
}
} else if buf[1] != 0x00 /* NO AUTHENTICATION REQUIRED */ {
} else if buf[1] != MethodNoAuth /* NO AUTHENTICATION REQUIRED */ {
return nil, errors.New("unsupported method")
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{{Version, byte(command), 0x00 /* RSV */}, addr}, nil)); err != nil {
req := bufferpool.Get()
defer bufferpool.Put(req)
req.Grow(3 + MaxAddrLen)
req.WriteByte(Version)
req.WriteByte(byte(command))
req.WriteByte(0x00 /* RSV */)
req.Write(addr)
if _, err := rw.Write(req.Bytes()); err != nil {
return nil, err
}
@@ -291,7 +318,7 @@ func SplitAddr(b []byte) Addr {
// SerializeAddr serializes destination address and port to Addr.
// If a domain name is provided, AtypDomainName would be used first.
func SerializeAddr(domainName string, dstIP net.IP, dstPort uint16) Addr {
func SerializeAddr(domainName string, dstIP netip.Addr, dstPort uint16) Addr {
var (
buf [][]byte
port [2]byte
@@ -301,10 +328,10 @@ func SerializeAddr(domainName string, dstIP net.IP, dstPort uint16) Addr {
if domainName != "" /* Domain Name */ {
length := len(domainName)
buf = [][]byte{{AtypDomainName, uint8(length)}, []byte(domainName), port[:]}
} else if dstIP.To4() != nil /* IPv4 */ {
buf = [][]byte{{AtypIPv4}, dstIP.To4(), port[:]}
} else if dstIP.Is4() /* IPv4 */ {
buf = [][]byte{{AtypIPv4}, dstIP.AsSlice(), port[:]}
} else /* IPv6 */ {
buf = [][]byte{{AtypIPv6}, dstIP.To16(), port[:]}
buf = [][]byte{{AtypIPv6}, dstIP.AsSlice(), port[:]}
}
return bytes.Join(buf, nil)
}
@@ -312,14 +339,13 @@ func SerializeAddr(domainName string, dstIP net.IP, dstPort uint16) Addr {
// ParseAddr parses a socks addr from net.Addr.
// This is a fast path of ParseAddrString(addr.String())
func ParseAddr(addr net.Addr) Addr {
switch v := addr.(type) {
case *net.TCPAddr:
return SerializeAddr("", v.IP, uint16(v.Port))
case *net.UDPAddr:
return SerializeAddr("", v.IP, uint16(v.Port))
default:
return ParseAddrString(addr.String())
if v, ok := addr.(interface {
AddrPort() netip.AddrPort
}); ok {
ap := v.AddrPort()
return SerializeAddr("", ap.Addr(), ap.Port())
}
return ParseAddrString(addr.String())
}
// ParseAddrString parses the address in string s to Addr. Returns nil if failed.
@@ -334,10 +360,10 @@ func ParseAddrString(s string) Addr {
return nil
}
if ip := net.ParseIP(host); ip != nil {
if ip, _ := netip.ParseAddr(host); ip.IsValid() {
return SerializeAddr("", ip, uint16(dstPort))
}
return SerializeAddr(host, nil, uint16(dstPort))
return SerializeAddr(host, netip.Addr{}, uint16(dstPort))
}
// DecodeUDPPacket split `packet` to addr payload, and this function is mutable with `packet`
+32
View File
@@ -0,0 +1,32 @@
package socks5
import (
"bufio"
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSocks5ClientHandshake(t *testing.T) {
// Mock server responses
readBuffer := &bytes.Buffer{}
readBuffer.Write([]byte{Version, MethodUserPass})
readBuffer.Write([]byte{Version, 0x00 /* STATUS of SUCCESS */})
readBuffer.Write([]byte{Version, 0x00 /* STATUS of SUCCESS */, 0x00 /* RSV */})
readBuffer.Write([]byte{AtypIPv4, 0x1, 0x2, 0x3, 0x4, 0x0, 0x0 /* IPv4: 1.2.3.4:0 */})
reader := bufio.NewReader(bytes.NewReader(readBuffer.Bytes()))
writeBuffer := &bytes.Buffer{}
writer := bufio.NewWriter(writeBuffer)
io := bufio.NewReadWriter(reader, writer)
addr, err := ClientHandshake(io, []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, CmdConnect, &User{
Username: "test",
Password: "6ab49d8b-a009-44e4-bd53-fbdb48fbe7eb",
})
assert.Nil(t, err, "Failed to perform SOCKS5 client handshake: %v", err)
assert.Equal(t, "1.2.3.4:0", addr.String(), "Incorrect address obtained from SOCKS5 client handshake")
}
+27 -16
View File
@@ -2,26 +2,37 @@ package tunnel
import (
"net"
"strconv"
"net/netip"
"gvisor.dev/gvisor/pkg/tcpip"
)
// parseAddr parses net.Addr to IP and port.
func parseAddr(addr net.Addr) (net.IP, uint16) {
switch v := addr.(type) {
case *net.TCPAddr:
return v.IP, uint16(v.Port)
case *net.UDPAddr:
return v.IP, uint16(v.Port)
case nil:
return nil, 0
default:
return parseAddrString(addr.String())
// parseNetAddr parses net.Addr to IP and port.
func parseNetAddr(addr net.Addr) (netip.Addr, uint16) {
if addr == nil {
return netip.Addr{}, 0
}
if v, ok := addr.(interface {
AddrPort() netip.AddrPort
}); ok {
ap := v.AddrPort()
return ap.Addr(), ap.Port()
}
return parseAddrString(addr.String())
}
// parseAddrString parses address string to IP and port.
func parseAddrString(addr string) (net.IP, uint16) {
host, port, _ := net.SplitHostPort(addr)
portInt, _ := strconv.ParseUint(port, 10, 16)
return net.ParseIP(host), uint16(portInt)
// It doesn't do any name resolution.
func parseAddrString(s string) (netip.Addr, uint16) {
ap, err := netip.ParseAddrPort(s)
if err != nil {
return netip.Addr{}, 0
}
return ap.Addr(), ap.Port()
}
// parseTCPIPAddress parses tcpip.Address to netip.Addr.
func parseTCPIPAddress(addr tcpip.Address) netip.Addr {
ip, _ := netip.AddrFromSlice(addr.AsSlice())
return ip
}
+37
View File
@@ -0,0 +1,37 @@
package tunnel
import (
"sync"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
)
var (
_globalMu sync.RWMutex
_globalT *Tunnel
)
func init() {
ReplaceGlobal(New(&proxy.Base{}, statistic.DefaultManager))
T().ProcessAsync()
}
// T returns the global Tunnel, which can be reconfigured with
// ReplaceGlobal. It's safe for concurrent use.
func T() *Tunnel {
_globalMu.RLock()
t := _globalT
_globalMu.RUnlock()
return t
}
// ReplaceGlobal replaces the global Tunnel, and returns a function
// to restore the original values. It's safe for concurrent use.
func ReplaceGlobal(t *Tunnel) func() {
_globalMu.Lock()
prev := _globalT
_globalT = t
_globalMu.Unlock()
return func() { ReplaceGlobal(prev) }
}
-1
View File
@@ -18,7 +18,6 @@ func init() {
uploadTotal: atomic.NewInt64(0),
downloadTotal: atomic.NewInt64(0),
}
go DefaultManager.handle()
}
-10
View File
@@ -50,11 +50,6 @@ func NewTCPTracker(conn net.Conn, metadata *M.Metadata, manager *Manager) net.Co
return tt
}
// DefaultTCPTracker returns a new net.Conn(*tcpTacker) with default manager.
func DefaultTCPTracker(conn net.Conn, metadata *M.Metadata) net.Conn {
return NewTCPTracker(conn, metadata, DefaultManager)
}
func (tt *tcpTracker) ID() string {
return tt.UUID.String()
}
@@ -120,11 +115,6 @@ func NewUDPTracker(conn net.PacketConn, metadata *M.Metadata, manager *Manager)
return ut
}
// DefaultUDPTracker returns a new net.PacketConn(*udpTacker) with default manager.
func DefaultUDPTracker(conn net.PacketConn, metadata *M.Metadata) net.PacketConn {
return NewUDPTracker(conn, metadata, DefaultManager)
}
func (ut *udpTracker) ID() string {
return ut.UUID.String()
}
+14 -16
View File
@@ -1,51 +1,49 @@
package tunnel
import (
"context"
"io"
"net"
"sync"
"time"
"github.com/xjasonlyu/tun2socks/v2/common/pool"
"github.com/xjasonlyu/tun2socks/v2/buffer"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/log"
M "github.com/xjasonlyu/tun2socks/v2/metadata"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
)
const (
// tcpWaitTimeout implements a TCP half-close timeout.
tcpWaitTimeout = 60 * time.Second
)
func handleTCPConn(originConn adapter.TCPConn) {
func (t *Tunnel) handleTCPConn(originConn adapter.TCPConn) {
defer originConn.Close()
id := originConn.ID()
metadata := &M.Metadata{
Network: M.TCP,
SrcIP: net.IP(id.RemoteAddress.AsSlice()),
SrcIP: parseTCPIPAddress(id.RemoteAddress),
SrcPort: id.RemotePort,
DstIP: net.IP(id.LocalAddress.AsSlice()),
DstIP: parseTCPIPAddress(id.LocalAddress),
DstPort: id.LocalPort,
}
remoteConn, err := proxy.Dial(metadata)
ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
defer cancel()
remoteConn, err := t.Dialer().DialContext(ctx, metadata)
if err != nil {
log.Warnf("[TCP] dial %s: %v", metadata.DestinationAddress(), err)
return
}
metadata.MidIP, metadata.MidPort = parseAddr(remoteConn.LocalAddr())
metadata.MidIP, metadata.MidPort = parseNetAddr(remoteConn.LocalAddr())
remoteConn = statistic.DefaultTCPTracker(remoteConn, metadata)
remoteConn = statistic.NewTCPTracker(remoteConn, metadata, t.manager)
defer remoteConn.Close()
log.Infof("[TCP] %s <-> %s", metadata.SourceAddress(), metadata.DestinationAddress())
pipe(originConn, remoteConn)
}
// pipe copies copy data to & from provided net.Conn(s) bidirectionally.
// pipe copies data to & from provided net.Conn(s) bidirectionally.
func pipe(origin, remote net.Conn) {
wg := sync.WaitGroup{}
wg.Add(2)
@@ -58,11 +56,11 @@ func pipe(origin, remote net.Conn) {
func unidirectionalStream(dst, src net.Conn, dir string, wg *sync.WaitGroup) {
defer wg.Done()
buf := pool.Get(pool.RelayBufferSize)
buf := buffer.Get(buffer.RelayBufferSize)
if _, err := io.CopyBuffer(dst, src, buf); err != nil {
log.Debugf("[TCP] copy data for %s: %v", dir, err)
}
pool.Put(buf)
buffer.Put(buf)
// Do the upload/download side TCP half-close.
if cr, ok := src.(interface{ CloseRead() error }); ok {
cr.CloseRead()
+95 -15
View File
@@ -1,36 +1,116 @@
package tunnel
import (
"context"
"sync"
"time"
"go.uber.org/atomic"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
)
// Unbuffered TCP/UDP queues.
var (
_tcpQueue = make(chan adapter.TCPConn)
_udpQueue = make(chan adapter.UDPConn)
const (
// tcpConnectTimeout is the default timeout for TCP handshakes.
tcpConnectTimeout = 5 * time.Second
// tcpWaitTimeout implements a TCP half-close timeout.
tcpWaitTimeout = 60 * time.Second
// udpSessionTimeout is the default timeout for UDP sessions.
udpSessionTimeout = 60 * time.Second
)
func init() {
go process()
var _ adapter.TransportHandler = (*Tunnel)(nil)
type Tunnel struct {
// Unbuffered TCP/UDP queues.
tcpQueue chan adapter.TCPConn
udpQueue chan adapter.UDPConn
// UDP session timeout.
udpTimeout *atomic.Duration
// Internal proxy.Dialer for Tunnel.
dialerMu sync.RWMutex
dialer proxy.Dialer
// Where the Tunnel statistics are sent to.
manager *statistic.Manager
procOnce sync.Once
procCancel context.CancelFunc
}
func New(dialer proxy.Dialer, manager *statistic.Manager) *Tunnel {
return &Tunnel{
tcpQueue: make(chan adapter.TCPConn),
udpQueue: make(chan adapter.UDPConn),
udpTimeout: atomic.NewDuration(udpSessionTimeout),
dialer: dialer,
manager: manager,
procCancel: func() { /* nop */ },
}
}
// TCPIn return fan-in TCP queue.
func TCPIn() chan<- adapter.TCPConn {
return _tcpQueue
func (t *Tunnel) TCPIn() chan<- adapter.TCPConn {
return t.tcpQueue
}
// UDPIn return fan-in UDP queue.
func UDPIn() chan<- adapter.UDPConn {
return _udpQueue
func (t *Tunnel) UDPIn() chan<- adapter.UDPConn {
return t.udpQueue
}
func process() {
func (t *Tunnel) HandleTCP(conn adapter.TCPConn) {
t.TCPIn() <- conn
}
func (t *Tunnel) HandleUDP(conn adapter.UDPConn) {
t.UDPIn() <- conn
}
func (t *Tunnel) process(ctx context.Context) {
for {
select {
case conn := <-_tcpQueue:
go handleTCPConn(conn)
case conn := <-_udpQueue:
go handleUDPConn(conn)
case conn := <-t.tcpQueue:
go t.handleTCPConn(conn)
case conn := <-t.udpQueue:
go t.handleUDPConn(conn)
case <-ctx.Done():
return
}
}
}
// ProcessAsync can be safely called multiple times, but will only be effective once.
func (t *Tunnel) ProcessAsync() {
t.procOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
t.procCancel = cancel
go t.process(ctx)
})
}
// Close closes the Tunnel and releases its resources.
func (t *Tunnel) Close() {
t.procCancel()
}
func (t *Tunnel) Dialer() proxy.Dialer {
t.dialerMu.RLock()
d := t.dialer
t.dialerMu.RUnlock()
return d
}
func (t *Tunnel) SetDialer(dialer proxy.Dialer) {
t.dialerMu.Lock()
t.dialer = dialer
t.dialerMu.Unlock()
}
func (t *Tunnel) SetUDPTimeout(timeout time.Duration) {
t.udpTimeout.Store(timeout)
}
+15 -23
View File
@@ -6,42 +6,34 @@ import (
"sync"
"time"
"github.com/xjasonlyu/tun2socks/v2/common/pool"
"github.com/xjasonlyu/tun2socks/v2/buffer"
"github.com/xjasonlyu/tun2socks/v2/core/adapter"
"github.com/xjasonlyu/tun2socks/v2/log"
M "github.com/xjasonlyu/tun2socks/v2/metadata"
"github.com/xjasonlyu/tun2socks/v2/proxy"
"github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
)
// _udpSessionTimeout is the default timeout for each UDP session.
var _udpSessionTimeout = 60 * time.Second
func SetUDPTimeout(t time.Duration) {
_udpSessionTimeout = t
}
// TODO: Port Restricted NAT support.
func handleUDPConn(uc adapter.UDPConn) {
func (t *Tunnel) handleUDPConn(uc adapter.UDPConn) {
defer uc.Close()
id := uc.ID()
metadata := &M.Metadata{
Network: M.UDP,
SrcIP: net.IP(id.RemoteAddress.AsSlice()),
SrcIP: parseTCPIPAddress(id.RemoteAddress),
SrcPort: id.RemotePort,
DstIP: net.IP(id.LocalAddress.AsSlice()),
DstIP: parseTCPIPAddress(id.LocalAddress),
DstPort: id.LocalPort,
}
pc, err := proxy.DialUDP(metadata)
pc, err := t.Dialer().DialUDP(metadata)
if err != nil {
log.Warnf("[UDP] dial %s: %v", metadata.DestinationAddress(), err)
return
}
metadata.MidIP, metadata.MidPort = parseAddr(pc.LocalAddr())
metadata.MidIP, metadata.MidPort = parseNetAddr(pc.LocalAddr())
pc = statistic.DefaultUDPTracker(pc, metadata)
pc = statistic.NewUDPTracker(pc, metadata, t.manager)
defer pc.Close()
var remote net.Addr
@@ -53,29 +45,29 @@ func handleUDPConn(uc adapter.UDPConn) {
pc = newSymmetricNATPacketConn(pc, metadata)
log.Infof("[UDP] %s <-> %s", metadata.SourceAddress(), metadata.DestinationAddress())
pipePacket(uc, pc, remote)
pipePacket(uc, pc, remote, t.udpTimeout.Load())
}
func pipePacket(origin, remote net.PacketConn, to net.Addr) {
func pipePacket(origin, remote net.PacketConn, to net.Addr, timeout time.Duration) {
wg := sync.WaitGroup{}
wg.Add(2)
go unidirectionalPacketStream(remote, origin, to, "origin->remote", &wg)
go unidirectionalPacketStream(origin, remote, nil, "remote->origin", &wg)
go unidirectionalPacketStream(remote, origin, to, "origin->remote", &wg, timeout)
go unidirectionalPacketStream(origin, remote, nil, "remote->origin", &wg, timeout)
wg.Wait()
}
func unidirectionalPacketStream(dst, src net.PacketConn, to net.Addr, dir string, wg *sync.WaitGroup) {
func unidirectionalPacketStream(dst, src net.PacketConn, to net.Addr, dir string, wg *sync.WaitGroup, timeout time.Duration) {
defer wg.Done()
if err := copyPacketData(dst, src, to, _udpSessionTimeout); err != nil {
if err := copyPacketData(dst, src, to, timeout); err != nil {
log.Debugf("[UDP] copy data for %s: %v", dir, err)
}
}
func copyPacketData(dst, src net.PacketConn, to net.Addr, timeout time.Duration) error {
buf := pool.Get(pool.MaxSegmentSize)
defer pool.Put(buf)
buf := buffer.Get(buffer.MaxSegmentSize)
defer buffer.Put(buf)
for {
src.SetReadDeadline(time.Now().Add(timeout))