├── .github
├── CODEOWNERS
└── workflows
│ └── build_and_test.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── go.sum
├── https
├── fetch.go
└── fetch_test.go
├── intra
├── android
│ ├── init.go
│ ├── tun.go
│ └── tun2socks.go
├── doh
│ ├── atomic.go
│ ├── client_auth.go
│ ├── client_auth_test.go
│ ├── doh.go
│ ├── doh_test.go
│ ├── ipmap
│ │ ├── ipmap.go
│ │ └── ipmap_test.go
│ └── padding.go
├── ip.go
├── ip_test.go
├── packet_proxy.go
├── protect
│ ├── protect.go
│ └── protect_test.go
├── sni_reporter.go
├── sni_reporter_test.go
├── split
│ ├── direct_split.go
│ ├── example
│ │ └── main.go
│ ├── retrier.go
│ └── retrier_test.go
├── stream_dialer.go
├── tcp.go
├── tunnel.go
└── udp.go
├── outline
├── client.go
├── connectivity
│ ├── connectivity.go
│ └── connectivity_test.go
├── electron
│ ├── connect_linux.sh
│ └── main.go
├── internal
│ └── utf8
│ │ ├── utf8.go
│ │ └── utf8_test.go
├── neterrors
│ └── neterrors.go
├── shadowsocks
│ ├── client.go
│ ├── client_test.go
│ ├── config.go
│ └── config_test.go
└── tun2socks
│ ├── tcp.go
│ ├── tunnel.go
│ ├── tunnel_android.go
│ ├── tunnel_darwin.go
│ └── udp.go
├── tools.go
└── tunnel
├── tun.go
├── tun_unix.go
└── tunnel.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @Jigsaw-Code/outline-networking-owners
2 |
3 | *.md @Jigsaw-Code/outline-strings-owners
4 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_test.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | concurrency:
4 | group: ${{ github.head_ref || github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | pull_request:
9 | types:
10 | - opened
11 | - synchronize
12 | push:
13 | branches:
14 | - master
15 |
16 | jobs:
17 | test:
18 | name: Test
19 | runs-on: ubuntu-22.04
20 | timeout-minutes: 10
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 |
25 | - name: Set up Go 1.20
26 | uses: actions/setup-go@v4
27 | with:
28 | go-version: '^1.20'
29 |
30 | - name: Build
31 | run: go build -v ./...
32 |
33 | - name: Test
34 | run: go test -v -race -bench=. -benchtime=100ms ./...
35 |
36 | linux:
37 | name: Electron Build
38 | runs-on: ubuntu-22.04
39 | timeout-minutes: 10
40 | needs: test
41 | steps:
42 | - name: Checkout
43 | uses: actions/checkout@v3
44 |
45 | - name: Set up Go 1.20
46 | uses: actions/setup-go@v4
47 | with:
48 | go-version: '^1.20'
49 |
50 | - name: Build for Linux
51 | run: make linux
52 |
53 | - name: Build for Windows
54 | run: make windows
55 |
56 | apple:
57 | name: Apple Build
58 | runs-on: macos-12
59 | timeout-minutes: 30
60 | needs: test
61 | env:
62 | # Prevent gomobile from interacting with the Android NDK. The runner's
63 | # default NDK is not compatible with gomobile, but we aren't trying
64 | # to build for Android anyway.
65 | ANDROID_HOME: ""
66 | ANDROID_NDK_HOME: ""
67 | steps:
68 | - name: Checkout
69 | uses: actions/checkout@v3
70 |
71 | - name: Set up Go 1.20
72 | uses: actions/setup-go@v4
73 | with:
74 | go-version: '^1.20'
75 |
76 | - name: Set XCode Version
77 | run: sudo xcode-select -switch /Applications/Xcode_13.3.app
78 |
79 | - name: Build for Apple platforms
80 | run: make apple
81 |
82 | android:
83 | name: Android Build
84 | runs-on: ubuntu-22.04
85 | timeout-minutes: 10
86 | needs: test
87 | env:
88 | # Let gomobile choose its preferred NDK version
89 | ANDROID_NDK_HOME: ""
90 | steps:
91 | - name: Checkout
92 | uses: actions/checkout@v3
93 |
94 | - name: Set up Go 1.20
95 | uses: actions/setup-go@v4
96 | with:
97 | go-version: '^1.20'
98 |
99 | - name: Build Outline Library
100 | run: make android
101 |
102 | - name: Build Intra Library
103 | run: make intra
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /bin
3 | intra/split/example/example
4 |
5 | # General
6 | .DS_Store
7 | .AppleDouble
8 | .LSOverride
9 |
10 | # Thumbnails
11 | ._*
12 |
13 | # Files that might appear in the root of a volume
14 | .DocumentRevisions-V100
15 | .fseventsd
16 | .Spotlight-V100
17 | .TemporaryItems
18 | .Trashes
19 | .VolumeIcon.icns
20 | .com.apple.timemachine.donotpresent
21 |
22 | # Directories potentially created on remote AFP share
23 | .AppleDB
24 | .AppleDesktop
25 | Network Trash Folder
26 | Temporary Items
27 | .apdisk
28 |
29 | # IDEs
30 | .vscode/
31 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BUILDDIR=$(CURDIR)/build
2 | GOBIN=$(CURDIR)/bin
3 |
4 | GOMOBILE=$(GOBIN)/gomobile
5 | # Add GOBIN to $PATH so `gomobile` can find `gobind`.
6 | GOBIND=env PATH="$(GOBIN):$(PATH)" "$(GOMOBILE)" bind
7 | IMPORT_HOST=github.com
8 | IMPORT_PATH=$(IMPORT_HOST)/Jigsaw-Code/outline-go-tun2socks
9 |
10 | .PHONY: android apple linux windows intra clean clean-all
11 |
12 | all: intra android linux apple windows
13 |
14 | # Don't strip Android debug symbols so we can upload them to crash reporting tools.
15 | ANDROID_BUILD_CMD=$(GOBIND) -a -ldflags '-w' -target=android -tags android -work
16 |
17 | intra: $(BUILDDIR)/intra/tun2socks.aar
18 |
19 | $(BUILDDIR)/intra/tun2socks.aar: $(GOMOBILE)
20 | mkdir -p "$(BUILDDIR)/intra"
21 | $(ANDROID_BUILD_CMD) -o "$@" $(IMPORT_PATH)/intra $(IMPORT_PATH)/intra/android $(IMPORT_PATH)/intra/doh $(IMPORT_PATH)/intra/split $(IMPORT_PATH)/intra/protect
22 |
23 | android: $(BUILDDIR)/android/tun2socks.aar
24 |
25 | $(BUILDDIR)/android/tun2socks.aar: $(GOMOBILE)
26 | mkdir -p "$(BUILDDIR)/android"
27 | $(ANDROID_BUILD_CMD) -o "$@" $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
28 |
29 | # TODO(fortuna): -s strips symbols and is obsolete. Why are we using it?
30 | $(BUILDDIR)/ios/Tun2socks.xcframework: $(GOMOBILE)
31 | # -iosversion should match what outline-client supports.
32 | $(GOBIND) -iosversion=11.0 -target=ios,iossimulator -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
33 |
34 | $(BUILDDIR)/macos/Tun2socks.xcframework: $(GOMOBILE)
35 | # MACOSX_DEPLOYMENT_TARGET and -iosversion should match what outline-client supports.
36 | export MACOSX_DEPLOYMENT_TARGET=10.14; $(GOBIND) -iosversion=13.1 -target=macos,maccatalyst -o $@ -ldflags '-s -w' -bundleid org.outline.tun2socks $(IMPORT_PATH)/outline/tun2socks $(IMPORT_PATH)/outline/shadowsocks
37 |
38 | apple: $(BUILDDIR)/apple/Tun2socks.xcframework
39 |
40 | $(BUILDDIR)/apple/Tun2socks.xcframework: $(BUILDDIR)/ios/Tun2socks.xcframework $(BUILDDIR)/macos/Tun2socks.xcframework
41 | find $^ -name "Tun2socks.framework" -type d | xargs -I {} echo " -framework {} " | \
42 | xargs xcrun xcodebuild -create-xcframework -output "$@"
43 |
44 | XGO=$(GOBIN)/xgo
45 | TUN2SOCKS_VERSION=v1.16.11
46 | XGO_LDFLAGS='-s -w -X main.version=$(TUN2SOCKS_VERSION)'
47 | ELECTRON_PKG=outline/electron
48 |
49 |
50 | LINUX_BUILDDIR=$(BUILDDIR)/linux
51 |
52 | linux: $(LINUX_BUILDDIR)/tun2socks
53 |
54 | $(LINUX_BUILDDIR)/tun2socks: $(XGO)
55 | mkdir -p "$(LINUX_BUILDDIR)/$(IMPORT_PATH)"
56 | $(XGO) -ldflags $(XGO_LDFLAGS) --targets=linux/amd64 -dest "$(LINUX_BUILDDIR)" -pkg $(ELECTRON_PKG) .
57 | mv "$(LINUX_BUILDDIR)/$(IMPORT_PATH)-linux-amd64" "$@"
58 | rm -r "$(LINUX_BUILDDIR)/$(IMPORT_HOST)"
59 |
60 |
61 | WINDOWS_BUILDDIR=$(BUILDDIR)/windows
62 |
63 | windows: $(WINDOWS_BUILDDIR)/tun2socks.exe
64 |
65 | $(WINDOWS_BUILDDIR)/tun2socks.exe: $(XGO)
66 | mkdir -p "$(WINDOWS_BUILDDIR)/$(IMPORT_PATH)"
67 | $(XGO) -ldflags $(XGO_LDFLAGS) --targets=windows/386 -dest "$(WINDOWS_BUILDDIR)" -pkg $(ELECTRON_PKG) .
68 | mv "$(WINDOWS_BUILDDIR)/$(IMPORT_PATH)-windows-386.exe" "$@"
69 | rm -r "$(WINDOWS_BUILDDIR)/$(IMPORT_HOST)"
70 |
71 |
72 | $(GOMOBILE): go.mod
73 | env GOBIN="$(GOBIN)" go install golang.org/x/mobile/cmd/gomobile
74 | env GOBIN="$(GOBIN)" $(GOMOBILE) init
75 |
76 | $(XGO): go.mod
77 | env GOBIN="$(GOBIN)" go install github.com/crazy-max/xgo
78 |
79 | go.mod: tools.go
80 | go mod tidy
81 | touch go.mod
82 |
83 | clean:
84 | rm -rf "$(BUILDDIR)"
85 | go clean
86 |
87 | clean-all: clean
88 | rm -rf "$(GOBIN)"
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # outline-go-tun2socks
2 |
3 | > [!WARNING]
4 | > This repository is no longer being maintained. The tun2socks source is now maintained in the [outline-apps repository](https://github.com/Jigsaw-Code/outline-apps/tree/master) and [Intra repository](https://github.com/Jigsaw-Code/Intra/tree/master/Android/app/src/go).
5 |
6 | Go package for building [go-tun2socks](https://github.com/eycorsican/go-tun2socks)-based clients for [Outline](https://getoutline.org) and [Intra](https://getintra.org) (now with support for [Choir](https://github.com/Jigsaw-Code/choir) metrics). For macOS, iOS, and Android, the output is a library; for Linux and Windows it is a command-line executable.
7 |
8 | ## Prerequisites
9 |
10 | - macOS host (iOS, macOS)
11 | - make
12 | - Go >= 1.18
13 | - A C compiler (e.g.: clang, gcc)
14 |
15 | ## Android
16 |
17 | ### Set up
18 |
19 | - [sdkmanager](https://developer.android.com/studio/command-line/sdkmanager)
20 | 1. Download the command line tools from https://developer.android.com/studio.
21 | 1. Unzip the pacakge as `~/Android/Sdk/cmdline-tools/latest/`. Make sure `sdkmanager` is located at `~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager`
22 | - Android NDK 23
23 | 1. Install the NDK with `~/Android/Sdk/cmdline-tools/latest/bin/sdkmanager "platforms;android-30" "ndk;23.1.7779620"` (platform from [outline-client](https://github.com/Jigsaw-Code/outline-client#building-the-android-app), exact NDK 23 version obtained from `sdkmanager --list`)
24 | 1. Set up the environment variables:
25 | ```
26 | export ANDROID_NDK_HOME=~/Android/Sdk/ndk/23.1.7779620 ANDROID_HOME=~/Android/Sdk
27 | ```
28 | - [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gobind) (installed as needed by `make`)
29 |
30 | ### Build
31 |
32 | ```bash
33 | make clean && make android
34 | ```
35 | This will create `build/android/{tun2socks.aar,tun2socks-sources.jar}`
36 |
37 | If needed, you can extract the jni files into `build/android/jni` with:
38 | ```bash
39 | unzip build/android/tun2socks.aar 'jni/*' -d build/android
40 | ```
41 |
42 | ## Apple (iOS and macOS)
43 |
44 | ### Set up
45 |
46 | - Xcode
47 | - [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gobind) (installed as needed by `make`)
48 |
49 |
50 | ### Build
51 | ```
52 | make clean && make apple
53 | ```
54 | This will create `build/apple/Tun2socks.xcframework`.
55 |
56 | ## Linux and Windows
57 |
58 | We build binaries for Linux and Windows from source without any custom integrations. `xgo` and Docker are required to support cross-compilation.
59 |
60 | ### Set up
61 |
62 | - [Docker](https://docs.docker.com/get-docker/) (for xgo)
63 | - [xgo](https://github.com/crazy-max/xgo) (installed as needed by `make`)
64 | - [ghcr.io/crazy-max/xgo Docker image](https://github.com/crazy-max/xgo/pkgs/container/xgo). This is pulled automatically by xgo and takes ~6.8 GB of disk space.
65 |
66 | ## Build
67 |
68 | For Linux:
69 | ```
70 | make clean && make linux
71 | ```
72 | This will create `build/linux/tun2socks`.
73 |
74 | For Windows:
75 | ```
76 | make clean && make windows
77 | ```
78 | This will create `build/windows/tun2socks.exe`.
79 |
80 | ## Intra (Android)
81 |
82 | Same set up as for the Outline Android library.
83 |
84 | Build with:
85 |
86 | ```bash
87 | make clean && make intra
88 | ```
89 | This will create `build/intra/{tun2socks.aar,tun2socks-sources.jar}`
90 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Jigsaw-Code/outline-go-tun2socks
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/Jigsaw-Code/choir v1.0.1
7 | github.com/Jigsaw-Code/getsni v1.0.0
8 | github.com/Jigsaw-Code/outline-sdk v0.0.7
9 | github.com/crazy-max/xgo v0.26.0
10 | github.com/eycorsican/go-tun2socks v1.16.11
11 | golang.org/x/mobile v0.0.0-20230906132913-2077a3224571
12 | golang.org/x/net v0.17.0
13 | golang.org/x/sys v0.13.0
14 | )
15 |
16 | require (
17 | github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
18 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
19 | golang.org/x/crypto v0.14.0 // indirect
20 | golang.org/x/mod v0.12.0 // indirect
21 | golang.org/x/sync v0.3.0 // indirect
22 | golang.org/x/tools v0.13.0 // indirect
23 | )
24 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Jigsaw-Code/choir v1.0.1 h1:WeRt6aTn5L+MtRNqRJ+J1RKgoO8CyXXt1dtZghy2KjE=
2 | github.com/Jigsaw-Code/choir v1.0.1/go.mod h1:c4Wd1y1PeCajZbKZV+ZmcFGMDoduyqMCEMHW5iqzWXI=
3 | github.com/Jigsaw-Code/getsni v1.0.0 h1:OUTIu7wTBi/7DMX+RkZrN7XhU3UDevTEsAWK4gsqSwE=
4 | github.com/Jigsaw-Code/getsni v1.0.0/go.mod h1:Ps0Ec3fVMKLyAItVbMKoQFq1lDjtFQXZ+G5nRNNh/QE=
5 | github.com/Jigsaw-Code/outline-sdk v0.0.7 h1:WlFaV1tFpIQ/pflrKwrQuNIP3kJpgh7yJuqiTb54sGA=
6 | github.com/Jigsaw-Code/outline-sdk v0.0.7/go.mod h1:hhlKz0+r9wSDFT8usvN8Zv/BFToCIFAUn1P2Qk8G2CM=
7 | github.com/crazy-max/xgo v0.26.0 h1:vK4OfeXJoDGvnjlzdTCgPbeWLKENbzj84DTpU/VRonM=
8 | github.com/crazy-max/xgo v0.26.0/go.mod h1:m/aqfKaN/cYzfw+Pzk7Mk0tkmShg3/rCS4Zdhdugi4o=
9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10 | github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8=
11 | github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ=
12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13 | github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
14 | github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
15 | github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
16 | github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
17 | github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
18 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
19 | github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
20 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
22 | golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
23 | golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
24 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
25 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
26 | golang.org/x/mobile v0.0.0-20230906132913-2077a3224571 h1:QDvQ2KLFHHQWRID6IkZOBf6uLIh9tZ0G+mw61pFQxuo=
27 | golang.org/x/mobile v0.0.0-20230906132913-2077a3224571/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
28 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
29 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
30 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
31 | golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
32 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
33 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
34 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
35 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
38 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
39 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
40 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
43 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
44 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
45 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
46 |
--------------------------------------------------------------------------------
/https/fetch.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package https
16 |
17 | import (
18 | "bytes"
19 | "crypto/sha256"
20 | "crypto/tls"
21 | "crypto/x509"
22 | "errors"
23 | "io/ioutil"
24 | "net/http"
25 | "time"
26 | )
27 |
28 | // Request encapsulates an HTTPs request.
29 | type Request struct {
30 | // URL is the HTTPs endpoint.
31 | URL string
32 | // Method is the HTTP method to use in the request.
33 | Method string
34 | // TrustedCertFingerprint is the sha256 hash of a server's trusted
35 | // (self-signed) TLS certificate.
36 | TrustedCertFingerprint []byte
37 | }
38 |
39 | // Response encapsulates an HTTPs response.
40 | type Response struct {
41 | // Data is the received request payload.
42 | Data []byte
43 | // HTTPStatusCode is the HTTP status code of the response.
44 | HTTPStatusCode int
45 | // RedirectURL is the Location header of a HTTP redirect response.
46 | RedirectURL string
47 | }
48 |
49 | // Fetch retrieves data from an HTTPs server that may have a self-singed TLS
50 | // certificate.
51 | // Pins the trusted certificate when req.TrustedCertFingerprint is non-empty.
52 | // Follows up to 10 HTTPs redirects and sets the response's RedirectURL to the
53 | // last Location header URL when the status code is a permantent redirect.
54 | // Returns an error if req.URL is a non-HTTPS URL, if there is a connection
55 | // error to the server, or if reading the response fails.
56 | func Fetch(req Request) (*Response, error) {
57 | httpreq, err := http.NewRequest(req.Method, req.URL, nil)
58 | if err != nil {
59 | return nil, err
60 | }
61 | if httpreq.URL.Scheme != "https" {
62 | return nil, errors.New("URL protocol must be HTTPs")
63 | }
64 |
65 | var redirectURL string
66 | client := &http.Client{
67 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
68 | // Do not follow redirects automatically, save the Location header.
69 | redirectURL = req.Response.Header.Get("Location")
70 | return http.ErrUseLastResponse
71 | },
72 | Timeout: 30 * time.Second,
73 | }
74 |
75 | if req.TrustedCertFingerprint != nil && len(req.TrustedCertFingerprint) > 0 {
76 | client.Transport = &http.Transport{
77 | // Perform custom server certificate verification by pinning the
78 | // trusted certificate fingerprint.
79 | TLSClientConfig: &tls.Config{
80 | InsecureSkipVerify: true,
81 | VerifyPeerCertificate: makePinnedCertVerifier(req.TrustedCertFingerprint),
82 | },
83 | }
84 | }
85 |
86 | httpres, err := client.Do(httpreq)
87 | if err != nil {
88 | return nil, err
89 | }
90 | res := &Response{nil, httpres.StatusCode, redirectURL}
91 | res.Data, err = ioutil.ReadAll(httpres.Body)
92 | httpres.Body.Close()
93 | return res, err
94 | }
95 |
96 | type certVerifier func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
97 |
98 | // Verifies whether the pinned certificate SHA256 fingerprint,
99 | // trustedCertFingerprint, matches the leaf certificate fingerprint, regardless
100 | // of the system's TLS certificate validation errors.
101 | func makePinnedCertVerifier(trustedCertFingerprint []byte) certVerifier {
102 | return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
103 | if len(rawCerts) == 0 {
104 | return x509.CertificateInvalidError{
105 | Cert: nil, Reason: x509.NotAuthorizedToSign, Detail: "Did not receive TLS certificate"}
106 | }
107 | // Compute the sha256 digest of the whole DER-encoded certificate.
108 | fingerprint := sha256.Sum256(rawCerts[0])
109 | if bytes.Equal(fingerprint[:], trustedCertFingerprint) {
110 | return nil
111 | }
112 | return x509.CertificateInvalidError{
113 | Cert: nil, Reason: x509.NotAuthorizedToSign, Detail: "Failed to verify TLS certificate"}
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/https/fetch_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package https
16 |
17 | import (
18 | "bytes"
19 | "crypto/rand"
20 | "crypto/rsa"
21 | "crypto/sha256"
22 | "crypto/tls"
23 | "crypto/x509"
24 | "crypto/x509/pkix"
25 | "errors"
26 | "fmt"
27 | "math/big"
28 | "net"
29 | "net/http"
30 | "reflect"
31 | "testing"
32 | "time"
33 | )
34 |
35 | const redirectURL = "https://redirect.url"
36 |
37 | var okResponseData = []byte("OK")
38 | var notFoundResponseData = []byte("Not Found")
39 |
40 | func TestFetch(t *testing.T) {
41 | cert, err := makeTLSCertificate()
42 | if err != nil {
43 | t.Fatalf("Failed to generate TLS certificate: %v", err)
44 | }
45 |
46 | certFingerprintBytes := sha256.Sum256(cert.Certificate[0])
47 | certFingerprint := certFingerprintBytes[:]
48 | server := makeHTTPSServer(cert)
49 | listener, err := net.Listen("tcp", "127.0.0.1:0")
50 | if err != nil {
51 | t.Fatalf("Failed to start server: %v", err)
52 | }
53 | serverAddr := listener.Addr()
54 | go server.ServeTLS(listener, "", "")
55 | defer server.Close()
56 |
57 | t.Run("Success", func(t *testing.T) {
58 | req := Request{
59 | fmt.Sprintf("https://%s/200", serverAddr), "GET", certFingerprint}
60 | res, err := Fetch(req)
61 | if err != nil {
62 | t.Fatalf("Unexpected error: %v", err)
63 | }
64 | if res.HTTPStatusCode != 200 {
65 | t.Errorf("Expected 200 HTTP status code, got %d", res.HTTPStatusCode)
66 | }
67 | if res.RedirectURL != "" {
68 | t.Errorf("Unexpected redirect URL: %s", res.RedirectURL)
69 | }
70 | if !bytes.Equal(res.Data, okResponseData) {
71 | t.Errorf("Data doesn't match. Want %v, got %v", okResponseData, res.Data)
72 | }
73 | })
74 |
75 | t.Run("NotFound", func(t *testing.T) {
76 | req := Request{
77 | fmt.Sprintf("https://%s/404", serverAddr), "GET", certFingerprint}
78 | res, err := Fetch(req)
79 | if err != nil {
80 | t.Fatalf("Unexpected error: %v", err)
81 | }
82 | if res.HTTPStatusCode != 404 {
83 | t.Errorf("Expected 404 HTTP status code, got %d", res.HTTPStatusCode)
84 | }
85 | if res.RedirectURL != "" {
86 | t.Errorf("Unexpected redirect URL: %s", res.RedirectURL)
87 | }
88 | if !bytes.Equal(res.Data, notFoundResponseData) {
89 | t.Errorf("Data doesn't match. Want %v, got %v", okResponseData, res.Data)
90 | }
91 | })
92 |
93 | t.Run("Redirect", func(t *testing.T) {
94 | req := Request{
95 | fmt.Sprintf("https://%s/301", serverAddr), "GET", certFingerprint}
96 | res, err := Fetch(req)
97 | if err != nil {
98 | t.Fatalf("Unexpected error: %v", err)
99 | }
100 | if res.HTTPStatusCode != 301 {
101 | t.Errorf("Expected 301 HTTP status code, got %d", res.HTTPStatusCode)
102 | }
103 | if res.RedirectURL != redirectURL {
104 | t.Errorf("Expected redirect URL %s, got %s", redirectURL, res.RedirectURL)
105 | }
106 | })
107 |
108 | t.Run("WrongCertificateFingerprint", func(t *testing.T) {
109 | wrongCertFp := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
110 | req := Request{
111 | fmt.Sprintf("https://%s/200", serverAddr), "GET", wrongCertFp}
112 | _, err := Fetch(req)
113 | if err == nil {
114 | t.Fatalf("Expected TLS certificate validation error")
115 | }
116 | var certErr x509.CertificateInvalidError
117 | if !errors.As(err, &certErr) {
118 | t.Errorf("Expected invalid certificate error, got %v",
119 | reflect.TypeOf(err))
120 | }
121 | })
122 |
123 | t.Run("MissingCertificateFingerprint", func(t *testing.T) {
124 | req := Request{
125 | fmt.Sprintf("https://%s/200", serverAddr), "GET", nil}
126 | _, err := Fetch(req)
127 | if err == nil {
128 | t.Fatalf("Expected certificate validation error")
129 | }
130 | var authErr x509.UnknownAuthorityError
131 | if !errors.As(err, &authErr) {
132 | t.Errorf("Expected unknown authority error, got %v",
133 | reflect.TypeOf(err))
134 | }
135 | })
136 |
137 | t.Run("Method", func(t *testing.T) {
138 | req := Request{
139 | fmt.Sprintf("https://%s/200-post", serverAddr), "POST", certFingerprint}
140 | res, err := Fetch(req)
141 | if err != nil {
142 | t.Fatalf("Unexpected error: %v", err)
143 | }
144 | if res.HTTPStatusCode != 200 {
145 | t.Errorf("Expected 200 HTTP status code, got %d", res.HTTPStatusCode)
146 | }
147 | if !bytes.Equal(res.Data, okResponseData) {
148 | t.Errorf("Data doesn't match. Want %v, got %v", okResponseData, res.Data)
149 | }
150 | })
151 |
152 | t.Run("NonHTTPSURL", func(t *testing.T) {
153 | req := Request{
154 | fmt.Sprintf("http://%s/200", serverAddr), "GET", certFingerprint}
155 | _, err := Fetch(req)
156 | if err == nil {
157 | t.Fatalf("Expected error for non-HTTPs URL")
158 | }
159 | })
160 | }
161 |
162 | // HTTP handler for a fake server.
163 | type httpHandler struct{}
164 |
165 | func (h httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
166 | if req.URL.Path == "/200" {
167 | h.sendResponse(w, 200, okResponseData)
168 | } else if req.URL.Path == "/200-post" && req.Method == "POST" {
169 | h.sendResponse(w, 200, okResponseData)
170 | } else if req.URL.Path == "/301" {
171 | w.Header().Add("Location", redirectURL)
172 | h.sendResponse(w, 301, []byte{})
173 | } else {
174 | h.sendResponse(w, 404, notFoundResponseData)
175 | }
176 | }
177 |
178 | func (httpHandler) sendResponse(w http.ResponseWriter, code int, data []byte) {
179 | w.Header().Add("Content-Type", "application/json")
180 | w.WriteHeader(code)
181 | w.Write(data)
182 | }
183 |
184 | // Returns a fake HTTPS server with a TLS certificate cert.
185 | func makeHTTPSServer(cert tls.Certificate) http.Server {
186 | tlsConfig := &tls.Config{
187 | Certificates: []tls.Certificate{cert},
188 | }
189 | return http.Server{
190 | TLSConfig: tlsConfig,
191 | Handler: httpHandler{},
192 | }
193 | }
194 |
195 | // Generates a self-signed TLS certificate for localhost.
196 | func makeTLSCertificate() (tls.Certificate, error) {
197 | now := time.Now()
198 | template := &x509.Certificate{
199 | SerialNumber: big.NewInt(now.Unix()),
200 | Subject: pkix.Name{
201 | Organization: []string{"fake"},
202 | },
203 | IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, // Valid for localhost
204 | NotBefore: now,
205 | NotAfter: now.AddDate(0, 0, 1), // Valid for one day
206 | BasicConstraintsValid: true,
207 | IsCA: true, // Self-signed
208 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
209 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature |
210 | x509.KeyUsageCertSign,
211 | }
212 |
213 | key, err := rsa.GenerateKey(rand.Reader, 4096)
214 | if err != nil {
215 | return tls.Certificate{}, err
216 | }
217 |
218 | derCert, err := x509.CreateCertificate(rand.Reader, template, template,
219 | key.Public(), key)
220 | if err != nil {
221 | return tls.Certificate{}, err
222 | }
223 |
224 | var cert tls.Certificate
225 | cert.Certificate = append(cert.Certificate, derCert)
226 | cert.PrivateKey = key
227 | return cert, nil
228 | }
229 |
--------------------------------------------------------------------------------
/intra/android/init.go:
--------------------------------------------------------------------------------
1 | package tun2socks
2 |
3 | // Copyright 2019 The Outline Authors
4 | //
5 | // Licensed under the Apache License, Version 2.0 (the "License");
6 | // you may not use this file except in compliance with the License.
7 | // You may obtain a copy of the License at
8 | //
9 | // http://www.apache.org/licenses/LICENSE-2.0
10 | //
11 | // Unless required by applicable law or agreed to in writing, software
12 | // distributed under the License is distributed on an "AS IS" BASIS,
13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | // See the License for the specific language governing permissions and
15 | // limitations under the License.
16 |
17 | import (
18 | "runtime/debug"
19 |
20 | "github.com/eycorsican/go-tun2socks/common/log"
21 | )
22 |
23 | func init() {
24 | // Conserve memory by increasing garbage collection frequency.
25 | debug.SetGCPercent(10)
26 | log.SetLevel(log.WARN)
27 | }
28 |
--------------------------------------------------------------------------------
/intra/android/tun.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Jigsaw Operations LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "errors"
19 | "os"
20 |
21 | "golang.org/x/sys/unix"
22 | )
23 |
24 | func makeTunFile(fd int) (*os.File, error) {
25 | if fd < 0 {
26 | return nil, errors.New("must provide a valid TUN file descriptor")
27 | }
28 | // Make a copy of `fd` so that os.File's finalizer doesn't close `fd`.
29 | newfd, err := unix.Dup(fd)
30 | if err != nil {
31 | return nil, err
32 | }
33 | file := os.NewFile(uintptr(newfd), "")
34 | if file == nil {
35 | return nil, errors.New("failed to open TUN file descriptor")
36 | }
37 | return file, nil
38 | }
39 |
--------------------------------------------------------------------------------
/intra/android/tun2socks.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "errors"
19 | "io"
20 | "io/fs"
21 | "log"
22 | "os"
23 | "strings"
24 |
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra"
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
27 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"
28 | "github.com/Jigsaw-Code/outline-sdk/network"
29 | )
30 |
31 | // ConnectIntraTunnel reads packets from a TUN device and applies the Intra routing
32 | // rules. Currently, this only consists of redirecting DNS packets to a specified
33 | // server; all other data flows directly to its destination.
34 | //
35 | // `fd` is the TUN device. The IntraTunnel acquires an additional reference to it, which
36 | //
37 | // is released by IntraTunnel.Disconnect(), so the caller must close `fd` _and_ call
38 | // Disconnect() in order to close the TUN device.
39 | //
40 | // `fakedns` is the DNS server that the system believes it is using, in "host:port" style.
41 | //
42 | // The port is normally 53.
43 | //
44 | // `udpdns` and `tcpdns` are the location of the actual DNS server being used. For DNS
45 | //
46 | // tunneling in Intra, these are typically high-numbered ports on localhost.
47 | //
48 | // `dohdns` is the initial DoH transport. It must not be `nil`.
49 | // `protector` is a wrapper for Android's VpnService.protect() method.
50 | // `eventListener` will be provided with a summary of each TCP and UDP socket when it is closed.
51 | //
52 | // Throws an exception if the TUN file descriptor cannot be opened, or if the tunnel fails to
53 | // connect.
54 | func ConnectIntraTunnel(
55 | fd int, fakedns string, dohdns doh.Transport, protector protect.Protector, eventListener intra.Listener,
56 | ) (*intra.Tunnel, error) {
57 | tun, err := makeTunFile(fd)
58 | if err != nil {
59 | return nil, err
60 | }
61 | t, err := intra.NewTunnel(fakedns, dohdns, tun, protector, eventListener)
62 | if err != nil {
63 | return nil, err
64 | }
65 | go copyUntilEOF(t, tun)
66 | go copyUntilEOF(tun, t)
67 | return t, nil
68 | }
69 |
70 | // NewDoHTransport returns a DNSTransport that connects to the specified DoH server.
71 | // `url` is the URL of a DoH server (no template, POST-only). If it is nonempty, it
72 | //
73 | // overrides `udpdns` and `tcpdns`.
74 | //
75 | // `ips` is an optional comma-separated list of IP addresses for the server. (This
76 | //
77 | // wrapper is required because gomobile can't make bindings for []string.)
78 | //
79 | // `protector` is the socket protector to use for all external network activity.
80 | // `auth` will provide a client certificate if required by the TLS server.
81 | // `eventListener` will be notified after each DNS query succeeds or fails.
82 | func NewDoHTransport(
83 | url string, ips string, protector protect.Protector, auth doh.ClientAuth, eventListener intra.Listener,
84 | ) (doh.Transport, error) {
85 | split := []string{}
86 | if len(ips) > 0 {
87 | split = strings.Split(ips, ",")
88 | }
89 | dialer := protect.MakeDialer(protector)
90 | return doh.NewTransport(url, split, dialer, auth, eventListener)
91 | }
92 |
93 | func copyUntilEOF(dst, src io.ReadWriteCloser) {
94 | log.Printf("[debug] start relaying traffic [%s] -> [%s]", src, dst)
95 | defer log.Printf("[debug] stop relaying traffic [%s] -> [%s]", src, dst)
96 |
97 | const commonMTU = 1500
98 | buf := make([]byte, commonMTU)
99 | defer dst.Close()
100 | for {
101 | _, err := io.CopyBuffer(dst, src, buf)
102 | if err == nil || isErrClosed(err) {
103 | return
104 | }
105 | }
106 | }
107 |
108 | func isErrClosed(err error) bool {
109 | return errors.Is(err, os.ErrClosed) || errors.Is(err, fs.ErrClosed) || errors.Is(err, network.ErrClosed)
110 | }
111 |
--------------------------------------------------------------------------------
/intra/doh/atomic.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package doh
16 |
17 | import (
18 | "sync/atomic"
19 | )
20 |
21 | // Atomic is atomic.Value, specialized for doh.Transport.
22 | type Atomic struct {
23 | v atomic.Value
24 | }
25 |
26 | // Store a DNSTransport. d must not be nil.
27 | func (a *Atomic) Store(t Transport) {
28 | a.v.Store(t)
29 | }
30 |
31 | // Load the DNSTransport, or nil if it has not been stored.
32 | func (a *Atomic) Load() Transport {
33 | v := a.v.Load()
34 | if v == nil {
35 | return nil
36 | }
37 | return v.(Transport)
38 | }
39 |
--------------------------------------------------------------------------------
/intra/doh/client_auth.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package doh
16 |
17 | import (
18 | "crypto"
19 | "crypto/ecdsa"
20 | "crypto/tls"
21 | "crypto/x509"
22 | "errors"
23 | "io"
24 |
25 | "github.com/eycorsican/go-tun2socks/common/log"
26 | )
27 |
28 | // ClientAuth interface for providing TLS certificates and signatures.
29 | type ClientAuth interface {
30 | // GetClientCertificate returns the client certificate (if any).
31 | // May block as the first call may cause certificates to load.
32 | // Returns a DER encoded X.509 client certificate.
33 | GetClientCertificate() []byte
34 | // GetIntermediateCertificate returns the chaining certificate (if any).
35 | // It does not block or cause certificates to load.
36 | // Returns a DER encoded X.509 certificate.
37 | GetIntermediateCertificate() []byte
38 | // Request a signature on a digest.
39 | Sign(digest []byte) []byte
40 | }
41 |
42 | // clientAuthWrapper manages certificate loading and usage during TLS handshakes.
43 | // Implements crypto.Signer.
44 | type clientAuthWrapper struct {
45 | signer ClientAuth
46 | }
47 |
48 | // GetClientCertificate returns the client certificate chain as a tls.Certificate.
49 | // Returns an empty Certificate on failure, permitting the handshake to
50 | // continue without authentication.
51 | // Implements tls.Config GetClientCertificate().
52 | func (ca *clientAuthWrapper) GetClientCertificate(
53 | info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
54 | if ca.signer == nil {
55 | log.Warnf("Client certificate requested but not supported")
56 | return &tls.Certificate{}, nil
57 | }
58 | cert := ca.signer.GetClientCertificate()
59 | if cert == nil {
60 | log.Warnf("Unable to fetch client certificate")
61 | return &tls.Certificate{}, nil
62 | }
63 | chain := [][]byte{cert}
64 | intermediate := ca.signer.GetIntermediateCertificate()
65 | if intermediate != nil {
66 | chain = append(chain, intermediate)
67 | }
68 | leaf, err := x509.ParseCertificate(cert)
69 | if err != nil {
70 | log.Warnf("Unable to parse client certificate: %v", err)
71 | return &tls.Certificate{}, nil
72 | }
73 | _, isECDSA := leaf.PublicKey.(*ecdsa.PublicKey)
74 | if !isECDSA {
75 | // RSA-PSS and RSA-SSA both need explicit signature generation support.
76 | log.Warnf("Only ECDSA client certificates are supported")
77 | return &tls.Certificate{}, nil
78 | }
79 | return &tls.Certificate{
80 | Certificate: chain,
81 | PrivateKey: ca,
82 | Leaf: leaf,
83 | }, nil
84 | }
85 |
86 | // Public returns the public key for the client certificate.
87 | func (ca *clientAuthWrapper) Public() crypto.PublicKey {
88 | if ca.signer == nil {
89 | return nil
90 | }
91 | cert := ca.signer.GetClientCertificate()
92 | leaf, err := x509.ParseCertificate(cert)
93 | if err != nil {
94 | log.Warnf("Unable to parse client certificate: %v", err)
95 | return nil
96 | }
97 | return leaf.PublicKey
98 | }
99 |
100 | // Sign a digest.
101 | func (ca *clientAuthWrapper) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
102 | if ca.signer == nil {
103 | return nil, errors.New("no client certificate")
104 | }
105 | signature := ca.signer.Sign(digest)
106 | if signature == nil {
107 | return nil, errors.New("failed to create signature")
108 | }
109 | return signature, nil
110 | }
111 |
112 | func newClientAuthWrapper(signer ClientAuth) clientAuthWrapper {
113 | return clientAuthWrapper{
114 | signer: signer,
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/intra/doh/client_auth_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package doh
16 |
17 | import (
18 | "bytes"
19 | "crypto"
20 | "crypto/ecdsa"
21 | "crypto/rand"
22 | "crypto/sha256"
23 | "crypto/tls"
24 | "crypto/x509"
25 | "encoding/pem"
26 | "fmt"
27 | "testing"
28 | )
29 |
30 | // PEM encoded test leaf certificate with ECDSA public key.
31 | var ecCertificate string = `-----BEGIN CERTIFICATE-----
32 | MIIBpTCCAQ4CAiAAMA0GCSqGSIb3DQEBCwUAMD4xCzAJBgNVBAYTAlVTMQswCQYD
33 | VQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEKMAgGA1UECgwBWDAeFw0y
34 | MDExMDQwNTU2MTZaFw0zMDExMDIwNTU2MTZaMD4xCzAJBgNVBAYTAlVTMQswCQYD
35 | VQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEKMAgGA1UECgwBWDBZMBMG
36 | ByqGSM49AgEGCCqGSM49AwEHA0IABNFVWlOs0tnaLgiutLbPISCd5Fn9UJz6oDen
37 | prTOrHz11PiO/XiqwpJY8yO72QappL/7RYV+uw9hJfU+YOE3tZQwDQYJKoZIhvcN
38 | AQELBQADgYEAdy6CNPvIA7DrS6WrN7N4ZjHjeUtjj2w8n5abTHhvANEvIHI0DARI
39 | AoJJWp4Pe41mzFhROzo+U/ofC2b+ukA8sYqoio4QUxlSW3HkzUAR4HZMi8Risvo3
40 | OxSR9Lw/mGvZrJ8xr070EwnsD+cCZLfYQ0mSKDM9uPfI3YrgCVKyUwE=
41 | -----END CERTIFICATE-----`
42 |
43 | // PKCS8 encoded test ECDSA private key.
44 | var ecKey string = `-----BEGIN PRIVATE KEY-----
45 | MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIlI6NB+skAYL36XP
46 | JvE+x5Nlbn0wvw2hlSqIqADiZhShRANCAATRVVpTrNLZ2i4IrrS2zyEgneRZ/VCc
47 | +qA3p6a0zqx89dT4jv14qsKSWPMju9kGqaS/+0WFfrsPYSX1PmDhN7WU
48 | -----END PRIVATE KEY-----`
49 |
50 | // PEM encoded test leaf certificate with RSA public key.
51 | // Doubles as an intermediate depending on the test.
52 | var rsaCertificate string = `-----BEGIN CERTIFICATE-----
53 | MIICWDCCAcGgAwIBAgIUS36guwZMKNO0ADReGLi0cZq8fOowDQYJKoZIhvcNAQEL
54 | BQAwPjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1Nb3VudGFp
55 | biBWaWV3MQowCAYDVQQKDAFYMB4XDTIwMTEwNDA1NDgyNVoXDTMwMTEwMjA1NDgy
56 | NVowPjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1Nb3VudGFp
57 | biBWaWV3MQowCAYDVQQKDAFYMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDd
58 | eznqVu1Rn0m8KR4mX/qVv6uytzZ+juqW5VD55D+w9N6JryPpFHPi4VIm8PKLXp3X
59 | GvY9mc8r+0Ow1qJZYoc/X0Na1c79bv9xwbD3aK28FlAs1+cmyesaFhCWa0bYAvcy
60 | mqQGYhObEWb46E5AANV82CitDE9C1aXRT4SvkLnc6wIDAQABo1MwUTAdBgNVHQ4E
61 | FgQUnUib8BhOHqjq9+gqPQ+ePyEW9zwwHwYDVR0jBBgwFoAUnUib8BhOHqjq9+gq
62 | PQ+ePyEW9zwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAx/uZG
63 | Gmb5w/u4UkdH7wnoOUNx6GwdraqtQWnFaXb87PmuVAjBwSAnzes2mlp/Vbcd6tYs
64 | pPuHrxOcWgw/aRV6rK3vJZIH3DGvy1pNphGgegEcG88nrUCDcQqPLxvPJ8bmbaee
65 | Tf+l5U2OHC3Yifb4FDOv47kGmq5VeWiYdp60/A==
66 | -----END CERTIFICATE-----`
67 |
68 | // PKCS8 encoded test RSA private key.
69 | var rsaKey string = `-----BEGIN PRIVATE KEY-----
70 | MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAN17OepW7VGfSbwp
71 | HiZf+pW/q7K3Nn6O6pblUPnkP7D03omvI+kUc+LhUibw8otendca9j2Zzyv7Q7DW
72 | ollihz9fQ1rVzv1u/3HBsPdorbwWUCzX5ybJ6xoWEJZrRtgC9zKapAZiE5sRZvjo
73 | TkAA1XzYKK0MT0LVpdFPhK+QudzrAgMBAAECgYEAoCdhI8Ej7qe+S993u8wfiXWG
74 | FL9DGpUBsYe03F5eZ/lJikopL3voqKDCJQKKgJk0jb0jXjwAgQ86TX+G+hezL5jp
75 | xOOfMmTYgMwnUuFYN1gHAd+TnYB9G1qSQr9TOw3K9Rf4q2x09GhLP75qdr+qzmIR
76 | YGle5ZSP0LqKNkpGNUECQQD+6CxOO8+knnzIFvqkUyNDVFR5ALRNpb53TGVITNf3
77 | ysT32oJ75ButA0l4q/jsL+MeLLvrHkJOHN+ydLaZOUkbAkEA3m5cICisW9lsT+Rj
78 | glXykkbj3Ougldy7rhPivAaS7clk8cl8cDcIvHna1mDlhSanUu/s4TFEXBLnSzee
79 | XLNIcQJBAJ0n3TD6lSEkCUB/UlX/X81B77aOZZs9pXj9o6/4mGoQHHHGyQ3C7AE1
80 | 9pUsSZKsT3UqFU124WAxUwU+CdnbxKMCQB/QrUC0UKL6oHF0+37DCGU/2ovY8Ck/
81 | X2Dw2zeFwTJd4iBrb28lkAxVaaXMSkgXVUuZoco8H8kDsy2hEPe1dSECQQCPw5Yg
82 | 2gdmdpUk+QetqqhSuuIDwILHU9m3CoX3rY+njaR5LOWDz3utC9Ogo+4wdIMamP/o
83 | 2SAWPAZPqDUbtqGH
84 | -----END PRIVATE KEY-----`
85 |
86 | // fakeClientAuth implements the ClientAuth interface for testing.
87 | type fakeClientAuth struct {
88 | certificate *x509.Certificate
89 | intermediate *x509.Certificate
90 | key crypto.PrivateKey
91 | }
92 |
93 | func (ca *fakeClientAuth) GetClientCertificate() []byte {
94 | if ca.certificate == nil {
95 | // Interface uses nil for errors to support binding.
96 | return nil
97 | }
98 | return ca.certificate.Raw
99 | }
100 |
101 | func (ca *fakeClientAuth) GetIntermediateCertificate() []byte {
102 | if ca.intermediate == nil {
103 | return nil
104 | }
105 | return ca.intermediate.Raw
106 | }
107 |
108 | func (ca *fakeClientAuth) Sign(digest []byte) []byte {
109 | if ca.key == nil {
110 | return nil
111 | }
112 | if k, isECDSA := ca.key.(*ecdsa.PrivateKey); isECDSA {
113 | signature, err := ecdsa.SignASN1(rand.Reader, k, digest)
114 | if err != nil {
115 | return nil
116 | }
117 | return signature
118 | }
119 | // Unsupported key type
120 | return nil
121 | }
122 |
123 | func newFakeClientAuth(certificate, intermediate, key []byte) (*fakeClientAuth, error) {
124 | ca := &fakeClientAuth{}
125 | if certificate != nil {
126 | certX509, err := x509.ParseCertificate(certificate)
127 | if err != nil {
128 | return nil, fmt.Errorf("certificate: %v", err)
129 | }
130 | ca.certificate = certX509
131 | }
132 | if intermediate != nil {
133 | intX509, err := x509.ParseCertificate(intermediate)
134 | if err != nil {
135 | return nil, fmt.Errorf("intermediate: %v", err)
136 | }
137 | ca.intermediate = intX509
138 | }
139 | if key != nil {
140 | key, err := x509.ParsePKCS8PrivateKey(key)
141 | if err != nil {
142 | return nil, fmt.Errorf("private key: %v", err)
143 | }
144 | ca.key = key
145 | }
146 | return ca, nil
147 | }
148 |
149 | func newCertificateRequestInfo() *tls.CertificateRequestInfo {
150 | return &tls.CertificateRequestInfo{
151 | Version: tls.VersionTLS13,
152 | }
153 | }
154 |
155 | func newToBeSigned(message []byte) ([]byte, crypto.SignerOpts) {
156 | digest := sha256.Sum256(message)
157 | opts := crypto.SignerOpts(crypto.SHA256)
158 | return digest[:], opts
159 | }
160 |
161 | // Simulate a TLS handshake that requires a client cert and signature.
162 | func TestSign(t *testing.T) {
163 | certDer, _ := pem.Decode([]byte(ecCertificate))
164 | keyDer, _ := pem.Decode([]byte(ecKey))
165 | intDer, _ := pem.Decode([]byte(rsaCertificate))
166 | ca, err := newFakeClientAuth(certDer.Bytes, intDer.Bytes, keyDer.Bytes)
167 | if err != nil {
168 | t.Fatal(err)
169 | }
170 | wrapper := newClientAuthWrapper(ca)
171 | // TLS stack requests the client cert.
172 | req := newCertificateRequestInfo()
173 | cert, err := wrapper.GetClientCertificate(req)
174 | if err != nil {
175 | t.Fatal("Expected to get a client certificate")
176 | }
177 | if cert == nil {
178 | // From the crypto.tls docs:
179 | // If GetClientCertificate returns an error, the handshake will
180 | // be aborted and that error will be returned. Otherwise
181 | // GetClientCertificate must return a non-nil Certificate.
182 | t.Error("GetClientCertificate must return a non-nil certificate")
183 | }
184 | if len(cert.Certificate) != 2 {
185 | t.Fatal("Certificate chain is the wrong length")
186 | }
187 | if !bytes.Equal(cert.Certificate[0], certDer.Bytes) {
188 | t.Error("Problem with certificate chain[0]")
189 | }
190 | if !bytes.Equal(cert.Certificate[1], intDer.Bytes) {
191 | t.Error("Problem with certificate chain[1]")
192 | }
193 | // TLS stack requests a signature.
194 | digest, opts := newToBeSigned([]byte("hello world"))
195 | signature, err := wrapper.Sign(rand.Reader, digest, opts)
196 | if err != nil {
197 | t.Fatal(err)
198 | }
199 | // Verify the signature.
200 | pub, ok := wrapper.Public().(*ecdsa.PublicKey)
201 | if !ok {
202 | t.Fatal("Expected public key to be ECDSA")
203 | }
204 | if !ecdsa.VerifyASN1(pub, digest, signature) {
205 | t.Fatal("Problem verifying signature")
206 | }
207 | }
208 |
209 | // Simulate a client that does not use an intermediate certificate.
210 | func TestSignNoIntermediate(t *testing.T) {
211 | certDer, _ := pem.Decode([]byte(ecCertificate))
212 | keyDer, _ := pem.Decode([]byte(ecKey))
213 | ca, err := newFakeClientAuth(certDer.Bytes, nil, keyDer.Bytes)
214 | if err != nil {
215 | t.Fatal(err)
216 | }
217 | wrapper := newClientAuthWrapper(ca)
218 | // TLS stack requests a client cert.
219 | req := newCertificateRequestInfo()
220 | cert, err := wrapper.GetClientCertificate(req)
221 | if err != nil {
222 | t.Error("Expected to get a client certificate")
223 | }
224 | if cert == nil {
225 | t.Error("GetClientCertificate must return a non-nil certificate")
226 | }
227 | if len(cert.Certificate) != 1 {
228 | t.Error("Certificate chain is the wrong length")
229 | }
230 | if !bytes.Equal(cert.Certificate[0], certDer.Bytes) {
231 | t.Error("Problem with certificate chain[0]")
232 | }
233 | // TLS stack requests a signature
234 | digest, opts := newToBeSigned([]byte("hello world"))
235 | signature, err := wrapper.Sign(rand.Reader, digest, opts)
236 | if err != nil {
237 | t.Error(err)
238 | }
239 | // Verify the signature.
240 | pub, ok := wrapper.Public().(*ecdsa.PublicKey)
241 | if !ok {
242 | t.Error("Expected public key to be ECDSA")
243 | }
244 | if !ecdsa.VerifyASN1(pub, digest, signature) {
245 | t.Error("Problem verifying signature")
246 | }
247 | }
248 |
249 | // Simulate a client that does not have a certificate.
250 | func TestNoAuth(t *testing.T) {
251 | ca, err := newFakeClientAuth(nil, nil, nil)
252 | if err != nil {
253 | t.Fatal(err)
254 | }
255 | wrapper := newClientAuthWrapper(ca)
256 | // TLS stack requests a client cert.
257 | req := newCertificateRequestInfo()
258 | cert, err := wrapper.GetClientCertificate(req)
259 | if err != nil {
260 | t.Error("Expected to get a client certificate")
261 | }
262 | if cert == nil {
263 | t.Error("GetClientCertificate must return a non-nil certificate")
264 | }
265 | if len(cert.Certificate) != 0 {
266 | t.Error("Certificate chain is the wrong length")
267 | }
268 | // TLS stack requests a signature. This should not happen in real life
269 | // because cert.Certificate is empty.
270 | public := wrapper.Public()
271 | if public != nil {
272 | t.Error("Expected public to be nil")
273 | }
274 | digest, opts := newToBeSigned([]byte("hello world"))
275 | _, err = wrapper.Sign(rand.Reader, digest, opts)
276 | if err == nil {
277 | t.Error("Expected Sign() to fail")
278 | }
279 | }
280 |
281 | // Simulate a client that has an RSA certificate.
282 | func TestRSACertificate(t *testing.T) {
283 | certDer, _ := pem.Decode([]byte(rsaCertificate))
284 | keyDer, _ := pem.Decode([]byte(rsaKey))
285 | ca, err := newFakeClientAuth(certDer.Bytes, nil, keyDer.Bytes)
286 | if err != nil {
287 | t.Fatal(err)
288 | }
289 | wrapper := newClientAuthWrapper(ca)
290 | // TLS stack requests a client cert. We should not return one because
291 | // we don't support RSA.
292 | req := newCertificateRequestInfo()
293 | cert, err := wrapper.GetClientCertificate(req)
294 | if err != nil {
295 | t.Error("Expected to get a client certificate")
296 | }
297 | if cert == nil {
298 | t.Error("GetClientCertificate must return a non-nil certificate")
299 | }
300 | if len(cert.Certificate) != 0 {
301 | t.Error("Unexpectedly loaded an RSA certificate")
302 | }
303 | // TLS stack requests a signature. This should not happen in real life
304 | // because cert.Certificate is empty.
305 | digest, opts := newToBeSigned([]byte("hello world"))
306 | _, err = wrapper.Sign(rand.Reader, digest, opts)
307 | if err == nil {
308 | t.Error("Expected Sign() to fail")
309 | }
310 | }
311 |
312 | // Simulate a nil loader.
313 | func TestNilLoader(t *testing.T) {
314 | wrapper := newClientAuthWrapper(nil)
315 | // TLS stack requests the client cert.
316 | req := newCertificateRequestInfo()
317 | cert, err := wrapper.GetClientCertificate(req)
318 | if err != nil {
319 | t.Fatal(err)
320 | }
321 | if cert == nil {
322 | // From the crypto.tls docs:
323 | // If GetClientCertificate returns an error, the handshake will
324 | // be aborted and that error will be returned. Otherwise
325 | // GetClientCertificate must return a non-nil Certificate.
326 | t.Error("GetClientCertificate must return a non-nil certificate")
327 | }
328 | if len(cert.Certificate) != 0 {
329 | t.Fatal("Expected an empty certificate chain")
330 | }
331 | // TLS stack requests a signature. This should not happen in real life
332 | // because cert.Certificate is empty.
333 | digest, opts := newToBeSigned([]byte("hello world"))
334 | _, err = wrapper.Sign(rand.Reader, digest, opts)
335 | if err == nil {
336 | t.Error("Expected Sign() to fail")
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/intra/doh/doh.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package doh
16 |
17 | import (
18 | "bytes"
19 | "crypto/tls"
20 | "encoding/binary"
21 | "errors"
22 | "fmt"
23 | "io"
24 | "io/ioutil"
25 | "math"
26 | "net"
27 | "net/http"
28 | "net/http/httptrace"
29 | "net/textproto"
30 | "net/url"
31 | "strconv"
32 | "sync"
33 | "time"
34 |
35 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh/ipmap"
36 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split"
37 | "github.com/eycorsican/go-tun2socks/common/log"
38 | "golang.org/x/net/dns/dnsmessage"
39 | )
40 |
41 | const (
42 | // Complete : Transaction completed successfully
43 | Complete = iota
44 | // SendFailed : Failed to send query
45 | SendFailed
46 | // HTTPError : Got a non-200 HTTP status
47 | HTTPError
48 | // BadQuery : Malformed input
49 | BadQuery
50 | // BadResponse : Response was invalid
51 | BadResponse
52 | // InternalError : This should never happen
53 | InternalError
54 | )
55 |
56 | // If the server sends an invalid reply, we start a "servfail hangover"
57 | // of this duration, during which all queries are rejected.
58 | // This rate-limits queries to misconfigured servers (e.g. wrong URL).
59 | const hangoverDuration = 10 * time.Second
60 |
61 | // Summary is a summary of a DNS transaction, reported when it is complete.
62 | type Summary struct {
63 | Latency float64 // Response (or failure) latency in seconds
64 | Query []byte
65 | Response []byte
66 | Server string
67 | Status int
68 | HTTPStatus int // Zero unless Status is Complete or HTTPError
69 | }
70 |
71 | // A Token is an opaque handle used to match responses to queries.
72 | type Token interface{}
73 |
74 | // Listener receives Summaries.
75 | type Listener interface {
76 | OnQuery(url string) Token
77 | OnResponse(Token, *Summary)
78 | }
79 |
80 | // Transport represents a DNS query transport. This interface is exported by gobind,
81 | // so it has to be very simple.
82 | type Transport interface {
83 | // Given a DNS query (including ID), returns a DNS response with matching
84 | // ID, or an error if no response was received. The error may be accompanied
85 | // by a SERVFAIL response if appropriate.
86 | Query(q []byte) ([]byte, error)
87 | // Return the server URL used to initialize this transport.
88 | GetURL() string
89 | }
90 |
91 | // TODO: Keep a context here so that queries can be canceled.
92 | type transport struct {
93 | Transport
94 | url string
95 | hostname string
96 | port int
97 | ips ipmap.IPMap
98 | client http.Client
99 | dialer *net.Dialer
100 | listener Listener
101 | hangoverLock sync.RWMutex
102 | hangoverExpiration time.Time
103 | }
104 |
105 | // Wait up to three seconds for the TCP handshake to complete.
106 | const tcpTimeout time.Duration = 3 * time.Second
107 |
108 | func (t *transport) dial(network, addr string) (net.Conn, error) {
109 | log.Debugf("Dialing %s", addr)
110 | domain, portStr, err := net.SplitHostPort(addr)
111 | if err != nil {
112 | return nil, err
113 | }
114 | port, err := strconv.Atoi(portStr)
115 | if err != nil {
116 | return nil, err
117 | }
118 |
119 | tcpaddr := func(ip net.IP) *net.TCPAddr {
120 | return &net.TCPAddr{IP: ip, Port: port}
121 | }
122 |
123 | // TODO: Improve IP fallback strategy with parallelism and Happy Eyeballs.
124 | var conn net.Conn
125 | ips := t.ips.Get(domain)
126 | confirmed := ips.Confirmed()
127 | if confirmed != nil {
128 | log.Debugf("Trying confirmed IP %s for addr %s", confirmed.String(), addr)
129 | if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(confirmed), nil); err == nil {
130 | log.Infof("Confirmed IP %s worked", confirmed.String())
131 | return conn, nil
132 | }
133 | log.Debugf("Confirmed IP %s failed with err %v", confirmed.String(), err)
134 | ips.Disconfirm(confirmed)
135 | }
136 |
137 | log.Debugf("Trying all IPs")
138 | for _, ip := range ips.GetAll() {
139 | if ip.Equal(confirmed) {
140 | // Don't try this IP twice.
141 | continue
142 | }
143 | if conn, err = split.DialWithSplitRetry(t.dialer, tcpaddr(ip), nil); err == nil {
144 | log.Infof("Found working IP: %s", ip.String())
145 | return conn, nil
146 | }
147 | }
148 | return nil, err
149 | }
150 |
151 | // NewTransport returns a DoH DNSTransport, ready for use.
152 | // This is a POST-only DoH implementation, so the DoH template should be a URL.
153 | // `rawurl` is the DoH template in string form.
154 | // `addrs` is a list of domains or IP addresses to use as fallback, if the hostname
155 | // lookup fails or returns non-working addresses.
156 | // `dialer` is the dialer that the transport will use. The transport will modify the dialer's
157 | // timeout but will not mutate it otherwise.
158 | // `auth` will provide a client certificate if required by the TLS server.
159 | // `listener` will receive the status of each DNS query when it is complete.
160 | func NewTransport(rawurl string, addrs []string, dialer *net.Dialer, auth ClientAuth, listener Listener) (Transport, error) {
161 | if dialer == nil {
162 | dialer = &net.Dialer{}
163 | }
164 | parsedurl, err := url.Parse(rawurl)
165 | if err != nil {
166 | return nil, err
167 | }
168 | if parsedurl.Scheme != "https" {
169 | return nil, fmt.Errorf("Bad scheme: %s", parsedurl.Scheme)
170 | }
171 | // Resolve the hostname and put those addresses first.
172 | portStr := parsedurl.Port()
173 | var port int
174 | if len(portStr) > 0 {
175 | port, err = strconv.Atoi(portStr)
176 | if err != nil {
177 | return nil, err
178 | }
179 | } else {
180 | port = 443
181 | }
182 |
183 | t := &transport{
184 | url: rawurl,
185 | hostname: parsedurl.Hostname(),
186 | port: port,
187 | listener: listener,
188 | dialer: dialer,
189 | ips: ipmap.NewIPMap(dialer.Resolver),
190 | }
191 | ips := t.ips.Get(t.hostname)
192 | for _, addr := range addrs {
193 | ips.Add(addr)
194 | }
195 | if ips.Empty() {
196 | return nil, fmt.Errorf("No IP addresses for %s", t.hostname)
197 | }
198 |
199 | // Supply a client certificate during TLS handshakes.
200 | var tlsconfig *tls.Config
201 | if auth != nil {
202 | signer := newClientAuthWrapper(auth)
203 | tlsconfig = &tls.Config{
204 | GetClientCertificate: signer.GetClientCertificate,
205 | }
206 | }
207 |
208 | // Override the dial function.
209 | t.client.Transport = &http.Transport{
210 | Dial: t.dial,
211 | ForceAttemptHTTP2: true,
212 | TLSHandshakeTimeout: 10 * time.Second,
213 | ResponseHeaderTimeout: 20 * time.Second, // Same value as Android DNS-over-TLS
214 | TLSClientConfig: tlsconfig,
215 | }
216 | return t, nil
217 | }
218 |
219 | type queryError struct {
220 | status int
221 | err error
222 | }
223 |
224 | func (e *queryError) Error() string {
225 | return e.err.Error()
226 | }
227 |
228 | func (e *queryError) Unwrap() error {
229 | return e.err
230 | }
231 |
232 | type httpError struct {
233 | status int
234 | }
235 |
236 | func (e *httpError) Error() string {
237 | return fmt.Sprintf("HTTP request failed: %d", e.status)
238 | }
239 |
240 | // Given a raw DNS query (including the query ID), this function sends the
241 | // query. If the query is successful, it returns the response and a nil qerr. Otherwise,
242 | // it returns a SERVFAIL response and a qerr with a status value indicating the cause.
243 | // Independent of the query's success or failure, this function also returns the
244 | // address of the server on a best-effort basis, or nil if the address could not
245 | // be determined.
246 | func (t *transport) doQuery(q []byte) (response []byte, server *net.TCPAddr, qerr *queryError) {
247 | if len(q) < 2 {
248 | qerr = &queryError{BadQuery, fmt.Errorf("Query length is %d", len(q))}
249 | return
250 | }
251 |
252 | t.hangoverLock.RLock()
253 | inHangover := time.Now().Before(t.hangoverExpiration)
254 | t.hangoverLock.RUnlock()
255 | if inHangover {
256 | response = tryServfail(q)
257 | qerr = &queryError{HTTPError, errors.New("Forwarder is in servfail hangover")}
258 | return
259 | }
260 |
261 | // Add padding to the raw query
262 | q, err := AddEdnsPadding(q)
263 | if err != nil {
264 | qerr = &queryError{InternalError, err}
265 | return
266 | }
267 |
268 | // Zero out the query ID.
269 | id := binary.BigEndian.Uint16(q)
270 | binary.BigEndian.PutUint16(q, 0)
271 | req, err := http.NewRequest(http.MethodPost, t.url, bytes.NewBuffer(q))
272 | if err != nil {
273 | qerr = &queryError{InternalError, err}
274 | return
275 | }
276 |
277 | var hostname string
278 | response, hostname, server, qerr = t.sendRequest(id, req)
279 |
280 | // Restore the query ID.
281 | binary.BigEndian.PutUint16(q, id)
282 | if qerr == nil {
283 | if len(response) >= 2 {
284 | if binary.BigEndian.Uint16(response) == 0 {
285 | binary.BigEndian.PutUint16(response, id)
286 | } else {
287 | qerr = &queryError{BadResponse, errors.New("Nonzero response ID")}
288 | }
289 | } else {
290 | qerr = &queryError{BadResponse, fmt.Errorf("Response length is %d", len(response))}
291 | }
292 | }
293 |
294 | if qerr != nil {
295 | if qerr.status != SendFailed {
296 | t.hangoverLock.Lock()
297 | t.hangoverExpiration = time.Now().Add(hangoverDuration)
298 | t.hangoverLock.Unlock()
299 | }
300 |
301 | response = tryServfail(q)
302 | } else if server != nil {
303 | // Record a working IP address for this server iff qerr is nil
304 | t.ips.Get(hostname).Confirm(server.IP)
305 | }
306 | return
307 | }
308 |
309 | func (t *transport) sendRequest(id uint16, req *http.Request) (response []byte, hostname string, server *net.TCPAddr, qerr *queryError) {
310 | hostname = t.hostname
311 |
312 | // The connection used for this request. If the request fails, we will close
313 | // this socket, in case it is no longer functioning.
314 | var conn net.Conn
315 |
316 | // Error cleanup function. If the query fails, this function will close the
317 | // underlying socket and disconfirm the server IP. Empirically, sockets often
318 | // become unresponsive after a network change, causing timeouts on all requests.
319 | defer func() {
320 | if qerr == nil {
321 | return
322 | }
323 | log.Infof("%d Query failed: %v", id, qerr)
324 | if server != nil {
325 | log.Debugf("%d Disconfirming %s", id, server.IP.String())
326 | t.ips.Get(hostname).Disconfirm(server.IP)
327 | }
328 | if conn != nil {
329 | log.Infof("%d Closing failing DoH socket", id)
330 | conn.Close()
331 | }
332 | }()
333 |
334 | // Add a trace to the request in order to expose the server's IP address.
335 | // Only GotConn performs any action; the other methods just provide debug logs.
336 | // GotConn runs before client.Do() returns, so there is no data race when
337 | // reading the variables it has set.
338 | trace := httptrace.ClientTrace{
339 | GetConn: func(hostPort string) {
340 | log.Debugf("%d GetConn(%s)", id, hostPort)
341 | },
342 | GotConn: func(info httptrace.GotConnInfo) {
343 | log.Debugf("%d GotConn(%v)", id, info)
344 | if info.Conn == nil {
345 | return
346 | }
347 | conn = info.Conn
348 | // info.Conn is a DuplexConn, so RemoteAddr is actually a TCPAddr.
349 | server = conn.RemoteAddr().(*net.TCPAddr)
350 | },
351 | PutIdleConn: func(err error) {
352 | log.Debugf("%d PutIdleConn(%v)", id, err)
353 | },
354 | GotFirstResponseByte: func() {
355 | log.Debugf("%d GotFirstResponseByte()", id)
356 | },
357 | Got100Continue: func() {
358 | log.Debugf("%d Got100Continue()", id)
359 | },
360 | Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
361 | log.Debugf("%d Got1xxResponse(%d, %v)", id, code, header)
362 | return nil
363 | },
364 | DNSStart: func(info httptrace.DNSStartInfo) {
365 | log.Debugf("%d DNSStart(%v)", id, info)
366 | },
367 | DNSDone: func(info httptrace.DNSDoneInfo) {
368 | log.Debugf("%d, DNSDone(%v)", id, info)
369 | },
370 | ConnectStart: func(network, addr string) {
371 | log.Debugf("%d ConnectStart(%s, %s)", id, network, addr)
372 | },
373 | ConnectDone: func(network, addr string, err error) {
374 | log.Debugf("%d ConnectDone(%s, %s, %v)", id, network, addr, err)
375 | },
376 | TLSHandshakeStart: func() {
377 | log.Debugf("%d TLSHandshakeStart()", id)
378 | },
379 | TLSHandshakeDone: func(state tls.ConnectionState, err error) {
380 | log.Debugf("%d TLSHandshakeDone(%v, %v)", id, state, err)
381 | },
382 | WroteHeaders: func() {
383 | log.Debugf("%d WroteHeaders()", id)
384 | },
385 | WroteRequest: func(info httptrace.WroteRequestInfo) {
386 | log.Debugf("%d WroteRequest(%v)", id, info)
387 | },
388 | }
389 | req = req.WithContext(httptrace.WithClientTrace(req.Context(), &trace))
390 |
391 | const mimetype = "application/dns-message"
392 | req.Header.Set("Content-Type", mimetype)
393 | req.Header.Set("Accept", mimetype)
394 | req.Header.Set("User-Agent", "Intra")
395 | log.Debugf("%d Sending query", id)
396 | httpResponse, err := t.client.Do(req)
397 | if err != nil {
398 | qerr = &queryError{SendFailed, err}
399 | return
400 | }
401 | log.Debugf("%d Got response", id)
402 | response, err = ioutil.ReadAll(httpResponse.Body)
403 | if err != nil {
404 | qerr = &queryError{BadResponse, err}
405 | return
406 | }
407 | httpResponse.Body.Close()
408 | log.Debugf("%d Closed response", id)
409 |
410 | // Update the hostname, which could have changed due to a redirect.
411 | hostname = httpResponse.Request.URL.Hostname()
412 |
413 | if httpResponse.StatusCode != http.StatusOK {
414 | reqBuf := new(bytes.Buffer)
415 | req.Write(reqBuf)
416 | respBuf := new(bytes.Buffer)
417 | httpResponse.Write(respBuf)
418 | log.Debugf("%d request: %s\nresponse: %s", id, reqBuf.String(), respBuf.String())
419 |
420 | qerr = &queryError{HTTPError, &httpError{httpResponse.StatusCode}}
421 | return
422 | }
423 |
424 | return
425 | }
426 |
427 | func (t *transport) Query(q []byte) ([]byte, error) {
428 | var token Token
429 | if t.listener != nil {
430 | token = t.listener.OnQuery(t.url)
431 | }
432 |
433 | before := time.Now()
434 | response, server, qerr := t.doQuery(q)
435 | after := time.Now()
436 |
437 | var err error
438 | status := Complete
439 | httpStatus := http.StatusOK
440 | if qerr != nil {
441 | err = qerr
442 | status = qerr.status
443 | httpStatus = 0
444 |
445 | var herr *httpError
446 | if errors.As(qerr.err, &herr) {
447 | httpStatus = herr.status
448 | }
449 | }
450 |
451 | if t.listener != nil {
452 | latency := after.Sub(before)
453 | var ip string
454 | if server != nil {
455 | ip = server.IP.String()
456 | }
457 |
458 | t.listener.OnResponse(token, &Summary{
459 | Latency: latency.Seconds(),
460 | Query: q,
461 | Response: response,
462 | Server: ip,
463 | Status: status,
464 | HTTPStatus: httpStatus,
465 | })
466 | }
467 | return response, err
468 | }
469 |
470 | func (t *transport) GetURL() string {
471 | return t.url
472 | }
473 |
474 | // Perform a query using the transport, and send the response to the writer.
475 | func forwardQuery(t Transport, q []byte, c io.Writer) error {
476 | resp, qerr := t.Query(q)
477 | if resp == nil && qerr != nil {
478 | return qerr
479 | }
480 | rlen := len(resp)
481 | if rlen > math.MaxUint16 {
482 | return fmt.Errorf("Oversize response: %d", rlen)
483 | }
484 | // Use a combined write to ensure atomicity. Otherwise, writes from two
485 | // responses could be interleaved.
486 | rlbuf := make([]byte, rlen+2)
487 | binary.BigEndian.PutUint16(rlbuf, uint16(rlen))
488 | copy(rlbuf[2:], resp)
489 | n, err := c.Write(rlbuf)
490 | if err != nil {
491 | return err
492 | }
493 | if int(n) != len(rlbuf) {
494 | return fmt.Errorf("Incomplete response write: %d < %d", n, len(rlbuf))
495 | }
496 | return qerr
497 | }
498 |
499 | // Perform a query using the transport, send the response to the writer,
500 | // and close the writer if there was an error.
501 | func forwardQueryAndCheck(t Transport, q []byte, c io.WriteCloser) {
502 | if err := forwardQuery(t, q, c); err != nil {
503 | log.Warnf("Query forwarding failed: %v", err)
504 | c.Close()
505 | }
506 | }
507 |
508 | // Accept a DNS-over-TCP socket from a stub resolver, and connect the socket
509 | // to this DNSTransport.
510 | func Accept(t Transport, c io.ReadWriteCloser) {
511 | qlbuf := make([]byte, 2)
512 | for {
513 | n, err := c.Read(qlbuf)
514 | if n == 0 {
515 | log.Debugf("TCP query socket clean shutdown")
516 | break
517 | }
518 | if err != nil {
519 | log.Warnf("Error reading from TCP query socket: %v", err)
520 | break
521 | }
522 | if n < 2 {
523 | log.Warnf("Incomplete query length")
524 | break
525 | }
526 | qlen := binary.BigEndian.Uint16(qlbuf)
527 | q := make([]byte, qlen)
528 | n, err = c.Read(q)
529 | if err != nil {
530 | log.Warnf("Error reading query: %v", err)
531 | break
532 | }
533 | if n != int(qlen) {
534 | log.Warnf("Incomplete query: %d < %d", n, qlen)
535 | break
536 | }
537 | go forwardQueryAndCheck(t, q, c)
538 | }
539 | // TODO: Cancel outstanding queries at this point.
540 | c.Close()
541 | }
542 |
543 | // Servfail returns a SERVFAIL response to the query q.
544 | func Servfail(q []byte) ([]byte, error) {
545 | var msg dnsmessage.Message
546 | if err := msg.Unpack(q); err != nil {
547 | return nil, err
548 | }
549 | msg.Response = true
550 | msg.RecursionAvailable = true
551 | msg.RCode = dnsmessage.RCodeServerFailure
552 | msg.Additionals = nil // Strip EDNS
553 | return msg.Pack()
554 | }
555 |
556 | func tryServfail(q []byte) []byte {
557 | response, err := Servfail(q)
558 | if err != nil {
559 | log.Warnf("Error constructing servfail: %v", err)
560 | }
561 | return response
562 | }
563 |
--------------------------------------------------------------------------------
/intra/doh/ipmap/ipmap.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package ipmap
16 |
17 | import (
18 | "context"
19 | "math/rand"
20 | "net"
21 | "sync"
22 |
23 | "github.com/eycorsican/go-tun2socks/common/log"
24 | )
25 |
26 | // IPMap maps hostnames to IPSets.
27 | type IPMap interface {
28 | // Get creates an IPSet for this hostname populated with the IPs
29 | // discovered by resolving it. Subsequent calls to Get return the
30 | // same IPSet.
31 | Get(hostname string) *IPSet
32 | }
33 |
34 | // NewIPMap returns a fresh IPMap.
35 | // `r` will be used to resolve any hostnames passed to `Get` or `Add`.
36 | func NewIPMap(r *net.Resolver) IPMap {
37 | return &ipMap{
38 | m: make(map[string]*IPSet),
39 | r: r,
40 | }
41 | }
42 |
43 | type ipMap struct {
44 | sync.RWMutex
45 | m map[string]*IPSet
46 | r *net.Resolver
47 | }
48 |
49 | func (m *ipMap) Get(hostname string) *IPSet {
50 | m.RLock()
51 | s := m.m[hostname]
52 | m.RUnlock()
53 | if s != nil {
54 | return s
55 | }
56 |
57 | s = &IPSet{r: m.r}
58 | s.Add(hostname)
59 |
60 | m.Lock()
61 | s2 := m.m[hostname]
62 | if s2 == nil {
63 | m.m[hostname] = s
64 | } else {
65 | // Another pending call to Get populated m[hostname]
66 | // while we were building s. Use that one to ensure
67 | // consistency.
68 | s = s2
69 | }
70 | m.Unlock()
71 |
72 | return s
73 | }
74 |
75 | // IPSet represents an unordered collection of IP addresses for a single host.
76 | // One IP can be marked as confirmed to be working correctly.
77 | type IPSet struct {
78 | sync.RWMutex
79 | ips []net.IP // All known IPs for the server.
80 | confirmed net.IP // IP address confirmed to be working
81 | r *net.Resolver // Resolver to use for hostname resolution
82 | }
83 |
84 | // Reports whether ip is in the set. Must be called under RLock.
85 | func (s *IPSet) has(ip net.IP) bool {
86 | for _, oldIP := range s.ips {
87 | if oldIP.Equal(ip) {
88 | return true
89 | }
90 | }
91 | return false
92 | }
93 |
94 | // Adds an IP to the set if it is not present. Must be called under Lock.
95 | func (s *IPSet) add(ip net.IP) {
96 | if !s.has(ip) {
97 | s.ips = append(s.ips, ip)
98 | }
99 | }
100 |
101 | // Add one or more IP addresses to the set.
102 | // The hostname can be a domain name or an IP address.
103 | func (s *IPSet) Add(hostname string) {
104 | // Don't hold the ipMap lock during blocking I/O.
105 | resolved, err := s.r.LookupIPAddr(context.TODO(), hostname)
106 | if err != nil {
107 | log.Warnf("Failed to resolve %s: %v", hostname, err)
108 | }
109 | s.Lock()
110 | for _, addr := range resolved {
111 | s.add(addr.IP)
112 | }
113 | s.Unlock()
114 | }
115 |
116 | // Empty reports whether the set is empty.
117 | func (s *IPSet) Empty() bool {
118 | s.RLock()
119 | defer s.RUnlock()
120 | return len(s.ips) == 0
121 | }
122 |
123 | // GetAll returns a copy of the IP set as a slice in random order.
124 | // The slice is owned by the caller, but the elements are owned by the set.
125 | func (s *IPSet) GetAll() []net.IP {
126 | s.RLock()
127 | c := append([]net.IP{}, s.ips...)
128 | s.RUnlock()
129 | rand.Shuffle(len(c), func(i, j int) {
130 | c[i], c[j] = c[j], c[i]
131 | })
132 | return c
133 | }
134 |
135 | // Confirmed returns the confirmed IP address, or nil if there is no such address.
136 | func (s *IPSet) Confirmed() net.IP {
137 | s.RLock()
138 | defer s.RUnlock()
139 | return s.confirmed
140 | }
141 |
142 | // Confirm marks ip as the confirmed address.
143 | func (s *IPSet) Confirm(ip net.IP) {
144 | // Optimization: Skip setting if it hasn't changed.
145 | if ip.Equal(s.Confirmed()) {
146 | // This is the common case.
147 | return
148 | }
149 | s.Lock()
150 | // Add is O(N)
151 | s.add(ip)
152 | s.confirmed = ip
153 | s.Unlock()
154 | }
155 |
156 | // Disconfirm sets the confirmed address to nil if the current confirmed address
157 | // is the provided ip.
158 | func (s *IPSet) Disconfirm(ip net.IP) {
159 | s.Lock()
160 | if ip.Equal(s.confirmed) {
161 | s.confirmed = nil
162 | }
163 | s.Unlock()
164 | }
165 |
--------------------------------------------------------------------------------
/intra/doh/ipmap/ipmap_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package ipmap
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "net"
21 | "sync/atomic"
22 | "testing"
23 | )
24 |
25 | // We use '.' at the end to make sure resolution treats it an inexistent root domain.
26 | // It must not resolve to any address.
27 | const invalidDomain = "invaliddomain."
28 |
29 | func TestGetTwice(t *testing.T) {
30 | m := NewIPMap(nil)
31 | a := m.Get("example")
32 | b := m.Get("example")
33 | if a != b {
34 | t.Error("Matched Get returned different objects")
35 | }
36 | }
37 |
38 | func TestGetInvalid(t *testing.T) {
39 | m := NewIPMap(nil)
40 | s := m.Get(invalidDomain)
41 | if !s.Empty() {
42 | t.Errorf("Invalid name should result in an empty set, got %v", s.ips)
43 | }
44 | if len(s.GetAll()) != 0 {
45 | t.Errorf("Empty set should be empty, got %v", s.GetAll())
46 | }
47 | }
48 |
49 | func TestGetDomain(t *testing.T) {
50 | m := NewIPMap(nil)
51 | s := m.Get("www.google.com")
52 | if s.Empty() {
53 | t.Error("Google lookup failed")
54 | }
55 | ips := s.GetAll()
56 | if len(ips) == 0 {
57 | t.Fatal("IP set is empty")
58 | }
59 | if ips[0] == nil {
60 | t.Error("nil IP in set")
61 | }
62 | }
63 |
64 | func TestGetIP(t *testing.T) {
65 | m := NewIPMap(nil)
66 | s := m.Get("192.0.2.1")
67 | if s.Empty() {
68 | t.Error("IP parsing failed")
69 | }
70 | ips := s.GetAll()
71 | if len(ips) != 1 {
72 | t.Errorf("Wrong IP set size %d", len(ips))
73 | }
74 | if ips[0].String() != "192.0.2.1" {
75 | t.Error("Wrong IP")
76 | }
77 | }
78 |
79 | func TestAddDomain(t *testing.T) {
80 | m := NewIPMap(nil)
81 | s := m.Get(invalidDomain)
82 | s.Add("www.google.com")
83 | if s.Empty() {
84 | t.Error("Google lookup failed")
85 | }
86 | ips := s.GetAll()
87 | if len(ips) == 0 {
88 | t.Fatal("IP set is empty")
89 | }
90 | if ips[0] == nil {
91 | t.Error("nil IP in set")
92 | }
93 | }
94 | func TestAddIP(t *testing.T) {
95 | m := NewIPMap(nil)
96 | s := m.Get(invalidDomain)
97 | s.Add("192.0.2.1")
98 | ips := s.GetAll()
99 | if len(ips) != 1 {
100 | t.Errorf("Wrong IP set size %d", len(ips))
101 | }
102 | if ips[0].String() != "192.0.2.1" {
103 | t.Error("Wrong IP")
104 | }
105 | }
106 |
107 | func TestConfirmed(t *testing.T) {
108 | m := NewIPMap(nil)
109 | s := m.Get("www.google.com")
110 | if s.Confirmed() != nil {
111 | t.Error("Confirmed should start out nil")
112 | }
113 |
114 | ips := s.GetAll()
115 | s.Confirm(ips[0])
116 | if !ips[0].Equal(s.Confirmed()) {
117 | t.Error("Confirmation failed")
118 | }
119 |
120 | s.Disconfirm(ips[0])
121 | if s.Confirmed() != nil {
122 | t.Error("Confirmed should now be nil")
123 | }
124 | }
125 |
126 | func TestConfirmNew(t *testing.T) {
127 | m := NewIPMap(nil)
128 | s := m.Get(invalidDomain)
129 | s.Add("192.0.2.1")
130 | // Confirm a new address.
131 | s.Confirm(net.ParseIP("192.0.2.2"))
132 | if s.Confirmed() == nil || s.Confirmed().String() != "192.0.2.2" {
133 | t.Error("Confirmation failed")
134 | }
135 | ips := s.GetAll()
136 | if len(ips) != 2 {
137 | t.Error("New address not added to the set")
138 | }
139 | }
140 |
141 | func TestDisconfirmMismatch(t *testing.T) {
142 | m := NewIPMap(nil)
143 | s := m.Get("www.google.com")
144 | ips := s.GetAll()
145 | s.Confirm(ips[0])
146 |
147 | // Make a copy
148 | otherIP := net.ParseIP(ips[0].String())
149 | // Alter it
150 | otherIP[0]++
151 | // Disconfirm. This should have no effect because otherIP
152 | // is not the confirmed IP.
153 | s.Disconfirm(otherIP)
154 |
155 | if !ips[0].Equal(s.Confirmed()) {
156 | t.Error("Mismatched disconfirmation")
157 | }
158 | }
159 |
160 | func TestResolver(t *testing.T) {
161 | var dialCount int32
162 | resolver := &net.Resolver{
163 | PreferGo: true,
164 | Dial: func(context context.Context, network, address string) (net.Conn, error) {
165 | atomic.AddInt32(&dialCount, 1)
166 | return nil, errors.New("Fake dialer")
167 | },
168 | }
169 | m := NewIPMap(resolver)
170 | s := m.Get("www.google.com")
171 | if !s.Empty() {
172 | t.Error("Google lookup should have failed due to fake dialer")
173 | }
174 | if atomic.LoadInt32(&dialCount) == 0 {
175 | t.Error("Fake dialer didn't run")
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/intra/doh/padding.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package doh
16 |
17 | import (
18 | "golang.org/x/net/dns/dnsmessage"
19 | )
20 |
21 | const (
22 | OptResourcePaddingCode = 12
23 | PaddingBlockSize = 128 // RFC8467 recommendation
24 | )
25 |
26 | const kOptRrHeaderLen int = 1 + // DOMAIN NAME
27 | 2 + // TYPE
28 | 2 + // CLASS
29 | 4 + // TTL
30 | 2 // RDLEN
31 |
32 | const kOptPaddingHeaderLen int = 2 + // OPTION-CODE
33 | 2 // OPTION-LENGTH
34 |
35 | // Compute the number of padding bytes needed, excluding headers.
36 | // Assumes that |msgLen| is the length of a raw DNS message that contains an
37 | // OPT RR with no RFC7830 padding option, and that the message is fully
38 | // label-compressed.
39 | func computePaddingSize(msgLen int, blockSize int) int {
40 | // We'll always be adding a new padding header inside the OPT
41 | // RR's data.
42 | extraPadding := kOptPaddingHeaderLen
43 |
44 | padSize := blockSize - (msgLen+extraPadding)%blockSize
45 | return padSize % blockSize
46 | }
47 |
48 | // Create an appropriately-sized padding option. Precondition: |msgLen| is the
49 | // length of a message that already contains an OPT RR.
50 | func getPadding(msgLen int) dnsmessage.Option {
51 | optPadding := dnsmessage.Option{
52 | Code: OptResourcePaddingCode,
53 | Data: make([]byte, computePaddingSize(msgLen, PaddingBlockSize)),
54 | }
55 | return optPadding
56 | }
57 |
58 | // Add EDNS padding, as defined in RFC7830, to a raw DNS message.
59 | func AddEdnsPadding(rawMsg []byte) ([]byte, error) {
60 | var msg dnsmessage.Message
61 | if err := msg.Unpack(rawMsg); err != nil {
62 | return nil, err
63 | }
64 |
65 | // Search for OPT resource and save |optRes| pointer if possible.
66 | var optRes *dnsmessage.OPTResource = nil
67 | for _, additional := range msg.Additionals {
68 | switch body := additional.Body.(type) {
69 | case *dnsmessage.OPTResource:
70 | optRes = body
71 | break
72 | }
73 | }
74 | if optRes != nil {
75 | // Search for a padding Option. If the message already contains
76 | // padding, we will respect the stub resolver's padding.
77 | for _, option := range optRes.Options {
78 | if option.Code == OptResourcePaddingCode {
79 | return rawMsg, nil
80 | }
81 | }
82 | // At this point, |optRes| points to an OPTResource that does
83 | // not contain a padding option.
84 | } else {
85 | // Create an empty OPTResource (contains no padding option) and
86 | // push it into |msg.Additionals|.
87 | optRes = &dnsmessage.OPTResource{
88 | Options: []dnsmessage.Option{},
89 | }
90 |
91 | optHeader := dnsmessage.ResourceHeader{}
92 | // SetEDNS0(udpPayloadLen int, extRCode RCode, dnssecOK bool) error
93 | err := optHeader.SetEDNS0(65535, dnsmessage.RCodeSuccess, false)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | msg.Additionals = append(msg.Additionals, dnsmessage.Resource{
99 | Header: optHeader,
100 | Body: optRes,
101 | })
102 | }
103 | // At this point, |msg| contains an OPT resource, and that OPT resource
104 | // does not contain a padding option.
105 |
106 | // Compress the message to determine its size before padding.
107 | compressedMsg, err := msg.Pack()
108 | if err != nil {
109 | return nil, err
110 | }
111 | // Add the padding option to |msg| that will round its size on the wire
112 | // up to the nearest block.
113 | paddingOption := getPadding(len(compressedMsg))
114 | optRes.Options = append(optRes.Options, paddingOption)
115 |
116 | // Re-pack the message, with compression unconditionally enabled.
117 | return msg.Pack()
118 | }
119 |
--------------------------------------------------------------------------------
/intra/ip.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Jigsaw Operations LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import "net/netip"
18 |
19 | // isEquivalentAddrPort checks if addr1 and addr2 are equivalent. More specifically, it will treat
20 | // "ffff::127.0.0.1" (IPv4-in-6) and "127.0.0.1" (IPv4) as equivalent, even though they are "!=" in Go.
21 | func isEquivalentAddrPort(addr1, addr2 netip.AddrPort) bool {
22 | return addr1.Addr().Unmap() == addr2.Addr().Unmap() && addr1.Port() == addr2.Port()
23 | }
24 |
--------------------------------------------------------------------------------
/intra/ip_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Jigsaw Operations LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "net/netip"
19 | "testing"
20 | )
21 |
22 | func TestIsEquivalentAddrPort(t *testing.T) {
23 | cases := []struct {
24 | in1, in2 netip.AddrPort
25 | want bool
26 | msg string
27 | }{
28 | {
29 | in1: netip.MustParseAddrPort("12.34.56.78:80"),
30 | in2: netip.AddrPortFrom(netip.AddrFrom4([4]byte{12, 34, 56, 78}), 80),
31 | want: true,
32 | },
33 | {
34 | in1: netip.MustParseAddrPort("[fe80::1234:5678]:443"),
35 | in2: netip.AddrPortFrom(netip.AddrFrom16([16]byte{0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78}), 443),
36 | want: true,
37 | },
38 | {
39 | in1: netip.MustParseAddrPort("0.0.0.0:80"),
40 | in2: netip.MustParseAddrPort("127.0.0.1:80"),
41 | want: false,
42 | },
43 | {
44 | in1: netip.AddrPortFrom(netip.IPv6Unspecified(), 80),
45 | in2: netip.AddrPortFrom(netip.IPv6Loopback(), 80),
46 | want: false,
47 | },
48 | {
49 | in1: netip.MustParseAddrPort("127.0.0.1:38880"),
50 | in2: netip.MustParseAddrPort("127.0.0.1:38888"),
51 | want: false,
52 | },
53 | {
54 | in1: netip.MustParseAddrPort("[2001:db8:85a3:8d3:1319:8a2e:370:7348]:33443"),
55 | in2: netip.MustParseAddrPort("[2001:db8:85a3:8d3:1319:8a2e:370:7348]:33444"),
56 | want: false,
57 | },
58 | {
59 | in1: netip.MustParseAddrPort("127.0.0.1:8080"),
60 | in2: netip.MustParseAddrPort("[::ffff:127.0.0.1]:8080"),
61 | want: true,
62 | },
63 | {
64 | in1: netip.AddrPortFrom(netip.IPv6Loopback(), 80),
65 | in2: netip.MustParseAddrPort("127.0.0.1:80"),
66 | want: false,
67 | },
68 | }
69 |
70 | for _, tc := range cases {
71 | actual := isEquivalentAddrPort(tc.in1, tc.in2)
72 | if actual != tc.want {
73 | t.Fatalf(`"%v" == "%v"? want %v, actual %v`, tc.in1, tc.in2, tc.want, actual)
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/intra/packet_proxy.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Jigsaw Operations LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "errors"
19 | "fmt"
20 | "net"
21 | "net/netip"
22 | "sync/atomic"
23 | "time"
24 |
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"
27 | "github.com/Jigsaw-Code/outline-sdk/network"
28 | "github.com/Jigsaw-Code/outline-sdk/transport"
29 | )
30 |
31 | type intraPacketProxy struct {
32 | fakeDNSAddr netip.AddrPort
33 | dns atomic.Pointer[doh.Transport]
34 | proxy network.PacketProxy
35 | listener UDPListener
36 | }
37 |
38 | var _ network.PacketProxy = (*intraPacketProxy)(nil)
39 |
40 | func newIntraPacketProxy(
41 | fakeDNS netip.AddrPort, dns doh.Transport, protector protect.Protector, listener UDPListener,
42 | ) (*intraPacketProxy, error) {
43 | if dns == nil {
44 | return nil, errors.New("dns is required")
45 | }
46 |
47 | pl := &transport.UDPPacketListener{
48 | ListenConfig: *protect.MakeListenConfig(protector),
49 | }
50 |
51 | // RFC 4787 REQ-5 requires a timeout no shorter than 5 minutes.
52 | pp, err := network.NewPacketProxyFromPacketListener(pl, network.WithPacketListenerWriteIdleTimeout(5*time.Minute))
53 | if err != nil {
54 | return nil, fmt.Errorf("failed to create packet proxy from listener: %w", err)
55 | }
56 |
57 | dohpp := &intraPacketProxy{
58 | fakeDNSAddr: fakeDNS,
59 | proxy: pp,
60 | listener: listener,
61 | }
62 | dohpp.dns.Store(&dns)
63 |
64 | return dohpp, nil
65 | }
66 |
67 | // NewSession implements PacketProxy.NewSession.
68 | func (p *intraPacketProxy) NewSession(resp network.PacketResponseReceiver) (network.PacketRequestSender, error) {
69 | dohResp := &dohPacketRespReceiver{
70 | PacketResponseReceiver: resp,
71 | stats: makeTracker(),
72 | listener: p.listener,
73 | }
74 | req, err := p.proxy.NewSession(dohResp)
75 | if err != nil {
76 | return nil, fmt.Errorf("failed to create new session: %w", err)
77 | }
78 |
79 | return &dohPacketReqSender{
80 | PacketRequestSender: req,
81 | proxy: p,
82 | response: dohResp,
83 | stats: dohResp.stats,
84 | }, nil
85 | }
86 |
87 | func (p *intraPacketProxy) SetDNS(dns doh.Transport) error {
88 | if dns == nil {
89 | return errors.New("dns is required")
90 | }
91 | p.dns.Store(&dns)
92 | return nil
93 | }
94 |
95 | // DoH PacketRequestSender wrapper
96 | type dohPacketReqSender struct {
97 | network.PacketRequestSender
98 |
99 | response *dohPacketRespReceiver
100 | proxy *intraPacketProxy
101 | stats *tracker
102 | }
103 |
104 | // DoH PacketResponseReceiver wrapper
105 | type dohPacketRespReceiver struct {
106 | network.PacketResponseReceiver
107 |
108 | stats *tracker
109 | listener UDPListener
110 | }
111 |
112 | var _ network.PacketRequestSender = (*dohPacketReqSender)(nil)
113 | var _ network.PacketResponseReceiver = (*dohPacketRespReceiver)(nil)
114 |
115 | // WriteTo implements PacketRequestSender.WriteTo. It will query the DoH server if the packet a DNS packet.
116 | func (req *dohPacketReqSender) WriteTo(p []byte, destination netip.AddrPort) (int, error) {
117 | if isEquivalentAddrPort(destination, req.proxy.fakeDNSAddr) {
118 | defer func() {
119 | // conn was only used for this DNS query, so it's unlikely to be used again
120 | if req.stats.download.Load() == 0 && req.stats.upload.Load() == 0 {
121 | req.Close()
122 | }
123 | }()
124 |
125 | resp, err := (*req.proxy.dns.Load()).Query(p)
126 | if err != nil {
127 | return 0, fmt.Errorf("DoH request error: %w", err)
128 | }
129 | if len(resp) == 0 {
130 | return 0, errors.New("empty DoH response")
131 | }
132 |
133 | return req.response.writeFrom(resp, net.UDPAddrFromAddrPort(req.proxy.fakeDNSAddr), false)
134 | }
135 |
136 | req.stats.upload.Add(int64(len(p)))
137 | return req.PacketRequestSender.WriteTo(p, destination)
138 | }
139 |
140 | // Close terminates the UDP session, and reports session stats to the listener.
141 | func (resp *dohPacketRespReceiver) Close() error {
142 | if resp.listener != nil {
143 | resp.listener.OnUDPSocketClosed(&UDPSocketSummary{
144 | Duration: int32(time.Since(resp.stats.start)),
145 | UploadBytes: resp.stats.upload.Load(),
146 | DownloadBytes: resp.stats.download.Load(),
147 | })
148 | }
149 | return resp.PacketResponseReceiver.Close()
150 | }
151 |
152 | // WriteFrom implements PacketResponseReceiver.WriteFrom.
153 | func (resp *dohPacketRespReceiver) WriteFrom(p []byte, source net.Addr) (int, error) {
154 | return resp.writeFrom(p, source, true)
155 | }
156 |
157 | // writeFrom writes to the underlying PacketResponseReceiver.
158 | // It will also add len(p) to downloadBytes if doStat is true.
159 | func (resp *dohPacketRespReceiver) writeFrom(p []byte, source net.Addr, doStat bool) (int, error) {
160 | if doStat {
161 | resp.stats.download.Add(int64(len(p)))
162 | }
163 | return resp.PacketResponseReceiver.WriteFrom(p, source)
164 | }
165 |
--------------------------------------------------------------------------------
/intra/protect/protect.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package protect
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "fmt"
21 | "net"
22 | "strings"
23 | "syscall"
24 |
25 | "github.com/eycorsican/go-tun2socks/common/log"
26 | )
27 |
28 | // Protector provides the ability to bypass a VPN on Android, pre-Lollipop.
29 | type Protector interface {
30 | // Protect a socket, i.e. exclude it from the VPN.
31 | // This is needed in order to avoid routing loops for the VPN's own sockets.
32 | // This is a wrapper for Android's VpnService.protect().
33 | Protect(socket int32) bool
34 |
35 | // Returns a comma-separated list of the system's configured DNS resolvers,
36 | // in roughly descending priority order.
37 | // This is needed because (1) Android Java cannot protect DNS lookups but Go can, and
38 | // (2) Android Java can determine the list of system DNS resolvers but Go cannot.
39 | // A comma-separated list is used because Gomobile cannot bind []string.
40 | GetResolvers() string
41 | }
42 |
43 | func makeControl(p Protector) func(string, string, syscall.RawConn) error {
44 | return func(network, address string, c syscall.RawConn) error {
45 | return c.Control(func(fd uintptr) {
46 | if !p.Protect(int32(fd)) {
47 | // TODO: Record and report these errors.
48 | log.Errorf("Failed to protect a %s socket", network)
49 | }
50 | })
51 | }
52 | }
53 |
54 | // Returns the first IP address that is of the desired family.
55 | func scan(ips []string, wantV4 bool) string {
56 | for _, ip := range ips {
57 | parsed := net.ParseIP(ip)
58 | if parsed == nil {
59 | // `ip` failed to parse. Skip it.
60 | continue
61 | }
62 | isV4 := parsed.To4() != nil
63 | if isV4 == wantV4 {
64 | return ip
65 | }
66 | }
67 | return ""
68 | }
69 |
70 | // Given a slice of IP addresses, and a transport address, return a transport
71 | // address with the IP replaced by the first IP of the same family in `ips`, or
72 | // by the first address of a different family if there are none of the same.
73 | func replaceIP(addr string, ips []string) (string, error) {
74 | if len(ips) == 0 {
75 | return "", errors.New("No resolvers available")
76 | }
77 | orighost, port, err := net.SplitHostPort(addr)
78 | if err != nil {
79 | return "", err
80 | }
81 | origip := net.ParseIP(orighost)
82 | if origip == nil {
83 | return "", fmt.Errorf("Can't parse resolver IP: %s", orighost)
84 | }
85 | isV4 := origip.To4() != nil
86 | newIP := scan(ips, isV4)
87 | if newIP == "" {
88 | // There are no IPs of the desired address family. Use a different family.
89 | newIP = ips[0]
90 | }
91 | return net.JoinHostPort(newIP, port), nil
92 | }
93 |
94 | // MakeDialer creates a new Dialer. Recipients can safely mutate
95 | // any public field except Control and Resolver, which are both populated.
96 | func MakeDialer(p Protector) *net.Dialer {
97 | if p == nil {
98 | return &net.Dialer{}
99 | }
100 | d := &net.Dialer{
101 | Control: makeControl(p),
102 | }
103 | resolverDialer := func(ctx context.Context, network, address string) (net.Conn, error) {
104 | resolvers := strings.Split(p.GetResolvers(), ",")
105 | newAddress, err := replaceIP(address, resolvers)
106 | if err != nil {
107 | return nil, err
108 | }
109 | return d.DialContext(ctx, network, newAddress)
110 | }
111 | d.Resolver = &net.Resolver{
112 | PreferGo: true,
113 | Dial: resolverDialer,
114 | }
115 | return d
116 | }
117 |
118 | // MakeListenConfig returns a new ListenConfig that creates protected
119 | // listener sockets.
120 | func MakeListenConfig(p Protector) *net.ListenConfig {
121 | if p == nil {
122 | return &net.ListenConfig{}
123 | }
124 | return &net.ListenConfig{
125 | Control: makeControl(p),
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/intra/protect/protect_test.go:
--------------------------------------------------------------------------------
1 | package protect
2 |
3 | import (
4 | "context"
5 | "net"
6 | "sync"
7 | "syscall"
8 | "testing"
9 | )
10 |
11 | // The fake protector just records the file descriptors it was given.
12 | type fakeProtector struct {
13 | mu sync.Mutex
14 | fds []int32
15 | }
16 |
17 | func (p *fakeProtector) Protect(fd int32) bool {
18 | p.mu.Lock()
19 | p.fds = append(p.fds, fd)
20 | p.mu.Unlock()
21 | return true
22 | }
23 |
24 | func (p *fakeProtector) GetResolvers() string {
25 | return "8.8.8.8,2001:4860:4860::8888"
26 | }
27 |
28 | // This interface serves as a supertype of net.TCPConn and net.UDPConn, so
29 | // that they can share the verifyMatch() function.
30 | type hasSyscallConn interface {
31 | SyscallConn() (syscall.RawConn, error)
32 | }
33 |
34 | func verifyMatch(t *testing.T, conn hasSyscallConn, p *fakeProtector) {
35 | rawconn, err := conn.SyscallConn()
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 | rawconn.Control(func(fd uintptr) {
40 | if len(p.fds) == 0 {
41 | t.Fatalf("No file descriptors")
42 | }
43 | if int32(fd) != p.fds[0] {
44 | t.Fatalf("File descriptor mismatch: %d != %d", fd, p.fds[0])
45 | }
46 | })
47 | }
48 |
49 | func TestDialTCP(t *testing.T) {
50 | l, err := net.Listen("tcp", "localhost:0")
51 | if err != nil {
52 | t.Fatal(err)
53 | }
54 | go l.Accept()
55 |
56 | p := &fakeProtector{}
57 | d := MakeDialer(p)
58 | if d.Control == nil {
59 | t.Errorf("Control function is nil")
60 | }
61 |
62 | conn, err := d.Dial("tcp", l.Addr().String())
63 | if err != nil {
64 | t.Fatal(err)
65 | }
66 | verifyMatch(t, conn.(*net.TCPConn), p)
67 | l.Close()
68 | conn.Close()
69 | }
70 |
71 | func TestListenUDP(t *testing.T) {
72 | udpaddr, err := net.ResolveUDPAddr("udp", "localhost:0")
73 | if err != nil {
74 | t.Fatal(err)
75 | }
76 |
77 | p := &fakeProtector{}
78 | c := MakeListenConfig(p)
79 |
80 | conn, err := c.ListenPacket(context.Background(), udpaddr.Network(), udpaddr.String())
81 | if err != nil {
82 | t.Fatal(err)
83 | }
84 | verifyMatch(t, conn.(*net.UDPConn), p)
85 | conn.Close()
86 | }
87 |
88 | func TestLookupIPAddr(t *testing.T) {
89 | p := &fakeProtector{}
90 | d := MakeDialer(p)
91 | d.Resolver.LookupIPAddr(context.Background(), "foo.test.")
92 | // Verify that Protect was called.
93 | if len(p.fds) == 0 {
94 | t.Fatal("Protect was not called")
95 | }
96 | }
97 |
98 | func TestNilDialer(t *testing.T) {
99 | l, err := net.Listen("tcp", "localhost:0")
100 | if err != nil {
101 | t.Fatal(err)
102 | }
103 | go l.Accept()
104 |
105 | d := MakeDialer(nil)
106 | conn, err := d.Dial("tcp", l.Addr().String())
107 | if err != nil {
108 | t.Fatal(err)
109 | }
110 |
111 | conn.Close()
112 | l.Close()
113 | }
114 |
115 | func TestNilListener(t *testing.T) {
116 | udpaddr, err := net.ResolveUDPAddr("udp", "localhost:0")
117 | if err != nil {
118 | t.Fatal(err)
119 | }
120 |
121 | c := MakeListenConfig(nil)
122 | conn, err := c.ListenPacket(context.Background(), udpaddr.Network(), udpaddr.String())
123 | if err != nil {
124 | t.Fatal(err)
125 | }
126 |
127 | conn.Close()
128 | }
129 |
--------------------------------------------------------------------------------
/intra/sni_reporter.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "io"
19 | "sync"
20 | "time"
21 |
22 | "github.com/Jigsaw-Code/choir"
23 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
24 | "github.com/eycorsican/go-tun2socks/common/log"
25 | )
26 |
27 | // Number of bins to assign reports to. Should be large enough for
28 | // k-anonymity goals. See the Choir documentation for more info.
29 | const bins = 32
30 |
31 | // Number of values in each report. The two values are
32 | // * success/failure
33 | // * timeout/close
34 | const values = 2
35 |
36 | // Burst duration. Only one report will be sent in each interval
37 | // to avoid correlated reports.
38 | const burst = 10 * time.Second
39 |
40 | // tcpSNIReporter is a thread-safe wrapper around choir.Reporter
41 | type tcpSNIReporter struct {
42 | mu sync.RWMutex // Protects dns, suffix, and r.
43 | dns doh.Transport
44 | suffix string
45 | r choir.Reporter
46 | }
47 |
48 | // SetDNS changes the DNS transport used for uploading reports.
49 | func (r *tcpSNIReporter) SetDNS(dns doh.Transport) {
50 | r.mu.Lock()
51 | r.dns = dns
52 | r.mu.Unlock()
53 | }
54 |
55 | // Send implements choir.ReportSender.
56 | func (r *tcpSNIReporter) Send(report choir.Report) error {
57 | r.mu.RLock()
58 | suffix := r.suffix
59 | dns := r.dns
60 | r.mu.RUnlock()
61 | q, err := choir.FormatQuery(report, suffix)
62 | if err != nil {
63 | log.Warnf("Failed to construct query for Choir: %v", err)
64 | return nil
65 | }
66 | if _, err = dns.Query(q); err != nil {
67 | log.Infof("Failed to deliver query for Choir: %v", err)
68 | }
69 | return nil
70 | }
71 |
72 | // Configure initializes or reinitializes the reporter.
73 | // `file` is the Choir salt file (persistent and initially empty).
74 | // `suffix` is the domain to which reports will be sent.
75 | // `country` is the two-letter ISO country code of the user's location.
76 | func (r *tcpSNIReporter) Configure(file io.ReadWriter, suffix, country string) (err error) {
77 | r.mu.Lock()
78 | r.suffix = suffix
79 | r.r, err = choir.NewReporter(file, bins, values, country, burst, r)
80 | r.mu.Unlock()
81 | return
82 | }
83 |
84 | // Report converts `summary` into a Choir report and queues it for delivery.
85 | func (r *tcpSNIReporter) Report(summary TCPSocketSummary) {
86 | if summary.Retry.Split == 0 {
87 | return // Nothing to report
88 | }
89 |
90 | r.mu.RLock()
91 | reporter := r.r
92 | r.mu.RUnlock()
93 |
94 | if reporter == nil {
95 | return // Reports are disabled
96 | }
97 | result := "failed"
98 | if summary.DownloadBytes > 0 {
99 | result = "success"
100 | }
101 | response := "closed"
102 | if summary.Retry.Timeout {
103 | response = "timeout"
104 | }
105 | resultValue, err := choir.NewValue(result)
106 | if err != nil {
107 | log.Fatalf("Bad result %s: %v", result, err)
108 | }
109 | responseValue, err := choir.NewValue(response)
110 | if err != nil {
111 | log.Fatalf("Bad response %s: %v", response, err)
112 | }
113 | if err := reporter.Report(summary.Retry.SNI, resultValue, responseValue); err != nil {
114 | log.Warnf("Choir report failed: %v", err)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/intra/sni_reporter_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "bytes"
19 | "errors"
20 | "strings"
21 | "testing"
22 |
23 | "golang.org/x/net/dns/dnsmessage"
24 |
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split"
27 | )
28 |
29 | type qfunc func(q []byte) ([]byte, error)
30 |
31 | type fakeTransport struct {
32 | doh.Transport
33 | query qfunc
34 | }
35 |
36 | func (t *fakeTransport) Query(q []byte) ([]byte, error) {
37 | return t.query(q)
38 | }
39 |
40 | func newFakeTransport(query qfunc) *fakeTransport {
41 | return &fakeTransport{query: query}
42 | }
43 |
44 | func sendReport(t *testing.T, r *tcpSNIReporter, summary TCPSocketSummary, response []byte, responseErr error) string {
45 | // This function blocks for the burst duration (10 seconds), so it's important that
46 | // all tests that use it run in parallel to avoid extreme test delays.
47 | t.Parallel()
48 |
49 | c := make(chan string)
50 | dns := newFakeTransport(func(q []byte) ([]byte, error) {
51 | var msg dnsmessage.Message
52 | err := msg.Unpack(q)
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | name := msg.Questions[0].Name.String()
57 | c <- name
58 | return response, responseErr
59 | })
60 | r.SetDNS(dns)
61 | r.Report(summary)
62 | return <-c
63 | }
64 |
65 | const suffix = "mydomain.example"
66 | const country = "zz"
67 |
68 | func runSuccessTest(t *testing.T, summary TCPSocketSummary) string {
69 | r := tcpSNIReporter{}
70 | var stubFile bytes.Buffer
71 | r.Configure(&stubFile, suffix, country)
72 | return sendReport(t, &r, summary, make([]byte, 100), nil)
73 | }
74 |
75 | func TestSuccessClosed(t *testing.T) {
76 | summary := TCPSocketSummary{
77 | DownloadBytes: 10000, // >0 indicates success
78 | UploadBytes: 5000,
79 | Retry: &split.RetryStats{
80 | Timeout: false, // Socket was explicitly closed
81 | Split: 48, // >0 indicates a split was attempted
82 | SNI: "user.domain.test", // SNI of the socket
83 | },
84 | }
85 | name := runSuccessTest(t, summary)
86 | labels := strings.Split(name, ".")
87 | if labels[0] != "success" {
88 | t.Errorf("Bad name %s, %s != success", name, labels[0])
89 | }
90 | if labels[1] != "closed" {
91 | t.Errorf("Bad name %s, %s != closed", name, labels[1])
92 | }
93 | // labels[2] is the bin, which is random.
94 | if labels[3] != "zz" {
95 | t.Errorf("Bad name %s, %s != zz", name, labels[1])
96 | }
97 | // labels[4] is the date, which is not controlled by the code under test.
98 | remainder := strings.Join(labels[5:], ".")
99 | expected := summary.Retry.SNI + "." + suffix + "."
100 | if remainder != expected {
101 | t.Errorf("Bad name %s, %s != %s", name, remainder, expected)
102 | }
103 | }
104 |
105 | func TestTimeout(t *testing.T) {
106 | summary := TCPSocketSummary{
107 | DownloadBytes: 10000, // >0 indicates success
108 | UploadBytes: 5000,
109 | Retry: &split.RetryStats{
110 | Timeout: true, // Socket timed out
111 | Split: 54, // >0 indicates a split was attempted
112 | SNI: "user.domain.test", // SNI of the socket
113 | },
114 | }
115 | name := runSuccessTest(t, summary)
116 | labels := strings.Split(name, ".")
117 | if labels[1] != "timeout" {
118 | t.Errorf("Bad name %s, %s != timeout", name, labels[1])
119 | }
120 | }
121 |
122 | func TestFail(t *testing.T) {
123 | summary := TCPSocketSummary{
124 | DownloadBytes: 0, // 0 indicates failure
125 | UploadBytes: 500,
126 | Retry: &split.RetryStats{
127 | Timeout: true, // Socket timed out
128 | Split: 36, // >0 indicates a split was attempted
129 | SNI: "user.domain.test", // SNI of the socket
130 | },
131 | }
132 | name := runSuccessTest(t, summary)
133 | labels := strings.Split(name, ".")
134 | if labels[0] != "failed" {
135 | t.Errorf("Bad name %s, %s != failed", name, labels[0])
136 | }
137 | }
138 |
139 | func TestError(t *testing.T) {
140 | r := tcpSNIReporter{}
141 | var stubFile bytes.Buffer
142 | r.Configure(&stubFile, suffix, country)
143 | summary := TCPSocketSummary{
144 | DownloadBytes: 5000,
145 | UploadBytes: 500,
146 | Retry: &split.RetryStats{
147 | Timeout: true,
148 | Split: 36,
149 | SNI: "user.domain.test",
150 | },
151 | }
152 | // Verify that I/O errors don't cause a panic.
153 | sendReport(t, &r, summary, nil, errors.New("DNS send failed"))
154 | }
155 |
156 | func TestNoSplit(t *testing.T) {
157 | r := tcpSNIReporter{}
158 | var stubFile bytes.Buffer
159 | r.Configure(&stubFile, suffix, country)
160 | summary := TCPSocketSummary{
161 | DownloadBytes: 5000,
162 | UploadBytes: 500,
163 | Retry: &split.RetryStats{
164 | Timeout: true,
165 | Split: 0,
166 | SNI: "user.domain.test",
167 | },
168 | }
169 | dns := newFakeTransport(func(q []byte) ([]byte, error) {
170 | t.Error("DNS query function should not be called because no split was performed")
171 | return nil, errors.New("Unreachable")
172 | })
173 | r.SetDNS(dns)
174 | r.Report(summary)
175 | }
176 |
177 | func TestUnconfigured(t *testing.T) {
178 | r := tcpSNIReporter{}
179 | summary := TCPSocketSummary{
180 | DownloadBytes: 5000,
181 | UploadBytes: 500,
182 | Retry: &split.RetryStats{
183 | Timeout: true,
184 | Split: 45,
185 | SNI: "user.domain.test",
186 | },
187 | }
188 | dns := newFakeTransport(func(q []byte) ([]byte, error) {
189 | t.Error("DNS query function should not be called because the reporter is not configured")
190 | return nil, errors.New("Unreachable")
191 | })
192 | r.SetDNS(dns)
193 | r.Report(summary)
194 | }
195 |
196 | func TestNoDNS(t *testing.T) {
197 | r := tcpSNIReporter{}
198 | summary := TCPSocketSummary{
199 | DownloadBytes: 5000,
200 | UploadBytes: 500,
201 | Retry: &split.RetryStats{
202 | Timeout: true,
203 | Split: 45,
204 | SNI: "user.domain.test",
205 | },
206 | }
207 | // Verify that this doesn't panic.
208 | r.Report(summary)
209 | }
210 |
--------------------------------------------------------------------------------
/intra/split/direct_split.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package split
16 |
17 | import (
18 | "io"
19 | "net"
20 | )
21 |
22 | // DuplexConn represents a bidirectional stream socket.
23 | type DuplexConn interface {
24 | net.Conn
25 | io.ReaderFrom
26 | CloseWrite() error
27 | CloseRead() error
28 | }
29 |
30 | type splitter struct {
31 | *net.TCPConn
32 | used bool // Initially false. Becomes true after the first write.
33 | }
34 |
35 | // DialWithSplit returns a TCP connection that always splits the initial upstream segment.
36 | // Like net.Conn, it is intended for two-threaded use, with one thread calling
37 | // Read and CloseRead, and another calling Write, ReadFrom, and CloseWrite.
38 | func DialWithSplit(d *net.Dialer, addr *net.TCPAddr) (DuplexConn, error) {
39 | conn, err := d.Dial(addr.Network(), addr.String())
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | return &splitter{TCPConn: conn.(*net.TCPConn)}, nil
45 | }
46 |
47 | // Write-related functions
48 | func (s *splitter) Write(b []byte) (int, error) {
49 | conn := s.TCPConn
50 | if s.used {
51 | // After the first write, there is no special write behavior.
52 | return conn.Write(b)
53 | }
54 |
55 | // Setting `used` to true ensures that this code only runs once per socket.
56 | s.used = true
57 | b1, b2 := splitHello(b)
58 | n1, err := conn.Write(b1)
59 | if err != nil {
60 | return n1, err
61 | }
62 | n2, err := conn.Write(b2)
63 | return n1 + n2, err
64 | }
65 |
66 | func (s *splitter) ReadFrom(reader io.Reader) (bytes int64, err error) {
67 | if !s.used {
68 | // This is the first write on this socket.
69 | // Use copyOnce(), which calls Write(), to get Write's splitting behavior for
70 | // the first segment.
71 | if bytes, err = copyOnce(s, reader); err != nil {
72 | return
73 | }
74 | }
75 |
76 | var b int64
77 | b, err = s.TCPConn.ReadFrom(reader)
78 | bytes += b
79 | return
80 | }
81 |
--------------------------------------------------------------------------------
/intra/split/example/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "crypto/tls"
19 | "flag"
20 | "fmt"
21 | "log"
22 | "net"
23 | "os"
24 |
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split"
26 | )
27 |
28 | func main() {
29 | flag.Usage = func() {
30 | fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [-sni=SNI] destination\n", os.Args[0])
31 | fmt.Fprintln(flag.CommandLine.Output(), "This tool attempts a TLS connection to the "+
32 | "destination (port 443), with and without splitting. If the SNI is specified, it "+
33 | "overrides the destination, which can be an IP address.")
34 | flag.PrintDefaults()
35 | }
36 |
37 | sni := flag.String("sni", "", "Server name override")
38 | flag.Parse()
39 | destination := flag.Arg(0)
40 | if destination == "" {
41 | flag.Usage()
42 | return
43 | }
44 |
45 | addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(destination, "443"))
46 | if err != nil {
47 | log.Fatalf("Couldn't resolve destination: %v", err)
48 | }
49 |
50 | if *sni == "" {
51 | *sni = destination
52 | }
53 | tlsConfig := &tls.Config{ServerName: *sni}
54 |
55 | log.Println("Trying direct connection")
56 | conn, err := net.DialTCP(addr.Network(), nil, addr)
57 | if err != nil {
58 | log.Fatalf("Could not establish a TCP connection: %v", err)
59 | }
60 | tlsConn := tls.Client(conn, tlsConfig)
61 | err = tlsConn.Handshake()
62 | if err != nil {
63 | log.Printf("Direct TLS handshake failed: %v", err)
64 | } else {
65 | log.Printf("Direct TLS succeeded")
66 | }
67 |
68 | log.Println("Trying split connection")
69 | splitConn, err := split.DialWithSplit(&net.Dialer{}, addr)
70 | if err != nil {
71 | log.Fatalf("Could not establish a splitting socket: %v", err)
72 | }
73 | tlsConn2 := tls.Client(splitConn, tlsConfig)
74 | err = tlsConn2.Handshake()
75 | if err != nil {
76 | log.Printf("Split TLS handshake failed: %v", err)
77 | } else {
78 | log.Printf("Split TLS succeeded")
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/intra/split/retrier.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package split
16 |
17 | import (
18 | "errors"
19 | "io"
20 | "math/rand"
21 | "net"
22 | "sync"
23 | "time"
24 |
25 | "github.com/Jigsaw-Code/getsni"
26 | )
27 |
28 | type RetryStats struct {
29 | SNI string // TLS SNI observed, if present.
30 | Bytes int32 // Number of bytes uploaded before the retry.
31 | Chunks int16 // Number of writes before the retry.
32 | Split int16 // Number of bytes in the first retried segment.
33 | Timeout bool // True if the retry was caused by a timeout.
34 | }
35 |
36 | // retrier implements the DuplexConn interface.
37 | type retrier struct {
38 | // mutex is a lock that guards `conn`, `hello`, and `retryCompleteFlag`.
39 | // These fields must not be modified except under this lock.
40 | // After retryCompletedFlag is closed, these values will not be modified
41 | // again so locking is no longer required for reads.
42 | mutex sync.Mutex
43 | dialer *net.Dialer
44 | network string
45 | addr *net.TCPAddr
46 | // conn is the current underlying connection. It is only modified by the reader
47 | // thread, so the reader functions may access it without acquiring a lock.
48 | conn *net.TCPConn
49 | // External read and write deadlines. These need to be stored here so that
50 | // they can be re-applied in the event of a retry.
51 | readDeadline time.Time
52 | writeDeadline time.Time
53 | // Time to wait between the first write and the first read before triggering a
54 | // retry.
55 | timeout time.Duration
56 | // hello is the contents written before the first read. It is initially empty,
57 | // and is cleared when the first byte is received.
58 | hello []byte
59 | // Flag indicating when retry is finished or unnecessary.
60 | retryCompleteFlag chan struct{}
61 | // Flags indicating whether the caller has called CloseRead and CloseWrite.
62 | readCloseFlag chan struct{}
63 | writeCloseFlag chan struct{}
64 | stats *RetryStats
65 | }
66 |
67 | // Helper functions for reading flags.
68 | // In this package, a "flag" is a thread-safe single-use status indicator that
69 | // starts in the "open" state and transitions to "closed" when close() is called.
70 | // It is implemented as a channel over which no data is ever sent.
71 | // Some advantages of this implementation:
72 | // - The language enforces the one-way transition.
73 | // - Nonblocking and blocking access are both straightforward.
74 | // - Checking the status of a closed flag should be extremely fast (although currently
75 | // it's not optimized: https://github.com/golang/go/issues/32529)
76 | func closed(c chan struct{}) bool {
77 | select {
78 | case <-c:
79 | // The channel has been closed.
80 | return true
81 | default:
82 | return false
83 | }
84 | }
85 |
86 | func (r *retrier) readClosed() bool {
87 | return closed(r.readCloseFlag)
88 | }
89 |
90 | func (r *retrier) writeClosed() bool {
91 | return closed(r.writeCloseFlag)
92 | }
93 |
94 | func (r *retrier) retryCompleted() bool {
95 | return closed(r.retryCompleteFlag)
96 | }
97 |
98 | // Given timestamps immediately before and after a successful socket connection
99 | // (i.e. the time the SYN was sent and the time the SYNACK was received), this
100 | // function returns a reasonable timeout for replies to a hello sent on this socket.
101 | func timeout(before, after time.Time) time.Duration {
102 | // These values were chosen to have a <1% false positive rate based on test data.
103 | // False positives trigger an unnecessary retry, which can make connections slower, so they are
104 | // worth avoiding. However, overly long timeouts make retry slower and less useful.
105 | rtt := after.Sub(before)
106 | return 1200*time.Millisecond + 2*rtt
107 | }
108 |
109 | // DefaultTimeout is the value that will cause DialWithSplitRetry to use the system's
110 | // default TCP timeout (typically 2-3 minutes).
111 | const DefaultTimeout time.Duration = 0
112 |
113 | // DialWithSplitRetry returns a TCP connection that transparently retries by
114 | // splitting the initial upstream segment if the socket closes without receiving a
115 | // reply. Like net.Conn, it is intended for two-threaded use, with one thread calling
116 | // Read and CloseRead, and another calling Write, ReadFrom, and CloseWrite.
117 | // `dialer` will be used to establish the connection.
118 | // `addr` is the destination.
119 | // If `stats` is non-nil, it will be populated with retry-related information.
120 | func DialWithSplitRetry(dialer *net.Dialer, addr *net.TCPAddr, stats *RetryStats) (DuplexConn, error) {
121 | before := time.Now()
122 | conn, err := dialer.Dial(addr.Network(), addr.String())
123 | if err != nil {
124 | return nil, err
125 | }
126 | after := time.Now()
127 |
128 | if stats == nil {
129 | // This is a fake stats object that will be written but never read. Its purpose
130 | // is to avoid the need for nil checks at each point where stats are updated.
131 | stats = &RetryStats{}
132 | }
133 |
134 | r := &retrier{
135 | dialer: dialer,
136 | addr: addr,
137 | conn: conn.(*net.TCPConn),
138 | timeout: timeout(before, after),
139 | retryCompleteFlag: make(chan struct{}),
140 | readCloseFlag: make(chan struct{}),
141 | writeCloseFlag: make(chan struct{}),
142 | stats: stats,
143 | }
144 |
145 | return r, nil
146 | }
147 |
148 | // Read-related functions.
149 | func (r *retrier) Read(buf []byte) (n int, err error) {
150 | n, err = r.conn.Read(buf)
151 | if n == 0 && err == nil {
152 | // If no data was read, a nil error doesn't rule out the need for a retry.
153 | return
154 | }
155 | if !r.retryCompleted() {
156 | r.mutex.Lock()
157 | if err != nil {
158 | var neterr net.Error
159 | if errors.As(err, &neterr) {
160 | r.stats.Timeout = neterr.Timeout()
161 | }
162 | // Read failed. Retry.
163 | n, err = r.retry(buf)
164 | }
165 | close(r.retryCompleteFlag)
166 | // Unset read deadline.
167 | r.conn.SetReadDeadline(time.Time{})
168 | r.hello = nil
169 | r.mutex.Unlock()
170 | }
171 | return
172 | }
173 |
174 | func (r *retrier) retry(buf []byte) (n int, err error) {
175 | r.conn.Close()
176 | var newConn net.Conn
177 | if newConn, err = r.dialer.Dial(r.addr.Network(), r.addr.String()); err != nil {
178 | return
179 | }
180 | r.conn = newConn.(*net.TCPConn)
181 | first, second := splitHello(r.hello)
182 | r.stats.Split = int16(len(first))
183 | if _, err = r.conn.Write(first); err != nil {
184 | return
185 | }
186 | if _, err = r.conn.Write(second); err != nil {
187 | return
188 | }
189 | // While we were creating the new socket, the caller might have called CloseRead
190 | // or CloseWrite on the old socket. Copy that state to the new socket.
191 | // CloseRead and CloseWrite are idempotent, so this is safe even if the user's
192 | // action actually affected the new socket.
193 | if r.readClosed() {
194 | r.conn.CloseRead()
195 | }
196 | if r.writeClosed() {
197 | r.conn.CloseWrite()
198 | }
199 | // The caller might have set read or write deadlines before the retry.
200 | r.conn.SetReadDeadline(r.readDeadline)
201 | r.conn.SetWriteDeadline(r.writeDeadline)
202 | return r.conn.Read(buf)
203 | }
204 |
205 | func (r *retrier) CloseRead() error {
206 | if !r.readClosed() {
207 | close(r.readCloseFlag)
208 | }
209 | r.mutex.Lock()
210 | defer r.mutex.Unlock()
211 | return r.conn.CloseRead()
212 | }
213 |
214 | func splitHello(hello []byte) ([]byte, []byte) {
215 | if len(hello) == 0 {
216 | return hello, hello
217 | }
218 | const (
219 | MIN_SPLIT int = 32
220 | MAX_SPLIT int = 64
221 | )
222 |
223 | // Random number in the range [MIN_SPLIT, MAX_SPLIT]
224 | s := MIN_SPLIT + rand.Intn(MAX_SPLIT+1-MIN_SPLIT)
225 | limit := len(hello) / 2
226 | if s > limit {
227 | s = limit
228 | }
229 | return hello[:s], hello[s:]
230 | }
231 |
232 | // Write-related functions
233 | func (r *retrier) Write(b []byte) (int, error) {
234 | // Double-checked locking pattern. This avoids lock acquisition on
235 | // every packet after retry completes, while also ensuring that r.hello is
236 | // empty at steady-state.
237 | if !r.retryCompleted() {
238 | n := 0
239 | var err error
240 | attempted := false
241 | r.mutex.Lock()
242 | if !r.retryCompleted() {
243 | n, err = r.conn.Write(b)
244 | attempted = true
245 | r.hello = append(r.hello, b[:n]...)
246 |
247 | r.stats.Chunks++
248 | r.stats.Bytes = int32(len(r.hello))
249 | if r.stats.SNI == "" {
250 | r.stats.SNI, _ = getsni.GetSNI(r.hello)
251 | }
252 |
253 | // We require a response or another write within the specified timeout.
254 | r.conn.SetReadDeadline(time.Now().Add(r.timeout))
255 | }
256 | r.mutex.Unlock()
257 | if attempted {
258 | if err == nil {
259 | return n, nil
260 | }
261 | // A write error occurred on the provisional socket. This should be handled
262 | // by the retry procedure. Block until we have a final socket (which will
263 | // already have replayed b[:n]), and retry.
264 | <-r.retryCompleteFlag
265 | r.mutex.Lock()
266 | r.mutex.Unlock()
267 | m, err := r.conn.Write(b[n:])
268 | return n + m, err
269 | }
270 | }
271 |
272 | // retryCompleted() is true, so r.conn is final and doesn't need locking.
273 | return r.conn.Write(b)
274 | }
275 |
276 | // Copy one buffer from src to dst, using dst.Write.
277 | func copyOnce(dst io.Writer, src io.Reader) (int64, error) {
278 | // This buffer is large enough to hold any ordinary first write
279 | // without introducing extra splitting.
280 | buf := make([]byte, 2048)
281 | n, err := src.Read(buf)
282 | if err != nil {
283 | return 0, err
284 | }
285 | n, err = dst.Write(buf[:n])
286 | return int64(n), err
287 | }
288 |
289 | func (r *retrier) ReadFrom(reader io.Reader) (bytes int64, err error) {
290 | for !r.retryCompleted() {
291 | if bytes, err = copyOnce(r, reader); err != nil {
292 | return
293 | }
294 | }
295 |
296 | var b int64
297 | b, err = r.conn.ReadFrom(reader)
298 | bytes += b
299 | return
300 | }
301 |
302 | func (r *retrier) CloseWrite() error {
303 | if !r.writeClosed() {
304 | close(r.writeCloseFlag)
305 | }
306 | r.mutex.Lock()
307 | defer r.mutex.Unlock()
308 | return r.conn.CloseWrite()
309 | }
310 |
311 | func (r *retrier) Close() error {
312 | if err := r.CloseWrite(); err != nil {
313 | return err
314 | }
315 | return r.CloseRead()
316 | }
317 |
318 | // LocalAddr behaves slightly strangely: its value may change as a
319 | // result of a retry. However, LocalAddr is largely useless for
320 | // TCP client sockets anyway, so nothing should be relying on this.
321 | func (r *retrier) LocalAddr() net.Addr {
322 | r.mutex.Lock()
323 | defer r.mutex.Unlock()
324 | return r.conn.LocalAddr()
325 | }
326 |
327 | func (r *retrier) RemoteAddr() net.Addr {
328 | return r.addr
329 | }
330 |
331 | func (r *retrier) SetReadDeadline(t time.Time) error {
332 | r.mutex.Lock()
333 | defer r.mutex.Unlock()
334 | r.readDeadline = t
335 | // Don't enforce read deadlines until after the retry
336 | // is complete. Retry relies on setting its own read
337 | // deadline, and we don't want this to interfere.
338 | if r.retryCompleted() {
339 | return r.conn.SetReadDeadline(t)
340 | }
341 | return nil
342 | }
343 |
344 | func (r *retrier) SetWriteDeadline(t time.Time) error {
345 | r.mutex.Lock()
346 | defer r.mutex.Unlock()
347 | r.writeDeadline = t
348 | return r.conn.SetWriteDeadline(t)
349 | }
350 |
351 | func (r *retrier) SetDeadline(t time.Time) error {
352 | e1 := r.SetReadDeadline(t)
353 | e2 := r.SetWriteDeadline(t)
354 | if e1 != nil {
355 | return e1
356 | }
357 | return e2
358 | }
359 |
--------------------------------------------------------------------------------
/intra/split/retrier_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package split
16 |
17 | import (
18 | "bytes"
19 | "io"
20 | "net"
21 | "testing"
22 | "time"
23 | )
24 |
25 | type setup struct {
26 | t *testing.T
27 | server *net.TCPListener
28 | clientSide DuplexConn
29 | serverSide *net.TCPConn
30 | serverReceived []byte
31 | stats *RetryStats
32 | }
33 |
34 | func makeSetup(t *testing.T) *setup {
35 | addr, err := net.ResolveTCPAddr("tcp", ":0")
36 | if err != nil {
37 | t.Error(err)
38 | }
39 | server, err := net.ListenTCP("tcp", addr)
40 | if err != nil {
41 | t.Error(err)
42 | }
43 |
44 | serverAddr, ok := server.Addr().(*net.TCPAddr)
45 | if !ok {
46 | t.Error("Server isn't TCP?")
47 | }
48 | var stats RetryStats
49 | clientSide, err := DialWithSplitRetry(&net.Dialer{}, serverAddr, &stats)
50 | if err != nil {
51 | t.Error(err)
52 | }
53 | serverSide, err := server.AcceptTCP()
54 | if err != nil {
55 | t.Error(err)
56 | }
57 | return &setup{t, server, clientSide, serverSide, nil, &stats}
58 | }
59 |
60 | const BUFSIZE = 256
61 |
62 | func makeBuffer() []byte {
63 | buffer := make([]byte, BUFSIZE)
64 | for i := 0; i < BUFSIZE; i++ {
65 | buffer[i] = byte(i)
66 | }
67 | return buffer
68 | }
69 |
70 | func send(src io.Writer, dest io.Reader, t *testing.T) []byte {
71 | buffer := makeBuffer()
72 | n, err := src.Write(buffer)
73 | if err != nil {
74 | t.Error(err)
75 | }
76 | if n != len(buffer) {
77 | t.Errorf("Failed to write whole buffer: %d", n)
78 | }
79 |
80 | buf := make([]byte, len(buffer))
81 | n, err = dest.Read(buf)
82 | if err != nil {
83 | t.Error(nil)
84 | }
85 | if n != len(buf) {
86 | t.Errorf("Not enough bytes: %d", n)
87 | }
88 | if !bytes.Equal(buf, buffer) {
89 | t.Errorf("Wrong contents")
90 | }
91 | return buf
92 | }
93 |
94 | func (s *setup) sendUp() {
95 | buf := send(s.clientSide, s.serverSide, s.t)
96 | s.serverReceived = append(s.serverReceived, buf...)
97 | }
98 |
99 | func (s *setup) sendDown() {
100 | send(s.serverSide, s.clientSide, s.t)
101 | }
102 |
103 | func closeRead(closed, blocked DuplexConn, t *testing.T) {
104 | closed.CloseRead()
105 | // TODO: Figure out if this is detectable on the opposite side.
106 | }
107 |
108 | func closeWrite(closed, blocked DuplexConn, t *testing.T) {
109 | closed.CloseWrite()
110 | n, err := blocked.Read(make([]byte, 1))
111 | if err != io.EOF || n > 0 {
112 | t.Errorf("Read should have failed with EOF")
113 | }
114 | }
115 |
116 | func (s *setup) closeReadUp() {
117 | closeRead(s.clientSide, s.serverSide, s.t)
118 | }
119 |
120 | func (s *setup) closeWriteUp() {
121 | closeWrite(s.clientSide, s.serverSide, s.t)
122 | }
123 |
124 | func (s *setup) closeReadDown() {
125 | closeRead(s.serverSide, s.clientSide, s.t)
126 | }
127 |
128 | func (s *setup) closeWriteDown() {
129 | closeWrite(s.serverSide, s.clientSide, s.t)
130 | }
131 |
132 | func (s *setup) close() {
133 | s.server.Close()
134 | }
135 |
136 | func (s *setup) confirmRetry() {
137 | done := make(chan struct{})
138 | go func() {
139 | buf := make([]byte, len(s.serverReceived))
140 | n, err := s.clientSide.Read(buf)
141 | if err != nil {
142 | s.t.Error(err)
143 | }
144 | if n != len(buf) {
145 | s.t.Error("Unexpected echo length")
146 | }
147 | close(done)
148 | }()
149 |
150 | var err error
151 | s.serverSide, err = s.server.AcceptTCP()
152 | if err != nil {
153 | s.t.Errorf("Second socket failed")
154 | }
155 | buf := make([]byte, len(s.serverReceived))
156 | var n int
157 | for n < len(buf) {
158 | var m int
159 | m, err = s.serverSide.Read(buf[n:])
160 | n += m
161 | if err != nil {
162 | s.t.Error(err)
163 | }
164 | }
165 | if !bytes.Equal(buf, s.serverReceived) {
166 | s.t.Errorf("Replay was corrupted")
167 | }
168 |
169 | n, err = s.serverSide.Write(buf)
170 | if err != nil {
171 | s.t.Error(err)
172 | }
173 | if n != len(buf) {
174 | s.t.Errorf("Couldn't echo all bytes: %d", n)
175 | }
176 | <-done
177 | }
178 |
179 | func (s *setup) checkNoSplit() {
180 | if s.stats.Split > 0 {
181 | s.t.Error("Retry should not have occurred")
182 | }
183 | }
184 |
185 | func (s *setup) checkStats(bytes int32, chunks int16, timeout bool) {
186 | r := s.stats
187 | if r.Bytes != bytes {
188 | s.t.Errorf("Expected %d bytes, got %d", bytes, r.Bytes)
189 | }
190 | if r.Chunks != chunks {
191 | s.t.Errorf("Expected %d chunks, got %d", chunks, r.Chunks)
192 | }
193 | if r.Timeout != timeout {
194 | s.t.Errorf("Expected timeout to be %t", timeout)
195 | }
196 | if r.Split < 32 || r.Split > 64 {
197 | s.t.Errorf("Unexpected split: %d", r.Split)
198 | }
199 | }
200 |
201 | func TestNormalConnection(t *testing.T) {
202 | s := makeSetup(t)
203 | s.sendUp()
204 | s.sendDown()
205 | s.closeReadUp()
206 | s.closeWriteUp()
207 | s.close()
208 | s.checkNoSplit()
209 | }
210 |
211 | func TestFinRetry(t *testing.T) {
212 | s := makeSetup(t)
213 | s.sendUp()
214 | s.serverSide.Close()
215 | s.confirmRetry()
216 | s.sendDown()
217 | s.closeReadUp()
218 | s.closeWriteUp()
219 | s.close()
220 | s.checkStats(BUFSIZE, 1, false)
221 | }
222 |
223 | func TestTimeoutRetry(t *testing.T) {
224 | s := makeSetup(t)
225 | s.sendUp()
226 | // Client should time out and retry after about 1.2 seconds
227 | time.Sleep(2 * time.Second)
228 | s.confirmRetry()
229 | s.sendDown()
230 | s.closeReadUp()
231 | s.closeWriteUp()
232 | s.close()
233 | s.checkStats(BUFSIZE, 1, true)
234 | }
235 |
236 | func TestTwoWriteRetry(t *testing.T) {
237 | s := makeSetup(t)
238 | s.sendUp()
239 | s.sendUp()
240 | s.serverSide.Close()
241 | s.confirmRetry()
242 | s.sendDown()
243 | s.closeReadUp()
244 | s.closeWriteUp()
245 | s.close()
246 | s.checkStats(2*BUFSIZE, 2, false)
247 | }
248 |
249 | func TestFailedRetry(t *testing.T) {
250 | s := makeSetup(t)
251 | s.sendUp()
252 | s.serverSide.Close()
253 | s.confirmRetry()
254 | s.closeReadDown()
255 | s.closeWriteDown()
256 | s.close()
257 | s.checkStats(BUFSIZE, 1, false)
258 | }
259 |
260 | func TestDisappearingServer(t *testing.T) {
261 | s := makeSetup(t)
262 | s.sendUp()
263 | s.close()
264 | s.serverSide.Close()
265 | // Try to read 1 byte to trigger the retry.
266 | n, err := s.clientSide.Read(make([]byte, 1))
267 | if n > 0 || err == nil {
268 | t.Error("Expected read to fail")
269 | }
270 | s.clientSide.CloseRead()
271 | s.clientSide.CloseWrite()
272 | s.checkNoSplit()
273 | }
274 |
275 | func TestSequentialClose(t *testing.T) {
276 | s := makeSetup(t)
277 | s.sendUp()
278 | s.closeWriteUp()
279 | s.sendDown()
280 | s.closeWriteDown()
281 | s.close()
282 | s.checkNoSplit()
283 | }
284 |
285 | func TestBackwardsUse(t *testing.T) {
286 | s := makeSetup(t)
287 | s.sendDown()
288 | s.closeWriteDown()
289 | s.sendUp()
290 | s.closeWriteUp()
291 | s.close()
292 | s.checkNoSplit()
293 | }
294 |
295 | // Regression test for an issue in which the initial handshake timeout
296 | // continued to apply after the handshake completed.
297 | func TestIdle(t *testing.T) {
298 | s := makeSetup(t)
299 | s.sendUp()
300 | s.sendDown()
301 | // Wait for longer than the 1.2-second response timeout
302 | time.Sleep(2 * time.Second)
303 | // Try to send down some more data.
304 | s.sendDown()
305 | s.close()
306 | s.checkNoSplit()
307 | }
308 |
--------------------------------------------------------------------------------
/intra/stream_dialer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Jigsaw Operations LLC
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "fmt"
21 | "net"
22 | "net/netip"
23 | "sync/atomic"
24 | "time"
25 |
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
27 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"
28 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split"
29 | "github.com/Jigsaw-Code/outline-sdk/transport"
30 | )
31 |
32 | type intraStreamDialer struct {
33 | fakeDNSAddr netip.AddrPort
34 | dns atomic.Pointer[doh.Transport]
35 | dialer *net.Dialer
36 | alwaysSplitHTTPS atomic.Bool
37 | listener TCPListener
38 | sniReporter *tcpSNIReporter
39 | }
40 |
41 | var _ transport.StreamDialer = (*intraStreamDialer)(nil)
42 |
43 | func newIntraStreamDialer(
44 | fakeDNS netip.AddrPort,
45 | dns doh.Transport,
46 | protector protect.Protector,
47 | listener TCPListener,
48 | sniReporter *tcpSNIReporter,
49 | ) (*intraStreamDialer, error) {
50 | if dns == nil {
51 | return nil, errors.New("dns is required")
52 | }
53 |
54 | dohsd := &intraStreamDialer{
55 | fakeDNSAddr: fakeDNS,
56 | dialer: protect.MakeDialer(protector),
57 | listener: listener,
58 | sniReporter: sniReporter,
59 | }
60 | dohsd.dns.Store(&dns)
61 | return dohsd, nil
62 | }
63 |
64 | // Dial implements StreamDialer.Dial.
65 | func (sd *intraStreamDialer) Dial(ctx context.Context, raddr string) (transport.StreamConn, error) {
66 | dest, err := netip.ParseAddrPort(raddr)
67 | if err != nil {
68 | return nil, fmt.Errorf("invalid raddr (%v): %w", raddr, err)
69 | }
70 |
71 | if isEquivalentAddrPort(dest, sd.fakeDNSAddr) {
72 | src, dst := net.Pipe()
73 | go doh.Accept(*sd.dns.Load(), dst)
74 | return newStreamConnFromPipeConns(src, dst)
75 | }
76 |
77 | stats := makeTCPSocketSummary(dest)
78 | beforeConn := time.Now()
79 | conn, err := sd.dial(ctx, dest, stats)
80 | if err != nil {
81 | return nil, fmt.Errorf("failed to dial to target: %w", err)
82 | }
83 | stats.Synack = int32(time.Since(beforeConn).Milliseconds())
84 |
85 | return makeTCPWrapConn(conn, stats, sd.listener, sd.sniReporter), nil
86 | }
87 |
88 | func (sd *intraStreamDialer) SetDNS(dns doh.Transport) error {
89 | if dns == nil {
90 | return errors.New("dns is required")
91 | }
92 | sd.dns.Store(&dns)
93 | return nil
94 | }
95 |
96 | func (sd *intraStreamDialer) dial(ctx context.Context, dest netip.AddrPort, stats *TCPSocketSummary) (transport.StreamConn, error) {
97 | if dest.Port() == 443 {
98 | if sd.alwaysSplitHTTPS.Load() {
99 | return split.DialWithSplit(sd.dialer, net.TCPAddrFromAddrPort(dest))
100 | } else {
101 | stats.Retry = &split.RetryStats{}
102 | return split.DialWithSplitRetry(sd.dialer, net.TCPAddrFromAddrPort(dest), stats.Retry)
103 | }
104 | } else {
105 | tcpsd := &transport.TCPStreamDialer{
106 | Dialer: *sd.dialer,
107 | }
108 | return tcpsd.Dial(ctx, dest.String())
109 | }
110 | }
111 |
112 | // transport.StreamConn wrapper around net.Pipe call
113 |
114 | type pipeconn struct {
115 | net.Conn
116 | remote net.Conn
117 | }
118 |
119 | var _ transport.StreamConn = (*pipeconn)(nil)
120 |
121 | // newStreamConnFromPipeConns creates a new [transport.StreamConn] that wraps around the local [net.Conn].
122 | // The remote [net.Conn] will be closed when you call CloseRead() on the returned [transport.StreamConn]
123 | func newStreamConnFromPipeConns(local, remote net.Conn) (transport.StreamConn, error) {
124 | if local == nil || remote == nil {
125 | return nil, errors.New("local conn and remote conn are required")
126 | }
127 | return &pipeconn{local, remote}, nil
128 | }
129 |
130 | func (c *pipeconn) Close() error {
131 | return errors.Join(c.CloseRead(), c.CloseWrite())
132 | }
133 |
134 | // CloseRead makes sure all read on the local conn returns io.EOF, and write on the remote conn returns ErrClosedPipe.
135 | func (c *pipeconn) CloseRead() error {
136 | return c.remote.Close()
137 | }
138 |
139 | // CloseWrite makes sure all read on the remote conn returns io.EOF, and write on the local conn returns ErrClosedPipe.
140 | func (c *pipeconn) CloseWrite() error {
141 | return c.Conn.Close()
142 | }
143 |
--------------------------------------------------------------------------------
/intra/tcp.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Derived from go-tun2socks's "direct" handler under the Apache 2.0 license.
16 |
17 | package intra
18 |
19 | import (
20 | "io"
21 | "net/netip"
22 | "sync"
23 | "sync/atomic"
24 | "time"
25 |
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/split"
27 | "github.com/Jigsaw-Code/outline-sdk/transport"
28 | )
29 |
30 | // TCPSocketSummary provides information about each TCP socket, reported when it is closed.
31 | type TCPSocketSummary struct {
32 | DownloadBytes int64 // Total bytes downloaded.
33 | UploadBytes int64 // Total bytes uploaded.
34 | Duration int32 // Duration in seconds.
35 | ServerPort int16 // The server port. All values except 80, 443, and 0 are set to -1.
36 | Synack int32 // TCP handshake latency (ms)
37 | // Retry is non-nil if retry was possible. Retry.Split is non-zero if a retry occurred.
38 | Retry *split.RetryStats
39 | }
40 |
41 | func makeTCPSocketSummary(dest netip.AddrPort) *TCPSocketSummary {
42 | stats := &TCPSocketSummary{
43 | ServerPort: int16(dest.Port()),
44 | }
45 | if stats.ServerPort != 0 && stats.ServerPort != 80 && stats.ServerPort != 443 {
46 | stats.ServerPort = -1
47 | }
48 | return stats
49 | }
50 |
51 | // TCPListener is notified when a socket closes.
52 | type TCPListener interface {
53 | OnTCPSocketClosed(*TCPSocketSummary)
54 | }
55 |
56 | type tcpWrapConn struct {
57 | transport.StreamConn
58 |
59 | wg *sync.WaitGroup
60 | rDone, wDone atomic.Bool
61 |
62 | beginTime time.Time
63 | stats *TCPSocketSummary
64 |
65 | listener TCPListener
66 | sniReporter *tcpSNIReporter
67 | }
68 |
69 | func makeTCPWrapConn(c transport.StreamConn, stats *TCPSocketSummary, listener TCPListener, sniReporter *tcpSNIReporter) (conn *tcpWrapConn) {
70 | conn = &tcpWrapConn{
71 | StreamConn: c,
72 | wg: &sync.WaitGroup{},
73 | beginTime: time.Now(),
74 | stats: stats,
75 | listener: listener,
76 | sniReporter: sniReporter,
77 | }
78 |
79 | // Wait until both read and write are done
80 | conn.wg.Add(2)
81 | go func() {
82 | conn.wg.Wait()
83 | conn.stats.Duration = int32(time.Since(conn.beginTime))
84 | if conn.listener != nil {
85 | conn.listener.OnTCPSocketClosed(conn.stats)
86 | }
87 | if conn.stats.Retry != nil && conn.sniReporter != nil {
88 | conn.sniReporter.Report(*conn.stats)
89 | }
90 | }()
91 |
92 | return
93 | }
94 |
95 | func (conn *tcpWrapConn) Close() error {
96 | defer conn.close(&conn.wDone)
97 | defer conn.close(&conn.rDone)
98 | return conn.StreamConn.Close()
99 | }
100 |
101 | func (conn *tcpWrapConn) CloseRead() error {
102 | defer conn.close(&conn.rDone)
103 | return conn.StreamConn.CloseRead()
104 | }
105 |
106 | func (conn *tcpWrapConn) CloseWrite() error {
107 | defer conn.close(&conn.wDone)
108 | return conn.StreamConn.CloseWrite()
109 | }
110 |
111 | func (conn *tcpWrapConn) Read(b []byte) (n int, err error) {
112 | defer func() {
113 | conn.stats.DownloadBytes += int64(n)
114 | }()
115 | return conn.StreamConn.Read(b)
116 | }
117 |
118 | func (conn *tcpWrapConn) WriteTo(w io.Writer) (n int64, err error) {
119 | defer func() {
120 | conn.stats.DownloadBytes += n
121 | }()
122 | return io.Copy(w, conn.StreamConn)
123 | }
124 |
125 | func (conn *tcpWrapConn) Write(b []byte) (n int, err error) {
126 | defer func() {
127 | conn.stats.UploadBytes += int64(n)
128 | }()
129 | return conn.StreamConn.Write(b)
130 | }
131 |
132 | func (conn *tcpWrapConn) ReadFrom(r io.Reader) (n int64, err error) {
133 | defer func() {
134 | conn.stats.UploadBytes += n
135 | }()
136 | return io.Copy(conn.StreamConn, r)
137 | }
138 |
139 | func (conn *tcpWrapConn) close(done *atomic.Bool) {
140 | // make sure conn.wg is being called at most once for a specific `done` flag
141 | if done.CompareAndSwap(false, true) {
142 | conn.wg.Done()
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/intra/tunnel.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package intra
16 |
17 | import (
18 | "errors"
19 | "fmt"
20 | "io"
21 | "net"
22 | "os"
23 | "strings"
24 |
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/doh"
26 | "github.com/Jigsaw-Code/outline-go-tun2socks/intra/protect"
27 | "github.com/Jigsaw-Code/outline-sdk/network"
28 | "github.com/Jigsaw-Code/outline-sdk/network/lwip2transport"
29 | )
30 |
31 | // Listener receives usage statistics when a UDP or TCP socket is closed,
32 | // or a DNS query is completed.
33 | type Listener interface {
34 | UDPListener
35 | TCPListener
36 | doh.Listener
37 | }
38 |
39 | // Tunnel represents an Intra session.
40 | type Tunnel struct {
41 | network.IPDevice
42 |
43 | sd *intraStreamDialer
44 | pp *intraPacketProxy
45 | sni *tcpSNIReporter
46 | tun io.Closer
47 | }
48 |
49 | // NewTunnel creates a connected Intra session.
50 | //
51 | // `fakedns` is the DNS server (IP and port) that will be used by apps on the TUN device.
52 | //
53 | // This will normally be a reserved or remote IP address, port 53.
54 | //
55 | // `udpdns` and `tcpdns` are the actual location of the DNS server in use.
56 | //
57 | // These will normally be localhost with a high-numbered port.
58 | //
59 | // `dohdns` is the initial DOH transport.
60 | // `eventListener` will be notified at the completion of every tunneled socket.
61 | func NewTunnel(
62 | fakedns string, dohdns doh.Transport, tun io.Closer, protector protect.Protector, eventListener Listener,
63 | ) (t *Tunnel, err error) {
64 | if eventListener == nil {
65 | return nil, errors.New("eventListener is required")
66 | }
67 |
68 | fakeDNSAddr, err := net.ResolveUDPAddr("udp", fakedns)
69 | if err != nil {
70 | return nil, fmt.Errorf("failed to resolve fakedns: %w", err)
71 | }
72 |
73 | t = &Tunnel{
74 | sni: &tcpSNIReporter{
75 | dns: dohdns,
76 | },
77 | tun: tun,
78 | }
79 |
80 | t.sd, err = newIntraStreamDialer(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener, t.sni)
81 | if err != nil {
82 | return nil, fmt.Errorf("failed to create stream dialer: %w", err)
83 | }
84 |
85 | t.pp, err = newIntraPacketProxy(fakeDNSAddr.AddrPort(), dohdns, protector, eventListener)
86 | if err != nil {
87 | return nil, fmt.Errorf("failed to create packet proxy: %w", err)
88 | }
89 |
90 | if t.IPDevice, err = lwip2transport.ConfigureDevice(t.sd, t.pp); err != nil {
91 | return nil, fmt.Errorf("failed to configure lwIP stack: %w", err)
92 | }
93 |
94 | t.SetDNS(dohdns)
95 | return
96 | }
97 |
98 | // Set the DNSTransport. This method must be called before connecting the transport
99 | // to the TUN device. The transport can be changed at any time during operation, but
100 | // must not be nil.
101 | func (t *Tunnel) SetDNS(dns doh.Transport) {
102 | t.sd.SetDNS(dns)
103 | t.pp.SetDNS(dns)
104 | t.sni.SetDNS(dns)
105 | }
106 |
107 | // Enable reporting of SNIs that resulted in connection failures, using the
108 | // Choir library for privacy-preserving error reports. `file` is the path
109 | // that Choir should use to store its persistent state, `suffix` is the
110 | // authoritative domain to which reports will be sent, and `country` is a
111 | // two-letter ISO country code for the user's current location.
112 | func (t *Tunnel) EnableSNIReporter(filename, suffix, country string) error {
113 | f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
114 | if err != nil {
115 | return err
116 | }
117 | return t.sni.Configure(f, suffix, strings.ToLower(country))
118 | }
119 |
120 | func (t *Tunnel) Disconnect() {
121 | t.Close()
122 | t.tun.Close()
123 | }
124 |
--------------------------------------------------------------------------------
/intra/udp.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Derived from go-tun2socks's "direct" handler under the Apache 2.0 license.
16 |
17 | package intra
18 |
19 | import (
20 | "sync/atomic"
21 | "time"
22 | )
23 |
24 | // UDPSocketSummary describes a non-DNS UDP association, reported when it is discarded.
25 | type UDPSocketSummary struct {
26 | UploadBytes int64 // Amount uploaded (bytes)
27 | DownloadBytes int64 // Amount downloaded (bytes)
28 | Duration int32 // How long the socket was open (seconds)
29 | }
30 |
31 | // UDPListener is notified when a non-DNS UDP association is discarded.
32 | type UDPListener interface {
33 | OnUDPSocketClosed(*UDPSocketSummary)
34 | }
35 |
36 | type tracker struct {
37 | start time.Time
38 | upload atomic.Int64 // Non-DNS upload bytes
39 | download atomic.Int64 // Non-DNS download bytes
40 | }
41 |
42 | func makeTracker() *tracker {
43 | return &tracker{
44 | start: time.Now(),
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/outline/client.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package outline
16 |
17 | import (
18 | "github.com/Jigsaw-Code/outline-sdk/transport"
19 | )
20 |
21 | // Client provides a transparent container for [transport.StreamDialer] and [transport.PacketListener]
22 | // that is exportable (as an opaque object) via gobind.
23 | // It's used by the connectivity test and the tun2socks handlers.
24 | type Client struct {
25 | transport.StreamDialer
26 | transport.PacketListener
27 | }
28 |
--------------------------------------------------------------------------------
/outline/connectivity/connectivity.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package connectivity
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "net"
21 | "net/http"
22 | "time"
23 |
24 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline"
25 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/neterrors"
26 | "github.com/Jigsaw-Code/outline-sdk/transport"
27 | )
28 |
29 | // TODO: make these values configurable by exposing a struct with the connectivity methods.
30 | const (
31 | tcpTimeout = 10 * time.Second
32 | udpTimeout = 1 * time.Second
33 | udpMaxRetryAttempts = 5
34 | bufferLength = 512
35 | )
36 |
37 | // authenticationError is used to signal failed authentication to the Shadowsocks proxy.
38 | type authenticationError struct {
39 | error
40 | }
41 |
42 | // reachabilityError is used to signal an unreachable proxy.
43 | type reachabilityError struct {
44 | error
45 | }
46 |
47 | // CheckConnectivity determines whether the Shadowsocks proxy can relay TCP and UDP traffic under
48 | // the current network. Parallelizes the execution of TCP and UDP checks, selects the appropriate
49 | // error code to return accounting for transient network failures.
50 | // Returns an error if an unexpected error ocurrs.
51 | func CheckConnectivity(client *outline.Client) (neterrors.Error, error) {
52 | // Start asynchronous UDP support check.
53 | udpChan := make(chan error)
54 | go func() {
55 | resolverAddr := &net.UDPAddr{IP: net.ParseIP("1.1.1.1"), Port: 53}
56 | udpChan <- CheckUDPConnectivityWithDNS(client, resolverAddr)
57 | }()
58 | // Check whether the proxy is reachable and that the client is able to authenticate to the proxy
59 | tcpErr := CheckTCPConnectivityWithHTTP(client, "http://example.com")
60 | if tcpErr == nil {
61 | udpErr := <-udpChan
62 | if udpErr == nil {
63 | return neterrors.NoError, nil
64 | }
65 | return neterrors.UDPConnectivity, nil
66 | }
67 | var authErr *authenticationError
68 | var reachabilityErr *reachabilityError
69 | if errors.As(tcpErr, &authErr) {
70 | return neterrors.AuthenticationFailure, nil
71 | } else if errors.As(tcpErr, &reachabilityErr) {
72 | return neterrors.Unreachable, nil
73 | }
74 | // The error is not related to the connectivity checks.
75 | return neterrors.Unexpected, tcpErr
76 | }
77 |
78 | // CheckUDPConnectivityWithDNS determines whether the Shadowsocks proxy represented by `client` and
79 | // the network support UDP traffic by issuing a DNS query though a resolver at `resolverAddr`.
80 | // Returns nil on success or an error on failure.
81 | func CheckUDPConnectivityWithDNS(client transport.PacketListener, resolverAddr net.Addr) error {
82 | conn, err := client.ListenPacket(context.Background())
83 | if err != nil {
84 | return err
85 | }
86 | defer conn.Close()
87 | buf := make([]byte, bufferLength)
88 | for attempt := 0; attempt < udpMaxRetryAttempts; attempt++ {
89 | conn.SetDeadline(time.Now().Add(udpTimeout))
90 | _, err := conn.WriteTo(getDNSRequest(), resolverAddr)
91 | if err != nil {
92 | continue
93 | }
94 | n, addr, err := conn.ReadFrom(buf)
95 | if n == 0 && err != nil {
96 | continue
97 | }
98 | if addr.String() != resolverAddr.String() {
99 | continue // Ensure we got a response from the resolver.
100 | }
101 | return nil
102 | }
103 | return errors.New("UDP connectivity check timed out")
104 | }
105 |
106 | // CheckTCPConnectivityWithHTTP determines whether the proxy is reachable over TCP and validates the
107 | // client's authentication credentials by performing an HTTP HEAD request to `targetURL`, which must
108 | // be of the form: http://[host](:[port])(/[path]). Returns nil on success, error if `targetURL` is
109 | // invalid, AuthenticationError or ReachabilityError on connectivity failure.
110 | func CheckTCPConnectivityWithHTTP(dialer transport.StreamDialer, targetURL string) error {
111 | deadline := time.Now().Add(tcpTimeout)
112 | ctx, cancel := context.WithDeadline(context.Background(), deadline)
113 | defer cancel()
114 | req, err := http.NewRequest("HEAD", targetURL, nil)
115 | if err != nil {
116 | return err
117 | }
118 | targetAddr := req.Host
119 | if !hasPort(targetAddr) {
120 | targetAddr = net.JoinHostPort(targetAddr, "80")
121 | }
122 | conn, err := dialer.Dial(ctx, targetAddr)
123 | if err != nil {
124 | return &reachabilityError{err}
125 | }
126 | defer conn.Close()
127 | conn.SetDeadline(deadline)
128 | err = req.Write(conn)
129 | if err != nil {
130 | return &authenticationError{err}
131 | }
132 | n, err := conn.Read(make([]byte, bufferLength))
133 | if n == 0 && err != nil {
134 | return &authenticationError{err}
135 | }
136 | return nil
137 | }
138 |
139 | func getDNSRequest() []byte {
140 | return []byte{
141 | 0, 0, // [0-1] query ID
142 | 1, 0, // [2-3] flags; byte[2] = 1 for recursion desired (RD).
143 | 0, 1, // [4-5] QDCOUNT (number of queries)
144 | 0, 0, // [6-7] ANCOUNT (number of answers)
145 | 0, 0, // [8-9] NSCOUNT (number of name server records)
146 | 0, 0, // [10-11] ARCOUNT (number of additional records)
147 | 3, 'c', 'o', 'm',
148 | 0, // null terminator of FQDN (root TLD)
149 | 0, 1, // QTYPE, set to A
150 | 0, 1, // QCLASS, set to 1 = IN (Internet)
151 | }
152 | }
153 |
154 | func hasPort(hostPort string) bool {
155 | _, _, err := net.SplitHostPort(hostPort)
156 | return err == nil
157 | }
158 |
--------------------------------------------------------------------------------
/outline/connectivity/connectivity_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package connectivity
16 |
17 | import (
18 | "context"
19 | "errors"
20 | "net"
21 | "reflect"
22 | "testing"
23 | "time"
24 |
25 | "github.com/Jigsaw-Code/outline-sdk/transport"
26 | "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
27 | )
28 |
29 | func TestCheckUDPConnectivityWithDNS_Success(t *testing.T) {
30 | client := &fakeSSClient{}
31 | err := CheckUDPConnectivityWithDNS(client, &net.UDPAddr{})
32 | if err != nil {
33 | t.Fatalf("Unexpected error: %v", err)
34 | }
35 | }
36 |
37 | func TestCheckUDPConnectivityWithDNS_Fail(t *testing.T) {
38 | client := &fakeSSClient{failUDP: true}
39 | err := CheckUDPConnectivityWithDNS(client, &net.UDPAddr{})
40 | if err == nil {
41 | t.Fail()
42 | }
43 | }
44 |
45 | func TestCheckTCPConnectivityWithHTTP_Success(t *testing.T) {
46 | client := &fakeSSClient{}
47 | err := CheckTCPConnectivityWithHTTP(client, "")
48 | if err != nil {
49 | t.Fail()
50 | }
51 | }
52 |
53 | func TestCheckTCPConnectivityWithHTTP_FailReachability(t *testing.T) {
54 | client := &fakeSSClient{failReachability: true}
55 | err := CheckTCPConnectivityWithHTTP(client, "")
56 | if err == nil {
57 | t.Fail()
58 | }
59 | if _, ok := err.(*reachabilityError); !ok {
60 | t.Fatalf("Expected reachability error, got: %v", reflect.TypeOf(err))
61 | }
62 | }
63 |
64 | func TestCheckTCPConnectivityWithHTTP_FailAuthentication(t *testing.T) {
65 | client := &fakeSSClient{failAuthentication: true}
66 | err := CheckTCPConnectivityWithHTTP(client, "")
67 | if err == nil {
68 | t.Fail()
69 | }
70 | if _, ok := err.(*authenticationError); !ok {
71 | t.Fatalf("Expected authentication error, got: %v", reflect.TypeOf(err))
72 | }
73 | }
74 |
75 | // Fake shadowsocks.Client that can be configured to return failing UDP and TCP connections.
76 | type fakeSSClient struct {
77 | failReachability bool
78 | failAuthentication bool
79 | failUDP bool
80 | }
81 |
82 | func (c *fakeSSClient) Dial(_ context.Context, raddr string) (transport.StreamConn, error) {
83 | if c.failReachability {
84 | return nil, &net.OpError{}
85 | }
86 | return &fakeDuplexConn{failRead: c.failAuthentication}, nil
87 | }
88 | func (c *fakeSSClient) ListenPacket(_ context.Context) (net.PacketConn, error) {
89 | conn, err := net.ListenPacket("udp", "")
90 | if err != nil {
91 | return nil, err
92 | }
93 | // The UDP check should fail if any of the failure conditions are true since it is a superset of the others.
94 | failRead := c.failAuthentication || c.failUDP || c.failReachability
95 | return &fakePacketConn{PacketConn: conn, failRead: failRead}, nil
96 | }
97 | func (c *fakeSSClient) SetTCPSaltGenerator(salter shadowsocks.SaltGenerator) {
98 | }
99 |
100 | // Fake PacketConn that fails `ReadFrom` calls when `failRead` is true.
101 | type fakePacketConn struct {
102 | net.PacketConn
103 | addr net.Addr
104 | failRead bool
105 | }
106 |
107 | func (c *fakePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
108 | c.addr = addr
109 | return len(b), nil // Write always succeeds
110 | }
111 |
112 | func (c *fakePacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
113 | if c.failRead {
114 | return 0, c.addr, errors.New("Fake read error")
115 | }
116 | return len(b), c.addr, nil
117 | }
118 |
119 | // Fake DuplexConn that fails `Read` calls when `failRead` is true.
120 | type fakeDuplexConn struct {
121 | transport.StreamConn
122 | failRead bool
123 | }
124 |
125 | func (c *fakeDuplexConn) Read(b []byte) (int, error) {
126 | if c.failRead {
127 | return 0, errors.New("Fake read error")
128 | }
129 | return len(b), nil
130 | }
131 |
132 | func (c *fakeDuplexConn) Write(b []byte) (int, error) {
133 | return len(b), nil // Write always succeeds
134 | }
135 |
136 | func (c *fakeDuplexConn) Close() error { return nil }
137 |
138 | func (c *fakeDuplexConn) LocalAddr() net.Addr { return nil }
139 |
140 | func (c *fakeDuplexConn) RemoteAddr() net.Addr { return nil }
141 |
142 | func (c *fakeDuplexConn) SetDeadline(t time.Time) error { return nil }
143 |
144 | func (c *fakeDuplexConn) SetReadDeadline(t time.Time) error { return nil }
145 |
146 | func (c *fakeDuplexConn) SetWriteDeadline(t time.Time) error { return nil }
147 |
148 | func (c *fakeDuplexConn) CloseRead() error { return nil }
149 |
150 | func (c *fakeDuplexConn) CloseWrite() error { return nil }
151 |
--------------------------------------------------------------------------------
/outline/electron/connect_linux.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This script allows debugging outline-go-tun2socks directly on linux.
4 | # Instructions:
5 | # 1. Install the Outline client for Linux, connect to a server, and disconnect.
6 | # This installs the outline controller service.
7 | # 2. $ ./connect_linux.sh
8 | # 3. Ctrl+C to stop proxying
9 |
10 | readonly PROXY_IP="$1"
11 | readonly PROXY_PORT="$2"
12 | readonly PROXY_PASSWORD="$3"
13 |
14 | go build -v .
15 |
16 | echo "{\"action\":\"configureRouting\",\"parameters\":{\"proxyIp\":\"${PROXY_IP}\",\"routerIp\":\"10.0.85.1\"}}" | socat UNIX-CONNECT:/var/run/outline_controller -
17 | ./electron -proxyHost "${PROXY_IP}" -proxyPort "${PROXY_PORT}" -proxyPassword "${PROXY_PASSWORD}" -logLevel debug -tunName outline-tun0
18 | echo '{"action":"resetRouting","parameters":{}}' | socat UNIX-CONNECT:/var/run/outline_controller -
19 |
--------------------------------------------------------------------------------
/outline/electron/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "flag"
19 | "fmt"
20 | "io"
21 | "os"
22 | "os/signal"
23 | "strings"
24 | "syscall"
25 | "time"
26 |
27 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/utf8"
28 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/neterrors"
29 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks"
30 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/tun2socks"
31 | "github.com/eycorsican/go-tun2socks/common/log"
32 | _ "github.com/eycorsican/go-tun2socks/common/log/simple" // Register a simple logger.
33 | "github.com/eycorsican/go-tun2socks/core"
34 | "github.com/eycorsican/go-tun2socks/proxy/dnsfallback"
35 | "github.com/eycorsican/go-tun2socks/tun"
36 | )
37 |
38 | const (
39 | mtu = 1500
40 | udpTimeout = 30 * time.Second
41 | persistTun = true // Linux: persist the TUN interface after the last open file descriptor is closed.
42 | )
43 |
44 | var args struct {
45 | tunAddr *string
46 | tunGw *string
47 | tunMask *string
48 | tunName *string
49 | tunDNS *string
50 |
51 | // Deprecated: Use proxyConfig instead.
52 | proxyHost *string
53 | proxyPort *int
54 | proxyPassword *string
55 | proxyCipher *string
56 | proxyPrefix *string
57 |
58 | proxyConfig *string
59 |
60 | logLevel *string
61 | checkConnectivity *bool
62 | dnsFallback *bool
63 | version *bool
64 | }
65 | var version string // Populated at build time through `-X main.version=...`
66 | var lwipWriter io.Writer
67 |
68 | func main() {
69 | args.tunAddr = flag.String("tunAddr", "10.0.85.2", "TUN interface IP address")
70 | args.tunGw = flag.String("tunGw", "10.0.85.1", "TUN interface gateway")
71 | args.tunMask = flag.String("tunMask", "255.255.255.0", "TUN interface network mask; prefixlen for IPv6")
72 | args.tunDNS = flag.String("tunDNS", "1.1.1.1,9.9.9.9,208.67.222.222", "Comma-separated list of DNS resolvers for the TUN interface (Windows only)")
73 | args.tunName = flag.String("tunName", "tun0", "TUN interface name")
74 | args.proxyHost = flag.String("proxyHost", "", "Shadowsocks proxy hostname or IP address")
75 | args.proxyPort = flag.Int("proxyPort", 0, "Shadowsocks proxy port number")
76 | args.proxyPassword = flag.String("proxyPassword", "", "Shadowsocks proxy password")
77 | args.proxyCipher = flag.String("proxyCipher", "chacha20-ietf-poly1305", "Shadowsocks proxy encryption cipher")
78 | args.proxyPrefix = flag.String("proxyPrefix", "", "Shadowsocks connection prefix, UTF8-encoded (unsafe)")
79 | args.proxyConfig = flag.String("proxyConfig", "", "A JSON object containing the proxy config, UTF8-encoded")
80 | args.logLevel = flag.String("logLevel", "info", "Logging level: debug|info|warn|error|none")
81 | args.dnsFallback = flag.Bool("dnsFallback", false, "Enable DNS fallback over TCP (overrides the UDP handler).")
82 | args.checkConnectivity = flag.Bool("checkConnectivity", false, "Check the proxy TCP and UDP connectivity and exit.")
83 | args.version = flag.Bool("version", false, "Print the version and exit.")
84 |
85 | flag.Parse()
86 |
87 | if *args.version {
88 | fmt.Println(version)
89 | os.Exit(0)
90 | }
91 |
92 | setLogLevel(*args.logLevel)
93 |
94 | client, err := newShadowsocksClientFromArgs()
95 | if err != nil {
96 | log.Errorf("Failed to create Shadowsocks client: %v", err)
97 | os.Exit(neterrors.IllegalConfiguration.Number())
98 | }
99 |
100 | if *args.checkConnectivity {
101 | connErrCode, err := shadowsocks.CheckConnectivity(client)
102 | log.Debugf("Connectivity checks error code: %v", connErrCode)
103 | if err != nil {
104 | log.Errorf("Failed to perform connectivity checks: %v", err)
105 | }
106 | os.Exit(connErrCode)
107 | }
108 |
109 | // Open TUN device
110 | dnsResolvers := strings.Split(*args.tunDNS, ",")
111 | tunDevice, err := tun.OpenTunDevice(*args.tunName, *args.tunAddr, *args.tunGw, *args.tunMask, dnsResolvers, persistTun)
112 | if err != nil {
113 | log.Errorf("Failed to open TUN device: %v", err)
114 | os.Exit(neterrors.SystemMisconfigured.Number())
115 | }
116 | // Output packets to TUN device
117 | core.RegisterOutputFn(tunDevice.Write)
118 |
119 | // Register TCP and UDP connection handlers
120 | core.RegisterTCPConnHandler(tun2socks.NewTCPHandler(client))
121 | if *args.dnsFallback {
122 | // UDP connectivity not supported, fall back to DNS over TCP.
123 | log.Debugf("Registering DNS fallback UDP handler")
124 | core.RegisterUDPConnHandler(dnsfallback.NewUDPHandler())
125 | } else {
126 | core.RegisterUDPConnHandler(tun2socks.NewUDPHandler(client, udpTimeout))
127 | }
128 |
129 | // Configure LWIP stack to receive input data from the TUN device
130 | lwipWriter := core.NewLWIPStack()
131 | go func() {
132 | _, err := io.CopyBuffer(lwipWriter, tunDevice, make([]byte, mtu))
133 | if err != nil {
134 | log.Errorf("Failed to write data to network stack: %v", err)
135 | os.Exit(neterrors.Unexpected.Number())
136 | }
137 | }()
138 |
139 | log.Infof("tun2socks running...")
140 |
141 | osSignals := make(chan os.Signal, 1)
142 | signal.Notify(osSignals, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGHUP)
143 | sig := <-osSignals
144 | log.Debugf("Received signal: %v", sig)
145 | }
146 |
147 | func setLogLevel(level string) {
148 | switch strings.ToLower(level) {
149 | case "debug":
150 | log.SetLevel(log.DEBUG)
151 | case "info":
152 | log.SetLevel(log.INFO)
153 | case "warn":
154 | log.SetLevel(log.WARN)
155 | case "error":
156 | log.SetLevel(log.ERROR)
157 | case "none":
158 | log.SetLevel(log.NONE)
159 | default:
160 | log.SetLevel(log.INFO)
161 | }
162 | }
163 |
164 | // newShadowsocksClientFromArgs creates a new shadowsocks.Client instance
165 | // from the global CLI argument object args.
166 | func newShadowsocksClientFromArgs() (*shadowsocks.Client, error) {
167 | if jsonConfig := *args.proxyConfig; len(jsonConfig) > 0 {
168 | return shadowsocks.NewClientFromJSON(jsonConfig)
169 | } else {
170 | // legacy raw flags
171 | config := shadowsocks.Config{
172 | Host: *args.proxyHost,
173 | Port: *args.proxyPort,
174 | CipherName: *args.proxyCipher,
175 | Password: *args.proxyPassword,
176 | }
177 | if prefixStr := *args.proxyPrefix; len(prefixStr) > 0 {
178 | if p, err := utf8.DecodeUTF8CodepointsToRawBytes(prefixStr); err != nil {
179 | return nil, fmt.Errorf("Failed to parse prefix string: %w", err)
180 | } else {
181 | config.Prefix = p
182 | }
183 | }
184 | return shadowsocks.NewClient(&config)
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/outline/internal/utf8/utf8.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // This packages provides helper functions to encode or decode UTF-8 strings
16 | package utf8
17 |
18 | import "fmt"
19 |
20 | // DecodeUTF8CodepointsToRawBytes parses a UTF-8 string as a raw byte array.
21 | // That is to say, each codepoint in the Unicode string will be treated as a
22 | // single byte (must be in range 0x00 ~ 0xff).
23 | //
24 | // If a codepoint falls out of the range, an error will be returned.
25 | func DecodeUTF8CodepointsToRawBytes(utf8Str string) ([]byte, error) {
26 | runes := []rune(utf8Str)
27 | rawBytes := make([]byte, len(runes))
28 | for i, r := range runes {
29 | if (r & 0xFF) != r {
30 | return nil, fmt.Errorf("character out of range: %d", r)
31 | }
32 | rawBytes[i] = byte(r)
33 | }
34 | return rawBytes, nil
35 | }
36 |
--------------------------------------------------------------------------------
/outline/internal/utf8/utf8_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package utf8
16 |
17 | import (
18 | "bytes"
19 | "testing"
20 | )
21 |
22 | func Test_DecodeUTF8CodepointsToRawBytes(t *testing.T) {
23 | tests := []struct {
24 | name string
25 | input string
26 | want []byte
27 | wantErr bool
28 | }{
29 | {
30 | name: "basic",
31 | input: "abc 123",
32 | want: []byte{97, 98, 99, 32, 49, 50, 51},
33 | }, {
34 | name: "empty",
35 | input: "",
36 | want: []byte{},
37 | }, {
38 | name: "edge cases (explicit)",
39 | input: "\x00\x01\x02 \x7e\x7f \xc2\x80\xc2\x81 \xc3\xbd\xc3\xbf",
40 | // 0xc2+0x80/0x81 will be decoded to 0x80/0x81 (two-byte sequence)
41 | // 0xc3+0xbd/0xbf will be decoded to 0xfd/0xff (two-byte sequence)
42 | want: []byte{0x00, 0x01, 0x02, 32, 0x7e, 0x7f, 32, 0x80, 0x81, 32, 0xfd, 0xff},
43 | }, {
44 | name: "unicode escapes",
45 | input: "\u0000\u0080\u00ff",
46 | want: []byte{0x00, 0x80, 0xff},
47 | }, {
48 | name: "edge cases (roundtrip)",
49 | input: string([]rune{0, 1, 2, 126, 127, 128, 129, 254, 255}),
50 | want: []byte{0, 1, 2, 126, 127, 128, 129, 254, 255},
51 | }, {
52 | name: "out of range 256",
53 | input: string([]rune{256}),
54 | wantErr: true,
55 | }, {
56 | name: "out of range 257",
57 | input: string([]rune{257}),
58 | wantErr: true,
59 | }, {
60 | name: "out of range 65537",
61 | input: string([]rune{65537}),
62 | wantErr: true,
63 | }, {
64 | name: "invalid UTF-8",
65 | input: "\xc3\x28",
66 | wantErr: true,
67 | }, {
68 | name: "invalid Unicode",
69 | input: "\xf8\xa1\xa1\xa1\xa1",
70 | wantErr: true,
71 | },
72 | }
73 | for _, tt := range tests {
74 | t.Run(tt.name, func(t *testing.T) {
75 | got, err := DecodeUTF8CodepointsToRawBytes(tt.input)
76 | if (err != nil) != tt.wantErr {
77 | t.Errorf("DecodeCodepointsToBytes() returns error %v, want error %v", err, tt.wantErr)
78 | return
79 | }
80 | if !bytes.Equal(got, tt.want) {
81 | t.Errorf("DecodeCodepointsToBytes() returns %v, want %v", got, tt.want)
82 | }
83 | })
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/outline/neterrors/neterrors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Package errors contains a model for errors shared with the Outline Client application.
16 | //
17 | // TODO(fortuna): Revamp error handling. This is an inverted dependency. The Go code should
18 | // provide its own standalone API, leaving translations to the consumer.
19 | package neterrors
20 |
21 | type Error int
22 |
23 | func (e Error) Number() int {
24 | return int(e)
25 | }
26 |
27 | // Outline error codes. Must be kept in sync with definitions in https://github.com/Jigsaw-Code/outline-client/blob/master/src/www/model/errors.ts
28 | const (
29 | NoError Error = 0
30 | Unexpected Error = 1
31 | NoVPNPermissions Error = 2 // Unused
32 | AuthenticationFailure Error = 3
33 | UDPConnectivity Error = 4
34 | Unreachable Error = 5
35 | VpnStartFailure Error = 6 // Unused
36 | IllegalConfiguration Error = 7 // Electron only
37 | ShadowsocksStartFailure Error = 8 // Unused
38 | ConfigureSystemProxyFailure Error = 9 // Unused
39 | NoAdminPermissions Error = 10 // Unused
40 | UnsupportedRoutingTable Error = 11 // Unused
41 | SystemMisconfigured Error = 12 // Electron only
42 | )
43 |
--------------------------------------------------------------------------------
/outline/shadowsocks/client.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // This package provides support of Shadowsocks client and the configuration
16 | // that can be used by Outline Client.
17 | //
18 | // All data structures and functions will also be exposed as libraries that
19 | // non-golang callers can use (for example, C/Java/Objective-C).
20 | package shadowsocks
21 |
22 | import (
23 | "fmt"
24 | "net"
25 | "strconv"
26 | "time"
27 |
28 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline"
29 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/connectivity"
30 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/internal/utf8"
31 | "github.com/Jigsaw-Code/outline-sdk/transport"
32 | "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
33 | "github.com/eycorsican/go-tun2socks/common/log"
34 | )
35 |
36 | // A client object that can be used to connect to a remote Shadowsocks proxy.
37 | type Client outline.Client
38 |
39 | // NewClient creates a new Shadowsocks client from a non-nil configuration.
40 | //
41 | // Deprecated: Please use NewClientFromJSON.
42 | func NewClient(config *Config) (*Client, error) {
43 | if config == nil {
44 | return nil, fmt.Errorf("shadowsocks configuration is required")
45 | }
46 | return newShadowsocksClient(config.Host, config.Port, config.CipherName, config.Password, config.Prefix)
47 | }
48 |
49 | // NewClientFromJSON creates a new Shadowsocks client from a JSON formatted
50 | // configuration.
51 | func NewClientFromJSON(configJSON string) (*Client, error) {
52 | config, err := parseConfigFromJSON(configJSON)
53 | if err != nil {
54 | return nil, fmt.Errorf("failed to parse Shadowsocks configuration JSON: %w", err)
55 | }
56 | var prefixBytes []byte = nil
57 | if len(config.Prefix) > 0 {
58 | if p, err := utf8.DecodeUTF8CodepointsToRawBytes(config.Prefix); err != nil {
59 | return nil, fmt.Errorf("failed to parse prefix string: %w", err)
60 | } else {
61 | prefixBytes = p
62 | }
63 | }
64 | return newShadowsocksClient(config.Host, int(config.Port), config.Method, config.Password, prefixBytes)
65 | }
66 |
67 | func newShadowsocksClient(host string, port int, cipherName, password string, prefix []byte) (*Client, error) {
68 | if err := validateConfig(host, port, cipherName, password); err != nil {
69 | return nil, fmt.Errorf("invalid Shadowsocks configuration: %w", err)
70 | }
71 |
72 | // TODO: consider using net.LookupIP to get a list of IPs, and add logic for optimal selection.
73 | proxyIP, err := net.ResolveIPAddr("ip", host)
74 | if err != nil {
75 | return nil, fmt.Errorf("failed to resolve proxy address: %w", err)
76 | }
77 | proxyAddress := net.JoinHostPort(proxyIP.String(), fmt.Sprint(port))
78 |
79 | cryptoKey, err := shadowsocks.NewEncryptionKey(cipherName, password)
80 | if err != nil {
81 | return nil, fmt.Errorf("failed to create Shadowsocks cipher: %w", err)
82 | }
83 |
84 | streamDialer, err := shadowsocks.NewStreamDialer(&transport.TCPEndpoint{Address: proxyAddress}, cryptoKey)
85 | if err != nil {
86 | return nil, fmt.Errorf("failed to create StreamDialer: %w", err)
87 | }
88 | if len(prefix) > 0 {
89 | log.Debugf("Using salt prefix: %s", string(prefix))
90 | streamDialer.SaltGenerator = shadowsocks.NewPrefixSaltGenerator(prefix)
91 | }
92 |
93 | packetListener, err := shadowsocks.NewPacketListener(&transport.UDPEndpoint{Address: proxyAddress}, cryptoKey)
94 | if err != nil {
95 | return nil, fmt.Errorf("failed to create PacketListener: %w", err)
96 | }
97 |
98 | return &Client{StreamDialer: streamDialer, PacketListener: packetListener}, nil
99 | }
100 |
101 | // Error number constants exported through gomobile
102 | const (
103 | NoError = 0
104 | Unexpected = 1
105 | NoVPNPermissions = 2 // Unused
106 | AuthenticationFailure = 3
107 | UDPConnectivity = 4
108 | Unreachable = 5
109 | VpnStartFailure = 6 // Unused
110 | IllegalConfiguration = 7 // Electron only
111 | ShadowsocksStartFailure = 8 // Unused
112 | ConfigureSystemProxyFailure = 9 // Unused
113 | NoAdminPermissions = 10 // Unused
114 | UnsupportedRoutingTable = 11 // Unused
115 | SystemMisconfigured = 12 // Electron only
116 | )
117 |
118 | const reachabilityTimeout = 10 * time.Second
119 |
120 | // CheckConnectivity determines whether the Shadowsocks proxy can relay TCP and UDP traffic under
121 | // the current network. Parallelizes the execution of TCP and UDP checks, selects the appropriate
122 | // error code to return accounting for transient network failures.
123 | // Returns an error if an unexpected error ocurrs.
124 | func CheckConnectivity(client *Client) (int, error) {
125 | errCode, err := connectivity.CheckConnectivity((*outline.Client)(client))
126 | return errCode.Number(), err
127 | }
128 |
129 | // CheckServerReachable determines whether the server at `host:port` is reachable over TCP.
130 | // Returns an error if the server is unreachable.
131 | func CheckServerReachable(host string, port int) error {
132 | conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), reachabilityTimeout)
133 | if err != nil {
134 | return err
135 | }
136 | conn.Close()
137 | return nil
138 | }
139 |
--------------------------------------------------------------------------------
/outline/shadowsocks/client_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package shadowsocks
16 |
17 | import "testing"
18 |
19 | func Test_NewClientFromJSON_Errors(t *testing.T) {
20 | tests := []struct {
21 | name string
22 | input string
23 | }{
24 | {
25 | name: "missing host",
26 | input: `{"port":12345,"method":"some-cipher","password":"abcd1234"}`,
27 | },
28 | {
29 | name: "missing port",
30 | input: `{"host":"192.0.2.1","method":"some-cipher","password":"abcd1234"}`,
31 | },
32 | {
33 | name: "missing method",
34 | input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`,
35 | },
36 | {
37 | name: "missing password",
38 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher"}`,
39 | },
40 | {
41 | name: "empty host",
42 | input: `{"host":"","port":12345,"method":"some-cipher","password":"abcd1234"}`,
43 | },
44 | {
45 | name: "zero port",
46 | input: `{"host":"192.0.2.1","port":0,"method":"some-cipher","password":"abcd1234"}`,
47 | },
48 | {
49 | name: "empty method",
50 | input: `{"host":"192.0.2.1","port":12345,"method":"","password":"abcd1234"}`,
51 | },
52 | {
53 | name: "empty password",
54 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":""}`,
55 | },
56 | {
57 | name: "port -1",
58 | input: `{"host":"192.0.2.1","port":-1,"method":"some-cipher","password":"abcd1234"}`,
59 | },
60 | {
61 | name: "port 65536",
62 | input: `{"host":"192.0.2.1","port":65536,"method":"some-cipher","password":"abcd1234"}`,
63 | },
64 | {
65 | name: "prefix out-of-range",
66 | input: `{"host":"192.0.2.1","port":8080,"method":"some-cipher","password":"abcd1234","prefix":"\x1234"}`,
67 | },
68 | }
69 | for _, tt := range tests {
70 | t.Run(tt.name, func(t *testing.T) {
71 | got, err := NewClientFromJSON(tt.input)
72 | if err == nil || got != nil {
73 | t.Errorf("NewClientFromJSON() expects an error, got = %v", got)
74 | return
75 | }
76 | })
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/outline/shadowsocks/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2022 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package shadowsocks
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | )
21 |
22 | // Config represents a (legacy) shadowsocks server configuration. You can use
23 | // NewClientFromJSON(string) instead.
24 | //
25 | // Deprecated: this object will be removed once we migrated from the old
26 | // Outline Client logic.
27 | type Config struct {
28 | Host string
29 | Port int
30 | Password string
31 | CipherName string
32 | Prefix []byte
33 | }
34 |
35 | // An internal data structure to be used by JSON deserialization.
36 | // Must match the ShadowsocksSessionConfig interface defined in Outline Client.
37 | type configJSON struct {
38 | Host string `json:"host"`
39 | Port uint16 `json:"port"`
40 | Password string `json:"password"`
41 | Method string `json:"method"`
42 | Prefix string `json:"prefix"`
43 | }
44 |
45 | // ParseConfigFromJSON parses a JSON string `in` as a configJSON object.
46 | // The JSON string `in` must match the ShadowsocksSessionConfig interface
47 | // defined in Outline Client.
48 | func parseConfigFromJSON(in string) (*configJSON, error) {
49 | var conf configJSON
50 | if err := json.Unmarshal([]byte(in), &conf); err != nil {
51 | return nil, err
52 | }
53 | return &conf, nil
54 | }
55 |
56 | // validateConfig validates whether a Shadowsocks server configuration is valid
57 | // (it won't do any connectivity tests)
58 | //
59 | // Returns nil if it is valid; or an error message.
60 | func validateConfig(host string, port int, cipher, password string) error {
61 | if len(host) == 0 {
62 | return fmt.Errorf("must provide a host name or IP address")
63 | }
64 | if port <= 0 || port > 65535 {
65 | return fmt.Errorf("port must be within range [1..65535]")
66 | }
67 | if len(cipher) == 0 {
68 | return fmt.Errorf("must provide an encryption cipher method")
69 | }
70 | if len(password) == 0 {
71 | return fmt.Errorf("must provide a password")
72 | }
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/outline/shadowsocks/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package shadowsocks
16 |
17 | import (
18 | "testing"
19 | )
20 |
21 | func Test_parseConfigFromJSON(t *testing.T) {
22 | tests := []struct {
23 | name string
24 | input string
25 | want *configJSON
26 | wantErr bool
27 | }{
28 | {
29 | name: "normal config",
30 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234"}`,
31 | want: &configJSON{
32 | Host: "192.0.2.1",
33 | Port: 12345,
34 | Method: "some-cipher",
35 | Password: "abcd1234",
36 | Prefix: "",
37 | },
38 | },
39 | {
40 | name: "normal config with prefix",
41 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123"}`,
42 | want: &configJSON{
43 | Host: "192.0.2.1",
44 | Port: 12345,
45 | Method: "some-cipher",
46 | Password: "abcd1234",
47 | Prefix: "abc 123",
48 | },
49 | },
50 | {
51 | name: "normal config with extra fields",
52 | input: `{"extra_field":"ignored","host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234"}`,
53 | want: &configJSON{
54 | Host: "192.0.2.1",
55 | Port: 12345,
56 | Method: "some-cipher",
57 | Password: "abcd1234",
58 | Prefix: "",
59 | },
60 | },
61 | {
62 | name: "unprintable prefix",
63 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123","prefix":"\u0000\u0080\u00ff"}`,
64 | want: &configJSON{
65 | Host: "192.0.2.1",
66 | Port: 12345,
67 | Method: "some-cipher",
68 | Password: "abcd1234",
69 | Prefix: "\u0000\u0080\u00ff",
70 | },
71 | },
72 | {
73 | name: "multi-byte utf-8 prefix",
74 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":"abc 123","prefix":"` + "\xc2\x80\xc2\x81\xc3\xbd\xc3\xbf" + `"}`,
75 | want: &configJSON{
76 | Host: "192.0.2.1",
77 | Port: 12345,
78 | Method: "some-cipher",
79 | Password: "abcd1234",
80 | Prefix: "\u0080\u0081\u00fd\u00ff",
81 | },
82 | },
83 | {
84 | name: "missing host",
85 | input: `{"port":12345,"method":"some-cipher","password":"abcd1234"}`,
86 | want: &configJSON{
87 | Host: "",
88 | Port: 12345,
89 | Method: "some-cipher",
90 | Password: "abcd1234",
91 | Prefix: "",
92 | },
93 | },
94 | {
95 | name: "missing port",
96 | input: `{"host":"192.0.2.1","method":"some-cipher","password":"abcd1234"}`,
97 | want: &configJSON{
98 | Host: "192.0.2.1",
99 | Port: 0,
100 | Method: "some-cipher",
101 | Password: "abcd1234",
102 | Prefix: "",
103 | },
104 | },
105 | {
106 | name: "missing method",
107 | input: `{"host":"192.0.2.1","port":12345,"password":"abcd1234"}`,
108 | want: &configJSON{
109 | Host: "192.0.2.1",
110 | Port: 12345,
111 | Method: "",
112 | Password: "abcd1234",
113 | Prefix: "",
114 | },
115 | },
116 | {
117 | name: "missing password",
118 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher"}`,
119 | want: &configJSON{
120 | Host: "192.0.2.1",
121 | Port: 12345,
122 | Method: "some-cipher",
123 | Password: "",
124 | Prefix: "",
125 | },
126 | },
127 | {
128 | name: "empty host",
129 | input: `{"host":"","port":12345,"method":"some-cipher","password":"abcd1234"}`,
130 | want: &configJSON{
131 | Host: "",
132 | Port: 12345,
133 | Method: "some-cipher",
134 | Password: "abcd1234",
135 | Prefix: "",
136 | },
137 | },
138 | {
139 | name: "zero port",
140 | input: `{"host":"192.0.2.1","port":0,"method":"some-cipher","password":"abcd1234"}`,
141 | want: &configJSON{
142 | Host: "192.0.2.1",
143 | Port: 0,
144 | Method: "some-cipher",
145 | Password: "abcd1234",
146 | Prefix: "",
147 | },
148 | },
149 | {
150 | name: "empty method",
151 | input: `{"host":"192.0.2.1","port":12345,"method":"","password":"abcd1234"}`,
152 | want: &configJSON{
153 | Host: "192.0.2.1",
154 | Port: 12345,
155 | Method: "",
156 | Password: "abcd1234",
157 | Prefix: "",
158 | },
159 | },
160 | {
161 | name: "empty password",
162 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":""}`,
163 | want: &configJSON{
164 | Host: "192.0.2.1",
165 | Port: 12345,
166 | Method: "some-cipher",
167 | Password: "",
168 | Prefix: "",
169 | },
170 | },
171 | {
172 | name: "empty prefix",
173 | input: `{"host":"192.0.2.1","port":12345,"method":"some-cipher","password":"abcd1234","prefix":""}`,
174 | want: &configJSON{
175 | Host: "192.0.2.1",
176 | Port: 12345,
177 | Method: "some-cipher",
178 | Password: "abcd1234",
179 | Prefix: "",
180 | },
181 | },
182 | {
183 | name: "port -1",
184 | input: `{"host":"192.0.2.1","port":-1,"method":"some-cipher","password":"abcd1234"}`,
185 | wantErr: true,
186 | },
187 | {
188 | name: "port 65536",
189 | input: `{"host":"192.0.2.1","port":65536,"method":"some-cipher","password":"abcd1234"}`,
190 | wantErr: true,
191 | },
192 | {
193 | name: "prefix out-of-range",
194 | input: `{"host":"192.0.2.1","port":8080,"method":"some-cipher","password":"abcd1234","prefix":"\x1234"}`,
195 | wantErr: true,
196 | },
197 | }
198 | for _, tt := range tests {
199 | t.Run(tt.name, func(t *testing.T) {
200 | got, err := parseConfigFromJSON(tt.input)
201 | if (err != nil) != tt.wantErr {
202 | t.Errorf("ParseConfigFromJSON() error = %v, wantErr %v", err, tt.wantErr)
203 | return
204 | }
205 | if tt.wantErr {
206 | return
207 | }
208 | if got.Host != tt.want.Host ||
209 | got.Port != tt.want.Port ||
210 | got.Method != tt.want.Method ||
211 | got.Password != tt.want.Password ||
212 | got.Prefix != tt.want.Prefix {
213 | t.Errorf("ParseConfigFromJSON() = %v (prefix %+q), want %v (prefix %+q)", got, got.Prefix, tt.want, tt.want.Prefix)
214 | }
215 | })
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/outline/tun2socks/tcp.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "context"
19 | "io"
20 | "net"
21 |
22 | "github.com/Jigsaw-Code/outline-sdk/transport"
23 | "github.com/eycorsican/go-tun2socks/core"
24 | )
25 |
26 | type tcpHandler struct {
27 | dialer transport.StreamDialer
28 | }
29 |
30 | // NewTCPHandler returns a Shadowsocks TCP connection handler.
31 | func NewTCPHandler(client transport.StreamDialer) core.TCPConnHandler {
32 | return &tcpHandler{client}
33 | }
34 |
35 | func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error {
36 | proxyConn, err := h.dialer.Dial(context.Background(), target.String())
37 | if err != nil {
38 | return err
39 | }
40 | // TODO: Request upstream to make `conn` a `core.TCPConn` so we can avoid this type assertion.
41 | go relay(conn.(core.TCPConn), proxyConn)
42 | return nil
43 | }
44 |
45 | func copyOneWay(leftConn, rightConn transport.StreamConn) (int64, error) {
46 | n, err := io.Copy(leftConn, rightConn)
47 | // Send FIN to indicate EOF
48 | leftConn.CloseWrite()
49 | // Release reader resources
50 | rightConn.CloseRead()
51 | return n, err
52 | }
53 |
54 | // relay copies between left and right bidirectionally. Returns number of
55 | // bytes copied from right to left, from left to right, and any error occurred.
56 | // Relay allows for half-closed connections: if one side is done writing, it can
57 | // still read all remaining data from its peer.
58 | func relay(leftConn, rightConn transport.StreamConn) (int64, int64, error) {
59 | type res struct {
60 | N int64
61 | Err error
62 | }
63 | ch := make(chan res)
64 |
65 | go func() {
66 | n, err := copyOneWay(rightConn, leftConn)
67 | ch <- res{n, err}
68 | }()
69 |
70 | n, err := copyOneWay(leftConn, rightConn)
71 | rs := <-ch
72 |
73 | if err == nil {
74 | err = rs.Err
75 | }
76 | return n, rs.N, err
77 | }
78 |
--------------------------------------------------------------------------------
/outline/tun2socks/tunnel.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "errors"
19 | "io"
20 | "net"
21 | "time"
22 |
23 | "github.com/eycorsican/go-tun2socks/core"
24 | "github.com/eycorsican/go-tun2socks/proxy/dnsfallback"
25 |
26 | "github.com/Jigsaw-Code/outline-sdk/transport"
27 |
28 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/connectivity"
29 | "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
30 | )
31 |
32 | // Tunnel represents a tunnel from a TUN device to a server.
33 | type Tunnel interface {
34 | tunnel.Tunnel
35 |
36 | // UpdateUDPSupport determines if UDP is supported following a network connectivity change.
37 | // Sets the tunnel's UDP connection handler accordingly, falling back to DNS over TCP if UDP is not supported.
38 | // Returns whether UDP proxying is supported in the new network.
39 | UpdateUDPSupport() bool
40 | }
41 |
42 | // Deprecated: use Tunnel directly.
43 | type OutlineTunnel = Tunnel
44 |
45 | type outlinetunnel struct {
46 | tunnel.Tunnel
47 | lwipStack core.LWIPStack
48 | streamDialer transport.StreamDialer
49 | packetDialer transport.PacketListener
50 | isUDPEnabled bool // Whether the tunnel supports proxying UDP.
51 | }
52 |
53 | // newTunnel connects a tunnel to a Shadowsocks proxy server and returns an `outline.Tunnel`.
54 | //
55 | // `host` is the IP or domain of the Shadowsocks proxy.
56 | // `port` is the port of the Shadowsocks proxy.
57 | // `password` is the password of the Shadowsocks proxy.
58 | // `cipher` is the encryption cipher used by the Shadowsocks proxy.
59 | // `isUDPEnabled` indicates if the Shadowsocks proxy and the network support proxying UDP traffic.
60 | // `tunWriter` is used to output packets back to the TUN device. OutlineTunnel.Disconnect() will close `tunWriter`.
61 | func newTunnel(streamDialer transport.StreamDialer, packetDialer transport.PacketListener, isUDPEnabled bool, tunWriter io.WriteCloser) (Tunnel, error) {
62 | if tunWriter == nil {
63 | return nil, errors.New("Must provide a TUN writer")
64 | }
65 | core.RegisterOutputFn(func(data []byte) (int, error) {
66 | return tunWriter.Write(data)
67 | })
68 | lwipStack := core.NewLWIPStack()
69 | base := tunnel.NewTunnel(tunWriter, lwipStack)
70 | t := &outlinetunnel{base, lwipStack, streamDialer, packetDialer, isUDPEnabled}
71 | t.registerConnectionHandlers()
72 | return t, nil
73 | }
74 |
75 | func (t *outlinetunnel) UpdateUDPSupport() bool {
76 | resolverAddr := &net.UDPAddr{IP: net.ParseIP("1.1.1.1"), Port: 53}
77 | isUDPEnabled := connectivity.CheckUDPConnectivityWithDNS(t.packetDialer, resolverAddr) == nil
78 | if t.isUDPEnabled != isUDPEnabled {
79 | t.isUDPEnabled = isUDPEnabled
80 | t.lwipStack.Close() // Close existing connections to avoid using the previous handlers.
81 | t.registerConnectionHandlers()
82 | }
83 | return isUDPEnabled
84 | }
85 |
86 | // Registers UDP and TCP Shadowsocks connection handlers to the tunnel's host and port.
87 | // Registers a DNS/TCP fallback UDP handler when UDP is disabled.
88 | func (t *outlinetunnel) registerConnectionHandlers() {
89 | var udpHandler core.UDPConnHandler
90 | if t.isUDPEnabled {
91 | udpHandler = NewUDPHandler(t.packetDialer, 30*time.Second)
92 | } else {
93 | udpHandler = dnsfallback.NewUDPHandler()
94 | }
95 | core.RegisterTCPConnHandler(NewTCPHandler(t.streamDialer))
96 | core.RegisterUDPConnHandler(udpHandler)
97 | }
98 |
--------------------------------------------------------------------------------
/outline/tun2socks/tunnel_android.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "runtime/debug"
19 |
20 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks"
21 | "github.com/Jigsaw-Code/outline-go-tun2socks/tunnel"
22 | "github.com/eycorsican/go-tun2socks/common/log"
23 | )
24 |
25 | func init() {
26 | // Conserve memory by increasing garbage collection frequency.
27 | debug.SetGCPercent(10)
28 | log.SetLevel(log.WARN)
29 | }
30 |
31 | // ConnectShadowsocksTunnel reads packets from a TUN device and routes it to a Shadowsocks proxy server.
32 | // Returns an OutlineTunnel instance and does *not* take ownership of the TUN file descriptor; the
33 | // caller is responsible for closing after OutlineTunnel disconnects.
34 | //
35 | // - `fd` is the TUN device. The OutlineTunnel acquires an additional reference to it, which
36 | // is released by OutlineTunnel.Disconnect(), so the caller must close `fd` _and_ call
37 | // Disconnect() in order to close the TUN device.
38 | // - `client` is the Shadowsocks client (created by [shadowsocks.NewClient]).
39 | // - `isUDPEnabled` indicates whether the tunnel and/or network enable UDP proxying.
40 | //
41 | // Returns an error if the TUN file descriptor cannot be opened, or if the tunnel fails to
42 | // connect.
43 | func ConnectShadowsocksTunnel(fd int, client *shadowsocks.Client, isUDPEnabled bool) (Tunnel, error) {
44 | tun, err := tunnel.MakeTunFile(fd)
45 | if err != nil {
46 | return nil, err
47 | }
48 | t, err := newTunnel(client, client, isUDPEnabled, tun)
49 | if err != nil {
50 | return nil, err
51 | }
52 | go tunnel.ProcessInputPackets(t, tun)
53 | return t, nil
54 | }
55 |
--------------------------------------------------------------------------------
/outline/tun2socks/tunnel_darwin.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "errors"
19 | "io"
20 | "runtime/debug"
21 | "time"
22 |
23 | "github.com/Jigsaw-Code/outline-go-tun2socks/outline/shadowsocks"
24 | )
25 |
26 | // TunWriter is an interface that allows for outputting packets to the TUN (VPN).
27 | type TunWriter interface {
28 | io.WriteCloser
29 | }
30 |
31 | func init() {
32 | // Apple VPN extensions have a memory limit of 15MB. Conserve memory by increasing garbage
33 | // collection frequency and returning memory to the OS every minute.
34 | debug.SetGCPercent(10)
35 | // TODO: Check if this is still needed in go 1.13, which returns memory to the OS
36 | // automatically.
37 | ticker := time.NewTicker(time.Minute * 1)
38 | go func() {
39 | for range ticker.C {
40 | debug.FreeOSMemory()
41 | }
42 | }()
43 | }
44 |
45 | // ConnectShadowsocksTunnel reads packets from a TUN device and routes it to a Shadowsocks proxy server.
46 | // Returns an OutlineTunnel instance that should be used to input packets to the tunnel.
47 | //
48 | // `tunWriter` is used to output packets to the TUN (VPN).
49 | // `client` is the Shadowsocks client (created by [shadowsocks.NewClient]).
50 | // `isUDPEnabled` indicates whether the tunnel and/or network enable UDP proxying.
51 | //
52 | // Sets an error if the tunnel fails to connect.
53 | func ConnectShadowsocksTunnel(tunWriter TunWriter, client *shadowsocks.Client, isUDPEnabled bool) (Tunnel, error) {
54 | if tunWriter == nil {
55 | return nil, errors.New("must provide a TunWriter")
56 | } else if client == nil {
57 | return nil, errors.New("must provide a client")
58 | }
59 | return newTunnel(client, client, isUDPEnabled, tunWriter)
60 | }
61 |
--------------------------------------------------------------------------------
/outline/tun2socks/udp.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tun2socks
16 |
17 | import (
18 | "context"
19 | "fmt"
20 | "net"
21 | "sync"
22 | "time"
23 |
24 | "github.com/Jigsaw-Code/outline-sdk/transport"
25 | "github.com/eycorsican/go-tun2socks/core"
26 | )
27 |
28 | type udpHandler struct {
29 | // Protects the connections map
30 | sync.Mutex
31 |
32 | // Used to establish connections to the proxy
33 | listener transport.PacketListener
34 |
35 | // How long to wait for a packet from the proxy. Longer than this and the connection
36 | // is closed.
37 | timeout time.Duration
38 |
39 | // Maps connections from TUN to connections to the proxy.
40 | conns map[core.UDPConn]net.PacketConn
41 | }
42 |
43 | // NewUDPHandler returns a Shadowsocks UDP connection handler.
44 | //
45 | // `client` provides the Shadowsocks functionality.
46 | // `timeout` is the UDP read and write timeout.
47 | func NewUDPHandler(dialer transport.PacketListener, timeout time.Duration) core.UDPConnHandler {
48 | return &udpHandler{
49 | listener: dialer,
50 | timeout: timeout,
51 | conns: make(map[core.UDPConn]net.PacketConn, 8),
52 | }
53 | }
54 |
55 | func (h *udpHandler) Connect(tunConn core.UDPConn, target *net.UDPAddr) error {
56 | proxyConn, err := h.listener.ListenPacket(context.Background())
57 | if err != nil {
58 | return err
59 | }
60 | h.Lock()
61 | h.conns[tunConn] = proxyConn
62 | h.Unlock()
63 | go h.relayPacketsFromProxy(tunConn, proxyConn)
64 | return nil
65 | }
66 |
67 | // relayPacketsFromProxy relays packets from the proxy to the TUN device.
68 | func (h *udpHandler) relayPacketsFromProxy(tunConn core.UDPConn, proxyConn net.PacketConn) {
69 | buf := core.NewBytes(core.BufSize)
70 | defer func() {
71 | h.close(tunConn)
72 | core.FreeBytes(buf)
73 | }()
74 | for {
75 | proxyConn.SetDeadline(time.Now().Add(h.timeout))
76 | n, sourceAddr, err := proxyConn.ReadFrom(buf)
77 | if err != nil {
78 | return
79 | }
80 | // No resolution will take place, the address sent by the proxy is a resolved IP.
81 | sourceUDPAddr, err := net.ResolveUDPAddr("udp", sourceAddr.String())
82 | if err != nil {
83 | return
84 | }
85 | _, err = tunConn.WriteFrom(buf[:n], sourceUDPAddr)
86 | if err != nil {
87 | return
88 | }
89 | }
90 | }
91 |
92 | // ReceiveTo relays packets from the TUN device to the proxy. It's called by tun2socks.
93 | func (h *udpHandler) ReceiveTo(tunConn core.UDPConn, data []byte, destAddr *net.UDPAddr) error {
94 | h.Lock()
95 | proxyConn, ok := h.conns[tunConn]
96 | h.Unlock()
97 | if !ok {
98 | return fmt.Errorf("connection %v->%v does not exist", tunConn.LocalAddr(), destAddr)
99 | }
100 | proxyConn.SetDeadline(time.Now().Add(h.timeout))
101 | _, err := proxyConn.WriteTo(data, destAddr)
102 | return err
103 | }
104 |
105 | func (h *udpHandler) close(tunConn core.UDPConn) {
106 | tunConn.Close()
107 | h.Lock()
108 | defer h.Unlock()
109 | if proxyConn, ok := h.conns[tunConn]; ok {
110 | proxyConn.Close()
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tools.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build tools
16 | // +build tools
17 |
18 | // See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
19 | // and https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md
20 |
21 | package tools
22 |
23 | import (
24 | _ "github.com/crazy-max/xgo"
25 | _ "golang.org/x/mobile/cmd/gomobile"
26 | )
27 |
--------------------------------------------------------------------------------
/tunnel/tun.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tunnel
16 |
17 | import (
18 | "os"
19 |
20 | "github.com/eycorsican/go-tun2socks/common/log"
21 | _ "github.com/eycorsican/go-tun2socks/common/log/simple" // Import simple log for the side effect of making logs printable.
22 | )
23 |
24 | const vpnMtu = 1500
25 |
26 | // ProcessInputPackets reads packets from a TUN device `tun` and writes them to `tunnel`.
27 | func ProcessInputPackets(tunnel Tunnel, tun *os.File) {
28 | buffer := make([]byte, vpnMtu)
29 | for tunnel.IsConnected() {
30 | len, err := tun.Read(buffer)
31 | if err != nil {
32 | log.Warnf("Failed to read packet from TUN: %v", err)
33 | continue
34 | }
35 | if len == 0 {
36 | log.Infof("Read EOF from TUN")
37 | continue
38 | }
39 | tunnel.Write(buffer)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tunnel/tun_unix.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | //go:build unix
16 |
17 | package tunnel
18 |
19 | import (
20 | "errors"
21 | "os"
22 |
23 | _ "github.com/eycorsican/go-tun2socks/common/log/simple" // Import simple log for the side effect of making logs printable.
24 | "golang.org/x/sys/unix"
25 | )
26 |
27 | // MakeTunFile returns an os.File object from a TUN file descriptor `fd`.
28 | // The returned os.File holds a separate reference to the underlying file,
29 | // so the file will not be closed until both `fd` and the os.File are
30 | // separately closed. (UNIX only.)
31 | func MakeTunFile(fd int) (*os.File, error) {
32 | if fd < 0 {
33 | return nil, errors.New("Must provide a valid TUN file descriptor")
34 | }
35 | // Make a copy of `fd` so that os.File's finalizer doesn't close `fd`.
36 | newfd, err := unix.Dup(fd)
37 | if err != nil {
38 | return nil, err
39 | }
40 | file := os.NewFile(uintptr(newfd), "")
41 | if file == nil {
42 | return nil, errors.New("Failed to open TUN file descriptor")
43 | }
44 | return file, nil
45 | }
46 |
--------------------------------------------------------------------------------
/tunnel/tunnel.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 The Outline Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package tunnel
16 |
17 | import (
18 | "errors"
19 | "io"
20 |
21 | "github.com/eycorsican/go-tun2socks/core"
22 | )
23 |
24 | // Tunnel represents a session on a TUN device.
25 | type Tunnel interface {
26 | // IsConnected is true if Disconnect has not been called.
27 | IsConnected() bool
28 | // Disconnect closes the underlying resources. Subsequent Write calls will fail.
29 | Disconnect()
30 | // Write writes input data to the TUN interface.
31 | Write(data []byte) (int, error)
32 | }
33 |
34 | type tunnel struct {
35 | tunWriter io.WriteCloser
36 | lwipStack core.LWIPStack
37 | isConnected bool
38 | }
39 |
40 | func (t *tunnel) IsConnected() bool {
41 | return t.isConnected
42 | }
43 |
44 | func (t *tunnel) Disconnect() {
45 | if !t.isConnected {
46 | return
47 | }
48 | t.isConnected = false
49 | t.lwipStack.Close()
50 | t.tunWriter.Close()
51 | }
52 |
53 | func (t *tunnel) Write(data []byte) (int, error) {
54 | if !t.isConnected {
55 | return 0, errors.New("Failed to write, network stack closed")
56 | }
57 | return t.lwipStack.Write(data)
58 | }
59 |
60 | func NewTunnel(tunWriter io.WriteCloser, lwipStack core.LWIPStack) Tunnel {
61 | return &tunnel{tunWriter, lwipStack, true}
62 | }
63 |
--------------------------------------------------------------------------------