├── .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 | --------------------------------------------------------------------------------