├── .dockerignore
├── .github
├── CODE_OF_CONDUCT.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
├── SECURITY.md
└── workflows
│ ├── codeql-analysis.yml
│ ├── docker.yml
│ ├── linter.yml
│ ├── release.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .golangci.yaml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── buffer
├── allocator
│ ├── allocator.go
│ └── allocator_test.go
└── pool.go
├── core
├── adapter
│ ├── adapter.go
│ └── handler.go
├── device
│ ├── device.go
│ ├── fdbased
│ │ ├── fd.go
│ │ ├── fd_unix.go
│ │ ├── fd_windows.go
│ │ ├── open_linux.go
│ │ └── open_unix.go
│ ├── iobased
│ │ └── endpoint.go
│ └── tun
│ │ ├── tun.go
│ │ ├── tun_netstack.go
│ │ ├── tun_wireguard.go
│ │ ├── tun_wireguard_unix.go
│ │ └── tun_wireguard_windows.go
├── nic.go
├── option
│ └── option.go
├── route.go
├── stack.go
├── tcp.go
└── udp.go
├── dialer
├── dialer.go
├── sockopt.go
├── sockopt_darwin.go
├── sockopt_freebsd.go
├── sockopt_linux.go
├── sockopt_openbsd.go
├── sockopt_others.go
└── sockopt_windows.go
├── dns
└── resolver.go
├── docker
└── entrypoint.sh
├── docs
├── benchmark.png
├── icon.png
└── logo.png
├── engine
├── engine.go
├── key.go
├── parse.go
├── parse_unix.go
└── parse_windows.go
├── go.mod
├── go.sum
├── internal
├── pool
│ ├── pool.go
│ └── pool_test.go
└── version
│ ├── module.go
│ └── version.go
├── log
├── doc.go
├── emitter.go
├── level.go
├── log.go
└── zap.go
├── main.go
├── metadata
├── metadata.go
└── network.go
├── proxy
├── base.go
├── direct.go
├── http.go
├── proto
│ └── proto.go
├── proxy.go
├── reject.go
├── relay.go
├── shadowsocks.go
├── socks4.go
├── socks5.go
└── util.go
├── restapi
├── connections.go
├── debug.go
├── errors.go
├── netstats.go
└── server.go
├── transport
├── internal
│ └── bufferpool
│ │ └── bufferpool.go
├── shadowsocks
│ ├── README.md
│ ├── core
│ │ └── cipher.go
│ ├── shadowaead
│ │ ├── cipher.go
│ │ ├── packet.go
│ │ └── stream.go
│ └── shadowstream
│ │ ├── cipher.go
│ │ ├── packet.go
│ │ └── stream.go
├── simple-obfs
│ ├── http.go
│ ├── obfs.go
│ └── tls.go
├── socks4
│ ├── socks4.go
│ ├── socks4.txt
│ ├── socks4_test.go
│ └── socks4a.txt
└── socks5
│ ├── rfc1928.txt
│ ├── rfc1929.txt
│ ├── socks5.go
│ └── socks5_test.go
└── tunnel
├── addr.go
├── global.go
├── statistic
├── manager.go
└── tracker.go
├── tcp.go
├── tunnel.go
└── udp.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | .github/
2 | .gitignore
3 | .golangci.yaml
4 |
5 | # Other
6 | docs/
7 | build/
8 | tests/
9 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | xjasonlyu@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ xjasonlyu ] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Create a report to help us improve
3 | title: "[Bug] "
4 | body:
5 | - type: checkboxes
6 | id: ensure
7 | attributes:
8 | label: Verify steps
9 | description: Please verify that you've followed these steps
10 | options:
11 | - label: Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
12 | required: true
13 |
14 | - label: I have searched on the [issue tracker](……/) for a related issue.
15 | required: true
16 |
17 | - type: input
18 | attributes:
19 | label: Version
20 | validations:
21 | required: true
22 |
23 | - type: dropdown
24 | id: os
25 | attributes:
26 | label: What OS are you seeing the problem on?
27 | multiple: true
28 | options:
29 | - Windows
30 | - Linux
31 | - macOS
32 | - OpenBSD/FreeBSD
33 | - Other
34 |
35 | - type: textarea
36 | attributes:
37 | label: Description
38 | validations:
39 | required: true
40 |
41 | - type: textarea
42 | attributes:
43 | label: CLI or Config
44 | description: Paste the command line parameters or configuration below.
45 |
46 | - type: textarea
47 | attributes:
48 | render: shell
49 | label: Logs
50 | description: Paste the logs below with the log level set to `DEBUG`.
51 |
52 | - type: textarea
53 | attributes:
54 | label: How to Reproduce
55 | description: Steps to reproduce the behavior, if any.
56 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
3 | contact_links:
4 | - name: tun2socks GitHub Wiki
5 | url: https://github.com/xjasonlyu/tun2socks/wiki
6 | about: Please see the wiki for common configurations
7 | - name: tun2socks GitHub Discussions
8 | url: https://github.com/xjasonlyu/tun2socks/discussions
9 | about: Ask questions and get help on GitHub Discussions
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea or improvement
3 | title: "[Feature] "
4 | body:
5 | - type: textarea
6 | id: description
7 | attributes:
8 | label: Description
9 | placeholder: A clear description of the feature or enhancement.
10 | validations:
11 | required: true
12 |
13 | - type: textarea
14 | id: related
15 | attributes:
16 | label: Is this feature related to a specific bug?
17 | description: Please include a bug references if yes.
18 |
19 | - type: textarea
20 | id: solution
21 | attributes:
22 | label: Do you have a specific solution in mind?
23 | description: >
24 | Please include any details about a solution that you have in mind,
25 | including any alternatives considered.
26 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | |:-------:|:------------------:|
7 | | 2.x | :white_check_mark: |
8 | | 1.x | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | If you believe you have found a security vulnerability in this repository, please report it to me through coordinated
13 | disclosure.
14 |
15 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
16 |
17 | Instead, please email to xjasonlyu[@]gmail.com.
18 |
19 | Please include as much of the information listed below as you can to help me better understand and resolve the issue:
20 |
21 | * The type of issue (e.g., buffer overflow, payload attack)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help me triage your report more quickly.
30 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | concurrency:
4 | group: codeql-${{ github.event_name }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches: [ main ]
10 | pull_request:
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | language: [ 'go' ]
21 |
22 | steps:
23 | - name: Checkout repository
24 | uses: actions/checkout@v4
25 |
26 | - name: Setup Go
27 | uses: actions/setup-go@v5
28 | with:
29 | check-latest: true
30 | go-version-file: 'go.mod'
31 |
32 | - name: Initialize CodeQL
33 | uses: github/codeql-action/init@v3
34 | with:
35 | languages: ${{ matrix.language }}
36 |
37 | - name: Autobuild
38 | uses: github/codeql-action/autobuild@v3
39 |
40 | - name: Perform CodeQL Analysis
41 | uses: github/codeql-action/analyze@v3
42 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker Image
2 |
3 | concurrency:
4 | group: docker-${{ github.event_name }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'main'
11 | tags:
12 | - '*'
13 |
14 | jobs:
15 | docker:
16 | name: Docker
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout code
20 | uses: actions/checkout@v4
21 | with:
22 | fetch-depth: 0
23 |
24 | - name: Set up QEMU
25 | uses: docker/setup-qemu-action@v3
26 | with:
27 | platforms: all
28 |
29 | - name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v3
31 | with:
32 | version: latest
33 |
34 | - name: Login to DockerHub
35 | uses: docker/login-action@v3
36 | with:
37 | username: ${{ secrets.DOCKER_USERNAME }}
38 | password: ${{ secrets.DOCKER_PASSWORD }}
39 |
40 | - name: Login to GitHub Container Registry
41 | uses: docker/login-action@v3
42 | with:
43 | registry: ghcr.io
44 | username: ${{ github.repository_owner }}
45 | password: ${{ secrets.GITHUB_TOKEN }}
46 |
47 | - name: Get Version
48 | id: shell
49 | run: |
50 | echo "version=$(git describe --tags --abbrev=0)" >> $GITHUB_OUTPUT
51 |
52 | - name: Build and Push (dev)
53 | if: github.ref == 'refs/heads/main'
54 | uses: docker/build-push-action@v6
55 | with:
56 | context: .
57 | push: true
58 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
59 | tags: |
60 | xjasonlyu/tun2socks:dev
61 | ghcr.io/xjasonlyu/tun2socks:dev
62 | cache-from: type=gha
63 | cache-to: type=gha,mode=max
64 |
65 | - name: Build and Push (latest)
66 | if: startsWith(github.ref, 'refs/tags/')
67 | uses: docker/build-push-action@v6
68 | with:
69 | context: .
70 | push: true
71 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
72 | tags: |
73 | xjasonlyu/tun2socks:latest
74 | xjasonlyu/tun2socks:${{ steps.shell.outputs.version }}
75 | ghcr.io/xjasonlyu/tun2socks:latest
76 | ghcr.io/xjasonlyu/tun2socks:${{ steps.shell.outputs.version }}
77 | cache-from: type=gha
78 | cache-to: type=gha,mode=max
79 |
--------------------------------------------------------------------------------
/.github/workflows/linter.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | concurrency:
4 | group: linter-${{ github.event_name }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'main'
11 | pull_request:
12 |
13 | jobs:
14 | linter:
15 | name: Linter
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 |
21 | - name: Setup Go
22 | uses: actions/setup-go@v5
23 | with:
24 | check-latest: true
25 | go-version-file: 'go.mod'
26 |
27 | - name: golangci-lint
28 | uses: golangci/golangci-lint-action@v6
29 | with:
30 | version: latest
31 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Go Releases
2 |
3 | concurrency:
4 | group: release-${{ github.event_name }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | tags:
10 | - '*'
11 |
12 | jobs:
13 | release:
14 | name: Release
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Set up Go
23 | uses: actions/setup-go@v5
24 | with:
25 | check-latest: true
26 | go-version-file: 'go.mod'
27 |
28 | - name: Cache go module
29 | uses: actions/cache@v4
30 | with:
31 | path: |
32 | ~/go/pkg/mod
33 | ~/.cache/go-build
34 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
35 | restore-keys: |
36 | ${{ runner.os }}-go-
37 |
38 | - name: Set prerelease flag
39 | if: startsWith(github.ref, 'refs/tags/')
40 | id: pre
41 | run: |
42 | TAG="$GITHUB_REF_NAME"
43 | if [[ "$TAG" =~ -(beta|alpha|rc) ]]; then
44 | echo "is_prerelease=true" >> $GITHUB_OUTPUT
45 | else
46 | echo "is_prerelease=false" >> $GITHUB_OUTPUT
47 | fi
48 |
49 | - name: Build
50 | if: startsWith(github.ref, 'refs/tags/')
51 | run: make -j releases
52 |
53 | - name: Upload Releases
54 | uses: softprops/action-gh-release@v2
55 | if: startsWith(github.ref, 'refs/tags/')
56 | with:
57 | files: build/*
58 | draft: true
59 | prerelease: ${{ steps.pre.outputs.is_prerelease }}
60 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale issues and pull requests
2 |
3 | permissions:
4 | contents: write
5 | issues: write
6 | pull-requests: write
7 |
8 | on:
9 | schedule:
10 | - cron: "0 10 * * *"
11 |
12 | jobs:
13 | stale:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/stale@v9
17 | with:
18 | 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'
19 | exempt-issue-labels: 'question,bug,enhancement,help wanted'
20 | exempt-pr-labels: 'pending,WIP,help wanted'
21 | days-before-stale: 60
22 | days-before-close: 7
23 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | concurrency:
4 | group: test-${{ github.event_name }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'main'
11 | pull_request:
12 |
13 | jobs:
14 | build-test:
15 | name: Build Test
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Setup Go
24 | uses: actions/setup-go@v5
25 | with:
26 | check-latest: true
27 | go-version-file: 'go.mod'
28 |
29 | - name: Run test
30 | run: |
31 | go test ./...
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 | go.work.sum
23 |
24 | # Build directory
25 | build/
26 |
27 | # IDE
28 | .idea/
29 | .vscode/
30 |
31 | # Misc
32 | .DS_Store
33 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | linters:
2 | disable-all: true
3 | enable:
4 | - gci
5 | - gofumpt
6 | - gosimple
7 | - govet
8 | - ineffassign
9 | - misspell
10 | - staticcheck
11 | - unconvert
12 | - unused
13 | - usestdlibvars
14 |
15 | linters-settings:
16 | gci:
17 | custom-order: true
18 | sections:
19 | - standard
20 | - default
21 | - prefix(github.com/xjasonlyu/tun2socks)
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:alpine AS builder
2 |
3 | WORKDIR /src
4 | COPY . /src
5 |
6 | RUN apk add --update --no-cache make git \
7 | && make tun2socks
8 |
9 | FROM alpine:latest
10 | LABEL org.opencontainers.image.source="https://github.com/xjasonlyu/tun2socks"
11 |
12 | COPY docker/entrypoint.sh /entrypoint.sh
13 | COPY --from=builder /src/build/tun2socks /usr/bin/tun2socks
14 |
15 | RUN apk add --update --no-cache iptables iproute2 tzdata \
16 | && chmod +x /entrypoint.sh
17 |
18 | ENV TUN=tun0
19 | ENV ADDR=198.18.0.1/15
20 | ENV LOGLEVEL=info
21 | ENV PROXY=direct://
22 | ENV MTU=9000
23 | ENV RESTAPI=
24 | ENV UDP_TIMEOUT=
25 | ENV TCP_SNDBUF=
26 | ENV TCP_RCVBUF=
27 | ENV TCP_AUTO_TUNING=
28 | ENV MULTICAST_GROUPS=
29 | ENV EXTRA_COMMANDS=
30 | ENV TUN_INCLUDED_ROUTES=
31 | ENV TUN_EXCLUDED_ROUTES=
32 |
33 | ENTRYPOINT ["/entrypoint.sh"]
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jason Lyu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BINARY := tun2socks
2 | MODULE := github.com/xjasonlyu/tun2socks/v2
3 |
4 | BUILD_DIR := build
5 | BUILD_TAGS :=
6 | BUILD_FLAGS := -v
7 | BUILD_COMMIT := $(shell git rev-parse --short HEAD)
8 | BUILD_VERSION := $(shell git describe --abbrev=0 --tags HEAD)
9 |
10 | CGO_ENABLED := 0
11 | GO111MODULE := on
12 |
13 | LDFLAGS += -w -s -buildid=
14 | LDFLAGS += -X "$(MODULE)/internal/version.Version=$(BUILD_VERSION)"
15 | LDFLAGS += -X "$(MODULE)/internal/version.GitCommit=$(BUILD_COMMIT)"
16 |
17 | GO_BUILD = GO111MODULE=$(GO111MODULE) CGO_ENABLED=$(CGO_ENABLED) \
18 | go build $(BUILD_FLAGS) -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -trimpath
19 |
20 | UNIX_ARCH_LIST = \
21 | darwin-amd64 \
22 | darwin-amd64-v3 \
23 | darwin-arm64 \
24 | freebsd-386 \
25 | freebsd-amd64 \
26 | freebsd-amd64-v3 \
27 | freebsd-arm64 \
28 | linux-386 \
29 | linux-amd64 \
30 | linux-amd64-v3 \
31 | linux-arm64 \
32 | linux-armv5 \
33 | linux-armv6 \
34 | linux-armv7 \
35 | linux-mips-softfloat \
36 | linux-mips-hardfloat \
37 | linux-mipsle-softfloat \
38 | linux-mipsle-hardfloat \
39 | linux-mips64 \
40 | linux-mips64le \
41 | linux-ppc64 \
42 | linux-ppc64le \
43 | linux-s390x \
44 | linux-loong64 \
45 | openbsd-amd64 \
46 | openbsd-amd64-v3 \
47 | openbsd-arm64
48 |
49 | WINDOWS_ARCH_LIST = \
50 | windows-386 \
51 | windows-amd64 \
52 | windows-amd64-v3 \
53 | windows-arm64 \
54 | windows-arm32v7
55 |
56 | all: linux-amd64 linux-arm64 darwin-amd64 darwin-arm64 windows-amd64
57 |
58 | debug: BUILD_TAGS += debug
59 | debug: all
60 |
61 | tun2socks:
62 | $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)
63 |
64 | darwin-amd64:
65 | GOARCH=amd64 GOOS=darwin $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
66 |
67 | darwin-amd64-v3:
68 | GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
69 |
70 | darwin-arm64:
71 | GOARCH=arm64 GOOS=darwin $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
72 |
73 | freebsd-386:
74 | GOARCH=386 GOOS=freebsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
75 |
76 | freebsd-amd64:
77 | GOARCH=amd64 GOOS=freebsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
78 |
79 | freebsd-amd64-v3:
80 | GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
81 |
82 | freebsd-arm64:
83 | GOARCH=arm64 GOOS=freebsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
84 |
85 | linux-386:
86 | GOARCH=386 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
87 |
88 | linux-amd64:
89 | GOARCH=amd64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
90 |
91 | linux-amd64-v3:
92 | GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
93 |
94 | linux-arm64:
95 | GOARCH=arm64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
96 |
97 | linux-armv5:
98 | GOARCH=arm GOARM=5 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
99 |
100 | linux-armv6:
101 | GOARCH=arm GOARM=6 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
102 |
103 | linux-armv7:
104 | GOARCH=arm GOARM=7 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
105 |
106 | linux-mips-softfloat:
107 | GOARCH=mips GOMIPS=softfloat GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
108 |
109 | linux-mips-hardfloat:
110 | GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
111 |
112 | linux-mipsle-softfloat:
113 | GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
114 |
115 | linux-mipsle-hardfloat:
116 | GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
117 |
118 | linux-mips64:
119 | GOARCH=mips64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
120 |
121 | linux-mips64le:
122 | GOARCH=mips64le GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
123 |
124 | linux-ppc64:
125 | GOARCH=ppc64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
126 |
127 | linux-ppc64le:
128 | GOARCH=ppc64le GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
129 |
130 | linux-s390x:
131 | GOARCH=s390x GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
132 |
133 | linux-loong64:
134 | GOARCH=loong64 GOOS=linux $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
135 |
136 | openbsd-amd64:
137 | GOARCH=amd64 GOOS=openbsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
138 |
139 | openbsd-amd64-v3:
140 | GOARCH=amd64 GOOS=openbsd GOAMD64=v3 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
141 |
142 | openbsd-arm64:
143 | GOARCH=arm64 GOOS=openbsd $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@
144 |
145 | windows-386:
146 | GOARCH=386 GOOS=windows $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@.exe
147 |
148 | windows-amd64:
149 | GOARCH=amd64 GOOS=windows $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@.exe
150 |
151 | windows-amd64-v3:
152 | GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@.exe
153 |
154 | windows-arm64:
155 | GOARCH=arm64 GOOS=windows $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@.exe
156 |
157 | windows-arm32v7:
158 | GOARCH=arm GOARM=7 GOOS=windows $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY)-$@.exe
159 |
160 | unix_releases := $(addsuffix .zip, $(UNIX_ARCH_LIST))
161 | windows_releases := $(addsuffix .zip, $(WINDOWS_ARCH_LIST))
162 |
163 | $(unix_releases): %.zip: %
164 | @zip -qmj $(BUILD_DIR)/$(BINARY)-$(basename $@).zip $(BUILD_DIR)/$(BINARY)-$(basename $@)
165 |
166 | $(windows_releases): %.zip: %
167 | @zip -qmj $(BUILD_DIR)/$(BINARY)-$(basename $@).zip $(BUILD_DIR)/$(BINARY)-$(basename $@).exe
168 |
169 | all-arch: $(UNIX_ARCH_LIST) $(WINDOWS_ARCH_LIST)
170 |
171 | releases: $(unix_releases) $(windows_releases)
172 |
173 | lint:
174 | GOOS=darwin golangci-lint run ./...
175 | GOOS=windows golangci-lint run ./...
176 | GOOS=linux golangci-lint run ./...
177 | GOOS=freebsd golangci-lint run ./...
178 | GOOS=openbsd golangci-lint run ./...
179 |
180 | clean:
181 | rm -rf $(BUILD_DIR)
182 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [![GitHub Workflow][1]](https://github.com/xjasonlyu/tun2socks/actions)
4 | [![Go Version][2]](https://github.com/xjasonlyu/tun2socks/blob/main/go.mod)
5 | [![Go Report][3]](https://goreportcard.com/badge/github.com/xjasonlyu/tun2socks)
6 | [![Maintainability][4]](https://codeclimate.com/github/xjasonlyu/tun2socks/maintainability)
7 | [![GitHub License][5]](https://github.com/xjasonlyu/tun2socks/blob/main/LICENSE)
8 | [![Docker Pulls][6]](https://hub.docker.com/r/xjasonlyu/tun2socks)
9 | [![Releases][7]](https://github.com/xjasonlyu/tun2socks/releases)
10 |
11 | ## Features
12 |
13 | - **Universal Proxying**: Transparently routes all network traffic from any application through a proxy.
14 | - **Multi-Protocol**: Supports HTTP/SOCKS4/SOCKS5/Shadowsocks proxies with optional authentication.
15 | - **Cross-Platform**: Runs on Linux/macOS/Windows/FreeBSD/OpenBSD with platform-specific optimizations.
16 | - **Gateway Mode**: Acts as a Layer 3 gateway to route traffic from other devices on the same network.
17 | - **Full IPv6 Compatibility**: Natively supports IPv6; seamlessly tunnels IPv4 over IPv6 and vice versa.
18 | - **User-Space Networking**: Leverages the **[gVisor](https://github.com/google/gvisor)** network stack for enhanced
19 | performance and flexibility.
20 |
21 | ## Benchmarks
22 |
23 | 
24 |
25 | For all scenarios of usage, tun2socks performs best.
26 | See [benchmarks](https://github.com/xjasonlyu/tun2socks/wiki/Benchmarks) for more details.
27 |
28 | ## Documentation
29 |
30 | - [Install from Source](https://github.com/xjasonlyu/tun2socks/wiki/Install-from-Source)
31 | - [Quickstart Examples](https://github.com/xjasonlyu/tun2socks/wiki/Examples)
32 | - [Memory Optimization](https://github.com/xjasonlyu/tun2socks/wiki/Memory-Optimization)
33 |
34 | Full documentation and technical guides can be found at [Wiki](https://github.com/xjasonlyu/tun2socks/wiki).
35 |
36 | ## Community
37 |
38 | Welcome and feel free to ask any questions at [Discussions](https://github.com/xjasonlyu/tun2socks/discussions).
39 |
40 | ## Credits
41 |
42 | - [google/gvisor](https://github.com/google/gvisor) - Application Kernel for Containers
43 | - [wireguard-go](https://git.zx2c4.com/wireguard-go) - Go Implementation of WireGuard
44 | - [wintun](https://git.zx2c4.com/wintun/) - Layer 3 TUN Driver for Windows
45 |
46 | ## License
47 |
48 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fxjasonlyu%2Ftun2socks?ref=badge_large)
49 |
50 | All versions starting from `v2.6.0` are available under the terms of the [MIT License](https://github.com/xjasonlyu/tun2socks/blob/main/LICENSE).
51 |
52 | ## Star History
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | [1]: https://img.shields.io/github/actions/workflow/status/xjasonlyu/tun2socks/docker.yml?logo=github
63 |
64 | [2]: https://img.shields.io/github/go-mod/go-version/xjasonlyu/tun2socks?logo=go
65 |
66 | [3]: https://goreportcard.com/badge/github.com/xjasonlyu/tun2socks
67 |
68 | [4]: https://api.codeclimate.com/v1/badges/b5b30239174fc6603aca/maintainability
69 |
70 | [5]: https://img.shields.io/github/license/xjasonlyu/tun2socks
71 |
72 | [6]: https://img.shields.io/docker/pulls/xjasonlyu/tun2socks?logo=docker
73 |
74 | [7]: https://img.shields.io/github/v/release/xjasonlyu/tun2socks?logo=smartthings
75 |
--------------------------------------------------------------------------------
/buffer/allocator/allocator.go:
--------------------------------------------------------------------------------
1 | package allocator
2 |
3 | import (
4 | "errors"
5 | "math/bits"
6 |
7 | "github.com/xjasonlyu/tun2socks/v2/internal/pool"
8 | )
9 |
10 | // Allocator for incoming frames, optimized to prevent overwriting
11 | // after zeroing.
12 | type Allocator struct {
13 | buffers []*pool.Pool[[]byte]
14 | }
15 |
16 | // New initiates a []byte allocator for frames less than 65536 bytes,
17 | // the waste(memory fragmentation) of space allocation is guaranteed
18 | // to be no more than 50%.
19 | func New() *Allocator {
20 | alloc := &Allocator{}
21 | alloc.buffers = make([]*pool.Pool[[]byte], 17) // 1B -> 64K
22 | for k := range alloc.buffers {
23 | i := k
24 | alloc.buffers[k] = pool.New(func() []byte {
25 | return make([]byte, 1< 65536 {
34 | return nil
35 | }
36 |
37 | b := msb(size)
38 | if size == 1< 65536 || cap(buf) != 1< mtu {
105 | continue
106 | }
107 |
108 | if !e.IsAttached() {
109 | continue /* unattached, drop packet */
110 | }
111 |
112 | pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
113 | Payload: buffer.MakeWithData(data[offset : offset+n]),
114 | })
115 |
116 | switch header.IPVersion(data[offset:]) {
117 | case header.IPv4Version:
118 | e.InjectInbound(header.IPv4ProtocolNumber, pkt)
119 | case header.IPv6Version:
120 | e.InjectInbound(header.IPv6ProtocolNumber, pkt)
121 | }
122 | pkt.DecRef()
123 | }
124 | }
125 |
126 | // outboundLoop reads outbound packets from channel, and then it calls
127 | // writePacket to send those packets back to lower layer.
128 | func (e *Endpoint) outboundLoop(ctx context.Context) {
129 | for {
130 | pkt := e.ReadContext(ctx)
131 | if pkt == nil {
132 | break
133 | }
134 | e.writePacket(pkt)
135 | }
136 | }
137 |
138 | // writePacket writes outbound packets to the io.Writer.
139 | func (e *Endpoint) writePacket(pkt *stack.PacketBuffer) tcpip.Error {
140 | defer pkt.DecRef()
141 |
142 | buf := pkt.ToBuffer()
143 | defer buf.Release()
144 | if e.offset != 0 {
145 | v := buffer.NewViewWithData(make([]byte, e.offset))
146 | _ = buf.Prepend(v)
147 | }
148 |
149 | if _, err := e.rw.Write(buf.Flatten()); err != nil {
150 | return &tcpip.ErrInvalidEndpointState{}
151 | }
152 | return nil
153 | }
154 |
--------------------------------------------------------------------------------
/core/device/tun/tun.go:
--------------------------------------------------------------------------------
1 | // Package tun provides TUN which implemented device.Device interface.
2 | package tun
3 |
4 | import (
5 | "github.com/xjasonlyu/tun2socks/v2/core/device"
6 | )
7 |
8 | const Driver = "tun"
9 |
10 | func (t *TUN) Type() string {
11 | return Driver
12 | }
13 |
14 | var _ device.Device = (*TUN)(nil)
15 |
--------------------------------------------------------------------------------
/core/device/tun/tun_netstack.go:
--------------------------------------------------------------------------------
1 | //go:build linux
2 |
3 | package tun
4 |
5 | import (
6 | "fmt"
7 |
8 | "golang.org/x/sys/unix"
9 | "gvisor.dev/gvisor/pkg/rawfile"
10 | "gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
11 | "gvisor.dev/gvisor/pkg/tcpip/link/tun"
12 | "gvisor.dev/gvisor/pkg/tcpip/stack"
13 |
14 | "github.com/xjasonlyu/tun2socks/v2/core/device"
15 | )
16 |
17 | type TUN struct {
18 | stack.LinkEndpoint
19 |
20 | fd int
21 | mtu uint32
22 | name string
23 | }
24 |
25 | func Open(name string, mtu uint32) (device.Device, error) {
26 | t := &TUN{name: name, mtu: mtu}
27 |
28 | if len(t.name) >= unix.IFNAMSIZ {
29 | return nil, fmt.Errorf("interface name too long: %s", t.name)
30 | }
31 |
32 | fd, err := tun.Open(t.name)
33 | if err != nil {
34 | return nil, fmt.Errorf("create tun: %w", err)
35 | }
36 | t.fd = fd
37 |
38 | if t.mtu > 0 {
39 | if err := setMTU(t.name, t.mtu); err != nil {
40 | return nil, fmt.Errorf("set mtu: %w", err)
41 | }
42 | }
43 |
44 | _mtu, err := rawfile.GetMTU(t.name)
45 | if err != nil {
46 | return nil, fmt.Errorf("get mtu: %w", err)
47 | }
48 | t.mtu = _mtu
49 |
50 | ep, err := fdbased.New(&fdbased.Options{
51 | FDs: []int{fd},
52 | MTU: t.mtu,
53 | // TUN only, ignore ethernet header.
54 | EthernetHeader: false,
55 | // SYS_READV support only for TUN fd.
56 | PacketDispatchMode: fdbased.Readv,
57 | // TAP/TUN fd's are not sockets and using the WritePackets calls results
58 | // in errors as it always defaults to using SendMMsg which is not supported
59 | // for tap/tun device fds.
60 | //
61 | // This CL changes WritePackets to gracefully degrade to using writev instead
62 | // of sendmmsg if the underlying fd is not a socket.
63 | //
64 | // Fixed: https://github.com/google/gvisor/commit/f33d034fecd7723a1e560ccc62aeeba328454fd0
65 | MaxSyscallHeaderBytes: 0x00,
66 | })
67 | if err != nil {
68 | return nil, fmt.Errorf("create endpoint: %w", err)
69 | }
70 | t.LinkEndpoint = ep
71 |
72 | return t, nil
73 | }
74 |
75 | func (t *TUN) Name() string {
76 | return t.name
77 | }
78 |
79 | func (t *TUN) Close() {
80 | defer t.LinkEndpoint.Close()
81 | _ = unix.Close(t.fd)
82 | }
83 |
84 | func setMTU(name string, n uint32) error {
85 | // open datagram socket
86 | fd, err := unix.Socket(
87 | unix.AF_INET,
88 | unix.SOCK_DGRAM,
89 | 0,
90 | )
91 | if err != nil {
92 | return err
93 | }
94 |
95 | defer unix.Close(fd)
96 |
97 | ifr, err := unix.NewIfreq(name)
98 | if err != nil {
99 | return err
100 | }
101 | ifr.SetUint32(n)
102 | return unix.IoctlIfreq(fd, unix.SIOCSIFMTU, ifr)
103 | }
104 |
--------------------------------------------------------------------------------
/core/device/tun/tun_wireguard.go:
--------------------------------------------------------------------------------
1 | //go:build !linux
2 |
3 | package tun
4 |
5 | import (
6 | "fmt"
7 | "sync"
8 |
9 | "golang.zx2c4.com/wireguard/tun"
10 |
11 | "github.com/xjasonlyu/tun2socks/v2/core/device"
12 | "github.com/xjasonlyu/tun2socks/v2/core/device/iobased"
13 | )
14 |
15 | type TUN struct {
16 | *iobased.Endpoint
17 |
18 | nt *tun.NativeTun
19 | mtu uint32
20 | name string
21 | offset int
22 |
23 | rSizes []int
24 | rBuffs [][]byte
25 | wBuffs [][]byte
26 | rMutex sync.Mutex
27 | wMutex sync.Mutex
28 | }
29 |
30 | func Open(name string, mtu uint32) (_ device.Device, err error) {
31 | defer func() {
32 | if r := recover(); r != nil {
33 | err = fmt.Errorf("open tun: %v", r)
34 | }
35 | }()
36 |
37 | t := &TUN{
38 | name: name,
39 | mtu: mtu,
40 | offset: offset,
41 | rSizes: make([]int, 1),
42 | rBuffs: make([][]byte, 1),
43 | wBuffs: make([][]byte, 1),
44 | }
45 |
46 | forcedMTU := defaultMTU
47 | if t.mtu > 0 {
48 | forcedMTU = int(t.mtu)
49 | }
50 |
51 | nt, err := createTUN(t.name, forcedMTU)
52 | if err != nil {
53 | return nil, fmt.Errorf("create tun: %w", err)
54 | }
55 | t.nt = nt.(*tun.NativeTun)
56 |
57 | tunMTU, err := nt.MTU()
58 | if err != nil {
59 | return nil, fmt.Errorf("get mtu: %w", err)
60 | }
61 | t.mtu = uint32(tunMTU)
62 |
63 | ep, err := iobased.New(t, t.mtu, offset)
64 | if err != nil {
65 | return nil, fmt.Errorf("create endpoint: %w", err)
66 | }
67 | t.Endpoint = ep
68 |
69 | return t, nil
70 | }
71 |
72 | func (t *TUN) Read(packet []byte) (int, error) {
73 | t.rMutex.Lock()
74 | defer t.rMutex.Unlock()
75 | t.rBuffs[0] = packet
76 | _, err := t.nt.Read(t.rBuffs, t.rSizes, t.offset)
77 | return t.rSizes[0], err
78 | }
79 |
80 | func (t *TUN) Write(packet []byte) (int, error) {
81 | t.wMutex.Lock()
82 | defer t.wMutex.Unlock()
83 | t.wBuffs[0] = packet
84 | return t.nt.Write(t.wBuffs, t.offset)
85 | }
86 |
87 | func (t *TUN) Name() string {
88 | name, _ := t.nt.Name()
89 | return name
90 | }
91 |
92 | func (t *TUN) Close() {
93 | defer t.Endpoint.Close()
94 | _ = t.nt.Close()
95 | }
96 |
--------------------------------------------------------------------------------
/core/device/tun/tun_wireguard_unix.go:
--------------------------------------------------------------------------------
1 | //go:build unix && !linux
2 |
3 | package tun
4 |
5 | import (
6 | "golang.zx2c4.com/wireguard/tun"
7 | )
8 |
9 | const (
10 | offset = 4 /* 4 bytes TUN_PI */
11 | defaultMTU = 1500
12 | )
13 |
14 | func createTUN(name string, mtu int) (tun.Device, error) {
15 | return tun.CreateTUN(name, mtu)
16 | }
17 |
--------------------------------------------------------------------------------
/core/device/tun/tun_wireguard_windows.go:
--------------------------------------------------------------------------------
1 | package tun
2 |
3 | import (
4 | "golang.zx2c4.com/wireguard/tun"
5 | )
6 |
7 | const (
8 | offset = 0
9 | defaultMTU = 0 /* auto */
10 | )
11 |
12 | func createTUN(name string, mtu int) (tun.Device, error) {
13 | return tun.CreateTUN(name, mtu)
14 | }
15 |
--------------------------------------------------------------------------------
/core/nic.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "net/netip"
6 |
7 | "gvisor.dev/gvisor/pkg/tcpip"
8 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
9 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
10 | "gvisor.dev/gvisor/pkg/tcpip/stack"
11 |
12 | "github.com/xjasonlyu/tun2socks/v2/core/option"
13 | )
14 |
15 | const (
16 | // nicPromiscuousModeEnabled is the value used by stack to enable
17 | // or disable NIC's promiscuous mode.
18 | nicPromiscuousModeEnabled = true
19 |
20 | // nicSpoofingEnabled is the value used by stack to enable or disable
21 | // NIC's spoofing.
22 | nicSpoofingEnabled = true
23 | )
24 |
25 | // withCreatingNIC creates NIC for stack.
26 | func withCreatingNIC(nicID tcpip.NICID, ep stack.LinkEndpoint) option.Option {
27 | return func(s *stack.Stack) error {
28 | if err := s.CreateNICWithOptions(nicID, ep,
29 | stack.NICOptions{
30 | Disabled: false,
31 | // If no queueing discipline was specified
32 | // provide a stub implementation that just
33 | // delegates to the lower link endpoint.
34 | QDisc: nil,
35 | }); err != nil {
36 | return fmt.Errorf("create NIC: %s", err)
37 | }
38 | return nil
39 | }
40 | }
41 |
42 | // withPromiscuousMode sets promiscuous mode in the given NICs.
43 | func withPromiscuousMode(nicID tcpip.NICID, v bool) option.Option {
44 | return func(s *stack.Stack) error {
45 | if err := s.SetPromiscuousMode(nicID, v); err != nil {
46 | return fmt.Errorf("set promiscuous mode: %s", err)
47 | }
48 | return nil
49 | }
50 | }
51 |
52 | // withSpoofing sets address spoofing in the given NICs, allowing
53 | // endpoints to bind to any address in the NIC.
54 | func withSpoofing(nicID tcpip.NICID, v bool) option.Option {
55 | return func(s *stack.Stack) error {
56 | if err := s.SetSpoofing(nicID, v); err != nil {
57 | return fmt.Errorf("set spoofing: %s", err)
58 | }
59 | return nil
60 | }
61 | }
62 |
63 | // withMulticastGroups adds a NIC to the given multicast groups.
64 | func withMulticastGroups(nicID tcpip.NICID, multicastGroups []netip.Addr) option.Option {
65 | return func(s *stack.Stack) error {
66 | if len(multicastGroups) == 0 {
67 | return nil
68 | }
69 | // The default NIC of tun2socks is working on Spoofing mode. When the UDP Endpoint
70 | // tries to use a non-local address to connect, the network stack will
71 | // generate a temporary addressState to build the route, which can be primary
72 | // but is ephemeral. Nevertheless, when the UDP Endpoint tries to use a
73 | // multicast address to connect, the network stack will select an available
74 | // primary addressState to build the route. However, when tun2socks is in the
75 | // just-initialized or idle state, there will be no available primary addressState,
76 | // and the connect operation will fail. Therefore, we need to add permanent addresses,
77 | // e.g. 10.0.0.1/8 and fd00:1/8, to the default NIC, which are only used to build
78 | // routes for multicast response and do not affect other connections.
79 | //
80 | // In fact, for multicast, the sender normally does not expect a response.
81 | // So, the ep.net.Connect is unnecessary. If we implement a custom UDP Forwarder
82 | // and ForwarderRequest in the future, we can remove these code.
83 | s.AddProtocolAddress(
84 | nicID,
85 | tcpip.ProtocolAddress{
86 | Protocol: ipv4.ProtocolNumber,
87 | AddressWithPrefix: tcpip.AddressWithPrefix{
88 | Address: tcpip.AddrFrom4([4]byte{0x0a, 0, 0, 0x01}),
89 | PrefixLen: 8,
90 | },
91 | },
92 | stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
93 | )
94 | s.AddProtocolAddress(
95 | nicID,
96 | tcpip.ProtocolAddress{
97 | Protocol: ipv6.ProtocolNumber,
98 | AddressWithPrefix: tcpip.AddressWithPrefix{
99 | Address: tcpip.AddrFrom16([16]byte{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}),
100 | PrefixLen: 8,
101 | },
102 | },
103 | stack.AddressProperties{PEB: stack.CanBePrimaryEndpoint},
104 | )
105 | for _, multicastGroup := range multicastGroups {
106 | var err tcpip.Error
107 | switch {
108 | case multicastGroup.Is4():
109 | err = s.JoinGroup(ipv4.ProtocolNumber, nicID, tcpip.AddrFrom4(multicastGroup.As4()))
110 | case multicastGroup.Is6():
111 | err = s.JoinGroup(ipv6.ProtocolNumber, nicID, tcpip.AddrFrom16(multicastGroup.As16()))
112 | }
113 | if err != nil {
114 | return fmt.Errorf("join multicast group: %s", err)
115 | }
116 | }
117 | return nil
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/core/route.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "gvisor.dev/gvisor/pkg/tcpip"
5 | "gvisor.dev/gvisor/pkg/tcpip/header"
6 | "gvisor.dev/gvisor/pkg/tcpip/stack"
7 |
8 | "github.com/xjasonlyu/tun2socks/v2/core/option"
9 | )
10 |
11 | func withRouteTable(nicID tcpip.NICID) option.Option {
12 | return func(s *stack.Stack) error {
13 | s.SetRouteTable([]tcpip.Route{
14 | {
15 | Destination: header.IPv4EmptySubnet,
16 | NIC: nicID,
17 | },
18 | {
19 | Destination: header.IPv6EmptySubnet,
20 | NIC: nicID,
21 | },
22 | })
23 | return nil
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/core/stack.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "net/netip"
5 |
6 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
7 | "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
8 | "gvisor.dev/gvisor/pkg/tcpip/stack"
9 | "gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
10 | "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
11 | "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
12 |
13 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
14 | "github.com/xjasonlyu/tun2socks/v2/core/option"
15 | )
16 |
17 | // Config is the configuration to create *stack.Stack.
18 | type Config struct {
19 | // LinkEndpoints is the interface implemented by
20 | // data link layer protocols.
21 | LinkEndpoint stack.LinkEndpoint
22 |
23 | // TransportHandler is the handler used by internal
24 | // stack to set transport handlers.
25 | TransportHandler adapter.TransportHandler
26 |
27 | // MulticastGroups is used by internal stack to add
28 | // nic to given groups.
29 | MulticastGroups []netip.Addr
30 |
31 | // Options are supplement options to apply settings
32 | // for the internal stack.
33 | Options []option.Option
34 | }
35 |
36 | // CreateStack creates *stack.Stack with given config.
37 | func CreateStack(cfg *Config) (*stack.Stack, error) {
38 | opts := []option.Option{option.WithDefault()}
39 | if len(opts) > 0 {
40 | opts = append(opts, cfg.Options...)
41 | }
42 |
43 | s := stack.New(stack.Options{
44 | NetworkProtocols: []stack.NetworkProtocolFactory{
45 | ipv4.NewProtocol,
46 | ipv6.NewProtocol,
47 | },
48 | TransportProtocols: []stack.TransportProtocolFactory{
49 | tcp.NewProtocol,
50 | udp.NewProtocol,
51 | icmp.NewProtocol4,
52 | icmp.NewProtocol6,
53 | },
54 | })
55 |
56 | // Generate unique NIC id.
57 | nicID := s.NextNICID()
58 |
59 | opts = append(opts,
60 | // Important: We must initiate transport protocol handlers
61 | // before creating NIC, otherwise NIC would dispatch packets
62 | // to stack and cause race condition.
63 | // Initiate transport protocol (TCP/UDP) with given handler.
64 | withTCPHandler(cfg.TransportHandler.HandleTCP),
65 | withUDPHandler(cfg.TransportHandler.HandleUDP),
66 |
67 | // Create stack NIC and then bind link endpoint to it.
68 | withCreatingNIC(nicID, cfg.LinkEndpoint),
69 |
70 | // In the past we did s.AddAddressRange to assign 0.0.0.0/0
71 | // onto the interface. We need that to be able to terminate
72 | // all the incoming connections - to any ip. AddressRange API
73 | // has been removed and the suggested workaround is to use
74 | // Promiscuous mode. https://github.com/google/gvisor/issues/3876
75 | //
76 | // Ref: https://github.com/cloudflare/slirpnetstack/blob/master/stack.go
77 | withPromiscuousMode(nicID, nicPromiscuousModeEnabled),
78 |
79 | // Enable spoofing if a stack may send packets from unowned
80 | // addresses. This change required changes to some netgophers
81 | // since previously, promiscuous mode was enough to let the
82 | // netstack respond to all incoming packets regardless of the
83 | // packet's destination address. Now that a stack.Route is not
84 | // held for each incoming packet, finding a route may fail with
85 | // local addresses we don't own but accepted packets for while
86 | // in promiscuous mode. Since we also want to be able to send
87 | // from any address (in response the received promiscuous mode
88 | // packets), we need to enable spoofing.
89 | //
90 | // Ref: https://github.com/google/gvisor/commit/8c0701462a84ff77e602f1626aec49479c308127
91 | withSpoofing(nicID, nicSpoofingEnabled),
92 |
93 | // Add default route table for IPv4 and IPv6. This will handle
94 | // all incoming ICMP packets.
95 | withRouteTable(nicID),
96 |
97 | // Add default NIC to the given multicast groups.
98 | withMulticastGroups(nicID, cfg.MulticastGroups),
99 | )
100 |
101 | for _, opt := range opts {
102 | if err := opt(s); err != nil {
103 | return nil, err
104 | }
105 | }
106 | return s, nil
107 | }
108 |
--------------------------------------------------------------------------------
/core/tcp.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "time"
5 |
6 | glog "gvisor.dev/gvisor/pkg/log"
7 | "gvisor.dev/gvisor/pkg/tcpip"
8 | "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
9 | "gvisor.dev/gvisor/pkg/tcpip/header"
10 | "gvisor.dev/gvisor/pkg/tcpip/stack"
11 | "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
12 | "gvisor.dev/gvisor/pkg/waiter"
13 |
14 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
15 | "github.com/xjasonlyu/tun2socks/v2/core/option"
16 | )
17 |
18 | const (
19 | // defaultWndSize if set to zero, the default
20 | // receive window buffer size is used instead.
21 | defaultWndSize = 0
22 |
23 | // maxConnAttempts specifies the maximum number
24 | // of in-flight tcp connection attempts.
25 | maxConnAttempts = 2 << 10
26 |
27 | // tcpKeepaliveCount is the maximum number of
28 | // TCP keep-alive probes to send before giving up
29 | // and killing the connection if no response is
30 | // obtained from the other end.
31 | tcpKeepaliveCount = 9
32 |
33 | // tcpKeepaliveIdle specifies the time a connection
34 | // must remain idle before the first TCP keepalive
35 | // packet is sent. Once this time is reached,
36 | // tcpKeepaliveInterval option is used instead.
37 | tcpKeepaliveIdle = 60 * time.Second
38 |
39 | // tcpKeepaliveInterval specifies the interval
40 | // time between sending TCP keepalive packets.
41 | tcpKeepaliveInterval = 30 * time.Second
42 | )
43 |
44 | func withTCPHandler(handle func(adapter.TCPConn)) option.Option {
45 | return func(s *stack.Stack) error {
46 | tcpForwarder := tcp.NewForwarder(s, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) {
47 | var (
48 | wq waiter.Queue
49 | ep tcpip.Endpoint
50 | err tcpip.Error
51 | id = r.ID()
52 | )
53 |
54 | defer func() {
55 | if err != nil {
56 | glog.Debugf("forward tcp request: %s:%d->%s:%d: %s",
57 | id.RemoteAddress, id.RemotePort, id.LocalAddress, id.LocalPort, err)
58 | }
59 | }()
60 |
61 | // Perform a TCP three-way handshake.
62 | ep, err = r.CreateEndpoint(&wq)
63 | if err != nil {
64 | // RST: prevent potential half-open TCP connection leak.
65 | r.Complete(true)
66 | return
67 | }
68 | defer r.Complete(false)
69 |
70 | err = setSocketOptions(s, ep)
71 |
72 | conn := &tcpConn{
73 | TCPConn: gonet.NewTCPConn(&wq, ep),
74 | id: id,
75 | }
76 | handle(conn)
77 | })
78 | s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
79 | return nil
80 | }
81 | }
82 |
83 | func setSocketOptions(s *stack.Stack, ep tcpip.Endpoint) tcpip.Error {
84 | { /* TCP keepalive options */
85 | ep.SocketOptions().SetKeepAlive(true)
86 |
87 | idle := tcpip.KeepaliveIdleOption(tcpKeepaliveIdle)
88 | if err := ep.SetSockOpt(&idle); err != nil {
89 | return err
90 | }
91 |
92 | interval := tcpip.KeepaliveIntervalOption(tcpKeepaliveInterval)
93 | if err := ep.SetSockOpt(&interval); err != nil {
94 | return err
95 | }
96 |
97 | if err := ep.SetSockOptInt(tcpip.KeepaliveCountOption, tcpKeepaliveCount); err != nil {
98 | return err
99 | }
100 | }
101 | { /* TCP recv/send buffer size */
102 | var ss tcpip.TCPSendBufferSizeRangeOption
103 | if err := s.TransportProtocolOption(header.TCPProtocolNumber, &ss); err == nil {
104 | ep.SocketOptions().SetSendBufferSize(int64(ss.Default), false)
105 | }
106 |
107 | var rs tcpip.TCPReceiveBufferSizeRangeOption
108 | if err := s.TransportProtocolOption(header.TCPProtocolNumber, &rs); err == nil {
109 | ep.SocketOptions().SetReceiveBufferSize(int64(rs.Default), false)
110 | }
111 | }
112 | return nil
113 | }
114 |
115 | type tcpConn struct {
116 | *gonet.TCPConn
117 | id stack.TransportEndpointID
118 | }
119 |
120 | func (c *tcpConn) ID() *stack.TransportEndpointID {
121 | return &c.id
122 | }
123 |
--------------------------------------------------------------------------------
/core/udp.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | glog "gvisor.dev/gvisor/pkg/log"
5 | "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
6 | "gvisor.dev/gvisor/pkg/tcpip/stack"
7 | "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
8 | "gvisor.dev/gvisor/pkg/waiter"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
11 | "github.com/xjasonlyu/tun2socks/v2/core/option"
12 | )
13 |
14 | func withUDPHandler(handle func(adapter.UDPConn)) option.Option {
15 | return func(s *stack.Stack) error {
16 | udpForwarder := udp.NewForwarder(s, func(r *udp.ForwarderRequest) {
17 | var (
18 | wq waiter.Queue
19 | id = r.ID()
20 | )
21 | ep, err := r.CreateEndpoint(&wq)
22 | if err != nil {
23 | glog.Debugf("forward udp request: %s:%d->%s:%d: %s",
24 | id.RemoteAddress, id.RemotePort, id.LocalAddress, id.LocalPort, err)
25 | return
26 | }
27 |
28 | conn := &udpConn{
29 | UDPConn: gonet.NewUDPConn(&wq, ep),
30 | id: id,
31 | }
32 | handle(conn)
33 | })
34 | s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
35 | return nil
36 | }
37 | }
38 |
39 | type udpConn struct {
40 | *gonet.UDPConn
41 | id stack.TransportEndpointID
42 | }
43 |
44 | func (c *udpConn) ID() *stack.TransportEndpointID {
45 | return &c.id
46 | }
47 |
--------------------------------------------------------------------------------
/dialer/dialer.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "context"
5 | "net"
6 | "syscall"
7 |
8 | "go.uber.org/atomic"
9 | )
10 |
11 | // DefaultDialer is the default Dialer and is used by DialContext and ListenPacket.
12 | var DefaultDialer = &Dialer{
13 | InterfaceName: atomic.NewString(""),
14 | InterfaceIndex: atomic.NewInt32(0),
15 | RoutingMark: atomic.NewInt32(0),
16 | }
17 |
18 | type Dialer struct {
19 | InterfaceName *atomic.String
20 | InterfaceIndex *atomic.Int32
21 | RoutingMark *atomic.Int32
22 | }
23 |
24 | type Options struct {
25 | // InterfaceName is the name of interface/device to bind.
26 | // If a socket is bound to an interface, only packets received
27 | // from that particular interface are processed by the socket.
28 | InterfaceName string
29 |
30 | // InterfaceIndex is the index of interface/device to bind.
31 | // It is almost the same as InterfaceName except it uses the
32 | // index of the interface instead of the name.
33 | InterfaceIndex int
34 |
35 | // RoutingMark is the mark for each packet sent through this
36 | // socket. Changing the mark can be used for mark-based routing
37 | // without netfilter or for packet filtering.
38 | RoutingMark int
39 | }
40 |
41 | // DialContext is a wrapper around DefaultDialer.DialContext.
42 | func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
43 | return DefaultDialer.DialContext(ctx, network, address)
44 | }
45 |
46 | // ListenPacket is a wrapper around DefaultDialer.ListenPacket.
47 | func ListenPacket(network, address string) (net.PacketConn, error) {
48 | return DefaultDialer.ListenPacket(network, address)
49 | }
50 |
51 | func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
52 | return d.DialContextWithOptions(ctx, network, address, &Options{
53 | InterfaceName: d.InterfaceName.Load(),
54 | InterfaceIndex: int(d.InterfaceIndex.Load()),
55 | RoutingMark: int(d.RoutingMark.Load()),
56 | })
57 | }
58 |
59 | func (_ *Dialer) DialContextWithOptions(ctx context.Context, network, address string, opts *Options) (net.Conn, error) {
60 | d := &net.Dialer{
61 | Control: func(network, address string, c syscall.RawConn) error {
62 | return setSocketOptions(network, address, c, opts)
63 | },
64 | }
65 | return d.DialContext(ctx, network, address)
66 | }
67 |
68 | func (d *Dialer) ListenPacket(network, address string) (net.PacketConn, error) {
69 | return d.ListenPacketWithOptions(network, address, &Options{
70 | InterfaceName: d.InterfaceName.Load(),
71 | InterfaceIndex: int(d.InterfaceIndex.Load()),
72 | RoutingMark: int(d.RoutingMark.Load()),
73 | })
74 | }
75 |
76 | func (_ *Dialer) ListenPacketWithOptions(network, address string, opts *Options) (net.PacketConn, error) {
77 | lc := &net.ListenConfig{
78 | Control: func(network, address string, c syscall.RawConn) error {
79 | return setSocketOptions(network, address, c, opts)
80 | },
81 | }
82 | return lc.ListenPacket(context.Background(), network, address)
83 | }
84 |
--------------------------------------------------------------------------------
/dialer/sockopt.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | func isTCPSocket(network string) bool {
4 | switch network {
5 | case "tcp", "tcp4", "tcp6":
6 | return true
7 | default:
8 | return false
9 | }
10 | }
11 |
12 | func isUDPSocket(network string) bool {
13 | switch network {
14 | case "udp", "udp4", "udp6":
15 | return true
16 | default:
17 | return false
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/dialer/sockopt_darwin.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "net"
5 | "syscall"
6 |
7 | "golang.org/x/sys/unix"
8 | )
9 |
10 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) (err error) {
11 | if opts == nil || !isTCPSocket(network) && !isUDPSocket(network) {
12 | return
13 | }
14 |
15 | var innerErr error
16 | err = c.Control(func(fd uintptr) {
17 | host, _, _ := net.SplitHostPort(address)
18 | if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
19 | return
20 | }
21 |
22 | if opts.InterfaceIndex == 0 && opts.InterfaceName != "" {
23 | if iface, err := net.InterfaceByName(opts.InterfaceName); err == nil {
24 | opts.InterfaceIndex = iface.Index
25 | }
26 | }
27 |
28 | if opts.InterfaceIndex != 0 {
29 | switch network {
30 | case "tcp4", "udp4":
31 | innerErr = unix.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, opts.InterfaceIndex)
32 | case "tcp6", "udp6":
33 | innerErr = unix.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, opts.InterfaceIndex)
34 | }
35 | if innerErr != nil {
36 | return
37 | }
38 | }
39 | })
40 |
41 | if innerErr != nil {
42 | err = innerErr
43 | }
44 | return
45 | }
46 |
--------------------------------------------------------------------------------
/dialer/sockopt_freebsd.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "net"
5 | "syscall"
6 |
7 | "golang.org/x/sys/unix"
8 | )
9 |
10 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) (err error) {
11 | if opts == nil || !isTCPSocket(network) && !isUDPSocket(network) {
12 | return
13 | }
14 |
15 | var innerErr error
16 | err = c.Control(func(fd uintptr) {
17 | host, _, _ := net.SplitHostPort(address)
18 | if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
19 | return
20 | }
21 |
22 | if opts.RoutingMark != 0 {
23 | if innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_USER_COOKIE, opts.RoutingMark); innerErr != nil {
24 | return
25 | }
26 | }
27 | })
28 |
29 | if innerErr != nil {
30 | err = innerErr
31 | }
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/dialer/sockopt_linux.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "net"
5 | "syscall"
6 |
7 | "golang.org/x/sys/unix"
8 | )
9 |
10 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) (err error) {
11 | if opts == nil || !isTCPSocket(network) && !isUDPSocket(network) {
12 | return
13 | }
14 |
15 | var innerErr error
16 | err = c.Control(func(fd uintptr) {
17 | host, _, _ := net.SplitHostPort(address)
18 | if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
19 | return
20 | }
21 |
22 | if opts.InterfaceName == "" && opts.InterfaceIndex != 0 {
23 | if iface, err := net.InterfaceByIndex(opts.InterfaceIndex); err == nil {
24 | opts.InterfaceName = iface.Name
25 | }
26 | }
27 |
28 | if opts.InterfaceName != "" {
29 | if innerErr = unix.BindToDevice(int(fd), opts.InterfaceName); innerErr != nil {
30 | return
31 | }
32 | }
33 | if opts.RoutingMark != 0 {
34 | if innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, opts.RoutingMark); innerErr != nil {
35 | return
36 | }
37 | }
38 | })
39 |
40 | if innerErr != nil {
41 | err = innerErr
42 | }
43 | return
44 | }
45 |
--------------------------------------------------------------------------------
/dialer/sockopt_openbsd.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "net"
5 | "syscall"
6 |
7 | "golang.org/x/sys/unix"
8 | )
9 |
10 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) (err error) {
11 | if opts == nil || !isTCPSocket(network) && !isUDPSocket(network) {
12 | return
13 | }
14 |
15 | var innerErr error
16 | err = c.Control(func(fd uintptr) {
17 | host, _, _ := net.SplitHostPort(address)
18 | if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
19 | return
20 | }
21 |
22 | if opts.RoutingMark != 0 {
23 | if innerErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_RTABLE, opts.RoutingMark); innerErr != nil {
24 | return
25 | }
26 | }
27 | })
28 |
29 | if innerErr != nil {
30 | err = innerErr
31 | }
32 | return
33 | }
34 |
--------------------------------------------------------------------------------
/dialer/sockopt_others.go:
--------------------------------------------------------------------------------
1 | //go:build !unix && !windows
2 |
3 | package dialer
4 |
5 | import "syscall"
6 |
7 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) error {
8 | return nil
9 | }
10 |
--------------------------------------------------------------------------------
/dialer/sockopt_windows.go:
--------------------------------------------------------------------------------
1 | package dialer
2 |
3 | import (
4 | "encoding/binary"
5 | "net"
6 | "syscall"
7 | "unsafe"
8 |
9 | "golang.org/x/sys/windows"
10 | )
11 |
12 | const (
13 | IP_UNICAST_IF = 31
14 | IPV6_UNICAST_IF = 31
15 | )
16 |
17 | func setSocketOptions(network, address string, c syscall.RawConn, opts *Options) (err error) {
18 | if opts == nil || !isTCPSocket(network) && !isUDPSocket(network) {
19 | return
20 | }
21 |
22 | var innerErr error
23 | err = c.Control(func(fd uintptr) {
24 | host, _, _ := net.SplitHostPort(address)
25 | ip := net.ParseIP(host)
26 | if ip != nil && !ip.IsGlobalUnicast() {
27 | return
28 | }
29 |
30 | if opts.InterfaceIndex == 0 && opts.InterfaceName != "" {
31 | if iface, err := net.InterfaceByName(opts.InterfaceName); err == nil {
32 | opts.InterfaceIndex = iface.Index
33 | }
34 | }
35 |
36 | if opts.InterfaceIndex != 0 {
37 | switch network {
38 | case "tcp4", "udp4":
39 | innerErr = bindSocketToInterface4(windows.Handle(fd), uint32(opts.InterfaceIndex))
40 | case "tcp6", "udp6":
41 | innerErr = bindSocketToInterface6(windows.Handle(fd), uint32(opts.InterfaceIndex))
42 | if network == "udp6" && ip == nil {
43 | // The underlying IP net maybe IPv4 even if the `network` param is `udp6`,
44 | // so we should bind socket to interface4 at the same time.
45 | innerErr = bindSocketToInterface4(windows.Handle(fd), uint32(opts.InterfaceIndex))
46 | }
47 | }
48 | }
49 | })
50 |
51 | if innerErr != nil {
52 | err = innerErr
53 | }
54 | return
55 | }
56 |
57 | func bindSocketToInterface4(handle windows.Handle, index uint32) error {
58 | // For IPv4, this parameter must be an interface index in network byte order.
59 | // Ref: https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
60 | var bytes [4]byte
61 | binary.BigEndian.PutUint32(bytes[:], index)
62 | index = *(*uint32)(unsafe.Pointer(&bytes[0]))
63 | return windows.SetsockoptInt(handle, windows.IPPROTO_IP, IP_UNICAST_IF, int(index))
64 | }
65 |
66 | func bindSocketToInterface6(handle windows.Handle, index uint32) error {
67 | return windows.SetsockoptInt(handle, windows.IPPROTO_IPV6, IPV6_UNICAST_IF, int(index))
68 | }
69 |
--------------------------------------------------------------------------------
/dns/resolver.go:
--------------------------------------------------------------------------------
1 | package dns
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/xjasonlyu/tun2socks/v2/dialer"
7 | )
8 |
9 | func init() {
10 | // We must use this DialContext to query DNS
11 | // when using net default resolver.
12 | net.DefaultResolver.PreferGo = true
13 | net.DefaultResolver.Dial = dialer.DefaultDialer.DialContext
14 | }
15 |
--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | TUN="${TUN:-tun0}"
4 | ADDR="${ADDR:-198.18.0.1/15}"
5 | LOGLEVEL="${LOGLEVEL:-info}"
6 |
7 | # default values
8 | TABLE="${TABLE:-0x22b}"
9 | FWMARK="${FWMARK:-0x22b}"
10 | CLONE_MAIN="${CLONE_MAIN:-1}"
11 |
12 | create_tun() {
13 | # create tun device
14 | ip tuntap add mode tun dev "$TUN"
15 | ip addr add "$ADDR" dev "$TUN"
16 | ip link set dev "$TUN" up
17 | }
18 |
19 | create_table() {
20 | if [ "$CLONE_MAIN" -ne 0 ]; then
21 | # clone main route table
22 | ip route show table main |
23 | while read -r route; do
24 | ip route add ${route%linkdown*} table "$TABLE"
25 | done
26 | # replace default route
27 | ip route replace default dev "$TUN" table "$TABLE"
28 | else
29 | # just add default route
30 | ip route add default dev "$TUN" table "$TABLE"
31 | fi
32 | }
33 |
34 | config_route() {
35 | # policy routing
36 | ip rule add not fwmark "$FWMARK" table "$TABLE"
37 | ip rule add fwmark "$FWMARK" to "$ADDR" prohibit
38 |
39 | # add tun included routes
40 | for addr in $(echo "$TUN_INCLUDED_ROUTES" | tr ',' '\n'); do
41 | ip rule add to "$addr" table "$TABLE"
42 | done
43 |
44 | # add tun excluded routes
45 | for addr in $(echo "$TUN_EXCLUDED_ROUTES" | tr ',' '\n'); do
46 | ip rule add to "$addr" table main
47 | done
48 | }
49 |
50 | run() {
51 | create_tun
52 | create_table
53 | config_route
54 |
55 | # execute extra commands
56 | if [ -n "$EXTRA_COMMANDS" ]; then
57 | sh -c "$EXTRA_COMMANDS"
58 | fi
59 |
60 | if [ -n "$MTU" ]; then
61 | ARGS="--mtu $MTU"
62 | fi
63 |
64 | if [ -n "$RESTAPI" ]; then
65 | ARGS="$ARGS --restapi $RESTAPI"
66 | fi
67 |
68 | if [ -n "$UDP_TIMEOUT" ]; then
69 | ARGS="$ARGS --udp-timeout $UDP_TIMEOUT"
70 | fi
71 |
72 | if [ -n "$TCP_SNDBUF" ]; then
73 | ARGS="$ARGS --tcp-sndbuf $TCP_SNDBUF"
74 | fi
75 |
76 | if [ -n "$TCP_RCVBUF" ]; then
77 | ARGS="$ARGS --tcp-rcvbuf $TCP_RCVBUF"
78 | fi
79 |
80 | if [ "$TCP_AUTO_TUNING" = 1 ]; then
81 | ARGS="$ARGS --tcp-auto-tuning"
82 | fi
83 |
84 | if [ -n "$MULTICAST_GROUPS" ]; then
85 | ARGS="$ARGS --multicast-groups $MULTICAST_GROUPS"
86 | fi
87 |
88 | exec tun2socks \
89 | --loglevel "$LOGLEVEL" \
90 | --fwmark "$FWMARK" \
91 | --device "$TUN" \
92 | --proxy "$PROXY" \
93 | $ARGS
94 | }
95 |
96 | run || exit 1
97 |
--------------------------------------------------------------------------------
/docs/benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjasonlyu/tun2socks/4127937ea7c450a5230b273f406c9410acec2be7/docs/benchmark.png
--------------------------------------------------------------------------------
/docs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjasonlyu/tun2socks/4127937ea7c450a5230b273f406c9410acec2be7/docs/icon.png
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xjasonlyu/tun2socks/4127937ea7c450a5230b273f406c9410acec2be7/docs/logo.png
--------------------------------------------------------------------------------
/engine/engine.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "net/netip"
7 | "os/exec"
8 | "sync"
9 | "time"
10 |
11 | "github.com/docker/go-units"
12 | "github.com/google/shlex"
13 | "gvisor.dev/gvisor/pkg/tcpip"
14 | "gvisor.dev/gvisor/pkg/tcpip/stack"
15 |
16 | "github.com/xjasonlyu/tun2socks/v2/core"
17 | "github.com/xjasonlyu/tun2socks/v2/core/device"
18 | "github.com/xjasonlyu/tun2socks/v2/core/option"
19 | "github.com/xjasonlyu/tun2socks/v2/dialer"
20 | "github.com/xjasonlyu/tun2socks/v2/log"
21 | "github.com/xjasonlyu/tun2socks/v2/proxy"
22 | "github.com/xjasonlyu/tun2socks/v2/restapi"
23 | "github.com/xjasonlyu/tun2socks/v2/tunnel"
24 | )
25 |
26 | var (
27 | _engineMu sync.Mutex
28 |
29 | // _defaultKey holds the default key for the engine.
30 | _defaultKey *Key
31 |
32 | // _defaultProxy holds the default proxy for the engine.
33 | _defaultProxy proxy.Proxy
34 |
35 | // _defaultDevice holds the default device for the engine.
36 | _defaultDevice device.Device
37 |
38 | // _defaultStack holds the default stack for the engine.
39 | _defaultStack *stack.Stack
40 | )
41 |
42 | // Start starts the default engine up.
43 | func Start() {
44 | if err := start(); err != nil {
45 | log.Fatalf("[ENGINE] failed to start: %v", err)
46 | }
47 | }
48 |
49 | // Stop shuts the default engine down.
50 | func Stop() {
51 | if err := stop(); err != nil {
52 | log.Fatalf("[ENGINE] failed to stop: %v", err)
53 | }
54 | }
55 |
56 | // Insert loads *Key to the default engine.
57 | func Insert(k *Key) {
58 | _engineMu.Lock()
59 | _defaultKey = k
60 | _engineMu.Unlock()
61 | }
62 |
63 | func start() error {
64 | _engineMu.Lock()
65 | defer _engineMu.Unlock()
66 |
67 | if _defaultKey == nil {
68 | return errors.New("empty key")
69 | }
70 |
71 | for _, f := range []func(*Key) error{
72 | general,
73 | restAPI,
74 | netstack,
75 | } {
76 | if err := f(_defaultKey); err != nil {
77 | return err
78 | }
79 | }
80 | return nil
81 | }
82 |
83 | func stop() (err error) {
84 | _engineMu.Lock()
85 | if _defaultDevice != nil {
86 | _defaultDevice.Close()
87 | }
88 | if _defaultStack != nil {
89 | _defaultStack.Close()
90 | _defaultStack.Wait()
91 | }
92 | _engineMu.Unlock()
93 | return nil
94 | }
95 |
96 | func execCommand(cmd string) error {
97 | parts, err := shlex.Split(cmd)
98 | if err != nil {
99 | return err
100 | }
101 | if len(parts) == 0 {
102 | return errors.New("empty command")
103 | }
104 | _, err = exec.Command(parts[0], parts[1:]...).Output()
105 | return err
106 | }
107 |
108 | func general(k *Key) error {
109 | level, err := log.ParseLevel(k.LogLevel)
110 | if err != nil {
111 | return err
112 | }
113 | log.SetLogger(log.Must(log.NewLeveled(level)))
114 |
115 | if k.Interface != "" {
116 | iface, err := net.InterfaceByName(k.Interface)
117 | if err != nil {
118 | return err
119 | }
120 | dialer.DefaultDialer.InterfaceName.Store(iface.Name)
121 | dialer.DefaultDialer.InterfaceIndex.Store(int32(iface.Index))
122 | log.Infof("[DIALER] bind to interface: %s", k.Interface)
123 | }
124 |
125 | if k.Mark != 0 {
126 | dialer.DefaultDialer.RoutingMark.Store(int32(k.Mark))
127 | log.Infof("[DIALER] set fwmark: %#x", k.Mark)
128 | }
129 |
130 | if k.UDPTimeout > 0 {
131 | if k.UDPTimeout < time.Second {
132 | return errors.New("invalid udp timeout value")
133 | }
134 | tunnel.T().SetUDPTimeout(k.UDPTimeout)
135 | }
136 | return nil
137 | }
138 |
139 | func restAPI(k *Key) error {
140 | if k.RestAPI != "" {
141 | u, err := parseRestAPI(k.RestAPI)
142 | if err != nil {
143 | return err
144 | }
145 | host, token := u.Host, u.User.String()
146 |
147 | restapi.SetStatsFunc(func() tcpip.Stats {
148 | _engineMu.Lock()
149 | defer _engineMu.Unlock()
150 |
151 | // default stack is not initialized.
152 | if _defaultStack == nil {
153 | return tcpip.Stats{}
154 | }
155 | return _defaultStack.Stats()
156 | })
157 |
158 | go func() {
159 | if err := restapi.Start(host, token); err != nil {
160 | log.Errorf("[RESTAPI] failed to start: %v", err)
161 | }
162 | }()
163 | log.Infof("[RESTAPI] serve at: %s", u)
164 | }
165 | return nil
166 | }
167 |
168 | func netstack(k *Key) (err error) {
169 | if k.Proxy == "" {
170 | return errors.New("empty proxy")
171 | }
172 | if k.Device == "" {
173 | return errors.New("empty device")
174 | }
175 |
176 | if k.TUNPreUp != "" {
177 | log.Infof("[TUN] pre-execute command: `%s`", k.TUNPreUp)
178 | if preUpErr := execCommand(k.TUNPreUp); preUpErr != nil {
179 | log.Errorf("[TUN] failed to pre-execute: %s: %v", k.TUNPreUp, preUpErr)
180 | }
181 | }
182 |
183 | defer func() {
184 | if k.TUNPostUp == "" || err != nil {
185 | return
186 | }
187 | log.Infof("[TUN] post-execute command: `%s`", k.TUNPostUp)
188 | if postUpErr := execCommand(k.TUNPostUp); postUpErr != nil {
189 | log.Errorf("[TUN] failed to post-execute: %s: %v", k.TUNPostUp, postUpErr)
190 | }
191 | }()
192 |
193 | if _defaultProxy, err = parseProxy(k.Proxy); err != nil {
194 | return
195 | }
196 | tunnel.T().SetDialer(_defaultProxy)
197 |
198 | if _defaultDevice, err = parseDevice(k.Device, uint32(k.MTU)); err != nil {
199 | return
200 | }
201 |
202 | var multicastGroups []netip.Addr
203 | if multicastGroups, err = parseMulticastGroups(k.MulticastGroups); err != nil {
204 | return err
205 | }
206 |
207 | var opts []option.Option
208 | if k.TCPModerateReceiveBuffer {
209 | opts = append(opts, option.WithTCPModerateReceiveBuffer(true))
210 | }
211 |
212 | if k.TCPSendBufferSize != "" {
213 | size, err := units.RAMInBytes(k.TCPSendBufferSize)
214 | if err != nil {
215 | return err
216 | }
217 | opts = append(opts, option.WithTCPSendBufferSize(int(size)))
218 | }
219 |
220 | if k.TCPReceiveBufferSize != "" {
221 | size, err := units.RAMInBytes(k.TCPReceiveBufferSize)
222 | if err != nil {
223 | return err
224 | }
225 | opts = append(opts, option.WithTCPReceiveBufferSize(int(size)))
226 | }
227 |
228 | if _defaultStack, err = core.CreateStack(&core.Config{
229 | LinkEndpoint: _defaultDevice,
230 | TransportHandler: tunnel.T(),
231 | MulticastGroups: multicastGroups,
232 | Options: opts,
233 | }); err != nil {
234 | return
235 | }
236 |
237 | log.Infof(
238 | "[STACK] %s://%s <-> %s://%s",
239 | _defaultDevice.Type(), _defaultDevice.Name(),
240 | _defaultProxy.Proto(), _defaultProxy.Addr(),
241 | )
242 | return nil
243 | }
244 |
--------------------------------------------------------------------------------
/engine/key.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import "time"
4 |
5 | type Key struct {
6 | MTU int `yaml:"mtu"`
7 | Mark int `yaml:"fwmark"`
8 | Proxy string `yaml:"proxy"`
9 | RestAPI string `yaml:"restapi"`
10 | Device string `yaml:"device"`
11 | LogLevel string `yaml:"loglevel"`
12 | Interface string `yaml:"interface"`
13 | TCPModerateReceiveBuffer bool `yaml:"tcp-moderate-receive-buffer"`
14 | TCPSendBufferSize string `yaml:"tcp-send-buffer-size"`
15 | TCPReceiveBufferSize string `yaml:"tcp-receive-buffer-size"`
16 | MulticastGroups string `yaml:"multicast-groups"`
17 | TUNPreUp string `yaml:"tun-pre-up"`
18 | TUNPostUp string `yaml:"tun-post-up"`
19 | UDPTimeout time.Duration `yaml:"udp-timeout"`
20 | }
21 |
--------------------------------------------------------------------------------
/engine/parse.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "net"
7 | "net/netip"
8 | "net/url"
9 | "runtime"
10 | "strings"
11 |
12 | "github.com/gorilla/schema"
13 |
14 | "github.com/xjasonlyu/tun2socks/v2/core/device"
15 | "github.com/xjasonlyu/tun2socks/v2/core/device/fdbased"
16 | "github.com/xjasonlyu/tun2socks/v2/core/device/tun"
17 | "github.com/xjasonlyu/tun2socks/v2/proxy"
18 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
19 | )
20 |
21 | func parseRestAPI(s string) (*url.URL, error) {
22 | if !strings.Contains(s, "://") {
23 | s = fmt.Sprintf("%s://%s", "http", s)
24 | }
25 |
26 | u, err := url.Parse(s)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | addr, err := net.ResolveTCPAddr("tcp", u.Host)
32 | if err != nil {
33 | return nil, err
34 | }
35 | if addr.IP == nil {
36 | addr.IP = net.IPv4zero /* default: 0.0.0.0 */
37 | }
38 | u.Host = addr.String()
39 |
40 | switch u.Scheme {
41 | case "http":
42 | return u, nil
43 | default:
44 | return nil, fmt.Errorf("unsupported scheme: %s", u.Scheme)
45 | }
46 | }
47 |
48 | func parseDevice(s string, mtu uint32) (device.Device, error) {
49 | if !strings.Contains(s, "://") {
50 | s = fmt.Sprintf("%s://%s", tun.Driver /* default driver */, s)
51 | }
52 |
53 | u, err := url.Parse(s)
54 | if err != nil {
55 | return nil, err
56 | }
57 |
58 | driver := strings.ToLower(u.Scheme)
59 |
60 | switch driver {
61 | case fdbased.Driver:
62 | return parseFD(u, mtu)
63 | case tun.Driver:
64 | return parseTUN(u, mtu)
65 | default:
66 | return nil, fmt.Errorf("unsupported driver: %s", driver)
67 | }
68 | }
69 |
70 | func parseFD(u *url.URL, mtu uint32) (device.Device, error) {
71 | offset := 0
72 | // fd offset in ios
73 | // https://stackoverflow.com/questions/69260852/ios-network-extension-packet-parsing/69487795#69487795
74 | if runtime.GOOS == "ios" {
75 | offset = 4
76 | }
77 | return fdbased.Open(u.Host, mtu, offset)
78 | }
79 |
80 | func parseProxy(s string) (proxy.Proxy, error) {
81 | if !strings.Contains(s, "://") {
82 | s = fmt.Sprintf("%s://%s", proto.Socks5 /* default protocol */, s)
83 | }
84 |
85 | u, err := url.Parse(s)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | protocol := strings.ToLower(u.Scheme)
91 |
92 | switch protocol {
93 | case proto.Direct.String():
94 | return proxy.NewDirect(), nil
95 | case proto.Reject.String():
96 | return proxy.NewReject(), nil
97 | case proto.HTTP.String():
98 | return parseHTTP(u)
99 | case proto.Socks4.String():
100 | return parseSocks4(u)
101 | case proto.Socks5.String():
102 | return parseSocks5(u)
103 | case proto.Shadowsocks.String():
104 | return parseShadowsocks(u)
105 | case proto.Relay.String():
106 | return parseRelay(u)
107 | default:
108 | return nil, fmt.Errorf("unsupported protocol: %s", protocol)
109 | }
110 | }
111 |
112 | func parseHTTP(u *url.URL) (proxy.Proxy, error) {
113 | address, username := u.Host, u.User.Username()
114 | password, _ := u.User.Password()
115 | return proxy.NewHTTP(address, username, password)
116 | }
117 |
118 | func parseSocks4(u *url.URL) (proxy.Proxy, error) {
119 | address, userID := u.Host, u.User.Username()
120 | return proxy.NewSocks4(address, userID)
121 | }
122 |
123 | func parseSocks5(u *url.URL) (proxy.Proxy, error) {
124 | address, username := u.Host, u.User.Username()
125 | password, _ := u.User.Password()
126 |
127 | // Socks5 over UDS
128 | if address == "" {
129 | address = u.Path
130 | }
131 | return proxy.NewSocks5(address, username, password)
132 | }
133 |
134 | func parseShadowsocks(u *url.URL) (proxy.Proxy, error) {
135 | var (
136 | address = u.Host
137 | method, password string
138 | obfsMode, obfsHost string
139 | )
140 |
141 | if ss := u.User.String(); ss == "" {
142 | method = "dummy" // none cipher mode
143 | } else if pass, set := u.User.Password(); set {
144 | method = u.User.Username()
145 | password = pass
146 | } else {
147 | data, _ := base64.RawURLEncoding.DecodeString(ss)
148 | userInfo := strings.SplitN(string(data), ":", 2)
149 | if len(userInfo) == 2 {
150 | method = userInfo[0]
151 | password = userInfo[1]
152 | }
153 | }
154 |
155 | rawQuery, _ := url.QueryUnescape(u.RawQuery)
156 | for _, s := range strings.Split(rawQuery, ";") {
157 | data := strings.SplitN(s, "=", 2)
158 | if len(data) != 2 {
159 | continue
160 | }
161 | key := data[0]
162 | value := data[1]
163 |
164 | switch key {
165 | case "obfs":
166 | obfsMode = value
167 | case "obfs-host":
168 | obfsHost = value
169 | }
170 | }
171 |
172 | return proxy.NewShadowsocks(address, method, password, obfsMode, obfsHost)
173 | }
174 |
175 | func parseRelay(u *url.URL) (proxy.Proxy, error) {
176 | address, username := u.Host, u.User.Username()
177 | password, _ := u.User.Password()
178 |
179 | opts := struct {
180 | NoDelay bool
181 | }{}
182 | if err := schema.NewDecoder().Decode(&opts, u.Query()); err != nil {
183 | return nil, err
184 | }
185 |
186 | return proxy.NewRelay(address, username, password, opts.NoDelay)
187 | }
188 |
189 | func parseMulticastGroups(s string) (multicastGroups []netip.Addr, _ error) {
190 | for _, ip := range strings.Split(s, ",") {
191 | if ip = strings.TrimSpace(ip); ip == "" {
192 | continue
193 | }
194 | addr, err := netip.ParseAddr(ip)
195 | if err != nil {
196 | return nil, err
197 | }
198 | if !addr.IsMulticast() {
199 | return nil, fmt.Errorf("invalid multicast IP: %s", addr)
200 | }
201 | multicastGroups = append(multicastGroups, addr)
202 | }
203 | return
204 | }
205 |
--------------------------------------------------------------------------------
/engine/parse_unix.go:
--------------------------------------------------------------------------------
1 | //go:build unix
2 |
3 | package engine
4 |
5 | import (
6 | "net/url"
7 |
8 | "github.com/xjasonlyu/tun2socks/v2/core/device"
9 | "github.com/xjasonlyu/tun2socks/v2/core/device/tun"
10 | )
11 |
12 | func parseTUN(u *url.URL, mtu uint32) (device.Device, error) {
13 | return tun.Open(u.Host, mtu)
14 | }
15 |
--------------------------------------------------------------------------------
/engine/parse_windows.go:
--------------------------------------------------------------------------------
1 | package engine
2 |
3 | import (
4 | "net/url"
5 |
6 | "github.com/gorilla/schema"
7 | "golang.org/x/sys/windows"
8 | wun "golang.zx2c4.com/wireguard/tun"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/core/device"
11 | "github.com/xjasonlyu/tun2socks/v2/core/device/tun"
12 | "github.com/xjasonlyu/tun2socks/v2/internal/version"
13 | )
14 |
15 | func init() {
16 | wun.WintunTunnelType = version.Name
17 | }
18 |
19 | func parseTUN(u *url.URL, mtu uint32) (device.Device, error) {
20 | opts := struct {
21 | GUID string
22 | }{}
23 | if err := schema.NewDecoder().Decode(&opts, u.Query()); err != nil {
24 | return nil, err
25 | }
26 | if opts.GUID != "" {
27 | guid, err := windows.GUIDFromString(opts.GUID)
28 | if err != nil {
29 | return nil, err
30 | }
31 | wun.WintunStaticRequestedGUID = &guid
32 | }
33 | return tun.Open(u.Host, mtu)
34 | }
35 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/xjasonlyu/tun2socks/v2
2 |
3 | go 1.23.1
4 |
5 | require (
6 | github.com/docker/go-units v0.5.0
7 | github.com/go-chi/chi/v5 v5.2.1
8 | github.com/go-chi/cors v1.2.1
9 | github.com/go-chi/render v1.0.3
10 | github.com/go-gost/relay v0.5.0
11 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
12 | github.com/google/uuid v1.6.0
13 | github.com/gorilla/schema v1.4.1
14 | github.com/gorilla/websocket v1.5.3
15 | github.com/stretchr/testify v1.9.0
16 | go.uber.org/atomic v1.11.0
17 | go.uber.org/automaxprocs v1.6.0
18 | go.uber.org/zap v1.27.0
19 | golang.org/x/crypto v0.38.0
20 | golang.org/x/sys v0.33.0
21 | golang.org/x/time v0.11.0
22 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
23 | gopkg.in/yaml.v3 v3.0.1
24 | gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20
25 | )
26 |
27 | require (
28 | github.com/ajg/form v1.5.1 // indirect
29 | github.com/davecgh/go-spew v1.1.1 // indirect
30 | github.com/google/btree v1.1.3 // indirect
31 | github.com/pmezard/go-difflib v1.0.0 // indirect
32 | go.uber.org/multierr v1.11.0 // indirect
33 | golang.org/x/net v0.40.0 // indirect
34 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
2 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
6 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
7 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
8 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
9 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
10 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
11 | github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
12 | github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
13 | github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
14 | github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
15 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
16 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
17 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
18 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
19 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
20 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
21 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
22 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
23 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
24 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
25 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
26 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
27 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
32 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
33 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
34 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
35 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
36 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
37 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
38 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
39 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
40 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
41 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
42 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
43 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
44 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
45 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
46 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
47 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
48 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
49 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
50 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
51 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
52 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
53 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
54 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
55 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
56 | golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
58 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
59 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
61 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
62 | gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 h1:0DxLu8hxI1OGp1qVRPqNd+2k1a7hMNUNqbZG0IrtKlM=
63 | gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
64 |
--------------------------------------------------------------------------------
/internal/pool/pool.go:
--------------------------------------------------------------------------------
1 | // Package pool provides internal pool utilities.
2 | package pool
3 |
4 | import (
5 | "sync"
6 | )
7 |
8 | // A Pool is a generic wrapper around [sync.Pool] to provide strongly-typed
9 | // object pooling.
10 | //
11 | // Note that SA6002 (ref: https://staticcheck.io/docs/checks/#SA6002) will
12 | // not be detected, so all internal pool use must take care to only store
13 | // pointer types.
14 | type Pool[T any] struct {
15 | pool sync.Pool
16 | }
17 |
18 | // New returns a new [Pool] for T, and will use fn to construct new Ts when
19 | // the pool is empty.
20 | func New[T any](fn func() T) *Pool[T] {
21 | return &Pool[T]{
22 | pool: sync.Pool{
23 | New: func() any {
24 | return fn()
25 | },
26 | },
27 | }
28 | }
29 |
30 | // Get gets a T from the pool, or creates a new one if the pool is empty.
31 | func (p *Pool[T]) Get() T {
32 | return p.pool.Get().(T)
33 | }
34 |
35 | // Put returns x into the pool.
36 | func (p *Pool[T]) Put(x T) {
37 | p.pool.Put(x)
38 | }
39 |
--------------------------------------------------------------------------------
/internal/pool/pool_test.go:
--------------------------------------------------------------------------------
1 | package pool
2 |
3 | import (
4 | "runtime/debug"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | type pooledValue[T any] struct {
12 | value T
13 | }
14 |
15 | func TestNew(t *testing.T) {
16 | // Disable GC to avoid the victim cache during the test.
17 | defer debug.SetGCPercent(debug.SetGCPercent(-1))
18 |
19 | p := New(func() *pooledValue[string] {
20 | return &pooledValue[string]{
21 | value: "new",
22 | }
23 | })
24 |
25 | // Probabilistically, 75% of sync.Pool.Put calls will succeed when -race
26 | // is enabled (see ref below); attempt to make this quasi-deterministic by
27 | // brute force (i.e., put significantly more objects in the pool than we
28 | // will need for the test) in order to avoid testing without race enabled.
29 | //
30 | // ref: https://cs.opensource.google/go/go/+/refs/tags/go1.20.2:src/sync/pool.go;l=100-103
31 | for i := 0; i < 1_000; i++ {
32 | p.Put(&pooledValue[string]{
33 | value: t.Name(),
34 | })
35 | }
36 |
37 | // Ensure that we always get the expected value. Note that this must only
38 | // run a fraction of the number of times that Put is called above.
39 | for i := 0; i < 10; i++ {
40 | func() {
41 | x := p.Get()
42 | defer p.Put(x)
43 | require.Equal(t, t.Name(), x.value)
44 | }()
45 | }
46 |
47 | // Depool all objects that might be in the pool to ensure that it's empty.
48 | for i := 0; i < 1_000; i++ {
49 | p.Get()
50 | }
51 |
52 | // Now that the pool is empty, it should use the value specified in the
53 | // underlying sync.Pool.New func.
54 | require.Equal(t, "new", p.Get().value)
55 | }
56 |
57 | func TestNew_Race(t *testing.T) {
58 | p := New(func() *pooledValue[int] {
59 | return &pooledValue[int]{
60 | value: -1,
61 | }
62 | })
63 |
64 | var wg sync.WaitGroup
65 | defer wg.Wait()
66 |
67 | // Run a number of goroutines that read and write pool object fields to
68 | // tease out races.
69 | for i := 0; i < 1_000; i++ {
70 | i := i
71 |
72 | wg.Add(1)
73 | go func() {
74 | defer wg.Done()
75 |
76 | x := p.Get()
77 | defer p.Put(x)
78 |
79 | // Must both read and write the field.
80 | if n := x.value; n >= -1 {
81 | x.value = i
82 | }
83 | }()
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/internal/version/module.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "runtime/debug"
5 | )
6 |
7 | // Info returns project dependencies as []*debug.Module.
8 | func Info() []*debug.Module {
9 | bi, _ := debug.ReadBuildInfo()
10 | return bi.Deps
11 | }
12 |
--------------------------------------------------------------------------------
/internal/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "strings"
7 | )
8 |
9 | const Name = "tun2socks"
10 |
11 | var (
12 | // Version can be set at link time by executing
13 | // the command: `git describe --abbrev=0 --tags HEAD`
14 | Version string
15 |
16 | // GitCommit can be set at link time by executing
17 | // the command: `git rev-parse --short HEAD`
18 | GitCommit string
19 | )
20 |
21 | func String() string {
22 | return fmt.Sprintf("%s-%s", Name, strings.TrimPrefix(Version, "v"))
23 | }
24 |
25 | func BuildString() string {
26 | return fmt.Sprintf("%s/%s, %s, %s", runtime.GOOS, runtime.GOARCH, runtime.Version(), GitCommit)
27 | }
28 |
--------------------------------------------------------------------------------
/log/doc.go:
--------------------------------------------------------------------------------
1 | // Package log is a thin wrapper based on "go.uber.org/zap".
2 | package log
3 |
--------------------------------------------------------------------------------
/log/emitter.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "runtime"
5 | "strings"
6 | "time"
7 |
8 | glog "gvisor.dev/gvisor/pkg/log"
9 | )
10 |
11 | var _globalE = &emitter{}
12 |
13 | func init() {
14 | glog.SetTarget(_globalE)
15 | }
16 |
17 | type emitter struct {
18 | logger *SugaredLogger
19 | }
20 |
21 | func (e *emitter) setLogger(logger *SugaredLogger) {
22 | e.logger = logger.WithOptions(pkgCallerSkip)
23 | }
24 |
25 | func (e *emitter) logf(level glog.Level, format string, args ...any) {
26 | e.logger.Logf(1-Level(level), "[STACK] "+format, args...)
27 | }
28 |
29 | func (e *emitter) Emit(depth int, level glog.Level, _ time.Time, format string, args ...any) {
30 | if _, file, line, ok := runtime.Caller(depth + 1); ok {
31 | // Ignore: gvisor.dev/gvisor/pkg/tcpip/adapters/gonet/gonet.go:457
32 | if line == 457 && strings.HasSuffix(file, "gonet/gonet.go") {
33 | return
34 | }
35 | }
36 | e.logf(level, format, args...)
37 | }
38 |
--------------------------------------------------------------------------------
/log/level.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "go.uber.org/zap/zapcore"
5 | )
6 |
7 | // Level is an alias for zapcore.Level.
8 | type Level = zapcore.Level
9 |
10 | // Levels are aliases for Level.
11 | const (
12 | DebugLevel = zapcore.DebugLevel
13 | InfoLevel = zapcore.InfoLevel
14 | WarnLevel = zapcore.WarnLevel
15 | ErrorLevel = zapcore.ErrorLevel
16 | DPanicLevel = zapcore.DPanicLevel
17 | PanicLevel = zapcore.PanicLevel
18 | FatalLevel = zapcore.FatalLevel
19 | InvalidLevel = zapcore.InvalidLevel
20 | SilentLevel = InvalidLevel + 1
21 | )
22 |
23 | // ParseLevel is a thin wrapper for zapcore.ParseLevel.
24 | func ParseLevel(text string) (Level, error) {
25 | switch text {
26 | case "silent", "SILENT":
27 | return SilentLevel, nil
28 | default:
29 | return zapcore.ParseLevel(text)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "go.uber.org/zap"
8 | )
9 |
10 | // global Logger and SugaredLogger.
11 | var (
12 | _globalMu sync.RWMutex
13 | _globalL *Logger
14 | _globalS *SugaredLogger
15 | )
16 |
17 | func init() {
18 | SetLogger(zap.Must(zap.NewProduction()))
19 | }
20 |
21 | func NewLeveled(l Level, options ...Option) (*Logger, error) {
22 | switch l {
23 | case SilentLevel:
24 | return zap.NewNop(), nil
25 | case DebugLevel:
26 | return zap.NewDevelopment(options...)
27 | case InfoLevel, WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, FatalLevel:
28 | cfg := zap.NewProductionConfig()
29 | cfg.Level.SetLevel(l)
30 | return cfg.Build(options...)
31 | default:
32 | return nil, fmt.Errorf("invalid level: %s", l)
33 | }
34 | }
35 |
36 | // SetLogger sets the global Logger and SugaredLogger.
37 | func SetLogger(logger *Logger) {
38 | _globalMu.Lock()
39 | defer _globalMu.Unlock()
40 | // apply pkgCallerSkip to global loggers.
41 | _globalL = logger.WithOptions(pkgCallerSkip)
42 | _globalS = _globalL.Sugar()
43 | _globalE.setLogger(_globalS)
44 | }
45 |
46 | func logf(lvl Level, template string, args ...any) {
47 | _globalMu.RLock()
48 | s := _globalS
49 | _globalMu.RUnlock()
50 | s.Logf(lvl, template, args...)
51 | }
52 |
53 | func Debugf(template string, args ...any) {
54 | logf(DebugLevel, template, args...)
55 | }
56 |
57 | func Infof(template string, args ...any) {
58 | logf(InfoLevel, template, args...)
59 | }
60 |
61 | func Warnf(template string, args ...any) {
62 | logf(WarnLevel, template, args...)
63 | }
64 |
65 | func Errorf(template string, args ...any) {
66 | logf(ErrorLevel, template, args...)
67 | }
68 |
69 | func Fatalf(template string, args ...any) {
70 | logf(FatalLevel, template, args...)
71 | }
72 |
--------------------------------------------------------------------------------
/log/zap.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "go.uber.org/zap"
5 | )
6 |
7 | // Must is an alias for zap.Must.
8 | var Must = zap.Must
9 |
10 | // logger aliases for zap.Logger and zap.SugaredLogger.
11 | type (
12 | Logger = zap.Logger
13 | SugaredLogger = zap.SugaredLogger
14 | )
15 |
16 | type (
17 | // Option is an alias for zap.Option.
18 | Option = zap.Option
19 | )
20 |
21 | // pkgCallerSkip skips the pkg wrapper code as the caller.
22 | var pkgCallerSkip = zap.AddCallerSkip(2)
23 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 |
10 | "go.uber.org/automaxprocs/maxprocs"
11 | "gopkg.in/yaml.v3"
12 |
13 | _ "github.com/xjasonlyu/tun2socks/v2/dns"
14 | "github.com/xjasonlyu/tun2socks/v2/engine"
15 | "github.com/xjasonlyu/tun2socks/v2/internal/version"
16 | "github.com/xjasonlyu/tun2socks/v2/log"
17 | )
18 |
19 | var (
20 | key = new(engine.Key)
21 |
22 | configFile string
23 | versionFlag bool
24 | )
25 |
26 | func init() {
27 | flag.IntVar(&key.Mark, "fwmark", 0, "Set firewall MARK (Linux only)")
28 | flag.IntVar(&key.MTU, "mtu", 0, "Set device maximum transmission unit (MTU)")
29 | flag.DurationVar(&key.UDPTimeout, "udp-timeout", 0, "Set timeout for each UDP session")
30 | flag.StringVar(&configFile, "config", "", "YAML format configuration file")
31 | flag.StringVar(&key.Device, "device", "", "Use this device [driver://]name")
32 | flag.StringVar(&key.Interface, "interface", "", "Use network INTERFACE (Linux/MacOS only)")
33 | flag.StringVar(&key.LogLevel, "loglevel", "info", "Log level [debug|info|warn|error|silent]")
34 | flag.StringVar(&key.Proxy, "proxy", "", "Use this proxy [protocol://]host[:port]")
35 | flag.StringVar(&key.RestAPI, "restapi", "", "HTTP statistic server listen address")
36 | flag.StringVar(&key.TCPSendBufferSize, "tcp-sndbuf", "", "Set TCP send buffer size for netstack")
37 | flag.StringVar(&key.TCPReceiveBufferSize, "tcp-rcvbuf", "", "Set TCP receive buffer size for netstack")
38 | flag.BoolVar(&key.TCPModerateReceiveBuffer, "tcp-auto-tuning", false, "Enable TCP receive buffer auto-tuning")
39 | flag.StringVar(&key.MulticastGroups, "multicast-groups", "", "Set multicast groups, separated by commas")
40 | flag.StringVar(&key.TUNPreUp, "tun-pre-up", "", "Execute a command before TUN device setup")
41 | flag.StringVar(&key.TUNPostUp, "tun-post-up", "", "Execute a command after TUN device setup")
42 | flag.BoolVar(&versionFlag, "version", false, "Show version and then quit")
43 | flag.Parse()
44 | }
45 |
46 | func main() {
47 | maxprocs.Set(maxprocs.Logger(func(string, ...any) {}))
48 |
49 | if versionFlag {
50 | fmt.Println(version.String())
51 | fmt.Println(version.BuildString())
52 | os.Exit(0)
53 | }
54 |
55 | if configFile != "" {
56 | data, err := os.ReadFile(configFile)
57 | if err != nil {
58 | log.Fatalf("Failed to read config file '%s': %v", configFile, err)
59 | }
60 | if err = yaml.Unmarshal(data, key); err != nil {
61 | log.Fatalf("Failed to unmarshal config file '%s': %v", configFile, err)
62 | }
63 | }
64 |
65 | engine.Insert(key)
66 |
67 | engine.Start()
68 | defer engine.Stop()
69 |
70 | sigCh := make(chan os.Signal, 1)
71 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
72 | <-sigCh
73 | }
74 |
--------------------------------------------------------------------------------
/metadata/metadata.go:
--------------------------------------------------------------------------------
1 | package metadata
2 |
3 | import (
4 | "net"
5 | "net/netip"
6 | )
7 |
8 | // Metadata contains metadata of transport protocol sessions.
9 | type Metadata struct {
10 | Network Network `json:"network"`
11 | SrcIP netip.Addr `json:"sourceIP"`
12 | MidIP netip.Addr `json:"dialerIP"`
13 | DstIP netip.Addr `json:"destinationIP"`
14 | SrcPort uint16 `json:"sourcePort"`
15 | MidPort uint16 `json:"dialerPort"`
16 | DstPort uint16 `json:"destinationPort"`
17 | }
18 |
19 | func (m *Metadata) DestinationAddrPort() netip.AddrPort {
20 | return netip.AddrPortFrom(m.DstIP, m.DstPort)
21 | }
22 |
23 | func (m *Metadata) DestinationAddress() string {
24 | return m.DestinationAddrPort().String()
25 | }
26 |
27 | func (m *Metadata) SourceAddrPort() netip.AddrPort {
28 | return netip.AddrPortFrom(m.SrcIP, m.SrcPort)
29 | }
30 |
31 | func (m *Metadata) SourceAddress() string {
32 | return m.SourceAddrPort().String()
33 | }
34 |
35 | func (m *Metadata) Addr() net.Addr {
36 | return &Addr{metadata: m}
37 | }
38 |
39 | func (m *Metadata) TCPAddr() *net.TCPAddr {
40 | if m.Network != TCP || !m.DstIP.IsValid() {
41 | return nil
42 | }
43 | return net.TCPAddrFromAddrPort(m.DestinationAddrPort())
44 | }
45 |
46 | func (m *Metadata) UDPAddr() *net.UDPAddr {
47 | if m.Network != UDP || !m.DstIP.IsValid() {
48 | return nil
49 | }
50 | return net.UDPAddrFromAddrPort(m.DestinationAddrPort())
51 | }
52 |
53 | // Addr implements the net.Addr interface.
54 | type Addr struct {
55 | metadata *Metadata
56 | }
57 |
58 | func (a *Addr) Metadata() *Metadata {
59 | return a.metadata
60 | }
61 |
62 | func (a *Addr) Network() string {
63 | return a.metadata.Network.String()
64 | }
65 |
66 | func (a *Addr) String() string {
67 | return a.metadata.DestinationAddress()
68 | }
69 |
--------------------------------------------------------------------------------
/metadata/network.go:
--------------------------------------------------------------------------------
1 | package metadata
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | const (
8 | TCP Network = iota
9 | UDP
10 | )
11 |
12 | type Network uint8
13 |
14 | func (n Network) String() string {
15 | switch n {
16 | case TCP:
17 | return "tcp"
18 | case UDP:
19 | return "udp"
20 | default:
21 | return fmt.Sprintf("network(%d)", n)
22 | }
23 | }
24 |
25 | func (n Network) MarshalText() ([]byte, error) {
26 | return []byte(n.String()), nil
27 | }
28 |
--------------------------------------------------------------------------------
/proxy/base.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 |
8 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
9 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
10 | )
11 |
12 | var _ Proxy = (*Base)(nil)
13 |
14 | type Base struct {
15 | addr string
16 | proto proto.Proto
17 | }
18 |
19 | func (b *Base) Addr() string {
20 | return b.addr
21 | }
22 |
23 | func (b *Base) Proto() proto.Proto {
24 | return b.proto
25 | }
26 |
27 | func (b *Base) DialContext(context.Context, *M.Metadata) (net.Conn, error) {
28 | return nil, errors.ErrUnsupported
29 | }
30 |
31 | func (b *Base) DialUDP(*M.Metadata) (net.PacketConn, error) {
32 | return nil, errors.ErrUnsupported
33 | }
34 |
--------------------------------------------------------------------------------
/proxy/direct.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "net"
6 |
7 | "github.com/xjasonlyu/tun2socks/v2/dialer"
8 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
9 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
10 | )
11 |
12 | var _ Proxy = (*Direct)(nil)
13 |
14 | type Direct struct {
15 | *Base
16 | }
17 |
18 | func NewDirect() *Direct {
19 | return &Direct{
20 | Base: &Base{
21 | proto: proto.Direct,
22 | },
23 | }
24 | }
25 |
26 | func (d *Direct) DialContext(ctx context.Context, metadata *M.Metadata) (net.Conn, error) {
27 | c, err := dialer.DialContext(ctx, "tcp", metadata.DestinationAddress())
28 | if err != nil {
29 | return nil, err
30 | }
31 | setKeepAlive(c)
32 | return c, nil
33 | }
34 |
35 | func (d *Direct) DialUDP(*M.Metadata) (net.PacketConn, error) {
36 | pc, err := dialer.ListenPacket("udp", "")
37 | if err != nil {
38 | return nil, err
39 | }
40 | return &directPacketConn{PacketConn: pc}, nil
41 | }
42 |
43 | type directPacketConn struct {
44 | net.PacketConn
45 | }
46 |
47 | func (pc *directPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
48 | if udpAddr, ok := addr.(*net.UDPAddr); ok {
49 | return pc.PacketConn.WriteTo(b, udpAddr)
50 | }
51 |
52 | udpAddr, err := net.ResolveUDPAddr("udp", addr.String())
53 | if err != nil {
54 | return 0, err
55 | }
56 | return pc.PacketConn.WriteTo(b, udpAddr)
57 | }
58 |
--------------------------------------------------------------------------------
/proxy/http.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bufio"
5 | "context"
6 | "encoding/base64"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "net"
11 | "net/http"
12 | "net/url"
13 |
14 | "github.com/xjasonlyu/tun2socks/v2/dialer"
15 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
16 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
17 | )
18 |
19 | type HTTP struct {
20 | *Base
21 |
22 | user string
23 | pass string
24 | }
25 |
26 | func NewHTTP(addr, user, pass string) (*HTTP, error) {
27 | return &HTTP{
28 | Base: &Base{
29 | addr: addr,
30 | proto: proto.HTTP,
31 | },
32 | user: user,
33 | pass: pass,
34 | }, nil
35 | }
36 |
37 | func (h *HTTP) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
38 | c, err = dialer.DialContext(ctx, "tcp", h.Addr())
39 | if err != nil {
40 | return nil, fmt.Errorf("connect to %s: %w", h.Addr(), err)
41 | }
42 | setKeepAlive(c)
43 |
44 | defer func(c net.Conn) {
45 | safeConnClose(c, err)
46 | }(c)
47 |
48 | err = h.shakeHand(metadata, c)
49 | return
50 | }
51 |
52 | func (h *HTTP) shakeHand(metadata *M.Metadata, rw io.ReadWriter) error {
53 | addr := metadata.DestinationAddress()
54 | req := &http.Request{
55 | Method: http.MethodConnect,
56 | URL: &url.URL{
57 | Host: addr,
58 | },
59 | Host: addr,
60 | Header: http.Header{
61 | "Proxy-Connection": []string{"Keep-Alive"},
62 | },
63 | }
64 |
65 | if h.user != "" && h.pass != "" {
66 | req.Header.Set("Proxy-Authorization", fmt.Sprintf("Basic %s", basicAuth(h.user, h.pass)))
67 | }
68 |
69 | if err := req.Write(rw); err != nil {
70 | return err
71 | }
72 |
73 | resp, err := http.ReadResponse(bufio.NewReader(rw), req)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | switch resp.StatusCode {
79 | case http.StatusOK:
80 | return nil
81 | case http.StatusProxyAuthRequired:
82 | return errors.New("HTTP auth required by proxy")
83 | case http.StatusMethodNotAllowed:
84 | return errors.New("CONNECT method not allowed by proxy")
85 | default:
86 | return fmt.Errorf("HTTP connect status: %s", resp.Status)
87 | }
88 | }
89 |
90 | // The Basic authentication scheme is based on the model that the client
91 | // needs to authenticate itself with a user-id and a password for each
92 | // protection space ("realm"). The realm value is a free-form string
93 | // that can only be compared for equality with other realms on that
94 | // server. The server will service the request only if it can validate
95 | // the user-id and password for the protection space applying to the
96 | // requested resource.
97 | func basicAuth(username, password string) string {
98 | auth := username + ":" + password
99 | return base64.StdEncoding.EncodeToString([]byte(auth))
100 | }
101 |
--------------------------------------------------------------------------------
/proxy/proto/proto.go:
--------------------------------------------------------------------------------
1 | package proto
2 |
3 | import "fmt"
4 |
5 | const (
6 | Direct Proto = iota
7 | Reject
8 | HTTP
9 | Socks4
10 | Socks5
11 | Shadowsocks
12 | Relay
13 | )
14 |
15 | type Proto uint8
16 |
17 | func (proto Proto) String() string {
18 | switch proto {
19 | case Direct:
20 | return "direct"
21 | case Reject:
22 | return "reject"
23 | case HTTP:
24 | return "http"
25 | case Socks4:
26 | return "socks4"
27 | case Socks5:
28 | return "socks5"
29 | case Shadowsocks:
30 | return "ss"
31 | case Relay:
32 | return "relay"
33 | default:
34 | return fmt.Sprintf("proto(%d)", proto)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/proxy/proxy.go:
--------------------------------------------------------------------------------
1 | // Package proxy provides implementations of proxy protocols.
2 | package proxy
3 |
4 | import (
5 | "context"
6 | "net"
7 | "time"
8 |
9 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
10 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
11 | )
12 |
13 | const (
14 | tcpConnectTimeout = 5 * time.Second
15 | )
16 |
17 | var _defaultDialer Dialer = &Base{}
18 |
19 | type Dialer interface {
20 | DialContext(context.Context, *M.Metadata) (net.Conn, error)
21 | DialUDP(*M.Metadata) (net.PacketConn, error)
22 | }
23 |
24 | type Proxy interface {
25 | Dialer
26 | Addr() string
27 | Proto() proto.Proto
28 | }
29 |
30 | // SetDialer sets default Dialer.
31 | func SetDialer(d Dialer) {
32 | _defaultDialer = d
33 | }
34 |
35 | // Dial uses default Dialer to dial TCP.
36 | func Dial(metadata *M.Metadata) (net.Conn, error) {
37 | ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
38 | defer cancel()
39 | return _defaultDialer.DialContext(ctx, metadata)
40 | }
41 |
42 | // DialContext uses default Dialer to dial TCP with context.
43 | func DialContext(ctx context.Context, metadata *M.Metadata) (net.Conn, error) {
44 | return _defaultDialer.DialContext(ctx, metadata)
45 | }
46 |
47 | // DialUDP uses default Dialer to dial UDP.
48 | func DialUDP(metadata *M.Metadata) (net.PacketConn, error) {
49 | return _defaultDialer.DialUDP(metadata)
50 | }
51 |
--------------------------------------------------------------------------------
/proxy/reject.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "io"
6 | "net"
7 | "time"
8 |
9 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
10 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
11 | )
12 |
13 | var _ Proxy = (*Reject)(nil)
14 |
15 | type Reject struct {
16 | *Base
17 | }
18 |
19 | func NewReject() *Reject {
20 | return &Reject{
21 | Base: &Base{
22 | proto: proto.Reject,
23 | },
24 | }
25 | }
26 |
27 | func (r *Reject) DialContext(context.Context, *M.Metadata) (net.Conn, error) {
28 | return &nopConn{}, nil
29 | }
30 |
31 | func (r *Reject) DialUDP(*M.Metadata) (net.PacketConn, error) {
32 | return &nopPacketConn{}, nil
33 | }
34 |
35 | type nopConn struct{}
36 |
37 | func (rw *nopConn) Read([]byte) (int, error) { return 0, io.EOF }
38 | func (rw *nopConn) Write([]byte) (int, error) { return 0, io.EOF }
39 | func (rw *nopConn) Close() error { return nil }
40 | func (rw *nopConn) LocalAddr() net.Addr { return nil }
41 | func (rw *nopConn) RemoteAddr() net.Addr { return nil }
42 | func (rw *nopConn) SetDeadline(time.Time) error { return nil }
43 | func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
44 | func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
45 |
46 | type nopPacketConn struct{}
47 |
48 | func (npc *nopPacketConn) WriteTo(b []byte, _ net.Addr) (n int, err error) { return len(b), nil }
49 | func (npc *nopPacketConn) ReadFrom([]byte) (int, net.Addr, error) { return 0, nil, io.EOF }
50 | func (npc *nopPacketConn) Close() error { return nil }
51 | func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
52 | func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
53 | func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
54 | func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
55 |
--------------------------------------------------------------------------------
/proxy/relay.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/binary"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "math"
11 | "net"
12 | "sync"
13 |
14 | "github.com/go-gost/relay"
15 |
16 | "github.com/xjasonlyu/tun2socks/v2/buffer"
17 | "github.com/xjasonlyu/tun2socks/v2/dialer"
18 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
19 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
20 | )
21 |
22 | var _ Proxy = (*Relay)(nil)
23 |
24 | type Relay struct {
25 | *Base
26 |
27 | user string
28 | pass string
29 |
30 | noDelay bool
31 | }
32 |
33 | func NewRelay(addr, user, pass string, noDelay bool) (*Relay, error) {
34 | return &Relay{
35 | Base: &Base{
36 | addr: addr,
37 | proto: proto.Relay,
38 | },
39 | user: user,
40 | pass: pass,
41 | noDelay: noDelay,
42 | }, nil
43 | }
44 |
45 | func (rl *Relay) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
46 | return rl.dialContext(ctx, metadata)
47 | }
48 |
49 | func (rl *Relay) DialUDP(metadata *M.Metadata) (net.PacketConn, error) {
50 | ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
51 | defer cancel()
52 |
53 | return rl.dialContext(ctx, metadata)
54 | }
55 |
56 | func (rl *Relay) dialContext(ctx context.Context, metadata *M.Metadata) (rc *relayConn, err error) {
57 | var c net.Conn
58 |
59 | c, err = dialer.DialContext(ctx, "tcp", rl.Addr())
60 | if err != nil {
61 | return nil, fmt.Errorf("connect to %s: %w", rl.Addr(), err)
62 | }
63 | setKeepAlive(c)
64 |
65 | defer func(c net.Conn) {
66 | safeConnClose(c, err)
67 | }(c)
68 |
69 | req := relay.Request{
70 | Version: relay.Version1,
71 | Cmd: relay.CmdConnect,
72 | }
73 |
74 | if metadata.Network == M.UDP {
75 | req.Cmd |= relay.FUDP
76 | req.Features = append(req.Features, &relay.NetworkFeature{
77 | Network: relay.NetworkUDP,
78 | })
79 | }
80 |
81 | if rl.user != "" {
82 | req.Features = append(req.Features, &relay.UserAuthFeature{
83 | Username: rl.user,
84 | Password: rl.pass,
85 | })
86 | }
87 |
88 | req.Features = append(req.Features, serializeRelayAddr(metadata))
89 |
90 | if rl.noDelay {
91 | if _, err = req.WriteTo(c); err != nil {
92 | return
93 | }
94 | if err = readRelayResponse(c); err != nil {
95 | return
96 | }
97 | }
98 |
99 | switch metadata.Network {
100 | case M.TCP:
101 | rc = newRelayConn(c, metadata.Addr(), rl.noDelay, false)
102 | if !rl.noDelay {
103 | if _, err = req.WriteTo(rc.wbuf); err != nil {
104 | return
105 | }
106 | }
107 | case M.UDP:
108 | rc = newRelayConn(c, metadata.Addr(), rl.noDelay, true)
109 | if !rl.noDelay {
110 | if _, err = req.WriteTo(rc.wbuf); err != nil {
111 | return
112 | }
113 | }
114 | default:
115 | err = fmt.Errorf("network %s is unsupported", metadata.Network)
116 | return
117 | }
118 |
119 | return
120 | }
121 |
122 | type relayConn struct {
123 | net.Conn
124 | udp bool
125 | addr net.Addr
126 | once sync.Once
127 | wbuf *bytes.Buffer
128 | }
129 |
130 | func newRelayConn(c net.Conn, addr net.Addr, noDelay, udp bool) *relayConn {
131 | rc := &relayConn{
132 | Conn: c,
133 | addr: addr,
134 | udp: udp,
135 | }
136 | if !noDelay {
137 | rc.wbuf = &bytes.Buffer{}
138 | }
139 | return rc
140 | }
141 |
142 | func (rc *relayConn) ReadFrom(b []byte) (int, net.Addr, error) {
143 | n, err := rc.Read(b)
144 | return n, rc.addr, err
145 | }
146 |
147 | func (rc *relayConn) Read(b []byte) (n int, err error) {
148 | rc.once.Do(func() {
149 | if rc.wbuf != nil {
150 | err = readRelayResponse(rc.Conn)
151 | }
152 | })
153 | if err != nil {
154 | return
155 | }
156 |
157 | if !rc.udp {
158 | return rc.Conn.Read(b)
159 | }
160 |
161 | var bb [2]byte
162 | _, err = io.ReadFull(rc.Conn, bb[:])
163 | if err != nil {
164 | return
165 | }
166 |
167 | dLen := int(binary.BigEndian.Uint16(bb[:]))
168 | if len(b) >= dLen {
169 | return io.ReadFull(rc.Conn, b[:dLen])
170 | }
171 |
172 | buf := buffer.Get(dLen)
173 | defer buffer.Put(buf)
174 | _, err = io.ReadFull(rc.Conn, buf)
175 | n = copy(b, buf)
176 |
177 | return
178 | }
179 |
180 | func (rc *relayConn) WriteTo(b []byte, _ net.Addr) (int, error) {
181 | return rc.Write(b)
182 | }
183 |
184 | func (rc *relayConn) Write(b []byte) (int, error) {
185 | if rc.udp {
186 | return rc.udpWrite(b)
187 | }
188 | return rc.tcpWrite(b)
189 | }
190 |
191 | func (rc *relayConn) tcpWrite(b []byte) (n int, err error) {
192 | if rc.wbuf != nil && rc.wbuf.Len() > 0 {
193 | n = len(b)
194 | rc.wbuf.Write(b)
195 | _, err = rc.Conn.Write(rc.wbuf.Bytes())
196 | rc.wbuf.Reset()
197 | return
198 | }
199 | return rc.Conn.Write(b)
200 | }
201 |
202 | func (rc *relayConn) udpWrite(b []byte) (n int, err error) {
203 | if len(b) > math.MaxUint16 {
204 | err = errors.New("write: data maximum exceeded")
205 | return
206 | }
207 |
208 | n = len(b)
209 | if rc.wbuf != nil && rc.wbuf.Len() > 0 {
210 | var bb [2]byte
211 | binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
212 | rc.wbuf.Write(bb[:])
213 | rc.wbuf.Write(b)
214 | _, err = rc.wbuf.WriteTo(rc.Conn)
215 | return
216 | }
217 |
218 | var bb [2]byte
219 | binary.BigEndian.PutUint16(bb[:], uint16(len(b)))
220 | _, err = rc.Conn.Write(bb[:])
221 | if err != nil {
222 | return
223 | }
224 | return rc.Conn.Write(b)
225 | }
226 |
227 | func readRelayResponse(r io.Reader) error {
228 | resp := relay.Response{}
229 | if _, err := resp.ReadFrom(r); err != nil {
230 | return err
231 | }
232 | if resp.Version != relay.Version1 {
233 | return relay.ErrBadVersion
234 | }
235 | if resp.Status != relay.StatusOK {
236 | return fmt.Errorf("status %d", resp.Status)
237 | }
238 | return nil
239 | }
240 |
241 | func serializeRelayAddr(m *M.Metadata) *relay.AddrFeature {
242 | af := &relay.AddrFeature{
243 | Host: m.DstIP.String(),
244 | Port: m.DstPort,
245 | }
246 | if m.DstIP.Is4() {
247 | af.AType = relay.AddrIPv4
248 | } else {
249 | af.AType = relay.AddrIPv6
250 | }
251 | return af
252 | }
253 |
--------------------------------------------------------------------------------
/proxy/shadowsocks.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net"
8 |
9 | "github.com/xjasonlyu/tun2socks/v2/dialer"
10 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
11 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
12 | "github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/core"
13 | obfs "github.com/xjasonlyu/tun2socks/v2/transport/simple-obfs"
14 | "github.com/xjasonlyu/tun2socks/v2/transport/socks5"
15 | )
16 |
17 | var _ Proxy = (*Shadowsocks)(nil)
18 |
19 | type Shadowsocks struct {
20 | *Base
21 |
22 | cipher core.Cipher
23 |
24 | // simple-obfs plugin
25 | obfsMode, obfsHost string
26 | }
27 |
28 | func NewShadowsocks(addr, method, password, obfsMode, obfsHost string) (*Shadowsocks, error) {
29 | cipher, err := core.PickCipher(method, nil, password)
30 | if err != nil {
31 | return nil, fmt.Errorf("ss initialize: %w", err)
32 | }
33 |
34 | return &Shadowsocks{
35 | Base: &Base{
36 | addr: addr,
37 | proto: proto.Shadowsocks,
38 | },
39 | cipher: cipher,
40 | obfsMode: obfsMode,
41 | obfsHost: obfsHost,
42 | }, nil
43 | }
44 |
45 | func (ss *Shadowsocks) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
46 | c, err = dialer.DialContext(ctx, "tcp", ss.Addr())
47 | if err != nil {
48 | return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err)
49 | }
50 | setKeepAlive(c)
51 |
52 | defer func(c net.Conn) {
53 | safeConnClose(c, err)
54 | }(c)
55 |
56 | switch ss.obfsMode {
57 | case "tls":
58 | c = obfs.NewTLSObfs(c, ss.obfsHost)
59 | case "http":
60 | _, port, _ := net.SplitHostPort(ss.addr)
61 | c = obfs.NewHTTPObfs(c, ss.obfsHost, port)
62 | }
63 |
64 | c = ss.cipher.StreamConn(c)
65 | _, err = c.Write(serializeSocksAddr(metadata))
66 | return
67 | }
68 |
69 | func (ss *Shadowsocks) DialUDP(*M.Metadata) (net.PacketConn, error) {
70 | pc, err := dialer.ListenPacket("udp", "")
71 | if err != nil {
72 | return nil, fmt.Errorf("listen packet: %w", err)
73 | }
74 |
75 | udpAddr, err := net.ResolveUDPAddr("udp", ss.Addr())
76 | if err != nil {
77 | return nil, fmt.Errorf("resolve udp address %s: %w", ss.Addr(), err)
78 | }
79 |
80 | pc = ss.cipher.PacketConn(pc)
81 | return &ssPacketConn{PacketConn: pc, rAddr: udpAddr}, nil
82 | }
83 |
84 | type ssPacketConn struct {
85 | net.PacketConn
86 |
87 | rAddr net.Addr
88 | }
89 |
90 | func (pc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
91 | var packet []byte
92 | if ma, ok := addr.(*M.Addr); ok {
93 | packet, err = socks5.EncodeUDPPacket(serializeSocksAddr(ma.Metadata()), b)
94 | } else {
95 | packet, err = socks5.EncodeUDPPacket(socks5.ParseAddr(addr), b)
96 | }
97 |
98 | if err != nil {
99 | return
100 | }
101 | return pc.PacketConn.WriteTo(packet[3:], pc.rAddr)
102 | }
103 |
104 | func (pc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
105 | n, _, err := pc.PacketConn.ReadFrom(b)
106 | if err != nil {
107 | return 0, nil, err
108 | }
109 |
110 | addr := socks5.SplitAddr(b[:n])
111 | if addr == nil {
112 | return 0, nil, errors.New("parse addr error")
113 | }
114 |
115 | udpAddr := addr.UDPAddr()
116 | if udpAddr == nil {
117 | return 0, nil, errors.New("parse addr error")
118 | }
119 |
120 | copy(b, b[len(addr):])
121 | return n - len(addr), udpAddr, err
122 | }
123 |
--------------------------------------------------------------------------------
/proxy/socks4.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 |
8 | "github.com/xjasonlyu/tun2socks/v2/dialer"
9 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
10 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
11 | "github.com/xjasonlyu/tun2socks/v2/transport/socks4"
12 | )
13 |
14 | var _ Proxy = (*Socks4)(nil)
15 |
16 | type Socks4 struct {
17 | *Base
18 |
19 | userID string
20 | }
21 |
22 | func NewSocks4(addr, userID string) (*Socks4, error) {
23 | return &Socks4{
24 | Base: &Base{
25 | addr: addr,
26 | proto: proto.Socks4,
27 | },
28 | userID: userID,
29 | }, nil
30 | }
31 |
32 | func (ss *Socks4) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
33 | c, err = dialer.DialContext(ctx, "tcp", ss.Addr())
34 | if err != nil {
35 | return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err)
36 | }
37 | setKeepAlive(c)
38 |
39 | defer func(c net.Conn) {
40 | safeConnClose(c, err)
41 | }(c)
42 |
43 | err = socks4.ClientHandshake(c, metadata.DestinationAddress(), socks4.CmdConnect, ss.userID)
44 | return
45 | }
46 |
--------------------------------------------------------------------------------
/proxy/socks5.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/dialer"
11 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
12 | "github.com/xjasonlyu/tun2socks/v2/proxy/proto"
13 | "github.com/xjasonlyu/tun2socks/v2/transport/socks5"
14 | )
15 |
16 | var _ Proxy = (*Socks5)(nil)
17 |
18 | type Socks5 struct {
19 | *Base
20 |
21 | user string
22 | pass string
23 |
24 | // unix indicates if socks5 over UDS is enabled.
25 | unix bool
26 | }
27 |
28 | func NewSocks5(addr, user, pass string) (*Socks5, error) {
29 | unix := len(addr) > 0 && addr[0] == '/'
30 |
31 | // For support Linux abstract namespace
32 | if len(addr) > 2 && addr[1] == '@' || addr[1] == 0x00 {
33 | addr = addr[1:]
34 | }
35 |
36 | return &Socks5{
37 | Base: &Base{
38 | addr: addr,
39 | proto: proto.Socks5,
40 | },
41 | user: user,
42 | pass: pass,
43 | unix: unix,
44 | }, nil
45 | }
46 |
47 | func (ss *Socks5) DialContext(ctx context.Context, metadata *M.Metadata) (c net.Conn, err error) {
48 | network := "tcp"
49 | if ss.unix {
50 | network = "unix"
51 | }
52 |
53 | c, err = dialer.DialContext(ctx, network, ss.Addr())
54 | if err != nil {
55 | return nil, fmt.Errorf("connect to %s: %w", ss.Addr(), err)
56 | }
57 | setKeepAlive(c)
58 |
59 | defer func(c net.Conn) {
60 | safeConnClose(c, err)
61 | }(c)
62 |
63 | var user *socks5.User
64 | if ss.user != "" {
65 | user = &socks5.User{
66 | Username: ss.user,
67 | Password: ss.pass,
68 | }
69 | }
70 |
71 | _, err = socks5.ClientHandshake(c, serializeSocksAddr(metadata), socks5.CmdConnect, user)
72 | return
73 | }
74 |
75 | func (ss *Socks5) DialUDP(*M.Metadata) (_ net.PacketConn, err error) {
76 | if ss.unix {
77 | return nil, fmt.Errorf("%w when unix domain socket is enabled", errors.ErrUnsupported)
78 | }
79 |
80 | ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
81 | defer cancel()
82 |
83 | c, err := dialer.DialContext(ctx, "tcp", ss.Addr())
84 | if err != nil {
85 | err = fmt.Errorf("connect to %s: %w", ss.Addr(), err)
86 | return
87 | }
88 | setKeepAlive(c)
89 |
90 | defer func() {
91 | if err != nil && c != nil {
92 | c.Close()
93 | }
94 | }()
95 |
96 | var user *socks5.User
97 | if ss.user != "" {
98 | user = &socks5.User{
99 | Username: ss.user,
100 | Password: ss.pass,
101 | }
102 | }
103 |
104 | // The UDP ASSOCIATE request is used to establish an association within
105 | // the UDP relay process to handle UDP datagrams. The DST.ADDR and
106 | // DST.PORT fields contain the address and port that the client expects
107 | // to use to send UDP datagrams on for the association. The server MAY
108 | // use this information to limit access to the association. If the
109 | // client is not in possession of the information at the time of the UDP
110 | // ASSOCIATE, the client MUST use a port number and address of all
111 | // zeros. RFC1928
112 | var targetAddr socks5.Addr = []byte{socks5.AtypIPv4, 0, 0, 0, 0, 0, 0}
113 |
114 | addr, err := socks5.ClientHandshake(c, targetAddr, socks5.CmdUDPAssociate, user)
115 | if err != nil {
116 | return nil, fmt.Errorf("client handshake: %w", err)
117 | }
118 |
119 | pc, err := dialer.ListenPacket("udp", "")
120 | if err != nil {
121 | return nil, fmt.Errorf("listen packet: %w", err)
122 | }
123 |
124 | go func() {
125 | io.Copy(io.Discard, c)
126 | c.Close()
127 | // A UDP association terminates when the TCP connection that the UDP
128 | // ASSOCIATE request arrived on terminates. RFC1928
129 | pc.Close()
130 | }()
131 |
132 | bindAddr := addr.UDPAddr()
133 | if bindAddr == nil {
134 | return nil, fmt.Errorf("invalid UDP binding address: %#v", addr)
135 | }
136 |
137 | if bindAddr.IP.IsUnspecified() { /* e.g. "0.0.0.0" or "::" */
138 | udpAddr, err := net.ResolveUDPAddr("udp", ss.Addr())
139 | if err != nil {
140 | return nil, fmt.Errorf("resolve udp address %s: %w", ss.Addr(), err)
141 | }
142 | bindAddr.IP = udpAddr.IP
143 | }
144 |
145 | return &socksPacketConn{PacketConn: pc, rAddr: bindAddr, tcpConn: c}, nil
146 | }
147 |
148 | type socksPacketConn struct {
149 | net.PacketConn
150 |
151 | rAddr net.Addr
152 | tcpConn net.Conn
153 | }
154 |
155 | func (pc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
156 | var packet []byte
157 | if ma, ok := addr.(*M.Addr); ok {
158 | packet, err = socks5.EncodeUDPPacket(serializeSocksAddr(ma.Metadata()), b)
159 | } else {
160 | packet, err = socks5.EncodeUDPPacket(socks5.ParseAddr(addr), b)
161 | }
162 |
163 | if err != nil {
164 | return
165 | }
166 | return pc.PacketConn.WriteTo(packet, pc.rAddr)
167 | }
168 |
169 | func (pc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
170 | n, _, err := pc.PacketConn.ReadFrom(b)
171 | if err != nil {
172 | return 0, nil, err
173 | }
174 |
175 | addr, payload, err := socks5.DecodeUDPPacket(b)
176 | if err != nil {
177 | return 0, nil, err
178 | }
179 |
180 | udpAddr := addr.UDPAddr()
181 | if udpAddr == nil {
182 | return 0, nil, fmt.Errorf("convert %s to UDPAddr is nil", addr)
183 | }
184 |
185 | // due to DecodeUDPPacket is mutable, record addr length
186 | copy(b, payload)
187 | return n - len(addr) - 3, udpAddr, nil
188 | }
189 |
190 | func (pc *socksPacketConn) Close() error {
191 | pc.tcpConn.Close()
192 | return pc.PacketConn.Close()
193 | }
194 |
195 | func serializeSocksAddr(m *M.Metadata) socks5.Addr {
196 | return socks5.SerializeAddr("", m.DstIP, m.DstPort)
197 | }
198 |
--------------------------------------------------------------------------------
/proxy/util.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "net"
5 | "time"
6 | )
7 |
8 | const (
9 | tcpKeepAlivePeriod = 30 * time.Second
10 | )
11 |
12 | // setKeepAlive sets tcp keepalive option for tcp connection.
13 | func setKeepAlive(c net.Conn) {
14 | if tcp, ok := c.(*net.TCPConn); ok {
15 | tcp.SetKeepAlive(true)
16 | tcp.SetKeepAlivePeriod(tcpKeepAlivePeriod)
17 | }
18 | }
19 |
20 | // safeConnClose closes tcp connection safely.
21 | func safeConnClose(c net.Conn, err error) {
22 | if c != nil && err != nil {
23 | c.Close()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/restapi/connections.go:
--------------------------------------------------------------------------------
1 | package restapi
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | "github.com/go-chi/chi/v5"
11 | "github.com/go-chi/render"
12 | "github.com/gorilla/websocket"
13 |
14 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
15 | )
16 |
17 | const defaultInterval = 1000
18 |
19 | func init() {
20 | registerEndpoint("/connections", connectionRouter())
21 | }
22 |
23 | func connectionRouter() http.Handler {
24 | r := chi.NewRouter()
25 | r.Get("/", getConnections)
26 | r.Delete("/", closeAllConnections)
27 | r.Delete("/{id}", closeConnection)
28 | return r
29 | }
30 |
31 | func getConnections(w http.ResponseWriter, r *http.Request) {
32 | if !websocket.IsWebSocketUpgrade(r) {
33 | render.JSON(w, r, statistic.DefaultManager.Snapshot())
34 | return
35 | }
36 |
37 | conn, err := _upgrader.Upgrade(w, r, nil)
38 | if err != nil {
39 | return
40 | }
41 |
42 | intervalStr := r.URL.Query().Get("interval")
43 | interval := defaultInterval
44 | if intervalStr != "" {
45 | t, err := strconv.Atoi(intervalStr)
46 | if err != nil {
47 | render.Status(r, http.StatusBadRequest)
48 | render.JSON(w, r, ErrBadRequest)
49 | return
50 | }
51 |
52 | interval = t
53 | }
54 |
55 | buf := &bytes.Buffer{}
56 | sendSnapshot := func() error {
57 | buf.Reset()
58 | if err := json.NewEncoder(buf).Encode(statistic.DefaultManager.Snapshot()); err != nil {
59 | return err
60 | }
61 |
62 | return conn.WriteMessage(websocket.TextMessage, buf.Bytes())
63 | }
64 |
65 | if err := sendSnapshot(); err != nil {
66 | return
67 | }
68 |
69 | tick := time.NewTicker(time.Millisecond * time.Duration(interval))
70 | defer tick.Stop()
71 | for range tick.C {
72 | if err := sendSnapshot(); err != nil {
73 | break
74 | }
75 | }
76 | }
77 |
78 | func closeConnection(w http.ResponseWriter, r *http.Request) {
79 | id := chi.URLParam(r, "id")
80 | snapshot := statistic.DefaultManager.Snapshot()
81 | for _, c := range snapshot.Connections {
82 | if id == c.ID() {
83 | _ = c.Close()
84 | break
85 | }
86 | }
87 | render.NoContent(w, r)
88 | }
89 |
90 | func closeAllConnections(w http.ResponseWriter, r *http.Request) {
91 | snapshot := statistic.DefaultManager.Snapshot()
92 | for _, c := range snapshot.Connections {
93 | _ = c.Close()
94 | }
95 | render.NoContent(w, r)
96 | }
97 |
--------------------------------------------------------------------------------
/restapi/debug.go:
--------------------------------------------------------------------------------
1 | //go:build debug
2 |
3 | package restapi
4 |
5 | import (
6 | "net/http"
7 | "net/http/pprof"
8 |
9 | "github.com/go-chi/chi/v5"
10 | )
11 |
12 | func init() {
13 | registerEndpoint("/debug/pprof/", pprofRouter())
14 | }
15 |
16 | func pprofRouter() http.Handler {
17 | r := chi.NewRouter()
18 | r.HandleFunc("/", pprof.Index)
19 | r.HandleFunc("/cmdline", pprof.Cmdline)
20 | r.HandleFunc("/profile", pprof.Profile)
21 | r.HandleFunc("/symbol", pprof.Symbol)
22 | r.HandleFunc("/trace", pprof.Trace)
23 | r.HandleFunc("/{name}", pprofHandler)
24 | return r
25 | }
26 |
27 | func pprofHandler(w http.ResponseWriter, r *http.Request) {
28 | name := chi.URLParam(r, "name")
29 | pprof.Handler(name).ServeHTTP(w, r)
30 | }
31 |
--------------------------------------------------------------------------------
/restapi/errors.go:
--------------------------------------------------------------------------------
1 | package restapi
2 |
3 | var (
4 | ErrBadRequest = newError("Body invalid")
5 | ErrUnauthorized = newError("Unauthorized")
6 | ErrUninitialized = newError("Uninitialized")
7 | )
8 |
9 | var _ error = (*HTTPError)(nil)
10 |
11 | // HTTPError is custom HTTP error for API
12 | type HTTPError struct {
13 | Message string `json:"message"`
14 | }
15 |
16 | func (e *HTTPError) Error() string {
17 | return e.Message
18 | }
19 |
20 | func newError(msg string) *HTTPError {
21 | return &HTTPError{Message: msg}
22 | }
23 |
--------------------------------------------------------------------------------
/restapi/netstats.go:
--------------------------------------------------------------------------------
1 | package restapi
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 | "reflect"
7 | "strconv"
8 | "time"
9 |
10 | "github.com/go-chi/render"
11 | "github.com/gorilla/websocket"
12 | "gvisor.dev/gvisor/pkg/tcpip"
13 | )
14 |
15 | var _stackStatsFunc func() tcpip.Stats
16 |
17 | func SetStatsFunc(s func() tcpip.Stats) {
18 | _stackStatsFunc = s
19 | }
20 |
21 | func init() {
22 | registerEndpoint("/netstats", http.HandlerFunc(getNetStats))
23 | }
24 |
25 | func getNetStats(w http.ResponseWriter, r *http.Request) {
26 | if _stackStatsFunc == nil {
27 | render.Status(r, http.StatusInternalServerError)
28 | render.JSON(w, r, ErrUninitialized)
29 | return
30 | }
31 |
32 | b := &bytes.Buffer{}
33 | snapshot := func() []byte {
34 | s := _stackStatsFunc()
35 | b.Reset() /* reset buffer */
36 | encodeToJSON(reflect.ValueOf(&s).Elem(), b)
37 | return b.Bytes()
38 | }
39 |
40 | if !websocket.IsWebSocketUpgrade(r) {
41 | w.Header().Set("Content-Type", "application/json")
42 | render.Status(r, http.StatusOK)
43 | w.Write(snapshot())
44 | w.(http.Flusher).Flush()
45 | return
46 | }
47 |
48 | conn, err := _upgrader.Upgrade(w, r, nil)
49 | if err != nil {
50 | return
51 | }
52 |
53 | tick := time.NewTicker(time.Second)
54 | defer tick.Stop()
55 |
56 | for range tick.C {
57 | if err = conn.WriteMessage(websocket.TextMessage, snapshot()); err != nil {
58 | break
59 | }
60 | }
61 | }
62 |
63 | func encodeToJSON(value reflect.Value, b *bytes.Buffer) {
64 | b.WriteByte('{')
65 | defer b.WriteByte('}')
66 |
67 | for i, numField := 0, value.NumField(); i < numField; i++ {
68 | field := value.Type().Field(i)
69 | value := value.Field(i)
70 |
71 | b.WriteString("\"" + field.Name + "\":")
72 |
73 | switch v := value.Addr().Interface().(type) {
74 | case **tcpip.StatCounter:
75 | b.WriteString(strconv.FormatUint((*v).Value(), 10))
76 | case **tcpip.IntegralStatCounterMap:
77 | b.WriteByte('{')
78 | for j, keys := 0, (*v).Keys(); j < len(keys); j++ {
79 | if counter, ok := (*v).Get(keys[j]); ok {
80 | k := strconv.FormatUint(keys[j], 10)
81 | v := strconv.FormatUint(counter.Value(), 10)
82 | b.WriteString("\"" + k + "\":" + v)
83 | if j < len(keys)-1 {
84 | b.WriteByte(',')
85 | }
86 | }
87 | }
88 | b.WriteByte('}')
89 | default:
90 | encodeToJSON(value, b)
91 | }
92 | if i < numField-1 {
93 | b.WriteByte(',')
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/restapi/server.go:
--------------------------------------------------------------------------------
1 | package restapi
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "net"
7 | "net/http"
8 | "strings"
9 | "time"
10 |
11 | "github.com/go-chi/chi/v5"
12 | "github.com/go-chi/cors"
13 | "github.com/go-chi/render"
14 | "github.com/gorilla/websocket"
15 |
16 | V "github.com/xjasonlyu/tun2socks/v2/internal/version"
17 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
18 | )
19 |
20 | var (
21 | _upgrader = websocket.Upgrader{
22 | CheckOrigin: func(r *http.Request) bool {
23 | return true
24 | },
25 | }
26 |
27 | _endpoints = make(map[string]http.Handler)
28 | )
29 |
30 | func registerEndpoint(pattern string, handler http.Handler) {
31 | _endpoints[pattern] = handler
32 | }
33 |
34 | func Start(addr, token string) error {
35 | r := chi.NewRouter()
36 |
37 | c := cors.New(cors.Options{
38 | AllowedOrigins: []string{"*"},
39 | AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
40 | AllowedHeaders: []string{"Content-Type", "Authorization"},
41 | MaxAge: 300,
42 | })
43 |
44 | r.Use(c.Handler)
45 | r.Group(func(r chi.Router) {
46 | r.Use(authenticator(token))
47 | r.Get("/", hello)
48 | r.Get("/traffic", traffic)
49 | r.Get("/version", version)
50 | // attach HTTP handlers
51 | for pattern, handler := range _endpoints {
52 | r.Mount(pattern, handler)
53 | }
54 | })
55 |
56 | listener, err := net.Listen("tcp", addr)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | return http.Serve(listener, r)
62 | }
63 |
64 | func hello(w http.ResponseWriter, r *http.Request) {
65 | render.JSON(w, r, render.M{"hello": V.Name})
66 | }
67 |
68 | func authenticator(token string) func(http.Handler) http.Handler {
69 | return func(next http.Handler) http.Handler {
70 | fn := func(w http.ResponseWriter, r *http.Request) {
71 | if token == "" {
72 | next.ServeHTTP(w, r)
73 | return
74 | }
75 |
76 | // Browser websocket not support custom header
77 | if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" {
78 | t := r.URL.Query().Get("token")
79 | if t != token {
80 | render.Status(r, http.StatusUnauthorized)
81 | render.JSON(w, r, ErrUnauthorized)
82 | return
83 | }
84 | next.ServeHTTP(w, r)
85 | return
86 | }
87 |
88 | header := r.Header.Get("Authorization")
89 | text := strings.SplitN(header, " ", 2)
90 |
91 | hasInvalidHeader := text[0] != "Bearer"
92 | hasInvalidToken := len(text) != 2 || text[1] != token
93 | if hasInvalidHeader || hasInvalidToken {
94 | render.Status(r, http.StatusUnauthorized)
95 | render.JSON(w, r, ErrUnauthorized)
96 | return
97 | }
98 | next.ServeHTTP(w, r)
99 | }
100 | return http.HandlerFunc(fn)
101 | }
102 | }
103 |
104 | func traffic(w http.ResponseWriter, r *http.Request) {
105 | var (
106 | err error
107 | wsConn *websocket.Conn
108 | )
109 | if websocket.IsWebSocketUpgrade(r) {
110 | wsConn, err = _upgrader.Upgrade(w, r, nil)
111 | if err != nil {
112 | return
113 | }
114 | }
115 |
116 | if wsConn == nil {
117 | w.Header().Set("Content-Type", "application/json")
118 | render.Status(r, http.StatusOK)
119 | }
120 |
121 | tick := time.NewTicker(time.Second)
122 | defer tick.Stop()
123 |
124 | buf := &bytes.Buffer{}
125 | for range tick.C {
126 | buf.Reset()
127 |
128 | up, down := statistic.DefaultManager.Now()
129 | if err = json.NewEncoder(buf).Encode(struct {
130 | Up int64 `json:"up"`
131 | Down int64 `json:"down"`
132 | }{
133 | Up: up,
134 | Down: down,
135 | }); err != nil {
136 | break
137 | }
138 |
139 | if wsConn == nil {
140 | _, err = w.Write(buf.Bytes())
141 | w.(http.Flusher).Flush()
142 | } else {
143 | err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
144 | }
145 |
146 | if err != nil {
147 | break
148 | }
149 | }
150 | }
151 |
152 | func version(w http.ResponseWriter, r *http.Request) {
153 | render.JSON(w, r, render.M{
154 | "version": V.Version,
155 | "commit": V.GitCommit,
156 | "modules": V.Info(),
157 | })
158 | }
159 |
--------------------------------------------------------------------------------
/transport/internal/bufferpool/bufferpool.go:
--------------------------------------------------------------------------------
1 | package bufferpool
2 |
3 | import (
4 | "bytes"
5 |
6 | "github.com/xjasonlyu/tun2socks/v2/internal/pool"
7 | )
8 |
9 | const _size = 1024 // by default, create 1 KiB buffers
10 |
11 | var _pool = pool.New(func() *bytes.Buffer {
12 | return bytes.NewBuffer(make([]byte, 0, _size))
13 | })
14 |
15 | func Get() *bytes.Buffer {
16 | buf := _pool.Get()
17 | buf.Reset()
18 | return buf
19 | }
20 |
21 | func Put(b *bytes.Buffer) {
22 | _pool.Put(b)
23 | }
24 |
--------------------------------------------------------------------------------
/transport/shadowsocks/README.md:
--------------------------------------------------------------------------------
1 | ## Embedded go-shadowsocks2
2 |
3 | origin https://github.com/riobard/go-shadowsocks2
4 |
--------------------------------------------------------------------------------
/transport/shadowsocks/core/cipher.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "crypto/md5"
5 | "errors"
6 | "net"
7 | "sort"
8 | "strings"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/shadowaead"
11 | "github.com/xjasonlyu/tun2socks/v2/transport/shadowsocks/shadowstream"
12 | )
13 |
14 | type Cipher interface {
15 | StreamConnCipher
16 | PacketConnCipher
17 | }
18 |
19 | type StreamConnCipher interface {
20 | StreamConn(net.Conn) net.Conn
21 | }
22 |
23 | type PacketConnCipher interface {
24 | PacketConn(net.PacketConn) net.PacketConn
25 | }
26 |
27 | // ErrCipherNotSupported occurs when a cipher is not supported (likely because of security concerns).
28 | var ErrCipherNotSupported = errors.New("cipher not supported")
29 |
30 | const (
31 | aeadAes128Gcm = "AEAD_AES_128_GCM"
32 | aeadAes192Gcm = "AEAD_AES_192_GCM"
33 | aeadAes256Gcm = "AEAD_AES_256_GCM"
34 | aeadChacha20Poly1305 = "AEAD_CHACHA20_POLY1305"
35 | aeadXChacha20Poly1305 = "AEAD_XCHACHA20_POLY1305"
36 | )
37 |
38 | // List of AEAD ciphers: key size in bytes and constructor
39 | var aeadList = map[string]struct {
40 | KeySize int
41 | New func([]byte) (shadowaead.Cipher, error)
42 | }{
43 | aeadAes128Gcm: {16, shadowaead.AESGCM},
44 | aeadAes192Gcm: {24, shadowaead.AESGCM},
45 | aeadAes256Gcm: {32, shadowaead.AESGCM},
46 | aeadChacha20Poly1305: {32, shadowaead.Chacha20Poly1305},
47 | aeadXChacha20Poly1305: {32, shadowaead.XChacha20Poly1305},
48 | }
49 |
50 | // List of stream ciphers: key size in bytes and constructor
51 | var streamList = map[string]struct {
52 | KeySize int
53 | New func(key []byte) (shadowstream.Cipher, error)
54 | }{
55 | "RC4-MD5": {16, shadowstream.RC4MD5},
56 | "AES-128-CTR": {16, shadowstream.AESCTR},
57 | "AES-192-CTR": {24, shadowstream.AESCTR},
58 | "AES-256-CTR": {32, shadowstream.AESCTR},
59 | "AES-128-CFB": {16, shadowstream.AESCFB},
60 | "AES-192-CFB": {24, shadowstream.AESCFB},
61 | "AES-256-CFB": {32, shadowstream.AESCFB},
62 | "CHACHA20-IETF": {32, shadowstream.Chacha20IETF},
63 | "XCHACHA20": {32, shadowstream.Xchacha20},
64 | }
65 |
66 | // ListCipher returns a list of available cipher names sorted alphabetically.
67 | func ListCipher() []string {
68 | var l []string
69 | for k := range aeadList {
70 | l = append(l, k)
71 | }
72 | for k := range streamList {
73 | l = append(l, k)
74 | }
75 | sort.Strings(l)
76 | return l
77 | }
78 |
79 | // PickCipher returns a Cipher of the given name. Derive key from password if given key is empty.
80 | func PickCipher(name string, key []byte, password string) (Cipher, error) {
81 | name = strings.ToUpper(name)
82 |
83 | switch name {
84 | case "DUMMY":
85 | return &dummy{}, nil
86 | case "CHACHA20-IETF-POLY1305":
87 | name = aeadChacha20Poly1305
88 | case "XCHACHA20-IETF-POLY1305":
89 | name = aeadXChacha20Poly1305
90 | case "AES-128-GCM":
91 | name = aeadAes128Gcm
92 | case "AES-192-GCM":
93 | name = aeadAes192Gcm
94 | case "AES-256-GCM":
95 | name = aeadAes256Gcm
96 | }
97 |
98 | if choice, ok := aeadList[name]; ok {
99 | if len(key) == 0 {
100 | key = Kdf(password, choice.KeySize)
101 | }
102 | if len(key) != choice.KeySize {
103 | return nil, shadowaead.KeySizeError(choice.KeySize)
104 | }
105 | aead, err := choice.New(key)
106 | return &AeadCipher{Cipher: aead, Key: key}, err
107 | }
108 |
109 | if choice, ok := streamList[name]; ok {
110 | if len(key) == 0 {
111 | key = Kdf(password, choice.KeySize)
112 | }
113 | if len(key) != choice.KeySize {
114 | return nil, shadowstream.KeySizeError(choice.KeySize)
115 | }
116 | ciph, err := choice.New(key)
117 | return &StreamCipher{Cipher: ciph, Key: key}, err
118 | }
119 |
120 | return nil, ErrCipherNotSupported
121 | }
122 |
123 | type AeadCipher struct {
124 | shadowaead.Cipher
125 |
126 | Key []byte
127 | }
128 |
129 | func (aead *AeadCipher) StreamConn(c net.Conn) net.Conn { return shadowaead.NewConn(c, aead) }
130 | func (aead *AeadCipher) PacketConn(c net.PacketConn) net.PacketConn {
131 | return shadowaead.NewPacketConn(c, aead)
132 | }
133 |
134 | type StreamCipher struct {
135 | shadowstream.Cipher
136 |
137 | Key []byte
138 | }
139 |
140 | func (ciph *StreamCipher) StreamConn(c net.Conn) net.Conn { return shadowstream.NewConn(c, ciph) }
141 | func (ciph *StreamCipher) PacketConn(c net.PacketConn) net.PacketConn {
142 | return shadowstream.NewPacketConn(c, ciph)
143 | }
144 |
145 | // dummy cipher does not encrypt
146 |
147 | type dummy struct{}
148 |
149 | func (dummy) StreamConn(c net.Conn) net.Conn { return c }
150 | func (dummy) PacketConn(c net.PacketConn) net.PacketConn { return c }
151 |
152 | // key-derivation function from original Shadowsocks
153 | func Kdf(password string, keyLen int) []byte {
154 | var b, prev []byte
155 | h := md5.New()
156 | for len(b) < keyLen {
157 | h.Write(prev)
158 | h.Write([]byte(password))
159 | b = h.Sum(b)
160 | prev = b[len(b)-h.Size():]
161 | h.Reset()
162 | }
163 | return b[:keyLen]
164 | }
165 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowaead/cipher.go:
--------------------------------------------------------------------------------
1 | package shadowaead
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/sha1"
7 | "io"
8 | "strconv"
9 |
10 | "golang.org/x/crypto/chacha20poly1305"
11 | "golang.org/x/crypto/hkdf"
12 | )
13 |
14 | type Cipher interface {
15 | KeySize() int
16 | SaltSize() int
17 | Encrypter(salt []byte) (cipher.AEAD, error)
18 | Decrypter(salt []byte) (cipher.AEAD, error)
19 | }
20 |
21 | type KeySizeError int
22 |
23 | func (e KeySizeError) Error() string {
24 | return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
25 | }
26 |
27 | func hkdfSHA1(secret, salt, info, outkey []byte) {
28 | r := hkdf.New(sha1.New, secret, salt, info)
29 | if _, err := io.ReadFull(r, outkey); err != nil {
30 | panic(err) // should never happen
31 | }
32 | }
33 |
34 | type metaCipher struct {
35 | psk []byte
36 | makeAEAD func(key []byte) (cipher.AEAD, error)
37 | }
38 |
39 | func (a *metaCipher) KeySize() int { return len(a.psk) }
40 | func (a *metaCipher) SaltSize() int {
41 | if ks := a.KeySize(); ks > 16 {
42 | return ks
43 | }
44 | return 16
45 | }
46 |
47 | func (a *metaCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
48 | subkey := make([]byte, a.KeySize())
49 | hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
50 | return a.makeAEAD(subkey)
51 | }
52 |
53 | func (a *metaCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
54 | subkey := make([]byte, a.KeySize())
55 | hkdfSHA1(a.psk, salt, []byte("ss-subkey"), subkey)
56 | return a.makeAEAD(subkey)
57 | }
58 |
59 | func aesGCM(key []byte) (cipher.AEAD, error) {
60 | blk, err := aes.NewCipher(key)
61 | if err != nil {
62 | return nil, err
63 | }
64 | return cipher.NewGCM(blk)
65 | }
66 |
67 | // AESGCM creates a new Cipher with a pre-shared key. len(psk) must be
68 | // one of 16, 24, or 32 to select AES-128/196/256-GCM.
69 | func AESGCM(psk []byte) (Cipher, error) {
70 | switch l := len(psk); l {
71 | case 16, 24, 32: // AES 128/196/256
72 | default:
73 | return nil, aes.KeySizeError(l)
74 | }
75 | return &metaCipher{psk: psk, makeAEAD: aesGCM}, nil
76 | }
77 |
78 | // Chacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
79 | // must be 32.
80 | func Chacha20Poly1305(psk []byte) (Cipher, error) {
81 | if len(psk) != chacha20poly1305.KeySize {
82 | return nil, KeySizeError(chacha20poly1305.KeySize)
83 | }
84 | return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.New}, nil
85 | }
86 |
87 | // XChacha20Poly1305 creates a new Cipher with a pre-shared key. len(psk)
88 | // must be 32.
89 | func XChacha20Poly1305(psk []byte) (Cipher, error) {
90 | if len(psk) != chacha20poly1305.KeySize {
91 | return nil, KeySizeError(chacha20poly1305.KeySize)
92 | }
93 | return &metaCipher{psk: psk, makeAEAD: chacha20poly1305.NewX}, nil
94 | }
95 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowaead/packet.go:
--------------------------------------------------------------------------------
1 | package shadowaead
2 |
3 | import (
4 | "crypto/rand"
5 | "errors"
6 | "io"
7 | "net"
8 |
9 | "github.com/xjasonlyu/tun2socks/v2/buffer"
10 | )
11 |
12 | // ErrShortPacket means that the packet is too short for a valid encrypted packet.
13 | var ErrShortPacket = errors.New("short packet")
14 |
15 | var _zerononce [128]byte // read-only. 128 bytes is more than enough.
16 |
17 | // Pack encrypts plaintext using Cipher with a randomly generated salt and
18 | // returns a slice of dst containing the encrypted packet and any error occurred.
19 | // Ensure len(dst) >= ciph.SaltSize() + len(plaintext) + aead.Overhead().
20 | func Pack(dst, plaintext []byte, ciph Cipher) ([]byte, error) {
21 | saltSize := ciph.SaltSize()
22 | salt := dst[:saltSize]
23 | if _, err := rand.Read(salt); err != nil {
24 | return nil, err
25 | }
26 | aead, err := ciph.Encrypter(salt)
27 | if err != nil {
28 | return nil, err
29 | }
30 | if len(dst) < saltSize+len(plaintext)+aead.Overhead() {
31 | return nil, io.ErrShortBuffer
32 | }
33 | b := aead.Seal(dst[saltSize:saltSize], _zerononce[:aead.NonceSize()], plaintext, nil)
34 | return dst[:saltSize+len(b)], nil
35 | }
36 |
37 | // Unpack decrypts pkt using Cipher and returns a slice of dst containing the decrypted payload and any error occurred.
38 | // Ensure len(dst) >= len(pkt) - aead.SaltSize() - aead.Overhead().
39 | func Unpack(dst, pkt []byte, ciph Cipher) ([]byte, error) {
40 | saltSize := ciph.SaltSize()
41 | if len(pkt) < saltSize {
42 | return nil, ErrShortPacket
43 | }
44 | salt := pkt[:saltSize]
45 | aead, err := ciph.Decrypter(salt)
46 | if err != nil {
47 | return nil, err
48 | }
49 | if len(pkt) < saltSize+aead.Overhead() {
50 | return nil, ErrShortPacket
51 | }
52 | if saltSize+len(dst)+aead.Overhead() < len(pkt) {
53 | return nil, io.ErrShortBuffer
54 | }
55 | b, err := aead.Open(dst[:0], _zerononce[:aead.NonceSize()], pkt[saltSize:], nil)
56 | return b, err
57 | }
58 |
59 | type PacketConn struct {
60 | net.PacketConn
61 | Cipher
62 | }
63 |
64 | const maxPacketSize = 64 * 1024
65 |
66 | // NewPacketConn wraps a net.PacketConn with cipher
67 | func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
68 | return &PacketConn{PacketConn: c, Cipher: ciph}
69 | }
70 |
71 | // WriteTo encrypts b and write to addr using the embedded PacketConn.
72 | func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
73 | buf := buffer.Get(maxPacketSize)
74 | defer buffer.Put(buf)
75 | buf, err := Pack(buf, b, c)
76 | if err != nil {
77 | return 0, err
78 | }
79 | _, err = c.PacketConn.WriteTo(buf, addr)
80 | return len(b), err
81 | }
82 |
83 | // ReadFrom reads from the embedded PacketConn and decrypts into b.
84 | func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
85 | n, addr, err := c.PacketConn.ReadFrom(b)
86 | if err != nil {
87 | return n, addr, err
88 | }
89 | bb, err := Unpack(b[c.Cipher.SaltSize():], b[:n], c)
90 | if err != nil {
91 | return n, addr, err
92 | }
93 | copy(b, bb)
94 | return len(bb), addr, err
95 | }
96 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowaead/stream.go:
--------------------------------------------------------------------------------
1 | package shadowaead
2 |
3 | import (
4 | "crypto/cipher"
5 | "crypto/rand"
6 | "errors"
7 | "io"
8 | "net"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/buffer"
11 | )
12 |
13 | const (
14 | // payloadSizeMask is the maximum size of payload in bytes.
15 | payloadSizeMask = 0x3FFF // 16*1024 - 1
16 | bufSize = 17 * 1024 // >= 2+aead.Overhead()+payloadSizeMask+aead.Overhead()
17 | )
18 |
19 | var ErrZeroChunk = errors.New("zero chunk")
20 |
21 | type Writer struct {
22 | io.Writer
23 | cipher.AEAD
24 | nonce [32]byte // should be sufficient for most nonce sizes
25 | }
26 |
27 | // NewWriter wraps an io.Writer with authenticated encryption.
28 | func NewWriter(w io.Writer, aead cipher.AEAD) *Writer { return &Writer{Writer: w, AEAD: aead} }
29 |
30 | // Write encrypts p and writes to the embedded io.Writer.
31 | func (w *Writer) Write(p []byte) (n int, err error) {
32 | buf := buffer.Get(bufSize)
33 | defer buffer.Put(buf)
34 | nonce := w.nonce[:w.NonceSize()]
35 | tag := w.Overhead()
36 | off := 2 + tag
37 |
38 | // compatible with snell
39 | if len(p) == 0 {
40 | buf = buf[:off]
41 | buf[0], buf[1] = byte(0), byte(0)
42 | w.Seal(buf[:0], nonce, buf[:2], nil)
43 | increment(nonce)
44 | _, err = w.Writer.Write(buf)
45 | return
46 | }
47 |
48 | for nr := 0; n < len(p) && err == nil; n += nr {
49 | nr = payloadSizeMask
50 | if n+nr > len(p) {
51 | nr = len(p) - n
52 | }
53 | buf = buf[:off+nr+tag]
54 | buf[0], buf[1] = byte(nr>>8), byte(nr) // big-endian payload size
55 | w.Seal(buf[:0], nonce, buf[:2], nil)
56 | increment(nonce)
57 | w.Seal(buf[:off], nonce, p[n:n+nr], nil)
58 | increment(nonce)
59 | _, err = w.Writer.Write(buf)
60 | }
61 | return
62 | }
63 |
64 | // ReadFrom reads from the given io.Reader until EOF or error, encrypts and
65 | // writes to the embedded io.Writer. Returns number of bytes read from r and
66 | // any error encountered.
67 | func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
68 | buf := buffer.Get(bufSize)
69 | defer buffer.Put(buf)
70 | nonce := w.nonce[:w.NonceSize()]
71 | tag := w.Overhead()
72 | off := 2 + tag
73 | for {
74 | nr, er := r.Read(buf[off : off+payloadSizeMask])
75 | n += int64(nr)
76 | buf[0], buf[1] = byte(nr>>8), byte(nr)
77 | w.Seal(buf[:0], nonce, buf[:2], nil)
78 | increment(nonce)
79 | w.Seal(buf[:off], nonce, buf[off:off+nr], nil)
80 | increment(nonce)
81 | if _, ew := w.Writer.Write(buf[:off+nr+tag]); ew != nil {
82 | err = ew
83 | return
84 | }
85 | if er != nil {
86 | if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
87 | err = er
88 | }
89 | return
90 | }
91 | }
92 | }
93 |
94 | type Reader struct {
95 | io.Reader
96 | cipher.AEAD
97 | nonce [32]byte // should be sufficient for most nonce sizes
98 | buf []byte // to be put back into bufPool
99 | off int // offset to unconsumed part of buf
100 | }
101 |
102 | // NewReader wraps an io.Reader with authenticated decryption.
103 | func NewReader(r io.Reader, aead cipher.AEAD) *Reader { return &Reader{Reader: r, AEAD: aead} }
104 |
105 | // Read and decrypt a record into p. len(p) >= max payload size + AEAD overhead.
106 | func (r *Reader) read(p []byte) (int, error) {
107 | nonce := r.nonce[:r.NonceSize()]
108 | tag := r.Overhead()
109 |
110 | // decrypt payload size
111 | p = p[:2+tag]
112 | if _, err := io.ReadFull(r.Reader, p); err != nil {
113 | return 0, err
114 | }
115 | _, err := r.Open(p[:0], nonce, p, nil)
116 | increment(nonce)
117 | if err != nil {
118 | return 0, err
119 | }
120 |
121 | // decrypt payload
122 | size := (int(p[0])<<8 + int(p[1])) & payloadSizeMask
123 | if size == 0 {
124 | return 0, ErrZeroChunk
125 | }
126 |
127 | p = p[:size+tag]
128 | if _, err := io.ReadFull(r.Reader, p); err != nil {
129 | return 0, err
130 | }
131 | _, err = r.Open(p[:0], nonce, p, nil)
132 | increment(nonce)
133 | if err != nil {
134 | return 0, err
135 | }
136 | return size, nil
137 | }
138 |
139 | // Read reads from the embedded io.Reader, decrypts and writes to p.
140 | func (r *Reader) Read(p []byte) (int, error) {
141 | if r.buf == nil {
142 | if len(p) >= payloadSizeMask+r.Overhead() {
143 | return r.read(p)
144 | }
145 | b := buffer.Get(bufSize)
146 | n, err := r.read(b)
147 | if err != nil {
148 | return 0, err
149 | }
150 | r.buf = b[:n]
151 | r.off = 0
152 | }
153 |
154 | n := copy(p, r.buf[r.off:])
155 | r.off += n
156 | if r.off == len(r.buf) {
157 | buffer.Put(r.buf[:cap(r.buf)])
158 | r.buf = nil
159 | }
160 | return n, nil
161 | }
162 |
163 | // WriteTo reads from the embedded io.Reader, decrypts and writes to w until
164 | // there's no more data to write or when an error occurs. Return number of
165 | // bytes written to w and any error encountered.
166 | func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
167 | if r.buf == nil {
168 | r.buf = buffer.Get(bufSize)
169 | r.off = len(r.buf)
170 | }
171 |
172 | for {
173 | for r.off < len(r.buf) {
174 | nw, ew := w.Write(r.buf[r.off:])
175 | r.off += nw
176 | n += int64(nw)
177 | if ew != nil {
178 | if r.off == len(r.buf) {
179 | buffer.Put(r.buf[:cap(r.buf)])
180 | r.buf = nil
181 | }
182 | err = ew
183 | return
184 | }
185 | }
186 |
187 | nr, er := r.read(r.buf)
188 | if er != nil {
189 | if er != io.EOF {
190 | err = er
191 | }
192 | return
193 | }
194 | r.buf = r.buf[:nr]
195 | r.off = 0
196 | }
197 | }
198 |
199 | // increment little-endian encoded unsigned integer b. Wrap around on overflow.
200 | func increment(b []byte) {
201 | for i := range b {
202 | b[i]++
203 | if b[i] != 0 {
204 | return
205 | }
206 | }
207 | }
208 |
209 | type Conn struct {
210 | net.Conn
211 | Cipher
212 | r *Reader
213 | w *Writer
214 | }
215 |
216 | // NewConn wraps a stream-oriented net.Conn with cipher.
217 | func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
218 |
219 | func (c *Conn) initReader() error {
220 | salt := make([]byte, c.SaltSize())
221 | if _, err := io.ReadFull(c.Conn, salt); err != nil {
222 | return err
223 | }
224 |
225 | aead, err := c.Decrypter(salt)
226 | if err != nil {
227 | return err
228 | }
229 |
230 | c.r = NewReader(c.Conn, aead)
231 | return nil
232 | }
233 |
234 | func (c *Conn) Read(b []byte) (int, error) {
235 | if c.r == nil {
236 | if err := c.initReader(); err != nil {
237 | return 0, err
238 | }
239 | }
240 | return c.r.Read(b)
241 | }
242 |
243 | func (c *Conn) WriteTo(w io.Writer) (int64, error) {
244 | if c.r == nil {
245 | if err := c.initReader(); err != nil {
246 | return 0, err
247 | }
248 | }
249 | return c.r.WriteTo(w)
250 | }
251 |
252 | func (c *Conn) initWriter() error {
253 | salt := make([]byte, c.SaltSize())
254 | if _, err := rand.Read(salt); err != nil {
255 | return err
256 | }
257 | aead, err := c.Encrypter(salt)
258 | if err != nil {
259 | return err
260 | }
261 | _, err = c.Conn.Write(salt)
262 | if err != nil {
263 | return err
264 | }
265 | c.w = NewWriter(c.Conn, aead)
266 | return nil
267 | }
268 |
269 | func (c *Conn) Write(b []byte) (int, error) {
270 | if c.w == nil {
271 | if err := c.initWriter(); err != nil {
272 | return 0, err
273 | }
274 | }
275 | return c.w.Write(b)
276 | }
277 |
278 | func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
279 | if c.w == nil {
280 | if err := c.initWriter(); err != nil {
281 | return 0, err
282 | }
283 | }
284 | return c.w.ReadFrom(r)
285 | }
286 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowstream/cipher.go:
--------------------------------------------------------------------------------
1 | package shadowstream
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/md5"
7 | "crypto/rc4"
8 | "strconv"
9 |
10 | "golang.org/x/crypto/chacha20"
11 | )
12 |
13 | // Cipher generates a pair of stream ciphers for encryption and decryption.
14 | type Cipher interface {
15 | IVSize() int
16 | Encrypter(iv []byte) cipher.Stream
17 | Decrypter(iv []byte) cipher.Stream
18 | }
19 |
20 | type KeySizeError int
21 |
22 | func (e KeySizeError) Error() string {
23 | return "key size error: need " + strconv.Itoa(int(e)) + " bytes"
24 | }
25 |
26 | // CTR mode
27 | type ctrStream struct{ cipher.Block }
28 |
29 | func (b *ctrStream) IVSize() int { return b.BlockSize() }
30 | func (b *ctrStream) Decrypter(iv []byte) cipher.Stream { return b.Encrypter(iv) }
31 | func (b *ctrStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCTR(b, iv) }
32 |
33 | func AESCTR(key []byte) (Cipher, error) {
34 | blk, err := aes.NewCipher(key)
35 | if err != nil {
36 | return nil, err
37 | }
38 | return &ctrStream{blk}, nil
39 | }
40 |
41 | // CFB mode
42 | type cfbStream struct{ cipher.Block }
43 |
44 | func (b *cfbStream) IVSize() int { return b.BlockSize() }
45 | func (b *cfbStream) Decrypter(iv []byte) cipher.Stream { return cipher.NewCFBDecrypter(b, iv) }
46 | func (b *cfbStream) Encrypter(iv []byte) cipher.Stream { return cipher.NewCFBEncrypter(b, iv) }
47 |
48 | func AESCFB(key []byte) (Cipher, error) {
49 | blk, err := aes.NewCipher(key)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return &cfbStream{blk}, nil
54 | }
55 |
56 | // IETF-variant of chacha20
57 | type chacha20ietfkey []byte
58 |
59 | func (k chacha20ietfkey) IVSize() int { return chacha20.NonceSize }
60 | func (k chacha20ietfkey) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
61 | func (k chacha20ietfkey) Encrypter(iv []byte) cipher.Stream {
62 | ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
63 | if err != nil {
64 | panic(err) // should never happen
65 | }
66 | return ciph
67 | }
68 |
69 | func Chacha20IETF(key []byte) (Cipher, error) {
70 | if len(key) != chacha20.KeySize {
71 | return nil, KeySizeError(chacha20.KeySize)
72 | }
73 | return chacha20ietfkey(key), nil
74 | }
75 |
76 | type xchacha20key []byte
77 |
78 | func (k xchacha20key) IVSize() int { return chacha20.NonceSizeX }
79 | func (k xchacha20key) Decrypter(iv []byte) cipher.Stream { return k.Encrypter(iv) }
80 | func (k xchacha20key) Encrypter(iv []byte) cipher.Stream {
81 | ciph, err := chacha20.NewUnauthenticatedCipher(k, iv)
82 | if err != nil {
83 | panic(err) // should never happen
84 | }
85 | return ciph
86 | }
87 |
88 | func Xchacha20(key []byte) (Cipher, error) {
89 | if len(key) != chacha20.KeySize {
90 | return nil, KeySizeError(chacha20.KeySize)
91 | }
92 | return xchacha20key(key), nil
93 | }
94 |
95 | type rc4Md5Key []byte
96 |
97 | func (k rc4Md5Key) IVSize() int {
98 | return 16
99 | }
100 |
101 | func (k rc4Md5Key) Encrypter(iv []byte) cipher.Stream {
102 | h := md5.New()
103 | h.Write([]byte(k))
104 | h.Write(iv)
105 | rc4key := h.Sum(nil)
106 | c, _ := rc4.NewCipher(rc4key)
107 | return c
108 | }
109 |
110 | func (k rc4Md5Key) Decrypter(iv []byte) cipher.Stream {
111 | return k.Encrypter(iv)
112 | }
113 |
114 | func RC4MD5(key []byte) (Cipher, error) {
115 | return rc4Md5Key(key), nil
116 | }
117 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowstream/packet.go:
--------------------------------------------------------------------------------
1 | package shadowstream
2 |
3 | import (
4 | "crypto/rand"
5 | "errors"
6 | "io"
7 | "net"
8 |
9 | "github.com/xjasonlyu/tun2socks/v2/buffer"
10 | )
11 |
12 | // ErrShortPacket means the packet is too short to be a valid encrypted packet.
13 | var ErrShortPacket = errors.New("short packet")
14 |
15 | // Pack encrypts plaintext using stream cipher s and a random IV.
16 | // Returns a slice of dst containing random IV and ciphertext.
17 | // Ensure len(dst) >= s.IVSize() + len(plaintext).
18 | func Pack(dst, plaintext []byte, s Cipher) ([]byte, error) {
19 | if len(dst) < s.IVSize()+len(plaintext) {
20 | return nil, io.ErrShortBuffer
21 | }
22 | iv := dst[:s.IVSize()]
23 | _, err := rand.Read(iv)
24 | if err != nil {
25 | return nil, err
26 | }
27 | s.Encrypter(iv).XORKeyStream(dst[len(iv):], plaintext)
28 | return dst[:len(iv)+len(plaintext)], nil
29 | }
30 |
31 | // Unpack decrypts pkt using stream cipher s.
32 | // Returns a slice of dst containing decrypted plaintext.
33 | func Unpack(dst, pkt []byte, s Cipher) ([]byte, error) {
34 | if len(pkt) < s.IVSize() {
35 | return nil, ErrShortPacket
36 | }
37 | if len(dst) < len(pkt)-s.IVSize() {
38 | return nil, io.ErrShortBuffer
39 | }
40 | iv := pkt[:s.IVSize()]
41 | s.Decrypter(iv).XORKeyStream(dst, pkt[len(iv):])
42 | return dst[:len(pkt)-len(iv)], nil
43 | }
44 |
45 | type PacketConn struct {
46 | net.PacketConn
47 | Cipher
48 | }
49 |
50 | // NewPacketConn wraps a net.PacketConn with stream cipher encryption/decryption.
51 | func NewPacketConn(c net.PacketConn, ciph Cipher) *PacketConn {
52 | return &PacketConn{PacketConn: c, Cipher: ciph}
53 | }
54 |
55 | const maxPacketSize = 64 * 1024
56 |
57 | func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
58 | buf := buffer.Get(maxPacketSize)
59 | defer buffer.Put(buf)
60 | buf, err := Pack(buf, b, c.Cipher)
61 | if err != nil {
62 | return 0, err
63 | }
64 | _, err = c.PacketConn.WriteTo(buf, addr)
65 | return len(b), err
66 | }
67 |
68 | func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
69 | n, addr, err := c.PacketConn.ReadFrom(b)
70 | if err != nil {
71 | return n, addr, err
72 | }
73 | bb, err := Unpack(b[c.IVSize():], b[:n], c.Cipher)
74 | if err != nil {
75 | return n, addr, err
76 | }
77 | copy(b, bb)
78 | return len(bb), addr, err
79 | }
80 |
--------------------------------------------------------------------------------
/transport/shadowsocks/shadowstream/stream.go:
--------------------------------------------------------------------------------
1 | package shadowstream
2 |
3 | import (
4 | "crypto/cipher"
5 | "crypto/rand"
6 | "io"
7 | "net"
8 | )
9 |
10 | const bufSize = 2048
11 |
12 | type Writer struct {
13 | io.Writer
14 | cipher.Stream
15 | buf [bufSize]byte
16 | }
17 |
18 | // NewWriter wraps an io.Writer with stream cipher encryption.
19 | func NewWriter(w io.Writer, s cipher.Stream) *Writer { return &Writer{Writer: w, Stream: s} }
20 |
21 | func (w *Writer) Write(p []byte) (n int, err error) {
22 | buf := w.buf[:]
23 | for nw := 0; n < len(p) && err == nil; n += nw {
24 | end := n + len(buf)
25 | if end > len(p) {
26 | end = len(p)
27 | }
28 | w.XORKeyStream(buf, p[n:end])
29 | nw, err = w.Writer.Write(buf[:end-n])
30 | }
31 | return
32 | }
33 |
34 | func (w *Writer) ReadFrom(r io.Reader) (n int64, err error) {
35 | buf := w.buf[:]
36 | for {
37 | nr, er := r.Read(buf)
38 | n += int64(nr)
39 | b := buf[:nr]
40 | w.XORKeyStream(b, b)
41 | if _, err = w.Writer.Write(b); err != nil {
42 | return
43 | }
44 | if er != nil {
45 | if er != io.EOF { // ignore EOF as per io.ReaderFrom contract
46 | err = er
47 | }
48 | return
49 | }
50 | }
51 | }
52 |
53 | type Reader struct {
54 | io.Reader
55 | cipher.Stream
56 | buf [bufSize]byte
57 | }
58 |
59 | // NewReader wraps an io.Reader with stream cipher decryption.
60 | func NewReader(r io.Reader, s cipher.Stream) *Reader { return &Reader{Reader: r, Stream: s} }
61 |
62 | func (r *Reader) Read(p []byte) (n int, err error) {
63 | n, err = r.Reader.Read(p)
64 | if err != nil {
65 | return 0, err
66 | }
67 | r.XORKeyStream(p, p[:n])
68 | return
69 | }
70 |
71 | func (r *Reader) WriteTo(w io.Writer) (n int64, err error) {
72 | buf := r.buf[:]
73 | for {
74 | nr, er := r.Reader.Read(buf)
75 | if nr > 0 {
76 | r.XORKeyStream(buf, buf[:nr])
77 | nw, ew := w.Write(buf[:nr])
78 | n += int64(nw)
79 | if ew != nil {
80 | err = ew
81 | return
82 | }
83 | }
84 | if er != nil {
85 | if er != io.EOF { // ignore EOF as per io.Copy contract (using src.WriteTo shortcut)
86 | err = er
87 | }
88 | return
89 | }
90 | }
91 | }
92 |
93 | // A Conn represents a Shadowsocks connection. It implements the net.Conn interface.
94 | type Conn struct {
95 | net.Conn
96 | Cipher
97 | r *Reader
98 | w *Writer
99 | readIV []byte
100 | writeIV []byte
101 | }
102 |
103 | // NewConn wraps a stream-oriented net.Conn with stream cipher encryption/decryption.
104 | func NewConn(c net.Conn, ciph Cipher) *Conn { return &Conn{Conn: c, Cipher: ciph} }
105 |
106 | func (c *Conn) initReader() error {
107 | if c.r == nil {
108 | iv, err := c.ObtainReadIV()
109 | if err != nil {
110 | return err
111 | }
112 | c.r = NewReader(c.Conn, c.Decrypter(iv))
113 | }
114 | return nil
115 | }
116 |
117 | func (c *Conn) Read(b []byte) (int, error) {
118 | if c.r == nil {
119 | if err := c.initReader(); err != nil {
120 | return 0, err
121 | }
122 | }
123 | return c.r.Read(b)
124 | }
125 |
126 | func (c *Conn) WriteTo(w io.Writer) (int64, error) {
127 | if c.r == nil {
128 | if err := c.initReader(); err != nil {
129 | return 0, err
130 | }
131 | }
132 | return c.r.WriteTo(w)
133 | }
134 |
135 | func (c *Conn) initWriter() error {
136 | if c.w == nil {
137 | iv, err := c.ObtainWriteIV()
138 | if err != nil {
139 | return err
140 | }
141 | if _, err := c.Conn.Write(iv); err != nil {
142 | return err
143 | }
144 | c.w = NewWriter(c.Conn, c.Encrypter(iv))
145 | }
146 | return nil
147 | }
148 |
149 | func (c *Conn) Write(b []byte) (int, error) {
150 | if c.w == nil {
151 | if err := c.initWriter(); err != nil {
152 | return 0, err
153 | }
154 | }
155 | return c.w.Write(b)
156 | }
157 |
158 | func (c *Conn) ReadFrom(r io.Reader) (int64, error) {
159 | if c.w == nil {
160 | if err := c.initWriter(); err != nil {
161 | return 0, err
162 | }
163 | }
164 | return c.w.ReadFrom(r)
165 | }
166 |
167 | func (c *Conn) ObtainWriteIV() ([]byte, error) {
168 | if len(c.writeIV) == c.IVSize() {
169 | return c.writeIV, nil
170 | }
171 |
172 | iv := make([]byte, c.IVSize())
173 |
174 | if _, err := rand.Read(iv); err != nil {
175 | return nil, err
176 | }
177 |
178 | c.writeIV = iv
179 |
180 | return iv, nil
181 | }
182 |
183 | func (c *Conn) ObtainReadIV() ([]byte, error) {
184 | if len(c.readIV) == c.IVSize() {
185 | return c.readIV, nil
186 | }
187 |
188 | iv := make([]byte, c.IVSize())
189 |
190 | if _, err := io.ReadFull(c.Conn, iv); err != nil {
191 | return nil, err
192 | }
193 |
194 | c.readIV = iv
195 |
196 | return iv, nil
197 | }
198 |
--------------------------------------------------------------------------------
/transport/simple-obfs/http.go:
--------------------------------------------------------------------------------
1 | package obfs
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "encoding/base64"
7 | "fmt"
8 | "io"
9 | mRand "math/rand"
10 | "net"
11 | "net/http"
12 |
13 | "github.com/xjasonlyu/tun2socks/v2/buffer"
14 | )
15 |
16 | // HTTPObfs is shadowsocks http simple-obfs implementation
17 | type HTTPObfs struct {
18 | net.Conn
19 | host string
20 | port string
21 | buf []byte
22 | offset int
23 | firstRequest bool
24 | firstResponse bool
25 | }
26 |
27 | func (ho *HTTPObfs) Read(b []byte) (int, error) {
28 | if ho.buf != nil {
29 | n := copy(b, ho.buf[ho.offset:])
30 | ho.offset += n
31 | if ho.offset == len(ho.buf) {
32 | buffer.Put(ho.buf)
33 | ho.buf = nil
34 | }
35 | return n, nil
36 | }
37 |
38 | if ho.firstResponse {
39 | buf := buffer.Get(buffer.RelayBufferSize)
40 | n, err := ho.Conn.Read(buf)
41 | if err != nil {
42 | buffer.Put(buf)
43 | return 0, err
44 | }
45 | idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
46 | if idx == -1 {
47 | buffer.Put(buf)
48 | return 0, io.EOF
49 | }
50 | ho.firstResponse = false
51 | length := n - (idx + 4)
52 | n = copy(b, buf[idx+4:n])
53 | if length > n {
54 | ho.buf = buf[:idx+4+length]
55 | ho.offset = idx + 4 + n
56 | } else {
57 | buffer.Put(buf)
58 | }
59 | return n, nil
60 | }
61 | return ho.Conn.Read(b)
62 | }
63 |
64 | func (ho *HTTPObfs) Write(b []byte) (int, error) {
65 | if ho.firstRequest {
66 | randBytes := make([]byte, 16)
67 | rand.Read(randBytes)
68 | req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:]))
69 | req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", mRand.Int()%54, mRand.Int()%2))
70 | req.Header.Set("Upgrade", "websocket")
71 | req.Header.Set("Connection", "Upgrade")
72 | req.Host = ho.host
73 | if ho.port != "80" {
74 | req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
75 | }
76 | req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
77 | req.ContentLength = int64(len(b))
78 | err := req.Write(ho.Conn)
79 | ho.firstRequest = false
80 | return len(b), err
81 | }
82 |
83 | return ho.Conn.Write(b)
84 | }
85 |
86 | // NewHTTPObfs return a HTTPObfs
87 | func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
88 | return &HTTPObfs{
89 | Conn: conn,
90 | firstRequest: true,
91 | firstResponse: true,
92 | host: host,
93 | port: port,
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/transport/simple-obfs/obfs.go:
--------------------------------------------------------------------------------
1 | // Package obfs provides obfuscation functionality for Shadowsocks protocol.
2 | package obfs
3 |
--------------------------------------------------------------------------------
/transport/simple-obfs/tls.go:
--------------------------------------------------------------------------------
1 | package obfs
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/binary"
6 | "io"
7 | "net"
8 | "time"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/buffer"
11 | "github.com/xjasonlyu/tun2socks/v2/transport/internal/bufferpool"
12 | )
13 |
14 | const (
15 | chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
16 | )
17 |
18 | // TLSObfs is shadowsocks tls simple-obfs implementation
19 | type TLSObfs struct {
20 | net.Conn
21 | server string
22 | remain int
23 | firstRequest bool
24 | firstResponse bool
25 | }
26 |
27 | func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
28 | buf := buffer.Get(discardN)
29 | _, err := io.ReadFull(to.Conn, buf)
30 | if err != nil {
31 | return 0, err
32 | }
33 | buffer.Put(buf)
34 |
35 | sizeBuf := make([]byte, 2)
36 | _, err = io.ReadFull(to.Conn, sizeBuf)
37 | if err != nil {
38 | return 0, nil
39 | }
40 |
41 | length := int(binary.BigEndian.Uint16(sizeBuf))
42 | if length > len(b) {
43 | n, err := to.Conn.Read(b)
44 | if err != nil {
45 | return n, err
46 | }
47 | to.remain = length - n
48 | return n, nil
49 | }
50 |
51 | return io.ReadFull(to.Conn, b[:length])
52 | }
53 |
54 | func (to *TLSObfs) Read(b []byte) (int, error) {
55 | if to.remain > 0 {
56 | length := to.remain
57 | if length > len(b) {
58 | length = len(b)
59 | }
60 |
61 | n, err := io.ReadFull(to.Conn, b[:length])
62 | to.remain -= n
63 | return n, err
64 | }
65 |
66 | if to.firstResponse {
67 | // type + ver + lensize + 91 = 96
68 | // type + ver + lensize + 1 = 6
69 | // type + ver = 3
70 | to.firstResponse = false
71 | return to.read(b, 105)
72 | }
73 |
74 | // type + ver = 3
75 | return to.read(b, 3)
76 | }
77 |
78 | func (to *TLSObfs) Write(b []byte) (int, error) {
79 | length := len(b)
80 | for i := 0; i < length; i += chunkSize {
81 | end := i + chunkSize
82 | if end > length {
83 | end = length
84 | }
85 |
86 | n, err := to.write(b[i:end])
87 | if err != nil {
88 | return n, err
89 | }
90 | }
91 | return length, nil
92 | }
93 |
94 | func (to *TLSObfs) write(b []byte) (int, error) {
95 | if to.firstRequest {
96 | helloMsg := makeClientHelloMsg(b, to.server)
97 | _, err := to.Conn.Write(helloMsg)
98 | to.firstRequest = false
99 | return len(b), err
100 | }
101 |
102 | buf := bufferpool.Get()
103 | defer bufferpool.Put(buf)
104 | buf.Write([]byte{0x17, 0x03, 0x03})
105 | binary.Write(buf, binary.BigEndian, uint16(len(b)))
106 | buf.Write(b)
107 | _, err := to.Conn.Write(buf.Bytes())
108 | return len(b), err
109 | }
110 |
111 | // NewTLSObfs return a SimpleObfs
112 | func NewTLSObfs(conn net.Conn, server string) net.Conn {
113 | return &TLSObfs{
114 | Conn: conn,
115 | server: server,
116 | firstRequest: true,
117 | firstResponse: true,
118 | }
119 | }
120 |
121 | func makeClientHelloMsg(data []byte, server string) []byte {
122 | random := make([]byte, 28)
123 | sessionID := make([]byte, 32)
124 | rand.Read(random)
125 | rand.Read(sessionID)
126 |
127 | buf := bufferpool.Get()
128 | defer bufferpool.Put(buf)
129 |
130 | // handshake, TLS 1.0 version, length
131 | buf.WriteByte(22)
132 | buf.Write([]byte{0x03, 0x01})
133 | length := uint16(212 + len(data) + len(server))
134 | buf.WriteByte(byte(length >> 8))
135 | buf.WriteByte(byte(length & 0xff))
136 |
137 | // clientHello, length, TLS 1.2 version
138 | buf.WriteByte(1)
139 | buf.WriteByte(0)
140 | binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
141 | buf.Write([]byte{0x03, 0x03})
142 |
143 | // random with timestamp, sid len, sid
144 | binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
145 | buf.Write(random)
146 | buf.WriteByte(32)
147 | buf.Write(sessionID)
148 |
149 | // cipher suites
150 | buf.Write([]byte{0x00, 0x38})
151 | buf.Write([]byte{
152 | 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
153 | 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
154 | 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
155 | 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
156 | })
157 |
158 | // compression
159 | buf.Write([]byte{0x01, 0x00})
160 |
161 | // extension length
162 | binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
163 |
164 | // session ticket
165 | buf.Write([]byte{0x00, 0x23})
166 | binary.Write(buf, binary.BigEndian, uint16(len(data)))
167 | buf.Write(data)
168 |
169 | // server name
170 | buf.Write([]byte{0x00, 0x00})
171 | binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
172 | binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
173 | buf.WriteByte(0)
174 | binary.Write(buf, binary.BigEndian, uint16(len(server)))
175 | buf.Write([]byte(server))
176 |
177 | // ec_point
178 | buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02})
179 |
180 | // groups
181 | buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
182 |
183 | // signature
184 | buf.Write([]byte{
185 | 0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05,
186 | 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01,
187 | 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
188 | })
189 |
190 | // encrypt then mac
191 | buf.Write([]byte{0x00, 0x16, 0x00, 0x00})
192 |
193 | // extended master secret
194 | buf.Write([]byte{0x00, 0x17, 0x00, 0x00})
195 |
196 | return buf.Bytes()
197 | }
198 |
--------------------------------------------------------------------------------
/transport/socks4/socks4.go:
--------------------------------------------------------------------------------
1 | // Package socks4 provides SOCKS4/SOCKS4A client functionalities.
2 | package socks4
3 |
4 | import (
5 | "encoding/binary"
6 | "errors"
7 | "io"
8 | "net"
9 | "net/netip"
10 | "strconv"
11 |
12 | "github.com/xjasonlyu/tun2socks/v2/transport/internal/bufferpool"
13 | )
14 |
15 | const Version = 0x04
16 |
17 | type Command = uint8
18 |
19 | const (
20 | CmdConnect Command = 0x01
21 | CmdBind Command = 0x02
22 | )
23 |
24 | type Code = uint8
25 |
26 | const (
27 | RequestGranted Code = 90
28 | RequestRejected Code = 91
29 | RequestIdentdFailed Code = 92
30 | RequestIdentdMismatched Code = 93
31 | )
32 |
33 | var (
34 | errVersionMismatched = errors.New("version code mismatched")
35 | errIPv6NotSupported = errors.New("IPv6 not supported")
36 | errCmdNotSupported = errors.New("command not supported")
37 |
38 | ErrRequestRejected = errors.New("request rejected or failed")
39 | ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client")
40 | ErrRequestIdentdMismatched = errors.New("request rejected because the client program and identd report different user-ids")
41 | ErrRequestUnknownCode = errors.New("request failed with unknown code")
42 | )
43 |
44 | func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) {
45 | if command == CmdBind {
46 | return errCmdNotSupported
47 | }
48 |
49 | var (
50 | host string
51 | port uint16
52 | )
53 | if host, port, err = splitHostPort(addr); err != nil {
54 | return err
55 | }
56 |
57 | ip, _ := netip.ParseAddr(host)
58 | switch {
59 | case !ip.IsValid(): /* HOST */
60 | ip = netip.AddrFrom4([4]byte{0, 0, 0, 1})
61 | case ip.Is4In6(): /* IPv4-mapped IPv6 */
62 | ip = netip.AddrFrom4(ip.As4())
63 | case ip.Is4(): /* IPv4 */
64 | case ip.Is6(): /* IPv6 */
65 | return errIPv6NotSupported
66 | }
67 |
68 | req := bufferpool.Get()
69 | defer bufferpool.Put(req)
70 | req.WriteByte(Version)
71 | req.WriteByte(command)
72 | _ = binary.Write(req, binary.BigEndian, port)
73 | req.Write(ip.AsSlice())
74 | req.WriteString(userID)
75 | req.WriteByte(0x00) /* NULL */
76 |
77 | if isReservedIP(ip) /* SOCKS4A */ {
78 | req.WriteString(host)
79 | req.WriteByte(0) /* NULL */
80 | }
81 |
82 | if _, err = rw.Write(req.Bytes()); err != nil {
83 | return err
84 | }
85 |
86 | var resp [8]byte
87 | if _, err = io.ReadFull(rw, resp[:]); err != nil {
88 | return err
89 | }
90 |
91 | if resp[0] != 0x00 {
92 | return errVersionMismatched
93 | }
94 |
95 | switch resp[1] {
96 | case RequestGranted:
97 | return nil
98 | case RequestRejected:
99 | return ErrRequestRejected
100 | case RequestIdentdFailed:
101 | return ErrRequestIdentdFailed
102 | case RequestIdentdMismatched:
103 | return ErrRequestIdentdMismatched
104 | default:
105 | return ErrRequestUnknownCode
106 | }
107 | }
108 |
109 | // For version 4A, if the client cannot resolve the destination host's
110 | // domain name to find its IP address, it should set the first three bytes
111 | // of DSTIP to NULL and the last byte to a non-zero value. (This corresponds
112 | // to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The
113 | // Internet Assigned Numbers Authority -- such an address is inadmissible
114 | // as a destination IP address and thus should never occur if the client
115 | // can resolve the domain name.)
116 | func isReservedIP(ip netip.Addr) bool {
117 | prefix := netip.PrefixFrom(netip.IPv4Unspecified(), 24)
118 | return !ip.IsUnspecified() && prefix.Contains(ip)
119 | }
120 |
121 | func splitHostPort(addr string) (string, uint16, error) {
122 | host, portStr, err := net.SplitHostPort(addr)
123 | if err != nil {
124 | return "", 0, err
125 | }
126 |
127 | portInt, err := strconv.ParseUint(portStr, 10, 16)
128 | if err != nil {
129 | return "", 0, err
130 | }
131 |
132 | return host, uint16(portInt), nil
133 | }
134 |
--------------------------------------------------------------------------------
/transport/socks4/socks4_test.go:
--------------------------------------------------------------------------------
1 | package socks4
2 |
3 | import (
4 | "net/netip"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestIsReservedIP(t *testing.T) {
11 | reservedIPs := []string{
12 | "0.0.0.1",
13 | "0.0.0.2",
14 | "0.0.0.50",
15 | "0.0.0.100",
16 | "0.0.0.255",
17 | }
18 | for _, ip := range reservedIPs {
19 | assert.True(t, isReservedIP(netip.MustParseAddr(ip)))
20 | }
21 |
22 | unReservedIPs := []string{
23 | "0.0.0.0",
24 | "0.0.1.0",
25 | "1.1.1.1",
26 | "10.0.0.0",
27 | "255.255.255.255",
28 | }
29 | for _, ip := range unReservedIPs {
30 | assert.False(t, isReservedIP(netip.MustParseAddr(ip)))
31 | }
32 | }
33 |
34 | func TestSplitHostPort(t *testing.T) {
35 | tests := []struct {
36 | addr string
37 | host string
38 | port uint16
39 | }{
40 | {
41 | "1.1.1.1:80",
42 | "1.1.1.1",
43 | 80,
44 | },
45 | {
46 | "1.1.1.1:0",
47 | "1.1.1.1",
48 | 0,
49 | },
50 | {
51 | "0.0.0.0:0",
52 | "0.0.0.0",
53 | 0,
54 | },
55 | {
56 | "[::1]:443",
57 | "::1",
58 | 443,
59 | },
60 | {
61 | "example.com:80",
62 | "example.com",
63 | 80,
64 | },
65 | }
66 | for _, tt := range tests {
67 | host, port, err := splitHostPort(tt.addr)
68 | assert.NoError(t, err)
69 | assert.Equal(t, tt.host, host)
70 | assert.Equal(t, tt.port, port)
71 | }
72 |
73 | addrs := []string{
74 | "1.1.1.1:-80",
75 | "1.1.1.1:abcd",
76 | "::1:80",
77 | "[::1]",
78 | "example.com",
79 | }
80 | for _, addr := range addrs {
81 | _, _, err := splitHostPort(addr)
82 | assert.Error(t, err)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/transport/socks4/socks4a.txt:
--------------------------------------------------------------------------------
1 | SOCKS 4A: A Simple Extension to SOCKS 4 Protocol
2 |
3 | Ying-Da Lee
4 | yingda@best.com or yingda@esd.sgi.com
5 |
6 | Please read SOCKS4.protocol first for an description of the version 4
7 | protocol. This extension is intended to allow the use of SOCKS on hosts
8 | which are not capable of resolving all domain names.
9 |
10 | In version 4, the client sends the following packet to the SOCKS server
11 | to request a CONNECT or a BIND operation:
12 |
13 | +----+----+----+----+----+----+----+----+----+----+....+----+
14 | | VN | CD | DSTPORT | DSTIP | USERID |NULL|
15 | +----+----+----+----+----+----+----+----+----+----+....+----+
16 | # of bytes: 1 1 2 4 variable 1
17 |
18 | VN is the SOCKS protocol version number and should be 4. CD is the
19 | SOCKS command code and should be 1 for CONNECT or 2 for BIND. NULL
20 | is a byte of all zero bits.
21 |
22 | For version 4A, if the client cannot resolve the destination host's
23 | domain name to find its IP address, it should set the first three bytes
24 | of DSTIP to NULL and the last byte to a non-zero value. (This corresponds
25 | to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The
26 | Internet Assigned Numbers Authority -- such an address is inadmissible
27 | as a destination IP address and thus should never occur if the client
28 | can resolve the domain name.) Following the NULL byte terminating
29 | USERID, the client must sends the destination domain name and termiantes
30 | it with another NULL byte. This is used for both CONNECT and BIND requests.
31 |
32 | A server using protocol 4A must check the DSTIP in the request packet.
33 | If it represent address 0.0.0.x with nonzero x, the server must read
34 | in the domain name that the client sends in the packet. The server
35 | should resolve the domain name and make connection to the destination
36 | host if it can.
37 |
38 | SOCKSified sockd may pass domain names that it cannot resolve to
39 | the next-hop SOCKS server.
40 |
--------------------------------------------------------------------------------
/transport/socks5/rfc1929.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Network Working Group M. Leech
8 | Request for Comments: 1929 Bell-Northern Research Ltd
9 | Category: Standards Track March 1996
10 |
11 |
12 | Username/Password Authentication for SOCKS V5
13 |
14 | Status of this Memo
15 |
16 | This document specifies an Internet standards track protocol for the
17 | Internet community, and requests discussion and suggestions for
18 | improvements. Please refer to the current edition of the "Internet
19 | Official Protocol Standards" (STD 1) for the standardization state
20 | and status of this protocol. Distribution of this memo is unlimited.
21 |
22 | 1. Introduction
23 |
24 | The protocol specification for SOCKS Version 5 specifies a
25 | generalized framework for the use of arbitrary authentication
26 | protocols in the initial socks connection setup. This document
27 | describes one of those protocols, as it fits into the SOCKS Version 5
28 | authentication "subnegotiation".
29 |
30 | Note:
31 |
32 | Unless otherwise noted, the decimal numbers appearing in packet-
33 | format diagrams represent the length of the corresponding field, in
34 | octets. Where a given octet must take on a specific value, the
35 | syntax X'hh' is used to denote the value of the single octet in that
36 | field. When the word 'Variable' is used, it indicates that the
37 | corresponding field has a variable length defined either by an
38 | associated (one or two octet) length field, or by a data type field.
39 |
40 | 2. Initial negotiation
41 |
42 | Once the SOCKS V5 server has started, and the client has selected the
43 | Username/Password Authentication protocol, the Username/Password
44 | subnegotiation begins. This begins with the client producing a
45 | Username/Password request:
46 |
47 | +----+------+----------+------+----------+
48 | |VER | ULEN | UNAME | PLEN | PASSWD |
49 | +----+------+----------+------+----------+
50 | | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
51 | +----+------+----------+------+----------+
52 |
53 |
54 |
55 |
56 |
57 |
58 | Leech Standards Track [Page 1]
59 |
60 | RFC 1929 Username Authentication for SOCKS V5 March 1996
61 |
62 |
63 | The VER field contains the current version of the subnegotiation,
64 | which is X'01'. The ULEN field contains the length of the UNAME field
65 | that follows. The UNAME field contains the username as known to the
66 | source operating system. The PLEN field contains the length of the
67 | PASSWD field that follows. The PASSWD field contains the password
68 | association with the given UNAME.
69 |
70 | The server verifies the supplied UNAME and PASSWD, and sends the
71 | following response:
72 |
73 | +----+--------+
74 | |VER | STATUS |
75 | +----+--------+
76 | | 1 | 1 |
77 | +----+--------+
78 |
79 | A STATUS field of X'00' indicates success. If the server returns a
80 | `failure' (STATUS value other than X'00') status, it MUST close the
81 | connection.
82 |
83 | 3. Security Considerations
84 |
85 | This document describes a subnegotiation that provides authentication
86 | services to the SOCKS protocol. Since the request carries the
87 | password in cleartext, this subnegotiation is not recommended for
88 | environments where "sniffing" is possible and practical.
89 |
90 | 4. Author's Address
91 |
92 | Marcus Leech
93 | Bell-Northern Research Ltd
94 | P.O. Box 3511, Station C
95 | Ottawa, ON
96 | CANADA K1Y 4H7
97 |
98 | Phone: +1 613 763 9145
99 | EMail: mleech@bnr.ca
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Leech Standards Track [Page 2]
115 |
116 |
--------------------------------------------------------------------------------
/transport/socks5/socks5_test.go:
--------------------------------------------------------------------------------
1 | package socks5
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestSocks5ClientHandshake(t *testing.T) {
12 | // Mock server responses
13 | readBuffer := &bytes.Buffer{}
14 | readBuffer.Write([]byte{Version, MethodUserPass})
15 | readBuffer.Write([]byte{Version, 0x00 /* STATUS of SUCCESS */})
16 | readBuffer.Write([]byte{Version, 0x00 /* STATUS of SUCCESS */, 0x00 /* RSV */})
17 | readBuffer.Write([]byte{AtypIPv4, 0x1, 0x2, 0x3, 0x4, 0x0, 0x0 /* IPv4: 1.2.3.4:0 */})
18 | reader := bufio.NewReader(bytes.NewReader(readBuffer.Bytes()))
19 |
20 | writeBuffer := &bytes.Buffer{}
21 | writer := bufio.NewWriter(writeBuffer)
22 |
23 | io := bufio.NewReadWriter(reader, writer)
24 |
25 | addr, err := ClientHandshake(io, []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, CmdConnect, &User{
26 | Username: "test",
27 | Password: "6ab49d8b-a009-44e4-bd53-fbdb48fbe7eb",
28 | })
29 |
30 | assert.Nil(t, err, "Failed to perform SOCKS5 client handshake: %v", err)
31 | assert.Equal(t, "1.2.3.4:0", addr.String(), "Incorrect address obtained from SOCKS5 client handshake")
32 | }
33 |
--------------------------------------------------------------------------------
/tunnel/addr.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "net"
5 | "net/netip"
6 |
7 | "gvisor.dev/gvisor/pkg/tcpip"
8 | )
9 |
10 | // parseNetAddr parses net.Addr to IP and port.
11 | func parseNetAddr(addr net.Addr) (netip.Addr, uint16) {
12 | if addr == nil {
13 | return netip.Addr{}, 0
14 | }
15 | if v, ok := addr.(interface {
16 | AddrPort() netip.AddrPort
17 | }); ok {
18 | ap := v.AddrPort()
19 | return ap.Addr(), ap.Port()
20 | }
21 | return parseAddrString(addr.String())
22 | }
23 |
24 | // parseAddrString parses address string to IP and port.
25 | // It doesn't do any name resolution.
26 | func parseAddrString(s string) (netip.Addr, uint16) {
27 | ap, err := netip.ParseAddrPort(s)
28 | if err != nil {
29 | return netip.Addr{}, 0
30 | }
31 | return ap.Addr(), ap.Port()
32 | }
33 |
34 | // parseTCPIPAddress parses tcpip.Address to netip.Addr.
35 | func parseTCPIPAddress(addr tcpip.Address) netip.Addr {
36 | ip, _ := netip.AddrFromSlice(addr.AsSlice())
37 | return ip
38 | }
39 |
--------------------------------------------------------------------------------
/tunnel/global.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/xjasonlyu/tun2socks/v2/proxy"
7 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
8 | )
9 |
10 | var (
11 | _globalMu sync.RWMutex
12 | _globalT *Tunnel
13 | )
14 |
15 | func init() {
16 | ReplaceGlobal(New(&proxy.Base{}, statistic.DefaultManager))
17 | T().ProcessAsync()
18 | }
19 |
20 | // T returns the global Tunnel, which can be reconfigured with
21 | // ReplaceGlobal. It's safe for concurrent use.
22 | func T() *Tunnel {
23 | _globalMu.RLock()
24 | t := _globalT
25 | _globalMu.RUnlock()
26 | return t
27 | }
28 |
29 | // ReplaceGlobal replaces the global Tunnel, and returns a function
30 | // to restore the original values. It's safe for concurrent use.
31 | func ReplaceGlobal(t *Tunnel) func() {
32 | _globalMu.Lock()
33 | prev := _globalT
34 | _globalT = t
35 | _globalMu.Unlock()
36 | return func() { ReplaceGlobal(prev) }
37 | }
38 |
--------------------------------------------------------------------------------
/tunnel/statistic/manager.go:
--------------------------------------------------------------------------------
1 | package statistic
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "go.uber.org/atomic"
8 | )
9 |
10 | var DefaultManager *Manager
11 |
12 | func init() {
13 | DefaultManager = &Manager{
14 | uploadTemp: atomic.NewInt64(0),
15 | downloadTemp: atomic.NewInt64(0),
16 | uploadBlip: atomic.NewInt64(0),
17 | downloadBlip: atomic.NewInt64(0),
18 | uploadTotal: atomic.NewInt64(0),
19 | downloadTotal: atomic.NewInt64(0),
20 | }
21 | go DefaultManager.handle()
22 | }
23 |
24 | type Manager struct {
25 | connections sync.Map
26 | uploadTemp *atomic.Int64
27 | downloadTemp *atomic.Int64
28 | uploadBlip *atomic.Int64
29 | downloadBlip *atomic.Int64
30 | uploadTotal *atomic.Int64
31 | downloadTotal *atomic.Int64
32 | }
33 |
34 | func (m *Manager) Join(c tracker) {
35 | m.connections.Store(c.ID(), c)
36 | }
37 |
38 | func (m *Manager) Leave(c tracker) {
39 | m.connections.Delete(c.ID())
40 | }
41 |
42 | func (m *Manager) PushUploaded(size int64) {
43 | m.uploadTemp.Add(size)
44 | m.uploadTotal.Add(size)
45 | }
46 |
47 | func (m *Manager) PushDownloaded(size int64) {
48 | m.downloadTemp.Add(size)
49 | m.downloadTotal.Add(size)
50 | }
51 |
52 | func (m *Manager) Now() (up int64, down int64) {
53 | return m.uploadBlip.Load(), m.downloadBlip.Load()
54 | }
55 |
56 | func (m *Manager) Snapshot() *Snapshot {
57 | var connections []tracker
58 | m.connections.Range(func(key, value any) bool {
59 | connections = append(connections, value.(tracker))
60 | return true
61 | })
62 |
63 | return &Snapshot{
64 | UploadTotal: m.uploadTotal.Load(),
65 | DownloadTotal: m.downloadTotal.Load(),
66 | Connections: connections,
67 | }
68 | }
69 |
70 | func (m *Manager) ResetStatistic() {
71 | m.uploadTemp.Store(0)
72 | m.uploadBlip.Store(0)
73 | m.uploadTotal.Store(0)
74 | m.downloadTemp.Store(0)
75 | m.downloadBlip.Store(0)
76 | m.downloadTotal.Store(0)
77 | }
78 |
79 | func (m *Manager) handle() {
80 | ticker := time.NewTicker(time.Second)
81 |
82 | for range ticker.C {
83 | m.uploadBlip.Store(m.uploadTemp.Load())
84 | m.uploadTemp.Store(0)
85 | m.downloadBlip.Store(m.downloadTemp.Load())
86 | m.downloadTemp.Store(0)
87 | }
88 | }
89 |
90 | type Snapshot struct {
91 | DownloadTotal int64 `json:"downloadTotal"`
92 | UploadTotal int64 `json:"uploadTotal"`
93 | Connections []tracker `json:"connections"`
94 | }
95 |
--------------------------------------------------------------------------------
/tunnel/statistic/tracker.go:
--------------------------------------------------------------------------------
1 | package statistic
2 |
3 | import (
4 | "errors"
5 | "net"
6 | "time"
7 |
8 | "github.com/google/uuid"
9 | "go.uber.org/atomic"
10 |
11 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
12 | )
13 |
14 | type tracker interface {
15 | ID() string
16 | Close() error
17 | }
18 |
19 | type trackerInfo struct {
20 | Start time.Time `json:"start"`
21 | UUID uuid.UUID `json:"id"`
22 | Metadata *M.Metadata `json:"metadata"`
23 | UploadTotal *atomic.Int64 `json:"upload"`
24 | DownloadTotal *atomic.Int64 `json:"download"`
25 | }
26 |
27 | type tcpTracker struct {
28 | net.Conn `json:"-"`
29 |
30 | *trackerInfo
31 | manager *Manager
32 | }
33 |
34 | func NewTCPTracker(conn net.Conn, metadata *M.Metadata, manager *Manager) net.Conn {
35 | id, _ := uuid.NewRandom()
36 |
37 | tt := &tcpTracker{
38 | Conn: conn,
39 | manager: manager,
40 | trackerInfo: &trackerInfo{
41 | UUID: id,
42 | Start: time.Now(),
43 | Metadata: metadata,
44 | UploadTotal: atomic.NewInt64(0),
45 | DownloadTotal: atomic.NewInt64(0),
46 | },
47 | }
48 |
49 | manager.Join(tt)
50 | return tt
51 | }
52 |
53 | func (tt *tcpTracker) ID() string {
54 | return tt.UUID.String()
55 | }
56 |
57 | func (tt *tcpTracker) Read(b []byte) (int, error) {
58 | n, err := tt.Conn.Read(b)
59 | download := int64(n)
60 | tt.manager.PushDownloaded(download)
61 | tt.DownloadTotal.Add(download)
62 | return n, err
63 | }
64 |
65 | func (tt *tcpTracker) Write(b []byte) (int, error) {
66 | n, err := tt.Conn.Write(b)
67 | upload := int64(n)
68 | tt.manager.PushUploaded(upload)
69 | tt.UploadTotal.Add(upload)
70 | return n, err
71 | }
72 |
73 | func (tt *tcpTracker) Close() error {
74 | tt.manager.Leave(tt)
75 | return tt.Conn.Close()
76 | }
77 |
78 | func (tt *tcpTracker) CloseRead() error {
79 | if cr, ok := tt.Conn.(interface{ CloseRead() error }); ok {
80 | return cr.CloseRead()
81 | }
82 | return errors.New("CloseRead is not implemented")
83 | }
84 |
85 | func (tt *tcpTracker) CloseWrite() error {
86 | if cw, ok := tt.Conn.(interface{ CloseWrite() error }); ok {
87 | return cw.CloseWrite()
88 | }
89 | return errors.New("CloseWrite is not implemented")
90 | }
91 |
92 | type udpTracker struct {
93 | net.PacketConn `json:"-"`
94 |
95 | *trackerInfo
96 | manager *Manager
97 | }
98 |
99 | func NewUDPTracker(conn net.PacketConn, metadata *M.Metadata, manager *Manager) net.PacketConn {
100 | id, _ := uuid.NewRandom()
101 |
102 | ut := &udpTracker{
103 | PacketConn: conn,
104 | manager: manager,
105 | trackerInfo: &trackerInfo{
106 | UUID: id,
107 | Start: time.Now(),
108 | Metadata: metadata,
109 | UploadTotal: atomic.NewInt64(0),
110 | DownloadTotal: atomic.NewInt64(0),
111 | },
112 | }
113 |
114 | manager.Join(ut)
115 | return ut
116 | }
117 |
118 | func (ut *udpTracker) ID() string {
119 | return ut.UUID.String()
120 | }
121 |
122 | func (ut *udpTracker) ReadFrom(b []byte) (int, net.Addr, error) {
123 | n, addr, err := ut.PacketConn.ReadFrom(b)
124 | download := int64(n)
125 | ut.manager.PushDownloaded(download)
126 | ut.DownloadTotal.Add(download)
127 | return n, addr, err
128 | }
129 |
130 | func (ut *udpTracker) WriteTo(b []byte, addr net.Addr) (int, error) {
131 | n, err := ut.PacketConn.WriteTo(b, addr)
132 | upload := int64(n)
133 | ut.manager.PushUploaded(upload)
134 | ut.UploadTotal.Add(upload)
135 | return n, err
136 | }
137 |
138 | func (ut *udpTracker) Close() error {
139 | ut.manager.Leave(ut)
140 | return ut.PacketConn.Close()
141 | }
142 |
--------------------------------------------------------------------------------
/tunnel/tcp.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "io"
6 | "net"
7 | "sync"
8 | "time"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/buffer"
11 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
12 | "github.com/xjasonlyu/tun2socks/v2/log"
13 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
14 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
15 | )
16 |
17 | func (t *Tunnel) handleTCPConn(originConn adapter.TCPConn) {
18 | defer originConn.Close()
19 |
20 | id := originConn.ID()
21 | metadata := &M.Metadata{
22 | Network: M.TCP,
23 | SrcIP: parseTCPIPAddress(id.RemoteAddress),
24 | SrcPort: id.RemotePort,
25 | DstIP: parseTCPIPAddress(id.LocalAddress),
26 | DstPort: id.LocalPort,
27 | }
28 |
29 | ctx, cancel := context.WithTimeout(context.Background(), tcpConnectTimeout)
30 | defer cancel()
31 |
32 | remoteConn, err := t.Dialer().DialContext(ctx, metadata)
33 | if err != nil {
34 | log.Warnf("[TCP] dial %s: %v", metadata.DestinationAddress(), err)
35 | return
36 | }
37 | metadata.MidIP, metadata.MidPort = parseNetAddr(remoteConn.LocalAddr())
38 |
39 | remoteConn = statistic.NewTCPTracker(remoteConn, metadata, t.manager)
40 | defer remoteConn.Close()
41 |
42 | log.Infof("[TCP] %s <-> %s", metadata.SourceAddress(), metadata.DestinationAddress())
43 | pipe(originConn, remoteConn)
44 | }
45 |
46 | // pipe copies data to & from provided net.Conn(s) bidirectionally.
47 | func pipe(origin, remote net.Conn) {
48 | wg := sync.WaitGroup{}
49 | wg.Add(2)
50 |
51 | go unidirectionalStream(remote, origin, "origin->remote", &wg)
52 | go unidirectionalStream(origin, remote, "remote->origin", &wg)
53 |
54 | wg.Wait()
55 | }
56 |
57 | func unidirectionalStream(dst, src net.Conn, dir string, wg *sync.WaitGroup) {
58 | defer wg.Done()
59 | buf := buffer.Get(buffer.RelayBufferSize)
60 | if _, err := io.CopyBuffer(dst, src, buf); err != nil {
61 | log.Debugf("[TCP] copy data for %s: %v", dir, err)
62 | }
63 | buffer.Put(buf)
64 | // Do the upload/download side TCP half-close.
65 | if cr, ok := src.(interface{ CloseRead() error }); ok {
66 | cr.CloseRead()
67 | }
68 | if cw, ok := dst.(interface{ CloseWrite() error }); ok {
69 | cw.CloseWrite()
70 | }
71 | // Set TCP half-close timeout.
72 | dst.SetReadDeadline(time.Now().Add(tcpWaitTimeout))
73 | }
74 |
--------------------------------------------------------------------------------
/tunnel/tunnel.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "sync"
6 | "time"
7 |
8 | "go.uber.org/atomic"
9 |
10 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
11 | "github.com/xjasonlyu/tun2socks/v2/proxy"
12 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
13 | )
14 |
15 | const (
16 | // tcpConnectTimeout is the default timeout for TCP handshakes.
17 | tcpConnectTimeout = 5 * time.Second
18 | // tcpWaitTimeout implements a TCP half-close timeout.
19 | tcpWaitTimeout = 60 * time.Second
20 | // udpSessionTimeout is the default timeout for UDP sessions.
21 | udpSessionTimeout = 60 * time.Second
22 | )
23 |
24 | var _ adapter.TransportHandler = (*Tunnel)(nil)
25 |
26 | type Tunnel struct {
27 | // Unbuffered TCP/UDP queues.
28 | tcpQueue chan adapter.TCPConn
29 | udpQueue chan adapter.UDPConn
30 |
31 | // UDP session timeout.
32 | udpTimeout *atomic.Duration
33 |
34 | // Internal proxy.Dialer for Tunnel.
35 | dialerMu sync.RWMutex
36 | dialer proxy.Dialer
37 |
38 | // Where the Tunnel statistics are sent to.
39 | manager *statistic.Manager
40 |
41 | procOnce sync.Once
42 | procCancel context.CancelFunc
43 | }
44 |
45 | func New(dialer proxy.Dialer, manager *statistic.Manager) *Tunnel {
46 | return &Tunnel{
47 | tcpQueue: make(chan adapter.TCPConn),
48 | udpQueue: make(chan adapter.UDPConn),
49 | udpTimeout: atomic.NewDuration(udpSessionTimeout),
50 | dialer: dialer,
51 | manager: manager,
52 | procCancel: func() { /* nop */ },
53 | }
54 | }
55 |
56 | // TCPIn return fan-in TCP queue.
57 | func (t *Tunnel) TCPIn() chan<- adapter.TCPConn {
58 | return t.tcpQueue
59 | }
60 |
61 | // UDPIn return fan-in UDP queue.
62 | func (t *Tunnel) UDPIn() chan<- adapter.UDPConn {
63 | return t.udpQueue
64 | }
65 |
66 | func (t *Tunnel) HandleTCP(conn adapter.TCPConn) {
67 | t.TCPIn() <- conn
68 | }
69 |
70 | func (t *Tunnel) HandleUDP(conn adapter.UDPConn) {
71 | t.UDPIn() <- conn
72 | }
73 |
74 | func (t *Tunnel) process(ctx context.Context) {
75 | for {
76 | select {
77 | case conn := <-t.tcpQueue:
78 | go t.handleTCPConn(conn)
79 | case conn := <-t.udpQueue:
80 | go t.handleUDPConn(conn)
81 | case <-ctx.Done():
82 | return
83 | }
84 | }
85 | }
86 |
87 | // ProcessAsync can be safely called multiple times, but will only be effective once.
88 | func (t *Tunnel) ProcessAsync() {
89 | t.procOnce.Do(func() {
90 | ctx, cancel := context.WithCancel(context.Background())
91 | t.procCancel = cancel
92 | go t.process(ctx)
93 | })
94 | }
95 |
96 | // Close closes the Tunnel and releases its resources.
97 | func (t *Tunnel) Close() {
98 | t.procCancel()
99 | }
100 |
101 | func (t *Tunnel) Dialer() proxy.Dialer {
102 | t.dialerMu.RLock()
103 | d := t.dialer
104 | t.dialerMu.RUnlock()
105 | return d
106 | }
107 |
108 | func (t *Tunnel) SetDialer(dialer proxy.Dialer) {
109 | t.dialerMu.Lock()
110 | t.dialer = dialer
111 | t.dialerMu.Unlock()
112 | }
113 |
114 | func (t *Tunnel) SetUDPTimeout(timeout time.Duration) {
115 | t.udpTimeout.Store(timeout)
116 | }
117 |
--------------------------------------------------------------------------------
/tunnel/udp.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "io"
5 | "net"
6 | "sync"
7 | "time"
8 |
9 | "github.com/xjasonlyu/tun2socks/v2/buffer"
10 | "github.com/xjasonlyu/tun2socks/v2/core/adapter"
11 | "github.com/xjasonlyu/tun2socks/v2/log"
12 | M "github.com/xjasonlyu/tun2socks/v2/metadata"
13 | "github.com/xjasonlyu/tun2socks/v2/tunnel/statistic"
14 | )
15 |
16 | // TODO: Port Restricted NAT support.
17 | func (t *Tunnel) handleUDPConn(uc adapter.UDPConn) {
18 | defer uc.Close()
19 |
20 | id := uc.ID()
21 | metadata := &M.Metadata{
22 | Network: M.UDP,
23 | SrcIP: parseTCPIPAddress(id.RemoteAddress),
24 | SrcPort: id.RemotePort,
25 | DstIP: parseTCPIPAddress(id.LocalAddress),
26 | DstPort: id.LocalPort,
27 | }
28 |
29 | pc, err := t.Dialer().DialUDP(metadata)
30 | if err != nil {
31 | log.Warnf("[UDP] dial %s: %v", metadata.DestinationAddress(), err)
32 | return
33 | }
34 | metadata.MidIP, metadata.MidPort = parseNetAddr(pc.LocalAddr())
35 |
36 | pc = statistic.NewUDPTracker(pc, metadata, t.manager)
37 | defer pc.Close()
38 |
39 | var remote net.Addr
40 | if udpAddr := metadata.UDPAddr(); udpAddr != nil {
41 | remote = udpAddr
42 | } else {
43 | remote = metadata.Addr()
44 | }
45 | pc = newSymmetricNATPacketConn(pc, metadata)
46 |
47 | log.Infof("[UDP] %s <-> %s", metadata.SourceAddress(), metadata.DestinationAddress())
48 | pipePacket(uc, pc, remote, t.udpTimeout.Load())
49 | }
50 |
51 | func pipePacket(origin, remote net.PacketConn, to net.Addr, timeout time.Duration) {
52 | wg := sync.WaitGroup{}
53 | wg.Add(2)
54 |
55 | go unidirectionalPacketStream(remote, origin, to, "origin->remote", &wg, timeout)
56 | go unidirectionalPacketStream(origin, remote, nil, "remote->origin", &wg, timeout)
57 |
58 | wg.Wait()
59 | }
60 |
61 | func unidirectionalPacketStream(dst, src net.PacketConn, to net.Addr, dir string, wg *sync.WaitGroup, timeout time.Duration) {
62 | defer wg.Done()
63 | if err := copyPacketData(dst, src, to, timeout); err != nil {
64 | log.Debugf("[UDP] copy data for %s: %v", dir, err)
65 | }
66 | }
67 |
68 | func copyPacketData(dst, src net.PacketConn, to net.Addr, timeout time.Duration) error {
69 | buf := buffer.Get(buffer.MaxSegmentSize)
70 | defer buffer.Put(buf)
71 |
72 | for {
73 | src.SetReadDeadline(time.Now().Add(timeout))
74 | n, _, err := src.ReadFrom(buf)
75 | if ne, ok := err.(net.Error); ok && ne.Timeout() {
76 | return nil /* ignore I/O timeout */
77 | } else if err == io.EOF {
78 | return nil /* ignore EOF */
79 | } else if err != nil {
80 | return err
81 | }
82 |
83 | if _, err = dst.WriteTo(buf[:n], to); err != nil {
84 | return err
85 | }
86 | dst.SetReadDeadline(time.Now().Add(timeout))
87 | }
88 | }
89 |
90 | type symmetricNATPacketConn struct {
91 | net.PacketConn
92 | src string
93 | dst string
94 | }
95 |
96 | func newSymmetricNATPacketConn(pc net.PacketConn, metadata *M.Metadata) *symmetricNATPacketConn {
97 | return &symmetricNATPacketConn{
98 | PacketConn: pc,
99 | src: metadata.SourceAddress(),
100 | dst: metadata.DestinationAddress(),
101 | }
102 | }
103 |
104 | func (pc *symmetricNATPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
105 | for {
106 | n, from, err := pc.PacketConn.ReadFrom(p)
107 |
108 | if from != nil && from.String() != pc.dst {
109 | log.Warnf("[UDP] symmetric NAT %s->%s: drop packet from %s", pc.src, pc.dst, from)
110 | continue
111 | }
112 |
113 | return n, from, err
114 | }
115 | }
116 |
--------------------------------------------------------------------------------