├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yaml │ ├── config.yml │ ├── feature-request.yaml │ └── question.yaml ├── release-drafter.yml └── workflows │ ├── close-inactive-issues.yaml │ └── release.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── _docs ├── BUILD.md ├── INSTALL.md ├── README_ja.md ├── README_ko.md ├── README_ru.md └── README_zh-cn.md ├── cmd └── spoofdpi │ └── main.go ├── dns ├── addrselect │ ├── LICENSE │ └── addrselect.go ├── dns.go └── resolver │ ├── doh.go │ ├── general.go │ ├── resolver.go │ └── system.go ├── go.mod ├── go.sum ├── install.sh ├── packet ├── http.go └── https.go ├── proxy ├── handler │ ├── conn.go │ ├── http.go │ ├── https.go │ └── io.go ├── http.go ├── https.go ├── proxy.go └── server.go ├── util ├── args.go ├── config.go ├── context.go ├── log │ └── log.go └── os.go └── version ├── VERSION └── version.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: xvzc 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Found something you weren't expecting? Report it here! 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # NOTE 9 | - Please speak English, this is the language all maintainers can speak and write. 10 | - Please take a moment to check that your issue hasn't been reported before. 11 | - Make sure you are using the latest release. 12 | 13 | - type: input 14 | id: version 15 | attributes: 16 | label: Version 17 | description: | 18 | Version of SpoofDPI that you are currently using 19 | This can be shown by running "spoof-dpi -v" in terminal 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: os-ver 25 | attributes: 26 | label: Operating System 27 | description: The operating system and version you are using 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: run-info 33 | attributes: 34 | label: How are you running SpoofDPI? 35 | description: | 36 | Please provide information of how you are running SpoofDPI. 37 | This may include command line options that you used to run SpoofDPI, or the way how you installed SpoofDPI, and some more details. 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: description 43 | attributes: 44 | label: Description 45 | description: | 46 | Please provide a description of your issue here 47 | validations: 48 | required: true 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # NOTE 9 | - Please speak English, this is the language all maintainers can speak and write. 10 | - Please take a moment to check that your issue hasn't been reported before. 11 | - Make sure you are using the latest release. 12 | 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: Description 17 | description: | 18 | Please provide the description of your feature request here 19 | validations: 20 | required: true 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yaml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Need any help? Ask for help here! 3 | labels: ["question"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # NOTE 9 | - Please speak English, this is the language all maintainers can speak and write. 10 | - Please take a moment to check that your issue hasn't been reported before. 11 | - Make sure you are using the latest release. 12 | 13 | - type: input 14 | id: version 15 | attributes: 16 | label: Version 17 | description: | 18 | Version of SpoofDPI that you are currently using 19 | This can be shown by running "spoof-dpi -v" in terminal 20 | validations: 21 | required: true 22 | 23 | - type: input 24 | id: os-ver 25 | attributes: 26 | label: Operating System 27 | description: The operating system and version you are using 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: run-info 33 | attributes: 34 | label: How are you running SpoofDPI? 35 | description: | 36 | Please provide information of how you are running SpoofDPI. 37 | This may include command line options that you used to run SpoofDPI, or the way how you installed SpoofDPI, and some more details. 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: description 43 | attributes: 44 | label: Description 45 | description: | 46 | Please provide a description of your question here 47 | validations: 48 | required: true 49 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | change-template: '* $TITLE (#$NUMBER) by @$AUTHOR' 2 | change-title-escapes: '\<*_&#@`' 3 | exclude-labels: 4 | - 'chore' 5 | - 'documentation' 6 | - 'help' 7 | template: | 8 | ## Changes 9 | $CHANGES 10 | -------------------------------------------------------------------------------- /.github/workflows/close-inactive-issues.yaml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: -1 16 | days-before-issue-close: 30 17 | close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." 18 | days-before-pr-stale: -1 19 | days-before-pr-close: -1 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release drafter 2 | 3 | on: workflow_dispatch 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | draft_release: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | tag_name: ${{ steps.release.outputs.tag_name }} 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Read version 19 | id: read_version 20 | run: | 21 | echo "version=$(cat version/VERSION | tr -d '[:space:]')" >> $GITHUB_OUTPUT 22 | - name: Print version 23 | run: | 24 | echo ${{ steps.read_version.outputs.version }} 25 | - uses: release-drafter/release-drafter@v6 26 | id: release 27 | with: 28 | version: ${{ steps.read_version.outputs.version }} 29 | name: v${{ steps.read_version.outputs.version }} 30 | tag: v${{ steps.read_version.outputs.version }} 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | build_and_publish: 35 | runs-on: ubuntu-latest 36 | needs: draft_release 37 | name: Release linux/amd64 38 | permissions: 39 | contents: write 40 | pull-requests: write 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | TAG_NAME: ${{ needs.draft_release.outputs.tag_name }} 44 | CGO_ENABLED: 0 45 | steps: 46 | - uses: actions/checkout@v4 47 | - name: Setup Go 48 | uses: actions/setup-go@v5 49 | with: 50 | go-version: 1.21 51 | - name: Display Go version 52 | run: go version 53 | 54 | - name: darwin/amd64 55 | env: 56 | GOOS: darwin 57 | GOARCH: amd64 58 | run: | 59 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 60 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 61 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 62 | 63 | - name: darwin/arm64 64 | env: 65 | GOOS: darwin 66 | GOARCH: arm64 67 | run: | 68 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 69 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 70 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 71 | 72 | - name: linux/amd64 73 | env: 74 | GOOS: linux 75 | GOARCH: amd64 76 | run: | 77 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 78 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 79 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 80 | 81 | - name: linux/arm 82 | env: 83 | GOOS: linux 84 | GOARCH: arm 85 | run: | 86 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 87 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 88 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 89 | 90 | - name: linux/arm64 91 | env: 92 | GOOS: linux 93 | GOARCH: arm64 94 | run: | 95 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 96 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 97 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 98 | 99 | - name: linux/mips 100 | env: 101 | GOOS: linux 102 | GOARCH: mips 103 | run: | 104 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 105 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 106 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 107 | 108 | - name: linux/mipsle 109 | env: 110 | GOOS: linux 111 | GOARCH: mipsle 112 | run: | 113 | go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoofdpi 114 | tar -zcvf "spoofdpi-$GOOS-$GOARCH.tar.gz" ./spoofdpi && rm -rf ./spoofdpi 115 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.tar.gz" 116 | 117 | - name: windows/amd64 118 | env: 119 | GOOS: windows 120 | GOARCH: amd64 121 | run: | 122 | go build -ldflags="-w -s" -o "spoofdpi-$GOOS-$GOARCH.exe" github.com/xvzc/SpoofDPI/cmd/spoofdpi 123 | gh release upload $TAG_NAME "./spoofdpi-$GOOS-$GOARCH.exe" 124 | 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spoof-dpi 2 | spoof-dpi-* 3 | spoof-dpi.* 4 | !*/spoof-dpi/ 5 | out/** 6 | 7 | .DS_Store 8 | .idea/ 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | WORKDIR /go 3 | RUN go install -ldflags '-w -s -extldflags "-static"' -tags timetzdata github.com/xvzc/SpoofDPI/cmd/spoofdpi@latest 4 | 5 | FROM scratch 6 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 7 | COPY --from=builder /go/bin/spoofdpi / 8 | ENTRYPOINT ["/spoofdpi"] 9 | -------------------------------------------------------------------------------- /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 [2023] [Kwanghoo Park] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpoofDPI 2 | 3 | Read in other Languages: [🇬🇧English](https://github.com/xvzc/SpoofDPI), [🇰🇷한국어](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ko.md), [🇨🇳简体中文](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_zh-cn.md), [🇷🇺Русский](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ru.md), [🇯🇵日本語](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ja.md) 4 | 5 | A simple and fast software designed to bypass **Deep Packet Inspection**. 6 | 7 | ![image](https://user-images.githubusercontent.com/45588457/148035986-8b0076cc-fefb-48a1-9939-a8d9ab1d6322.png) 8 | 9 | # Installation 10 | See the installation guide for SpoofDPI [here](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md). 11 | 12 | 13 | Packaging status 14 | 15 | 16 | # Usage 17 | ``` 18 | Usage: spoofdpi [options...] 19 | -addr string 20 | listen address (default "127.0.0.1") 21 | -debug 22 | enable debug output 23 | -dns-addr string 24 | dns address (default "8.8.8.8") 25 | -dns-ipv4-only 26 | resolve only version 4 addresses 27 | -dns-port value 28 | port number for dns (default 53) 29 | -enable-doh 30 | enable 'dns-over-https' 31 | -pattern value 32 | bypass DPI only on packets matching this regex pattern; can be given multiple times 33 | -port value 34 | port (default 8080) 35 | -silent 36 | do not show the banner and server information at start up 37 | -system-proxy 38 | enable system-wide proxy (default true) 39 | -timeout value 40 | timeout in milliseconds; no timeout when not given 41 | -v print spoofdpi's version; this may contain some other relevant information 42 | -window-size value 43 | chunk size, in number of bytes, for fragmented client hello, 44 | try lower values if the default value doesn't bypass the DPI; 45 | when not given, the client hello packet will be sent in two parts: 46 | fragmentation for the first data packet and the rest 47 | ``` 48 | > If you are using any vpn extensions such as Hotspot Shield in Chrome browser, 49 | go to Settings > Extensions, and disable them. 50 | 51 | ### OSX 52 | Run `spoofdpi` and it will automatically set your proxy 53 | 54 | ### Linux 55 | Run `spoofdpi` and open your favorite browser with proxy option 56 | ```bash 57 | google-chrome --proxy-server="http://127.0.0.1:8080" 58 | ``` 59 | 60 | # How it works 61 | ### HTTP 62 | Since most websites in the world now support HTTPS, SpoofDPI doesn't bypass Deep Packet Inspections for HTTP requests, However, it still serves proxy connection for all HTTP requests. 63 | 64 | ### HTTPS 65 | Although TLS encrypts every handshake process, the domain names are still shown as plaintext in the Client hello packet. 66 | In other words, when someone else looks on the packet, they can easily guess where the packet is headed to. 67 | The domain name can offer significant information while DPI is being processed, and we can actually see that the connection is blocked right after sending Client hello packet. 68 | I had tried some ways to bypass this and found out that it seemed like only the first chunk gets inspected when we send the Client hello packet split into chunks. 69 | What SpoofDPI does to bypass this is to send the first 1 byte of a request to the server, 70 | and then send the rest. 71 | 72 | # Inspirations 73 | [Green Tunnel](https://github.com/SadeghHayeri/GreenTunnel) by @SadeghHayeri 74 | [GoodbyeDPI](https://github.com/ValdikSS/GoodbyeDPI) by @ValdikSS 75 | -------------------------------------------------------------------------------- /_docs/BUILD.md: -------------------------------------------------------------------------------- 1 | # Building from Source 2 | Although pre-built binaries are available for multiple platforms, you can also build your own binaries on your need. 3 | 4 | ## Prerequisites 5 | 1. Ensure you've installed go version `1.21` 6 | 2. Clone this repository to a location of your choice. 7 | 8 | ## Build 9 | ```bash 10 | CGO_ENABLED=0 go build -ldflags="-w -s" ./cmd/... 11 | ``` 12 | -------------------------------------------------------------------------------- /_docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation Guide 2 | 3 | ## Table of Contents 4 | 5 | 6 | * [Binary](#binary) 7 | * [Go](#go) 8 | 9 | 10 | ## Binary 11 | SpoofDPI will be installed in `~/.spoofdpi/bin`. 12 | To run SpoofDPI in any directory, add the line below to your `~/.bashrc || ~/.zshrc || ...` 13 | ```bash 14 | export PATH=$PATH:~/.spoofdpi/bin 15 | ``` 16 | ```bash 17 | # macOS Intel 18 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s darwin-amd64 19 | 20 | # macOS Apple Silicon 21 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s darwin-arm64 22 | 23 | # linux-amd64 24 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s linux-amd64 25 | 26 | # linux-arm 27 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s linux-arm 28 | 29 | # linux-arm64 30 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s linux-arm64 31 | 32 | # linux-mips 33 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s linux-mips 34 | 35 | # linux-mipsle 36 | curl -fsSL https://raw.githubusercontent.com/xvzc/SpoofDPI/main/install.sh | bash -s linux-mipsle 37 | ``` 38 | 39 | ## Go 40 | ```bash 41 | go install github.com/xvzc/SpoofDPI/cmd/spoofdpi@latest 42 | ``` 43 | -------------------------------------------------------------------------------- /_docs/README_ja.md: -------------------------------------------------------------------------------- 1 | # SpoofDPI 2 | 3 | 他の言語で読む: [🇬🇧English](https://github.com/xvzc/SpoofDPI), [🇰🇷한국어](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ko.md), [🇨🇳简体中文](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_zh-cn.md), [🇷🇺Русский](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ru.md), [🇯🇵日本語](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ja.md) 4 | 5 | **Deep Packet Inspection**をバイパスするために設計されたシンプルで高速なソフトウェア 6 | 7 | ![image](https://user-images.githubusercontent.com/45588457/148035986-8b0076cc-fefb-48a1-9939-a8d9ab1d6322.png) 8 | 9 | # Installation 10 | See the installation guide for SpoofDPI [here](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md). 11 | 12 | 13 | 14 | Packaging status 15 | 16 | 17 | # 使用方法 18 | ``` 19 | Usage: spoofdpi [options...] 20 | -addr string 21 | listen address (default "127.0.0.1") 22 | -debug 23 | enable debug output 24 | -dns-addr string 25 | dns address (default "8.8.8.8") 26 | -dns-ipv4-only 27 | resolve only version 4 addresses 28 | -dns-port value 29 | port number for dns (default 53) 30 | -enable-doh 31 | enable 'dns-over-https' 32 | -pattern value 33 | bypass DPI only on packets matching this regex pattern; can be given multiple times 34 | -port value 35 | port (default 8080) 36 | -silent 37 | do not show the banner and server information at start up 38 | -system-proxy 39 | enable system-wide proxy (default true) 40 | -timeout value 41 | timeout in milliseconds; no timeout when not given 42 | -v print spoofdpi's version; this may contain some other relevant information 43 | -window-size value 44 | chunk size, in number of bytes, for fragmented client hello, 45 | try lower values if the default value doesn't bypass the DPI; 46 | when not given, the client hello packet will be sent in two parts: 47 | fragmentation for the first data packet and the rest 48 | ``` 49 | > ChromeブラウザでHotspot ShieldなどのVPN拡張機能を使用している場合は、 50 | 設定 > 拡張機能に移動して無効にしてください。 51 | 52 | ### OSX 53 | `spoofdpi`を実行すると、自動的にプロキシが設定されます。 54 | 55 | ### Linux 56 | `spoofdpi`を実行し、プロキシオプションを使用してブラウザを開きます。 57 | ```bash 58 | google-chrome --proxy-server="http://127.0.0.1:8080" 59 | ``` 60 | 61 | # 仕組み 62 | ### HTTP 63 | 世界中のほとんどのウェブサイトがHTTPSをサポートしているため、SpoofDPIはHTTPリクエストのDeep Packet Inspectionをバイパスしませんが、すべてのHTTPリクエストに対してプロキシ接続を提供します。 64 | 65 | ### HTTPS 66 | TLS はすべてのハンドシェイクプロセスを暗号化しますが、Client helloパケットには依然としてドメイン名がプレーンテキストで表示されます。 67 | つまり、他の誰かがパケットを見た場合、パケットがどこに向かっているのかを簡単に推測することができます。 68 | ドメイン名はDPIが処理されている間に重要な情報を提供することができ、実際にClient helloパケットを送信した直後に接続がブロックされることがわかります。 69 | これをバイパスするためにいくつかの方法を試してみましたが、Client helloパケットをチャンクに分割して送信すると、最初のチャンクだけが検査されるように見えることがわかりました。 70 | SpoofDPIがこれをバイパスするために行うことは、リクエストの最初の1バイトをサーバーに送信し、その後に残りを送信することです。 71 | 72 | # インスピレーション 73 | [Green Tunnel](https://github.com/SadeghHayeri/GreenTunnel) by @SadeghHayeri 74 | [GoodbyeDPI](https://github.com/ValdikSS/GoodbyeDPI) by @ValdikSS 75 | -------------------------------------------------------------------------------- /_docs/README_ko.md: -------------------------------------------------------------------------------- 1 | # SpoofDPI 2 | 3 | 다른 언어로 읽기: [🇬🇧English](https://github.com/xvzc/SpoofDPI), [🇰🇷한국어](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ko.md), [🇨🇳简体中文](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_zh-cn.md), [🇷🇺Русский](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ru.md), [🇯🇵日本語](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ja.md) 4 | 5 | DPI(Deep Packet Inspection) 우회를 위해 고안된 소프트웨어 6 | 7 | ![image](https://user-images.githubusercontent.com/45588457/148035986-8b0076cc-fefb-48a1-9939-a8d9ab1d6322.png) 8 | 9 | # Installation 10 | SpoofDPI의 설치과정은 [여기](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md)를 참고바랍니다. 11 | 12 | 13 | 14 | Packaging status 15 | 16 | 17 | # 사용법 18 | ``` 19 | Usage: spoofdpi [options...] 20 | -addr string 21 | listen address (default "127.0.0.1") 22 | -debug 23 | enable debug output 24 | -dns-addr string 25 | dns address (default "8.8.8.8") 26 | -dns-ipv4-only 27 | resolve only version 4 addresses 28 | -dns-port value 29 | port number for dns (default 53) 30 | -enable-doh 31 | enable 'dns-over-https' 32 | -pattern value 33 | bypass DPI only on packets matching this regex pattern; can be given multiple times 34 | -port value 35 | port (default 8080) 36 | -silent 37 | do not show the banner and server information at start up 38 | -system-proxy 39 | enable system-wide proxy (default true) 40 | -timeout value 41 | timeout in milliseconds; no timeout when not given 42 | -v print spoofdpi's version; this may contain some other relevant information 43 | -window-size value 44 | chunk size, in number of bytes, for fragmented client hello, 45 | try lower values if the default value doesn't bypass the DPI; 46 | when not given, the client hello packet will be sent in two parts: 47 | fragmentation for the first data packet and the rest 48 | ``` 49 | > 만약 브라우저에서 Hotspot Shield와 같은 크롬 VPN 확장프로그램을 사용중이라면 50 | Settings > Extension 으로 이동해 비활성화 해주시기바랍니다. 51 | ### OSX 52 | 터미널에서 `$ spoofdpi`를 실행합니다. Proxy 설정은 자동으로 수행됩니다. 53 | 54 | ### Linux 55 | 터미널에서 `$ spoofdpi`를 실행하고, 프록시 옵션과 함께 브라우저를 실행합니다. 56 | `google-chrome --proxy-server="http://127.0.0.1:8080"` 57 | 58 | # 원리 59 | ### HTTP 60 | 최근 대부분의 웹사이트가 HTTPS를 지원하기 때문에, 61 | SpoofDPI는 HTTP 요청에 대한 DPI 우회는 지원하지 않습니다. 62 | 다만 모든 HTTP 요청에 대한 Proxy 연결은 지원합니다. 63 | 64 | ### HTTPS 65 | TLS는 모든 Handshake 과정을 암호화 합니다. 하지만, Client hello 패킷의 일부에는 여전히 서버의 도메인 네임이 평문으로 노출되어있습니다. 66 | 다시 말하자면, 누군가가 암호화된 패킷을 본다면 해당 패킷의 목적지가 어딘지 손쉽게 알아차릴 수 있다는 뜻입니다. 67 | 노출된 도메인은 DPI 검열에 매우 유용하게 사용될 수도 있고, 실제로 HTTPS 요청을 보냈을 때 차단이 이루어지는 시점도 Client hello 패킷을 보낸 시점입니다. 68 | 여러가지 방법을 시도해본 결과, Client hello 패킷을 여러 조각으로 나누어 요청을 보냈을 때, 첫번째 조각에 대해서만 도메인 검열이 이루어지는 듯한 동작을 발견했습니다. 따라서 SpoofDPI는 해당 패킷을 두번에 나누어 보냅니다. 자세히 말하자면, 첫번째 1 바이트를 우선적으로 보내고, 나머지를 그 이후에 보내는 동작을 수행합니다. 69 | 70 | # 참고 71 | [Green Tunnel](https://github.com/SadeghHayeri/GreenTunnel) by @SadeghHayeri 72 | [GoodbyeDPI](https://github.com/ValdikSS/GoodbyeDPI) by @ValdikSS 73 | 74 | 75 | -------------------------------------------------------------------------------- /_docs/README_ru.md: -------------------------------------------------------------------------------- 1 | # SpoofDPI 2 | 3 | Можете прочитать на других языках: [🇬🇧English](https://github.com/xvzc/SpoofDPI), [🇰🇷한국어](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ko.md), [🇨🇳简体中文](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_zh-cn.md), [🇷🇺Русский](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ru.md), [🇯🇵日本語](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ja.md) 4 | 5 | Простое и быстрое ПО, созданное для обхода **Deep Packet Inspection** 6 | 7 | ![image](https://user-images.githubusercontent.com/45588457/148035986-8b0076cc-fefb-48a1-9939-a8d9ab1d6322.png) 8 | 9 | # Installation 10 | Инструкции по установке SpoofDPI вы можете найти [здесь](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md). 11 | 12 | 13 | Packaging status 14 | 15 | 16 | # Использование 17 | ``` 18 | Usage: spoofdpi [опции...] 19 | -addr string 20 | listen address (default "127.0.0.1") 21 | -debug 22 | enable debug output 23 | -dns-addr string 24 | dns address (default "8.8.8.8") 25 | -dns-ipv4-only 26 | resolve only version 4 addresses 27 | -dns-port value 28 | port number for dns (default 53) 29 | -enable-doh 30 | enable 'dns-over-https' 31 | -pattern value 32 | bypass DPI only on packets matching this regex pattern; can be given multiple times 33 | -port value 34 | port (default 8080) 35 | -silent 36 | do not show the banner and server information at start up 37 | -system-proxy 38 | enable system-wide proxy (default true) 39 | -timeout value 40 | timeout in milliseconds; no timeout when not given 41 | -v print spoofdpi's version; this may contain some other relevant information 42 | -window-size value 43 | chunk size, in number of bytes, for fragmented client hello, 44 | try lower values if the default value doesn't bypass the DPI; 45 | when not given, the client hello packet will be sent in two parts: 46 | fragmentation for the first data packet and the rest 47 | ``` 48 | > Если Вы используете любые VPN-расширения по типу Hotspot Shield в браузере 49 | Chrome, зайдите в Настройки > Расширения и отключите их. 50 | 51 | ### OSX 52 | Выполните команду `spoofdpi` и прокси будет сконфигурирован автоматически 53 | 54 | ### Linux 55 | Выполните команду `spoofdpi` и откройте Chrome с параметром прокси: 56 | ```bash 57 | google-chrome --proxy-server="http://127.0.0.1:8080" 58 | ``` 59 | 60 | # Как это работает 61 | ### HTTP 62 | Поскольку большинство веб-сайтов работают поверх HTTPS, SpoofDPI не обходит Deep Packet Inspection для HTTP запросов, однако он по-прежнему обеспечивает проксирование для всех запросов по HTTP. 63 | 64 | ### HTTPS 65 | Несмотря на то, что шифрование используется в TLS даже во время установки соединения, имена доменов по-прежнему пересылаются в открытом виде в пакете Client Hello. Другими словами, когда кто-то посторонний смотрит на пакет, он может легко понять, куда этот пакет направляется. Доменное имя может предоставить важную информацию во время обработки DPI, и видно, что соединение блокируется сразу после отправки пакета Client Hello. 66 | Я попробовал несколько способов обойти это и обнаружил, что, похоже, когда мы отправляем пакет Client Hello, разделенный на фрагменты, проверяется только первый фрагмент. Поэтому, чтобы обойти DPI, SpoofDPI отправляет на сервер первый 1 байт запроса, а затем отправляет все остальное. 67 | 68 | # Проекты, повлиявшие на SpoofDPI 69 | [Green Tunnel](https://github.com/SadeghHayeri/GreenTunnel) от @SadeghHayeri 70 | [GoodbyeDPI](https://github.com/ValdikSS/GoodbyeDPI) от @ValdikSS 71 | -------------------------------------------------------------------------------- /_docs/README_zh-cn.md: -------------------------------------------------------------------------------- 1 | # SpoofDPI 2 | 3 | 选择语言: [🇬🇧English](https://github.com/xvzc/SpoofDPI), [🇰🇷한국어](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ko.md), [🇨🇳简体中文](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_zh-cn.md), [🇷🇺Русский](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ru.md), [🇯🇵日本語](https://github.com/xvzc/SpoofDPI/blob/main/_docs/README_ja.md) 4 | 5 | 6 | 7 | 规避**深度包检测**的简单工具 8 | 9 | ![image](https://user-images.githubusercontent.com/45588457/148035986-8b0076cc-fefb-48a1-9939-a8d9ab1d6322.png) 10 | 11 | # Installation 12 | See the installation guide for SpoofDPI [here](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md). 13 | 14 | 15 | Packaging status 16 | 17 | 18 | # 使用方法 19 | 20 | ``` 21 | Usage: spoofdpi [options...] 22 | -addr string 23 | listen address (default "127.0.0.1") 24 | -debug 25 | enable debug output 26 | -dns-addr string 27 | dns address (default "8.8.8.8") 28 | -dns-ipv4-only 29 | resolve only version 4 addresses 30 | -dns-port value 31 | port number for dns (default 53) 32 | -enable-doh 33 | enable 'dns-over-https' 34 | -pattern value 35 | bypass DPI only on packets matching this regex pattern; can be given multiple times 36 | -port value 37 | port (default 8080) 38 | -silent 39 | do not show the banner and server information at start up 40 | -system-proxy 41 | enable system-wide proxy (default true) 42 | -timeout value 43 | timeout in milliseconds; no timeout when not given 44 | -v print spoofdpi's version; this may contain some other relevant information 45 | -window-size value 46 | chunk size, in number of bytes, for fragmented client hello, 47 | try lower values if the default value doesn't bypass the DPI; 48 | when not given, the client hello packet will be sent in two parts: 49 | fragmentation for the first data packet and the rest 50 | ``` 51 | 52 | > 如果你在 Chrome 浏览器使用其他 VPN 扩展比如 Hotspot Shield 请去 设置 > 扩展程序禁用它们 53 | 54 | ### OSX 55 | 运行 `spoofdpi` ,然后它会自动设置自身为代理 56 | 57 | ### Linux 58 | 运行 `spoofdpi` 然后加上代理参数运行你的浏览器 59 | ```bash 60 | google-chrome --proxy-server="http://127.0.0.1:8080" 61 | ``` 62 | 63 | # 工作原理 64 | 65 | ### HTTP 66 | 67 | 因为世界上许多网站都已支持 HTTPS ,SpoofDPI 不会规避对 HTTP 请求的 DPI,但是它仍会为 HTTP 请求提供代理。 68 | 69 | ### HTTPS 70 | 尽管 TLS 加密了握手的每一步,但是在 Client Hello 中的域名仍然是明文的。因此如果有人看到 Client Hello 包就可以知道你在连接什么网站。这给 DPI 提供了很大方便,我们也看到连接在 Client Hello 之后就会被屏蔽掉。我之前尝试了规避这种审查,并发现,如果把 Client Hello 分包,只有第一个 chunk 会被检测。SpoofDPI 只要在第一个分包发送 1 byte,然后再发送其他部分就能规避。 71 | 72 | # 启发 73 | 74 | [Green Tunnel](https://github.com/SadeghHayeri/GreenTunnel) by @SadeghHayeri 75 | [GoodbyeDPI](https://github.com/ValdikSS/GoodbyeDPI) by @ValdikSS 76 | -------------------------------------------------------------------------------- /cmd/spoofdpi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/xvzc/SpoofDPI/util/log" 10 | 11 | "github.com/xvzc/SpoofDPI/proxy" 12 | "github.com/xvzc/SpoofDPI/util" 13 | "github.com/xvzc/SpoofDPI/version" 14 | ) 15 | 16 | func main() { 17 | args := util.ParseArgs() 18 | if args.Version { 19 | version.PrintVersion() 20 | os.Exit(0) 21 | } 22 | 23 | config := util.GetConfig() 24 | config.Load(args) 25 | 26 | log.InitLogger(config) 27 | ctx := util.GetCtxWithScope(context.Background(), "MAIN") 28 | logger := log.GetCtxLogger(ctx) 29 | 30 | pxy := proxy.New(config) 31 | 32 | if !config.Silent { 33 | util.PrintColoredBanner() 34 | } 35 | 36 | if config.SystemProxy { 37 | if err := util.SetOsProxy(uint16(config.Port)); err != nil { 38 | logger.Fatal().Msgf("error while changing proxy settings: %s", err) 39 | } 40 | defer func() { 41 | if err := util.UnsetOsProxy(); err != nil { 42 | logger.Fatal().Msgf("error while disabling proxy: %s", err) 43 | } 44 | }() 45 | } 46 | 47 | go pxy.Start(context.Background()) 48 | 49 | // Handle signals 50 | sigs := make(chan os.Signal, 1) 51 | done := make(chan bool, 1) 52 | 53 | signal.Notify( 54 | sigs, 55 | syscall.SIGKILL, 56 | syscall.SIGINT, 57 | syscall.SIGTERM, 58 | syscall.SIGQUIT, 59 | syscall.SIGHUP) 60 | 61 | go func() { 62 | _ = <-sigs 63 | done <- true 64 | }() 65 | 66 | <-done 67 | } 68 | -------------------------------------------------------------------------------- /dns/addrselect/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2009 The Go Authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dns/addrselect/addrselect.go: -------------------------------------------------------------------------------- 1 | package addrselect 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "sort" 7 | ) 8 | 9 | // Copyright 2015 The Go Authors. All rights reserved. 10 | // Use of this source code is governed by a BSD-style 11 | // license that can be found in the LICENSE file. 12 | 13 | // Minimal RFC 6724 address selection. 14 | 15 | func SortByRFC6724(addrs []net.IPAddr) { 16 | if len(addrs) < 2 { 17 | return 18 | } 19 | sortByRFC6724withSrcs(addrs, srcAddrs(addrs)) 20 | } 21 | 22 | func sortByRFC6724withSrcs(addrs []net.IPAddr, srcs []netip.Addr) { 23 | if len(addrs) != len(srcs) { 24 | panic("internal error") 25 | } 26 | addrAttr := make([]ipAttr, len(addrs)) 27 | srcAttr := make([]ipAttr, len(srcs)) 28 | for i, v := range addrs { 29 | addrAttrIP, _ := netip.AddrFromSlice(v.IP) 30 | addrAttr[i] = ipAttrOf(addrAttrIP) 31 | srcAttr[i] = ipAttrOf(srcs[i]) 32 | } 33 | sort.Stable(&byRFC6724{ 34 | addrs: addrs, 35 | addrAttr: addrAttr, 36 | srcs: srcs, 37 | srcAttr: srcAttr, 38 | }) 39 | } 40 | 41 | // srcAddrs tries to UDP-connect to each address to see if it has a 42 | // route. (This doesn't send any packets). The destination port 43 | // number is irrelevant. 44 | func srcAddrs(addrs []net.IPAddr) []netip.Addr { 45 | srcs := make([]netip.Addr, len(addrs)) 46 | dst := net.UDPAddr{Port: 9} 47 | for i := range addrs { 48 | dst.IP = addrs[i].IP 49 | dst.Zone = addrs[i].Zone 50 | c, err := net.DialUDP("udp", nil, &dst) 51 | if err == nil { 52 | if src, ok := c.LocalAddr().(*net.UDPAddr); ok { 53 | srcs[i], _ = netip.AddrFromSlice(src.IP) 54 | } 55 | c.Close() 56 | } 57 | } 58 | return srcs 59 | } 60 | 61 | type ipAttr struct { 62 | Scope scope 63 | Precedence uint8 64 | Label uint8 65 | } 66 | 67 | func ipAttrOf(ip netip.Addr) ipAttr { 68 | if !ip.IsValid() { 69 | return ipAttr{} 70 | } 71 | match := rfc6724policyTable.Classify(ip) 72 | return ipAttr{ 73 | Scope: classifyScope(ip), 74 | Precedence: match.Precedence, 75 | Label: match.Label, 76 | } 77 | } 78 | 79 | type byRFC6724 struct { 80 | addrs []net.IPAddr // addrs to sort 81 | addrAttr []ipAttr 82 | srcs []netip.Addr // or not valid addr if unreachable 83 | srcAttr []ipAttr 84 | } 85 | 86 | func (s *byRFC6724) Len() int { return len(s.addrs) } 87 | 88 | func (s *byRFC6724) Swap(i, j int) { 89 | s.addrs[i], s.addrs[j] = s.addrs[j], s.addrs[i] 90 | s.srcs[i], s.srcs[j] = s.srcs[j], s.srcs[i] 91 | s.addrAttr[i], s.addrAttr[j] = s.addrAttr[j], s.addrAttr[i] 92 | s.srcAttr[i], s.srcAttr[j] = s.srcAttr[j], s.srcAttr[i] 93 | } 94 | 95 | // Less reports whether i is a better destination address for this 96 | // host than j. 97 | // 98 | // The algorithm and variable names comes from RFC 6724 section 6. 99 | func (s *byRFC6724) Less(i, j int) bool { 100 | DA := s.addrs[i].IP 101 | DB := s.addrs[j].IP 102 | SourceDA := s.srcs[i] 103 | SourceDB := s.srcs[j] 104 | attrDA := &s.addrAttr[i] 105 | attrDB := &s.addrAttr[j] 106 | attrSourceDA := &s.srcAttr[i] 107 | attrSourceDB := &s.srcAttr[j] 108 | 109 | const preferDA = true 110 | const preferDB = false 111 | 112 | // Rule 1: Avoid unusable destinations. 113 | // If DB is known to be unreachable or if Source(DB) is undefined, then 114 | // prefer DA. Similarly, if DA is known to be unreachable or if 115 | // Source(DA) is undefined, then prefer DB. 116 | if !SourceDA.IsValid() && !SourceDB.IsValid() { 117 | return false // "equal" 118 | } 119 | if !SourceDB.IsValid() { 120 | return preferDA 121 | } 122 | if !SourceDA.IsValid() { 123 | return preferDB 124 | } 125 | 126 | // Rule 2: Prefer matching scope. 127 | // If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)), 128 | // then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and 129 | // Scope(DB) = Scope(Source(DB)), then prefer DB. 130 | if attrDA.Scope == attrSourceDA.Scope && attrDB.Scope != attrSourceDB.Scope { 131 | return preferDA 132 | } 133 | if attrDA.Scope != attrSourceDA.Scope && attrDB.Scope == attrSourceDB.Scope { 134 | return preferDB 135 | } 136 | 137 | // Rule 3: Avoid deprecated addresses. 138 | // If Source(DA) is deprecated and Source(DB) is not, then prefer DB. 139 | // Similarly, if Source(DA) is not deprecated and Source(DB) is 140 | // deprecated, then prefer DA. 141 | 142 | // TODO(bradfitz): implement? low priority for now. 143 | 144 | // Rule 4: Prefer home addresses. 145 | // If Source(DA) is simultaneously a home address and care-of address 146 | // and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is 147 | // simultaneously a home address and care-of address and Source(DA) is 148 | // not, then prefer DB. 149 | 150 | // TODO(bradfitz): implement? low priority for now. 151 | 152 | // Rule 5: Prefer matching label. 153 | // If Label(Source(DA)) = Label(DA) and Label(Source(DB)) <> Label(DB), 154 | // then prefer DA. Similarly, if Label(Source(DA)) <> Label(DA) and 155 | // Label(Source(DB)) = Label(DB), then prefer DB. 156 | if attrSourceDA.Label == attrDA.Label && 157 | attrSourceDB.Label != attrDB.Label { 158 | return preferDA 159 | } 160 | if attrSourceDA.Label != attrDA.Label && 161 | attrSourceDB.Label == attrDB.Label { 162 | return preferDB 163 | } 164 | 165 | // Rule 6: Prefer higher precedence. 166 | // If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if 167 | // Precedence(DA) < Precedence(DB), then prefer DB. 168 | if attrDA.Precedence > attrDB.Precedence { 169 | return preferDA 170 | } 171 | if attrDA.Precedence < attrDB.Precedence { 172 | return preferDB 173 | } 174 | 175 | // Rule 7: Prefer native transport. 176 | // If DA is reached via an encapsulating transition mechanism (e.g., 177 | // IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is 178 | // reached via encapsulation and DA is not, then prefer DA. 179 | 180 | // TODO(bradfitz): implement? low priority for now. 181 | 182 | // Rule 8: Prefer smaller scope. 183 | // If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) > 184 | // Scope(DB), then prefer DB. 185 | if attrDA.Scope < attrDB.Scope { 186 | return preferDA 187 | } 188 | if attrDA.Scope > attrDB.Scope { 189 | return preferDB 190 | } 191 | 192 | // Rule 9: Use the longest matching prefix. 193 | // When DA and DB belong to the same address family (both are IPv6 or 194 | // both are IPv4 [but see below]): If CommonPrefixLen(Source(DA), DA) > 195 | // CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if 196 | // CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB), 197 | // then prefer DB. 198 | // 199 | // However, applying this rule to IPv4 addresses causes 200 | // problems (see issues 13283 and 18518), so limit to IPv6. 201 | if DA.To4() == nil && DB.To4() == nil { 202 | commonA := commonPrefixLen(SourceDA, DA) 203 | commonB := commonPrefixLen(SourceDB, DB) 204 | 205 | if commonA > commonB { 206 | return preferDA 207 | } 208 | if commonA < commonB { 209 | return preferDB 210 | } 211 | } 212 | 213 | // Rule 10: Otherwise, leave the order unchanged. 214 | // If DA preceded DB in the original list, prefer DA. 215 | // Otherwise, prefer DB. 216 | return false // "equal" 217 | } 218 | 219 | type policyTableEntry struct { 220 | Prefix netip.Prefix 221 | Precedence uint8 222 | Label uint8 223 | } 224 | 225 | type policyTable []policyTableEntry 226 | 227 | // RFC 6724 section 2.1. 228 | // Items are sorted by the size of their Prefix.Mask.Size, 229 | var rfc6724policyTable = policyTable{ 230 | { 231 | // "::1/128" 232 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}), 128), 233 | Precedence: 50, 234 | Label: 0, 235 | }, 236 | { 237 | // "::ffff:0:0/96" 238 | // IPv4-compatible, etc. 239 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}), 96), 240 | Precedence: 35, 241 | Label: 4, 242 | }, 243 | { 244 | // "::/96" 245 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 96), 246 | Precedence: 1, 247 | Label: 3, 248 | }, 249 | { 250 | // "2001::/32" 251 | // Teredo 252 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01}), 32), 253 | Precedence: 5, 254 | Label: 5, 255 | }, 256 | { 257 | // "2002::/16" 258 | // 6to4 259 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x02}), 16), 260 | Precedence: 30, 261 | Label: 2, 262 | }, 263 | { 264 | // "3ffe::/16" 265 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x3f, 0xfe}), 16), 266 | Precedence: 1, 267 | Label: 12, 268 | }, 269 | { 270 | // "fec0::/10" 271 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfe, 0xc0}), 10), 272 | Precedence: 1, 273 | Label: 11, 274 | }, 275 | { 276 | // "fc00::/7" 277 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfc}), 7), 278 | Precedence: 3, 279 | Label: 13, 280 | }, 281 | { 282 | // "::/0" 283 | Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0), 284 | Precedence: 40, 285 | Label: 1, 286 | }, 287 | } 288 | 289 | // Classify returns the policyTableEntry of the entry with the longest 290 | // matching prefix that contains ip. 291 | // The table t must be sorted from largest mask size to smallest. 292 | func (t policyTable) Classify(ip netip.Addr) policyTableEntry { 293 | // Prefix.Contains() will not match an IPv6 prefix for an IPv4 address. 294 | if ip.Is4() { 295 | ip = netip.AddrFrom16(ip.As16()) 296 | } 297 | for _, ent := range t { 298 | if ent.Prefix.Contains(ip) { 299 | return ent 300 | } 301 | } 302 | return policyTableEntry{} 303 | } 304 | 305 | // RFC 6724 section 3.1. 306 | type scope uint8 307 | 308 | const ( 309 | scopeInterfaceLocal scope = 0x1 310 | scopeLinkLocal scope = 0x2 311 | scopeAdminLocal scope = 0x4 312 | scopeSiteLocal scope = 0x5 313 | scopeOrgLocal scope = 0x8 314 | scopeGlobal scope = 0xe 315 | ) 316 | 317 | func classifyScope(ip netip.Addr) scope { 318 | if ip.IsLoopback() || ip.IsLinkLocalUnicast() { 319 | return scopeLinkLocal 320 | } 321 | ipv6 := ip.Is6() && !ip.Is4In6() 322 | ipv6AsBytes := ip.As16() 323 | if ipv6 && ip.IsMulticast() { 324 | return scope(ipv6AsBytes[1] & 0xf) 325 | } 326 | // Site-local addresses are defined in RFC 3513 section 2.5.6 327 | // (and deprecated in RFC 3879). 328 | if ipv6 && ipv6AsBytes[0] == 0xfe && ipv6AsBytes[1]&0xc0 == 0xc0 { 329 | return scopeSiteLocal 330 | } 331 | return scopeGlobal 332 | } 333 | 334 | // commonPrefixLen reports the length of the longest prefix (looking 335 | // at the most significant, or leftmost, bits) that the 336 | // two addresses have in common, up to the length of a's prefix (i.e., 337 | // the portion of the address not including the interface ID). 338 | // 339 | // If a or b is an IPv4 address as an IPv6 address, the IPv4 addresses 340 | // are compared (with max common prefix length of 32). 341 | // If a and b are different IP versions, 0 is returned. 342 | // 343 | // See https://tools.ietf.org/html/rfc6724#section-2.2 344 | func commonPrefixLen(a netip.Addr, b net.IP) (cpl int) { 345 | if b4 := b.To4(); b4 != nil { 346 | b = b4 347 | } 348 | aAsSlice := a.AsSlice() 349 | if len(aAsSlice) != len(b) { 350 | return 0 351 | } 352 | // If IPv6, only up to the prefix (first 64 bits) 353 | if len(aAsSlice) > 8 { 354 | aAsSlice = aAsSlice[:8] 355 | b = b[:8] 356 | } 357 | for len(aAsSlice) > 0 { 358 | if aAsSlice[0] == b[0] { 359 | cpl += 8 360 | aAsSlice = aAsSlice[1:] 361 | b = b[1:] 362 | continue 363 | } 364 | bits := 8 365 | ab, bb := aAsSlice[0], b[0] 366 | for { 367 | ab >>= 1 368 | bb >>= 1 369 | bits-- 370 | if ab == bb { 371 | cpl += bits 372 | return 373 | } 374 | } 375 | } 376 | return 377 | } 378 | -------------------------------------------------------------------------------- /dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/miekg/dns" 11 | "github.com/xvzc/SpoofDPI/dns/resolver" 12 | "github.com/xvzc/SpoofDPI/util" 13 | "github.com/xvzc/SpoofDPI/util/log" 14 | ) 15 | 16 | const scopeDNS = "DNS" 17 | 18 | type Resolver interface { 19 | Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) 20 | String() string 21 | } 22 | 23 | type Dns struct { 24 | host string 25 | port string 26 | systemClient Resolver 27 | generalClient Resolver 28 | dohClient Resolver 29 | qTypes []uint16 30 | } 31 | 32 | func NewDns(config *util.Config) *Dns { 33 | addr := config.DnsAddr 34 | port := strconv.Itoa(config.DnsPort) 35 | var qTypes []uint16 36 | if config.DnsIPv4Only { 37 | qTypes = []uint16{dns.TypeA} 38 | } else { 39 | qTypes = []uint16{dns.TypeAAAA, dns.TypeA} 40 | } 41 | return &Dns{ 42 | host: config.DnsAddr, 43 | port: port, 44 | systemClient: resolver.NewSystemResolver(), 45 | generalClient: resolver.NewGeneralResolver(net.JoinHostPort(addr, port)), 46 | dohClient: resolver.NewDOHResolver(addr), 47 | qTypes: qTypes, 48 | } 49 | } 50 | 51 | func (d *Dns) ResolveHost(ctx context.Context, host string, enableDoh bool, useSystemDns bool) (string, error) { 52 | ctx = util.GetCtxWithScope(ctx, scopeDNS) 53 | logger := log.GetCtxLogger(ctx) 54 | 55 | if ip, err := parseIpAddr(host); err == nil { 56 | return ip.String(), nil 57 | } 58 | 59 | clt := d.clientFactory(enableDoh, useSystemDns) 60 | ctx, cancel := context.WithTimeout(ctx, 3*time.Second) 61 | defer cancel() 62 | 63 | logger.Debug().Msgf("resolving %s using %s", host, clt) 64 | 65 | t := time.Now() 66 | 67 | addrs, err := clt.Resolve(ctx, host, d.qTypes) 68 | // addrs, err := clt.Resolve(ctx, host, []uint16{dns.TypeAAAA}) 69 | if err != nil { 70 | return "", fmt.Errorf("%s: %w", clt, err) 71 | } 72 | 73 | if len(addrs) > 0 { 74 | d := time.Since(t).Milliseconds() 75 | logger.Debug().Msgf("resolved %s from %s in %d ms", addrs[0].String(), host, d) 76 | return addrs[0].String(), nil 77 | } 78 | 79 | return "", fmt.Errorf("could not resolve %s using %s", host, clt) 80 | } 81 | 82 | func (d *Dns) clientFactory(enableDoh bool, useSystemDns bool) Resolver { 83 | if useSystemDns { 84 | return d.systemClient 85 | } 86 | 87 | if enableDoh { 88 | return d.dohClient 89 | } 90 | 91 | return d.generalClient 92 | } 93 | 94 | func parseIpAddr(addr string) (*net.IPAddr, error) { 95 | ip := net.ParseIP(addr) 96 | if ip == nil { 97 | return nil, fmt.Errorf("%s is not an ip address", addr) 98 | } 99 | 100 | ipAddr := &net.IPAddr{ 101 | IP: ip, 102 | } 103 | 104 | return ipAddr, nil 105 | } 106 | -------------------------------------------------------------------------------- /dns/resolver/doh.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "regexp" 12 | "time" 13 | 14 | "github.com/miekg/dns" 15 | ) 16 | 17 | type DOHResolver struct { 18 | upstream string 19 | client *http.Client 20 | } 21 | 22 | func NewDOHResolver(host string) *DOHResolver { 23 | c := &http.Client{ 24 | Timeout: 5 * time.Second, 25 | Transport: &http.Transport{ 26 | DialContext: (&net.Dialer{ 27 | Timeout: 3 * time.Second, 28 | KeepAlive: 30 * time.Second, 29 | }).DialContext, 30 | TLSHandshakeTimeout: 5 * time.Second, 31 | MaxIdleConnsPerHost: 100, 32 | MaxIdleConns: 100, 33 | }, 34 | } 35 | 36 | host = regexp.MustCompile(`^https://|/dns-query$`).ReplaceAllString(host, "") 37 | if ip := net.ParseIP(host); ip != nil && ip.To4() == nil { 38 | host = fmt.Sprintf("[%s]", ip) 39 | } 40 | 41 | return &DOHResolver{ 42 | upstream: "https://" + host + "/dns-query", 43 | client: c, 44 | } 45 | } 46 | 47 | func (r *DOHResolver) Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) { 48 | resultCh := lookupAllTypes(ctx, host, qTypes, r.exchange) 49 | addrs, err := processResults(ctx, resultCh) 50 | return addrs, err 51 | } 52 | 53 | func (r *DOHResolver) String() string { 54 | return fmt.Sprintf("doh resolver(%s)", r.upstream) 55 | } 56 | 57 | func (r *DOHResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { 58 | pack, err := msg.Pack() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | url := fmt.Sprintf("%s?dns=%s", r.upstream, base64.RawStdEncoding.EncodeToString(pack)) 64 | req, err := http.NewRequest("GET", url, nil) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | req = req.WithContext(ctx) 70 | req.Header.Set("Accept", "application/dns-message") 71 | 72 | resp, err := r.client.Do(req) 73 | if err != nil { 74 | return nil, err 75 | } 76 | defer resp.Body.Close() 77 | 78 | if resp.StatusCode != http.StatusOK { 79 | return nil, errors.New("doh status error") 80 | } 81 | 82 | buf := bytes.Buffer{} 83 | _, err = buf.ReadFrom(resp.Body) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | resultMsg := new(dns.Msg) 89 | err = resultMsg.Unpack(buf.Bytes()) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | if resultMsg.Rcode != dns.RcodeSuccess { 95 | return nil, errors.New("doh rcode wasn't successful") 96 | } 97 | 98 | return resultMsg, nil 99 | } 100 | -------------------------------------------------------------------------------- /dns/resolver/general.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | type GeneralResolver struct { 12 | client *dns.Client 13 | server string 14 | } 15 | 16 | func NewGeneralResolver(server string) *GeneralResolver { 17 | return &GeneralResolver{ 18 | client: &dns.Client{}, 19 | server: server, 20 | } 21 | } 22 | 23 | func (r *GeneralResolver) Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) { 24 | resultCh := lookupAllTypes(ctx, host, qTypes, r.exchange) 25 | addrs, err := processResults(ctx, resultCh) 26 | return addrs, err 27 | } 28 | 29 | func (r *GeneralResolver) String() string { 30 | return fmt.Sprintf("general resolver(%s)", r.server) 31 | } 32 | 33 | func (r *GeneralResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { 34 | resp, _, err := r.client.Exchange(msg, r.server) 35 | return resp, err 36 | } 37 | -------------------------------------------------------------------------------- /dns/resolver/resolver.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "sync" 10 | 11 | "github.com/miekg/dns" 12 | "github.com/xvzc/SpoofDPI/dns/addrselect" 13 | ) 14 | 15 | type exchangeFunc = func(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) 16 | 17 | type DNSResult struct { 18 | msg *dns.Msg 19 | err error 20 | } 21 | 22 | func recordTypeIDToName(id uint16) string { 23 | switch id { 24 | case 1: 25 | return "A" 26 | case 28: 27 | return "AAAA" 28 | } 29 | return strconv.FormatUint(uint64(id), 10) 30 | } 31 | 32 | func parseAddrsFromMsg(msg *dns.Msg) []net.IPAddr { 33 | var addrs []net.IPAddr 34 | 35 | for _, record := range msg.Answer { 36 | switch ipRecord := record.(type) { 37 | case *dns.A: 38 | addrs = append(addrs, net.IPAddr{IP: ipRecord.A}) 39 | case *dns.AAAA: 40 | addrs = append(addrs, net.IPAddr{IP: ipRecord.AAAA}) 41 | } 42 | } 43 | return addrs 44 | } 45 | 46 | func sortAddrs(addrs []net.IPAddr) { 47 | addrselect.SortByRFC6724(addrs) 48 | } 49 | 50 | func lookupAllTypes(ctx context.Context, host string, qTypes []uint16, exchange exchangeFunc) <-chan *DNSResult { 51 | var wg sync.WaitGroup 52 | resCh := make(chan *DNSResult) 53 | 54 | for _, qType := range qTypes { 55 | wg.Add(1) 56 | go func(qType uint16) { 57 | defer wg.Done() 58 | select { 59 | case <-ctx.Done(): 60 | return 61 | case resCh <- lookupType(ctx, host, qType, exchange): 62 | } 63 | }(qType) 64 | } 65 | 66 | go func() { 67 | wg.Wait() 68 | close(resCh) 69 | }() 70 | 71 | return resCh 72 | } 73 | 74 | func lookupType(ctx context.Context, host string, queryType uint16, exchange exchangeFunc) *DNSResult { 75 | msg := newMsg(host, queryType) 76 | resp, err := exchange(ctx, msg) 77 | if err != nil { 78 | queryName := recordTypeIDToName(queryType) 79 | err = fmt.Errorf("resolving %s, query type %s: %w", host, queryName, err) 80 | return &DNSResult{err: err} 81 | } 82 | return &DNSResult{msg: resp} 83 | } 84 | 85 | func newMsg(host string, qType uint16) *dns.Msg { 86 | msg := new(dns.Msg) 87 | msg.SetQuestion(dns.Fqdn(host), qType) 88 | return msg 89 | } 90 | 91 | func processResults(ctx context.Context, resCh <-chan *DNSResult) ([]net.IPAddr, error) { 92 | var errs []error 93 | var addrs []net.IPAddr 94 | 95 | for result := range resCh { 96 | if result.err != nil { 97 | errs = append(errs, result.err) 98 | continue 99 | } 100 | resultAddrs := parseAddrsFromMsg(result.msg) 101 | addrs = append(addrs, resultAddrs...) 102 | } 103 | select { 104 | case <-ctx.Done(): 105 | return nil, errors.New("canceled") 106 | default: 107 | if len(addrs) == 0 { 108 | return addrs, errors.Join(errs...) 109 | } 110 | } 111 | 112 | sortAddrs(addrs) 113 | return addrs, nil 114 | } 115 | -------------------------------------------------------------------------------- /dns/resolver/system.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type SystemResolver struct { 9 | *net.Resolver 10 | } 11 | 12 | func NewSystemResolver() *SystemResolver { 13 | return &SystemResolver{ 14 | &net.Resolver{PreferGo: true}, 15 | } 16 | } 17 | 18 | func (r *SystemResolver) String() string { 19 | return "system resolver" 20 | } 21 | 22 | func (r *SystemResolver) Resolve(ctx context.Context, host string, _ []uint16) ([]net.IPAddr, error) { 23 | addrs, err := r.LookupIPAddr(ctx, host) 24 | if err != nil { 25 | return []net.IPAddr{}, err 26 | } 27 | return addrs, nil 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xvzc/SpoofDPI 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/miekg/dns v1.1.61 9 | github.com/pterm/pterm v0.12.79 10 | github.com/rs/zerolog v1.33.0 11 | ) 12 | 13 | require ( 14 | atomicgo.dev/cursor v0.2.0 // indirect 15 | atomicgo.dev/keyboard v0.2.9 // indirect 16 | atomicgo.dev/schedule v0.1.0 // indirect 17 | github.com/containerd/console v1.0.3 // indirect 18 | github.com/gookit/color v1.5.4 // indirect 19 | github.com/lithammer/fuzzysearch v1.1.8 // indirect 20 | github.com/mattn/go-colorable v0.1.13 // indirect 21 | github.com/mattn/go-isatty v0.0.19 // indirect 22 | github.com/mattn/go-runewidth v0.0.15 // indirect 23 | github.com/rivo/uniseg v0.4.4 // indirect 24 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 25 | golang.org/x/mod v0.18.0 // indirect 26 | golang.org/x/net v0.27.0 // indirect 27 | golang.org/x/sync v0.7.0 // indirect 28 | golang.org/x/sys v0.22.0 // indirect 29 | golang.org/x/term v0.22.0 // indirect 30 | golang.org/x/text v0.16.0 // indirect 31 | golang.org/x/tools v0.22.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= 2 | atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= 3 | atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= 4 | atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= 5 | atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= 6 | atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= 7 | atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= 8 | atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= 9 | github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= 10 | github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= 11 | github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= 12 | github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= 13 | github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= 14 | github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= 15 | github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= 16 | github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= 17 | github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= 18 | github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= 19 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 20 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 21 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 26 | github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= 27 | github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= 28 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 29 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 30 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 31 | github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 32 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 33 | github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= 34 | github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 35 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 38 | github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= 39 | github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= 40 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 41 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 42 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 43 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 44 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 45 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 46 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 47 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 48 | github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= 49 | github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= 50 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 51 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= 54 | github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= 55 | github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= 56 | github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= 57 | github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= 58 | github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= 59 | github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= 60 | github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= 61 | github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= 62 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 63 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 64 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 65 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 66 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 67 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 68 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 69 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 72 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 75 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 76 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= 77 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 78 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 79 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 82 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 83 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 84 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 85 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 86 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 87 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 88 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 89 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 90 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 91 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 92 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 93 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 98 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 99 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 101 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 113 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 114 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 115 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 116 | golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 117 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 118 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 119 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 120 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 121 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 124 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 125 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 126 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 127 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 128 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 129 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 130 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 131 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 132 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 133 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 134 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 138 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 139 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 140 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl "https://api.github.com/repos/xvzc/SpoofDPI/releases/latest" | 4 | grep '"tag_name":' | 5 | sed -E 's/.*"([^"]+)".*/\1/' | 6 | xargs -I {} curl -OL "https://github.com/xvzc/SpoofDPI/releases/download/"\{\}"/spoofdpi-${1}.tar.gz" 7 | 8 | mkdir -p ~/.spoofdpi/bin 9 | 10 | tar -xzvf ./spoofdpi-${1}.tar.gz && \ 11 | rm -rf ./spoofdpi-${1}.tar.gz && \ 12 | mv ./spoofdpi ~/.spoofdpi/bin 13 | 14 | if [ $? -ne 0 ]; then 15 | echo "Error. exiting now" 16 | exit 17 | fi 18 | 19 | export PATH=$PATH:~/.spoofdpi/bin 20 | 21 | echo "" 22 | echo "Successfully installed SpoofDPI." 23 | echo "Please add the line below to your rcfile(.bashrc or .zshrc etc..)" 24 | echo "" 25 | echo ">> export PATH=\$PATH:~/.spoofdpi/bin" 26 | -------------------------------------------------------------------------------- /packet/http.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | var validMethod = map[string]struct{}{ 13 | "DELETE": {}, 14 | "GET": {}, 15 | "HEAD": {}, 16 | "POST": {}, 17 | "PUT": {}, 18 | "CONNECT": {}, 19 | "OPTIONS": {}, 20 | "TRACE": {}, 21 | "COPY": {}, 22 | "LOCK": {}, 23 | "MKCOL": {}, 24 | "MOVE": {}, 25 | "PROPFIND": {}, 26 | "PROPPATCH": {}, 27 | "SEARCH": {}, 28 | "UNLOCK": {}, 29 | "BIND": {}, 30 | "REBIND": {}, 31 | "UNBIND": {}, 32 | "ACL": {}, 33 | "REPORT": {}, 34 | "MKACTIVITY": {}, 35 | "CHECKOUT": {}, 36 | "MERGE": {}, 37 | "M-SEARCH": {}, 38 | "NOTIFY": {}, 39 | "SUBSCRIBE": {}, 40 | "UNSUBSCRIBE": {}, 41 | "PATCH": {}, 42 | "PURGE": {}, 43 | "MKCALENDAR": {}, 44 | "LINK": {}, 45 | "UNLINK": {}, 46 | } 47 | 48 | type HttpRequest struct { 49 | raw []byte 50 | method string 51 | domain string 52 | port string 53 | path string 54 | version string 55 | } 56 | 57 | func ReadHttpRequest(rdr io.Reader) (*HttpRequest, error) { 58 | p, err := parse(rdr) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return p, nil 64 | } 65 | 66 | func (p *HttpRequest) Raw() []byte { 67 | return p.raw 68 | } 69 | func (p *HttpRequest) Method() string { 70 | return p.method 71 | } 72 | 73 | func (p *HttpRequest) Domain() string { 74 | return p.domain 75 | } 76 | 77 | func (p *HttpRequest) Port() string { 78 | return p.port 79 | } 80 | 81 | func (p *HttpRequest) Version() string { 82 | return p.version 83 | } 84 | 85 | func (p *HttpRequest) IsValidMethod() bool { 86 | if _, exists := validMethod[p.Method()]; exists { 87 | return true 88 | } 89 | 90 | return false 91 | } 92 | 93 | func (p *HttpRequest) IsConnectMethod() bool { 94 | return p.Method() == "CONNECT" 95 | } 96 | 97 | func (p *HttpRequest) Tidy() { 98 | s := string(p.raw) 99 | 100 | parts := strings.Split(s, "\r\n\r\n") 101 | meta := strings.Split(parts[0], "\r\n") 102 | 103 | meta[0] = p.method + " " + p.path + " " + p.version 104 | 105 | var buf bytes.Buffer 106 | buf.Grow(len(p.raw)) 107 | 108 | crLF := []byte{0xD, 0xA} 109 | for _, m := range meta { 110 | if strings.HasPrefix(m, "Proxy-Connection") { 111 | continue 112 | } 113 | buf.WriteString(m) 114 | buf.Write(crLF) 115 | } 116 | buf.Write(crLF) 117 | buf.WriteString(parts[1]) 118 | 119 | p.raw = buf.Bytes() 120 | } 121 | 122 | func parse(rdr io.Reader) (*HttpRequest, error) { 123 | sb := strings.Builder{} 124 | tee := io.TeeReader(rdr, &sb) 125 | request, err := http.ReadRequest(bufio.NewReader(tee)) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | p := &HttpRequest{} 131 | p.raw = []byte(sb.String()) 132 | 133 | p.domain, p.port, err = net.SplitHostPort(request.Host) 134 | if err != nil { 135 | p.domain = request.Host 136 | p.port = "" 137 | } 138 | 139 | p.method = request.Method 140 | p.version = request.Proto 141 | p.path = request.URL.Path 142 | 143 | if request.URL.RawQuery != "" { 144 | p.path += "?" + request.URL.RawQuery 145 | } 146 | 147 | if request.URL.RawFragment != "" { 148 | p.path += "#" + request.URL.RawFragment 149 | } 150 | if p.path == "" { 151 | p.path = "/" 152 | } 153 | 154 | request.Body.Close() 155 | return p, nil 156 | } 157 | -------------------------------------------------------------------------------- /packet/https.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | type TLSMessageType byte 10 | 11 | const ( 12 | TLSMaxPayloadLen uint16 = 16384 // 16 KB 13 | TLSHeaderLen = 5 14 | TLSInvalid TLSMessageType = 0x0 15 | TLSChangeCipherSpec TLSMessageType = 0x14 16 | TLSAlert TLSMessageType = 0x15 17 | TLSHandshake TLSMessageType = 0x16 18 | TLSApplicationData TLSMessageType = 0x17 19 | TLSHeartbeat TLSMessageType = 0x18 20 | ) 21 | 22 | type TLSMessage struct { 23 | Header TLSHeader 24 | Raw []byte //Header + Payload 25 | RawHeader []byte 26 | RawPayload []byte 27 | } 28 | 29 | type TLSHeader struct { 30 | Type TLSMessageType 31 | ProtoVersion uint16 // major | minor 32 | PayloadLen uint16 33 | } 34 | 35 | func ReadTLSMessage(r io.Reader) (*TLSMessage, error) { 36 | var rawHeader [TLSHeaderLen]byte 37 | _, err := io.ReadFull(r, rawHeader[:]) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | header := TLSHeader{ 43 | Type: TLSMessageType(rawHeader[0]), 44 | ProtoVersion: binary.BigEndian.Uint16(rawHeader[1:3]), 45 | PayloadLen: binary.BigEndian.Uint16(rawHeader[3:5]), 46 | } 47 | if header.PayloadLen > TLSMaxPayloadLen { 48 | // Corrupted header? Check integer overflow 49 | return nil, fmt.Errorf("invalid TLS header. Type: %x, ProtoVersion: %x, PayloadLen: %x", header.Type, header.ProtoVersion, header.PayloadLen) 50 | } 51 | raw := make([]byte, header.PayloadLen+TLSHeaderLen) 52 | copy(raw[0:TLSHeaderLen], rawHeader[:]) 53 | _, err = io.ReadFull(r, raw[TLSHeaderLen:]) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | hello := &TLSMessage{ 59 | Header: header, 60 | Raw: raw, 61 | RawHeader: raw[:TLSHeaderLen], 62 | RawPayload: raw[TLSHeaderLen:], 63 | } 64 | return hello, nil 65 | } 66 | 67 | func (m *TLSMessage) IsClientHello() bool { 68 | // According to RFC 8446 section 4. 69 | // first byte (Raw[5]) of handshake message should be 0x1 - means client_hello 70 | return len(m.Raw) > TLSHeaderLen && 71 | m.Header.Type == TLSHandshake && 72 | m.Raw[5] == 0x01 73 | } 74 | -------------------------------------------------------------------------------- /proxy/handler/conn.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | func setConnectionTimeout(conn *net.TCPConn, timeout int) error { 9 | if timeout <= 0 { 10 | return nil 11 | } 12 | 13 | return conn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(timeout))) 14 | } 15 | -------------------------------------------------------------------------------- /proxy/handler/http.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/xvzc/SpoofDPI/packet" 9 | "github.com/xvzc/SpoofDPI/util" 10 | "github.com/xvzc/SpoofDPI/util/log" 11 | ) 12 | 13 | type HttpHandler struct { 14 | bufferSize int 15 | protocol string 16 | port int 17 | timeout int 18 | } 19 | 20 | func NewHttpHandler(timeout int) *HttpHandler { 21 | return &HttpHandler{ 22 | bufferSize: 1024, 23 | protocol: "HTTP", 24 | port: 80, 25 | timeout: timeout, 26 | } 27 | } 28 | 29 | func (h *HttpHandler) Serve(ctx context.Context, lConn *net.TCPConn, pkt *packet.HttpRequest, ip string) { 30 | ctx = util.GetCtxWithScope(ctx, h.protocol) 31 | logger := log.GetCtxLogger(ctx) 32 | 33 | // Create a connection to the requested server 34 | var port int = 80 35 | var err error 36 | if pkt.Port() != "" { 37 | port, err = strconv.Atoi(pkt.Port()) 38 | if err != nil { 39 | logger.Debug().Msgf("error while parsing port for %s aborting..", pkt.Domain()) 40 | } 41 | } 42 | 43 | rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: port}) 44 | if err != nil { 45 | lConn.Close() 46 | logger.Debug().Msgf("%s", err) 47 | return 48 | } 49 | 50 | logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), pkt.Domain()) 51 | 52 | go h.deliverResponse(ctx, rConn, lConn, pkt.Domain(), lConn.RemoteAddr().String()) 53 | go h.deliverRequest(ctx, lConn, rConn, lConn.RemoteAddr().String(), pkt.Domain()) 54 | 55 | _, err = rConn.Write(pkt.Raw()) 56 | if err != nil { 57 | logger.Debug().Msgf("error sending request to %s: %s", pkt.Domain(), err) 58 | return 59 | } 60 | } 61 | 62 | func (h *HttpHandler) deliverRequest(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { 63 | ctx = util.GetCtxWithScope(ctx, h.protocol) 64 | logger := log.GetCtxLogger(ctx) 65 | 66 | defer func() { 67 | from.Close() 68 | to.Close() 69 | 70 | logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) 71 | }() 72 | 73 | for { 74 | err := setConnectionTimeout(from, h.timeout) 75 | if err != nil { 76 | logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) 77 | } 78 | 79 | pkt, err := packet.ReadHttpRequest(from) 80 | if err != nil { 81 | logger.Debug().Msgf("error reading from %s: %s", fd, err) 82 | return 83 | } 84 | 85 | pkt.Tidy() 86 | 87 | if _, err := to.Write(pkt.Raw()); err != nil { 88 | logger.Debug().Msgf("error writing to %s", td) 89 | return 90 | } 91 | } 92 | } 93 | 94 | func (h *HttpHandler) deliverResponse(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { 95 | ctx = util.GetCtxWithScope(ctx, h.protocol) 96 | logger := log.GetCtxLogger(ctx) 97 | 98 | defer func() { 99 | from.Close() 100 | to.Close() 101 | 102 | logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) 103 | }() 104 | 105 | buf := make([]byte, h.bufferSize) 106 | for { 107 | err := setConnectionTimeout(from, h.timeout) 108 | if err != nil { 109 | logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) 110 | } 111 | 112 | bytesRead, err := ReadBytes(ctx, from, buf) 113 | if err != nil { 114 | logger.Debug().Msgf("error reading from %s: %s", fd, err) 115 | return 116 | } 117 | 118 | if _, err := to.Write(bytesRead); err != nil { 119 | logger.Debug().Msgf("error writing to %s", td) 120 | return 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /proxy/handler/https.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "regexp" 7 | "strconv" 8 | 9 | "github.com/xvzc/SpoofDPI/packet" 10 | "github.com/xvzc/SpoofDPI/util" 11 | "github.com/xvzc/SpoofDPI/util/log" 12 | ) 13 | 14 | type HttpsHandler struct { 15 | bufferSize int 16 | protocol string 17 | port int 18 | timeout int 19 | windowsize int 20 | exploit bool 21 | allowedPatterns []*regexp.Regexp 22 | } 23 | 24 | func NewHttpsHandler(timeout int, windowSize int, allowedPatterns []*regexp.Regexp, exploit bool) *HttpsHandler { 25 | return &HttpsHandler{ 26 | bufferSize: 1024, 27 | protocol: "HTTPS", 28 | port: 443, 29 | timeout: timeout, 30 | windowsize: windowSize, 31 | allowedPatterns: allowedPatterns, 32 | exploit: exploit, 33 | } 34 | } 35 | 36 | func (h *HttpsHandler) Serve(ctx context.Context, lConn *net.TCPConn, initPkt *packet.HttpRequest, ip string) { 37 | ctx = util.GetCtxWithScope(ctx, h.protocol) 38 | logger := log.GetCtxLogger(ctx) 39 | 40 | // Create a connection to the requested server 41 | var err error 42 | if initPkt.Port() != "" { 43 | h.port, err = strconv.Atoi(initPkt.Port()) 44 | if err != nil { 45 | logger.Debug().Msgf("error parsing port for %s aborting..", initPkt.Domain()) 46 | } 47 | } 48 | 49 | rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: h.port}) 50 | if err != nil { 51 | lConn.Close() 52 | logger.Debug().Msgf("%s", err) 53 | return 54 | } 55 | 56 | logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), initPkt.Domain()) 57 | 58 | _, err = lConn.Write([]byte(initPkt.Version() + " 200 Connection Established\r\n\r\n")) 59 | if err != nil { 60 | logger.Debug().Msgf("error sending 200 connection established to the client: %s", err) 61 | return 62 | } 63 | 64 | logger.Debug().Msgf("sent connection established to %s", lConn.RemoteAddr()) 65 | 66 | // Read client hello 67 | m, err := packet.ReadTLSMessage(lConn) 68 | if err != nil || !m.IsClientHello() { 69 | logger.Debug().Msgf("error reading client hello from %s: %s", lConn.RemoteAddr().String(), err) 70 | return 71 | } 72 | clientHello := m.Raw 73 | 74 | logger.Debug().Msgf("client sent hello %d bytes", len(clientHello)) 75 | 76 | // Generate a go routine that reads from the server 77 | go h.communicate(ctx, rConn, lConn, initPkt.Domain(), lConn.RemoteAddr().String()) 78 | go h.communicate(ctx, lConn, rConn, lConn.RemoteAddr().String(), initPkt.Domain()) 79 | 80 | if h.exploit { 81 | logger.Debug().Msgf("writing chunked client hello to %s", initPkt.Domain()) 82 | chunks := splitInChunks(ctx, clientHello, h.windowsize) 83 | if _, err := writeChunks(rConn, chunks); err != nil { 84 | logger.Debug().Msgf("error writing chunked client hello to %s: %s", initPkt.Domain(), err) 85 | return 86 | } 87 | } else { 88 | logger.Debug().Msgf("writing plain client hello to %s", initPkt.Domain()) 89 | if _, err := rConn.Write(clientHello); err != nil { 90 | logger.Debug().Msgf("error writing plain client hello to %s: %s", initPkt.Domain(), err) 91 | return 92 | } 93 | } 94 | } 95 | 96 | func (h *HttpsHandler) communicate(ctx context.Context, from *net.TCPConn, to *net.TCPConn, fd string, td string) { 97 | ctx = util.GetCtxWithScope(ctx, h.protocol) 98 | logger := log.GetCtxLogger(ctx) 99 | 100 | defer func() { 101 | from.Close() 102 | to.Close() 103 | 104 | logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) 105 | }() 106 | 107 | buf := make([]byte, h.bufferSize) 108 | for { 109 | err := setConnectionTimeout(from, h.timeout) 110 | if err != nil { 111 | logger.Debug().Msgf("error while setting connection deadline for %s: %s", fd, err) 112 | } 113 | 114 | bytesRead, err := ReadBytes(ctx, from, buf) 115 | if err != nil { 116 | logger.Debug().Msgf("error reading from %s: %s", fd, err) 117 | return 118 | } 119 | 120 | if _, err := to.Write(bytesRead); err != nil { 121 | logger.Debug().Msgf("error writing to %s", td) 122 | return 123 | } 124 | } 125 | } 126 | 127 | func splitInChunks(ctx context.Context, bytes []byte, size int) [][]byte { 128 | logger := log.GetCtxLogger(ctx) 129 | 130 | var chunks [][]byte 131 | var raw []byte = bytes 132 | 133 | logger.Debug().Msgf("window-size: %d", size) 134 | 135 | if size > 0 { 136 | for { 137 | if len(raw) == 0 { 138 | break 139 | } 140 | 141 | // necessary check to avoid slicing beyond 142 | // slice capacity 143 | if len(raw) < size { 144 | size = len(raw) 145 | } 146 | 147 | chunks = append(chunks, raw[0:size]) 148 | raw = raw[size:] 149 | } 150 | 151 | return chunks 152 | } 153 | 154 | // When the given window-size <= 0 155 | 156 | if len(raw) < 1 { 157 | return [][]byte{raw} 158 | } 159 | 160 | logger.Debug().Msg("using legacy fragmentation") 161 | 162 | return [][]byte{raw[:1], raw[1:]} 163 | } 164 | 165 | func writeChunks(conn *net.TCPConn, c [][]byte) (n int, err error) { 166 | total := 0 167 | for i := 0; i < len(c); i++ { 168 | b, err := conn.Write(c[i]) 169 | if err != nil { 170 | return 0, nil 171 | } 172 | 173 | total += b 174 | } 175 | 176 | return total, nil 177 | } 178 | -------------------------------------------------------------------------------- /proxy/handler/io.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | ) 8 | 9 | func ReadBytes(ctx context.Context, conn *net.TCPConn, dest []byte) ([]byte, error) { 10 | n, err := readBytesInternal(ctx, conn, dest) 11 | return dest[:n], err 12 | } 13 | 14 | func readBytesInternal(ctx context.Context, conn *net.TCPConn, dest []byte) (int, error) { 15 | totalRead, err := conn.Read(dest) 16 | if err != nil { 17 | var opError *net.OpError 18 | switch { 19 | case errors.As(err, &opError) && opError.Timeout(): 20 | return totalRead, errors.New("timed out") 21 | default: 22 | return totalRead, err 23 | } 24 | } 25 | return totalRead, nil 26 | } 27 | -------------------------------------------------------------------------------- /proxy/http.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "github.com/xvzc/SpoofDPI/util" 6 | "net" 7 | "strconv" 8 | 9 | "github.com/xvzc/SpoofDPI/util/log" 10 | 11 | "github.com/xvzc/SpoofDPI/packet" 12 | ) 13 | 14 | const protoHTTP = "HTTP" 15 | 16 | func (pxy *Proxy) handleHttp(ctx context.Context, lConn *net.TCPConn, pkt *packet.HttpRequest, ip string) { 17 | ctx = util.GetCtxWithScope(ctx, protoHTTP) 18 | logger := log.GetCtxLogger(ctx) 19 | 20 | pkt.Tidy() 21 | 22 | // Create a connection to the requested server 23 | var port int = 80 24 | var err error 25 | if pkt.Port() != "" { 26 | port, err = strconv.Atoi(pkt.Port()) 27 | if err != nil { 28 | logger.Debug().Msgf("error while parsing port for %s aborting..", pkt.Domain()) 29 | } 30 | } 31 | 32 | rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: port}) 33 | if err != nil { 34 | lConn.Close() 35 | logger.Debug().Msgf("%s", err) 36 | return 37 | } 38 | 39 | logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), pkt.Domain()) 40 | 41 | go Serve(ctx, rConn, lConn, protoHTTP, pkt.Domain(), lConn.RemoteAddr().String(), pxy.timeout) 42 | 43 | _, err = rConn.Write(pkt.Raw()) 44 | if err != nil { 45 | logger.Debug().Msgf("error sending request to %s: %s", pkt.Domain(), err) 46 | return 47 | } 48 | 49 | logger.Debug().Msgf("sent a request to %s", pkt.Domain()) 50 | 51 | go Serve(ctx, lConn, rConn, protoHTTP, lConn.RemoteAddr().String(), pkt.Domain(), pxy.timeout) 52 | } 53 | -------------------------------------------------------------------------------- /proxy/https.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/xvzc/SpoofDPI/packet" 9 | "github.com/xvzc/SpoofDPI/util" 10 | "github.com/xvzc/SpoofDPI/util/log" 11 | ) 12 | 13 | const protoHTTPS = "HTTPS" 14 | 15 | func (pxy *Proxy) handleHttps(ctx context.Context, lConn *net.TCPConn, exploit bool, initPkt *packet.HttpRequest, ip string) { 16 | ctx = util.GetCtxWithScope(ctx, protoHTTPS) 17 | logger := log.GetCtxLogger(ctx) 18 | 19 | // Create a connection to the requested server 20 | var port int = 443 21 | var err error 22 | if initPkt.Port() != "" { 23 | port, err = strconv.Atoi(initPkt.Port()) 24 | if err != nil { 25 | logger.Debug().Msgf("error parsing port for %s aborting..", initPkt.Domain()) 26 | } 27 | } 28 | 29 | rConn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP(ip), Port: port}) 30 | if err != nil { 31 | lConn.Close() 32 | logger.Debug().Msgf("%s", err) 33 | return 34 | } 35 | 36 | logger.Debug().Msgf("new connection to the server %s -> %s", rConn.LocalAddr(), initPkt.Domain()) 37 | 38 | _, err = lConn.Write([]byte(initPkt.Version() + " 200 Connection Established\r\n\r\n")) 39 | if err != nil { 40 | logger.Debug().Msgf("error sending 200 connection established to the client: %s", err) 41 | return 42 | } 43 | 44 | logger.Debug().Msgf("sent connection established to %s", lConn.RemoteAddr()) 45 | 46 | // Read client hello 47 | m, err := packet.ReadTLSMessage(lConn) 48 | if err != nil || !m.IsClientHello() { 49 | logger.Debug().Msgf("error reading client hello from %s: %s", lConn.RemoteAddr().String(), err) 50 | return 51 | } 52 | clientHello := m.Raw 53 | 54 | logger.Debug().Msgf("client sent hello %d bytes", len(clientHello)) 55 | 56 | // Generate a go routine that reads from the server 57 | go Serve(ctx, rConn, lConn, protoHTTPS, initPkt.Domain(), lConn.RemoteAddr().String(), pxy.timeout) 58 | 59 | if exploit { 60 | logger.Debug().Msgf("writing chunked client hello to %s", initPkt.Domain()) 61 | chunks := splitInChunks(ctx, clientHello, pxy.windowSize) 62 | if _, err := writeChunks(rConn, chunks); err != nil { 63 | logger.Debug().Msgf("error writing chunked client hello to %s: %s", initPkt.Domain(), err) 64 | return 65 | } 66 | } else { 67 | logger.Debug().Msgf("writing plain client hello to %s", initPkt.Domain()) 68 | if _, err := rConn.Write(clientHello); err != nil { 69 | logger.Debug().Msgf("error writing plain client hello to %s: %s", initPkt.Domain(), err) 70 | return 71 | } 72 | } 73 | 74 | go Serve(ctx, lConn, rConn, protoHTTPS, lConn.RemoteAddr().String(), initPkt.Domain(), pxy.timeout) 75 | } 76 | 77 | func splitInChunks(ctx context.Context, bytes []byte, size int) [][]byte { 78 | logger := log.GetCtxLogger(ctx) 79 | 80 | var chunks [][]byte 81 | var raw []byte = bytes 82 | 83 | logger.Debug().Msgf("window-size: %d", size) 84 | 85 | if size > 0 { 86 | for { 87 | if len(raw) == 0 { 88 | break 89 | } 90 | 91 | // necessary check to avoid slicing beyond 92 | // slice capacity 93 | if len(raw) < size { 94 | size = len(raw) 95 | } 96 | 97 | chunks = append(chunks, raw[0:size]) 98 | raw = raw[size:] 99 | } 100 | 101 | return chunks 102 | } 103 | 104 | // When the given window-size <= 0 105 | 106 | if len(raw) < 1 { 107 | return [][]byte{raw} 108 | } 109 | 110 | logger.Debug().Msg("using legacy fragmentation") 111 | 112 | return [][]byte{raw[:1], raw[1:]} 113 | } 114 | 115 | func writeChunks(conn *net.TCPConn, c [][]byte) (n int, err error) { 116 | total := 0 117 | for i := 0; i < len(c); i++ { 118 | b, err := conn.Write(c[i]) 119 | if err != nil { 120 | return 0, nil 121 | } 122 | 123 | total += b 124 | } 125 | 126 | return total, nil 127 | } 128 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/xvzc/SpoofDPI/dns" 11 | "github.com/xvzc/SpoofDPI/packet" 12 | "github.com/xvzc/SpoofDPI/proxy/handler" 13 | "github.com/xvzc/SpoofDPI/util" 14 | "github.com/xvzc/SpoofDPI/util/log" 15 | ) 16 | 17 | const scopeProxy = "PROXY" 18 | 19 | type Proxy struct { 20 | addr string 21 | port int 22 | timeout int 23 | resolver *dns.Dns 24 | windowSize int 25 | enableDoh bool 26 | allowedPattern []*regexp.Regexp 27 | } 28 | 29 | type Handler interface { 30 | Serve(ctx context.Context, lConn *net.TCPConn, pkt *packet.HttpRequest, ip string) 31 | } 32 | 33 | func New(config *util.Config) *Proxy { 34 | return &Proxy{ 35 | addr: config.Addr, 36 | port: config.Port, 37 | timeout: config.Timeout, 38 | windowSize: config.WindowSize, 39 | enableDoh: config.EnableDoh, 40 | allowedPattern: config.AllowedPatterns, 41 | resolver: dns.NewDns(config), 42 | } 43 | } 44 | 45 | func (pxy *Proxy) Start(ctx context.Context) { 46 | ctx = util.GetCtxWithScope(ctx, scopeProxy) 47 | logger := log.GetCtxLogger(ctx) 48 | 49 | l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP(pxy.addr), Port: pxy.port}) 50 | if err != nil { 51 | logger.Fatal().Msgf("error creating listener: %s", err) 52 | os.Exit(1) 53 | } 54 | 55 | if pxy.timeout > 0 { 56 | logger.Info().Msgf("connection timeout is set to %d ms", pxy.timeout) 57 | } 58 | 59 | logger.Info().Msgf("created a listener on port %d", pxy.port) 60 | if len(pxy.allowedPattern) > 0 { 61 | logger.Info().Msgf("number of white-listed pattern: %d", len(pxy.allowedPattern)) 62 | } 63 | 64 | for { 65 | conn, err := l.Accept() 66 | if err != nil { 67 | logger.Fatal().Msgf("error accepting connection: %s", err) 68 | continue 69 | } 70 | 71 | go func() { 72 | ctx := util.GetCtxWithTraceId(ctx) 73 | logger := log.GetCtxLogger(ctx) 74 | 75 | pkt, err := packet.ReadHttpRequest(conn) 76 | if err != nil { 77 | logger.Debug().Msgf("error while parsing request: %s", err) 78 | conn.Close() 79 | return 80 | } 81 | 82 | pkt.Tidy() 83 | 84 | logger.Debug().Msgf("request from %s\n\n%s", conn.RemoteAddr(), string(pkt.Raw())) 85 | 86 | if !pkt.IsValidMethod() { 87 | logger.Debug().Msgf("unsupported method: %s", pkt.Method()) 88 | conn.Close() 89 | return 90 | } 91 | 92 | matched := pxy.patternMatches([]byte(pkt.Domain())) 93 | useSystemDns := !matched 94 | 95 | ip, err := pxy.resolver.ResolveHost(ctx, pkt.Domain(), pxy.enableDoh, useSystemDns) 96 | if err != nil { 97 | logger.Debug().Msgf("error while dns lookup: %s %s", pkt.Domain(), err) 98 | conn.Write([]byte(pkt.Version() + " 502 Bad Gateway\r\n\r\n")) 99 | conn.Close() 100 | return 101 | } 102 | 103 | // Avoid recursively querying self 104 | if pkt.Port() == strconv.Itoa(pxy.port) && isLoopedRequest(ctx, net.ParseIP(ip)) { 105 | logger.Error().Msg("looped request has been detected. aborting.") 106 | conn.Close() 107 | return 108 | } 109 | 110 | var h Handler 111 | if pkt.IsConnectMethod() { 112 | h = handler.NewHttpsHandler(pxy.timeout, pxy.windowSize, pxy.allowedPattern, matched) 113 | } else { 114 | h = handler.NewHttpHandler(pxy.timeout) 115 | } 116 | 117 | h.Serve(ctx, conn.(*net.TCPConn), pkt, ip) 118 | }() 119 | } 120 | } 121 | 122 | func (pxy *Proxy) patternMatches(bytes []byte) bool { 123 | if pxy.allowedPattern == nil { 124 | return true 125 | } 126 | 127 | for _, pattern := range pxy.allowedPattern { 128 | if pattern.Match(bytes) { 129 | return true 130 | } 131 | } 132 | 133 | return false 134 | } 135 | 136 | func isLoopedRequest(ctx context.Context, ip net.IP) bool { 137 | if ip.IsLoopback() { 138 | return true 139 | } 140 | 141 | logger := log.GetCtxLogger(ctx) 142 | 143 | // Get list of available addresses 144 | // See `ip -4 addr show` 145 | addr, err := net.InterfaceAddrs() // needs AF_NETLINK on linux 146 | if err != nil { 147 | logger.Error().Msgf("error while getting addresses of our network interfaces: %s", err) 148 | return false 149 | } 150 | 151 | for _, addr := range addr { 152 | if ipnet, ok := addr.(*net.IPNet); ok { 153 | if ipnet.IP.Equal(ip) { 154 | return true 155 | } 156 | } 157 | } 158 | 159 | return false 160 | } 161 | -------------------------------------------------------------------------------- /proxy/server.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "net" 8 | "time" 9 | 10 | "github.com/xvzc/SpoofDPI/util" 11 | "github.com/xvzc/SpoofDPI/util/log" 12 | ) 13 | 14 | const ( 15 | BufferSize = 1024 16 | TLSHeaderLen = 5 17 | ) 18 | 19 | func ReadBytes(ctx context.Context, conn *net.TCPConn, dest []byte) ([]byte, error) { 20 | n, err := readBytesInternal(ctx, conn, dest) 21 | return dest[:n], err 22 | } 23 | 24 | func readBytesInternal(ctx context.Context, conn *net.TCPConn, dest []byte) (int, error) { 25 | totalRead, err := conn.Read(dest) 26 | if err != nil { 27 | var opError *net.OpError 28 | switch { 29 | case errors.As(err, &opError) && opError.Timeout(): 30 | return totalRead, errors.New("timed out") 31 | default: 32 | return totalRead, err 33 | } 34 | } 35 | return totalRead, nil 36 | } 37 | 38 | func Serve(ctx context.Context, from *net.TCPConn, to *net.TCPConn, proto string, fd string, td string, timeout int) { 39 | ctx = util.GetCtxWithScope(ctx, proto) 40 | logger := log.GetCtxLogger(ctx) 41 | 42 | defer func() { 43 | from.Close() 44 | to.Close() 45 | 46 | logger.Debug().Msgf("closing proxy connection: %s -> %s", fd, td) 47 | }() 48 | 49 | buf := make([]byte, BufferSize) 50 | for { 51 | if timeout > 0 { 52 | from.SetReadDeadline( 53 | time.Now().Add(time.Millisecond * time.Duration(timeout)), 54 | ) 55 | } 56 | 57 | bytesRead, err := ReadBytes(ctx, from, buf) 58 | if err != nil { 59 | if err == io.EOF { 60 | logger.Debug().Msgf("finished reading from %s", fd) 61 | return 62 | } 63 | logger.Debug().Msgf("error reading from %s: %s", fd, err) 64 | return 65 | } 66 | 67 | if _, err := to.Write(bytesRead); err != nil { 68 | logger.Debug().Msgf("error writing to %s", td) 69 | return 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /util/args.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "strconv" 8 | "unsafe" 9 | ) 10 | 11 | type Args struct { 12 | Addr string 13 | Port uint16 14 | DnsAddr string 15 | DnsPort uint16 16 | DnsIPv4Only bool 17 | EnableDoh bool 18 | Debug bool 19 | Silent bool 20 | SystemProxy bool 21 | Timeout uint16 22 | AllowedPattern StringArray 23 | WindowSize uint16 24 | Version bool 25 | } 26 | 27 | type StringArray []string 28 | 29 | func (arr *StringArray) String() string { 30 | return fmt.Sprintf("%s", *arr) 31 | } 32 | 33 | func (arr *StringArray) Set(value string) error { 34 | *arr = append(*arr, value) 35 | return nil 36 | } 37 | 38 | func ParseArgs() *Args { 39 | args := new(Args) 40 | 41 | flag.StringVar(&args.Addr, "addr", "127.0.0.1", "listen address") 42 | uintNVar(&args.Port, "port", 8080, "port") 43 | flag.StringVar(&args.DnsAddr, "dns-addr", "8.8.8.8", "dns address") 44 | uintNVar(&args.DnsPort, "dns-port", 53, "port number for dns") 45 | flag.BoolVar(&args.EnableDoh, "enable-doh", false, "enable 'dns-over-https'") 46 | flag.BoolVar(&args.Debug, "debug", false, "enable debug output") 47 | flag.BoolVar(&args.Silent, "silent", false, "do not show the banner and server information at start up") 48 | flag.BoolVar(&args.SystemProxy, "system-proxy", true, "enable system-wide proxy") 49 | uintNVar(&args.Timeout, "timeout", 0, "timeout in milliseconds; no timeout when not given") 50 | uintNVar(&args.WindowSize, "window-size", 0, `chunk size, in number of bytes, for fragmented client hello, 51 | try lower values if the default value doesn't bypass the DPI; 52 | when not given, the client hello packet will be sent in two parts: 53 | fragmentation for the first data packet and the rest 54 | `) 55 | flag.BoolVar(&args.Version, "v", false, "print spoofdpi's version; this may contain some other relevant information") 56 | flag.Var( 57 | &args.AllowedPattern, 58 | "pattern", 59 | "bypass DPI only on packets matching this regex pattern; can be given multiple times", 60 | ) 61 | flag.BoolVar(&args.DnsIPv4Only, "dns-ipv4-only", false, "resolve only version 4 addresses") 62 | 63 | flag.Parse() 64 | 65 | return args 66 | } 67 | 68 | var ( 69 | errParse = errors.New("parse error") 70 | errRange = errors.New("value out of range") 71 | ) 72 | 73 | type unsigned interface { 74 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 75 | } 76 | 77 | func uintNVar[T unsigned](p *T, name string, value T, usage string) { 78 | flag.CommandLine.Var(newUintNValue(value, p), name, usage) 79 | } 80 | 81 | type uintNValue[T unsigned] struct { 82 | val *T 83 | } 84 | 85 | func newUintNValue[T unsigned](val T, p *T) *uintNValue[T] { 86 | *p = val 87 | return &uintNValue[T]{val: p} 88 | } 89 | 90 | func (u *uintNValue[T]) Set(s string) error { 91 | size := int(unsafe.Sizeof(*u.val) * 8) 92 | v, err := strconv.ParseUint(s, 0, size) 93 | if err != nil { 94 | err = numError(err) 95 | } 96 | *u.val = T(v) 97 | return err 98 | } 99 | 100 | func (u *uintNValue[T]) Get() any { 101 | if u.val == nil { 102 | return T(0) 103 | } 104 | return *u.val 105 | } 106 | 107 | func (u *uintNValue[T]) String() string { 108 | if u.val == nil { 109 | return "0" 110 | } 111 | return strconv.FormatUint(uint64(*u.val), 10) 112 | } 113 | 114 | func numError(err error) error { 115 | if errors.Is(err, strconv.ErrSyntax) { 116 | return errParse 117 | } 118 | if errors.Is(err, strconv.ErrRange) { 119 | return errRange 120 | } 121 | return err 122 | } 123 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/pterm/pterm" 8 | "github.com/pterm/pterm/putils" 9 | ) 10 | 11 | type Config struct { 12 | Addr string 13 | Port int 14 | DnsAddr string 15 | DnsPort int 16 | DnsIPv4Only bool 17 | EnableDoh bool 18 | Debug bool 19 | Silent bool 20 | SystemProxy bool 21 | Timeout int 22 | WindowSize int 23 | AllowedPatterns []*regexp.Regexp 24 | } 25 | 26 | var config *Config 27 | 28 | func GetConfig() *Config { 29 | if config == nil { 30 | config = new(Config) 31 | } 32 | return config 33 | } 34 | 35 | func (c *Config) Load(args *Args) { 36 | c.Addr = args.Addr 37 | c.Port = int(args.Port) 38 | c.DnsAddr = args.DnsAddr 39 | c.DnsPort = int(args.DnsPort) 40 | c.DnsIPv4Only = args.DnsIPv4Only 41 | c.Debug = args.Debug 42 | c.EnableDoh = args.EnableDoh 43 | c.Silent = args.Silent 44 | c.SystemProxy = args.SystemProxy 45 | c.Timeout = int(args.Timeout) 46 | c.AllowedPatterns = parseAllowedPattern(args.AllowedPattern) 47 | c.WindowSize = int(args.WindowSize) 48 | } 49 | 50 | func parseAllowedPattern(patterns StringArray) []*regexp.Regexp { 51 | var allowedPatterns []*regexp.Regexp 52 | 53 | for _, pattern := range patterns { 54 | allowedPatterns = append(allowedPatterns, regexp.MustCompile(pattern)) 55 | } 56 | 57 | return allowedPatterns 58 | } 59 | 60 | func PrintColoredBanner() { 61 | cyan := putils.LettersFromStringWithStyle("Spoof", pterm.NewStyle(pterm.FgCyan)) 62 | purple := putils.LettersFromStringWithStyle("DPI", pterm.NewStyle(pterm.FgLightMagenta)) 63 | pterm.DefaultBigText.WithLetters(cyan, purple).Render() 64 | 65 | pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{ 66 | {Level: 0, Text: "ADDR : " + fmt.Sprint(config.Addr)}, 67 | {Level: 0, Text: "PORT : " + fmt.Sprint(config.Port)}, 68 | {Level: 0, Text: "DNS : " + fmt.Sprint(config.DnsAddr)}, 69 | {Level: 0, Text: "DEBUG : " + fmt.Sprint(config.Debug)}, 70 | }).Render() 71 | 72 | pterm.DefaultBasicText.Println("Press 'CTRL + c' to quit") 73 | } 74 | -------------------------------------------------------------------------------- /util/context.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "strings" 7 | ) 8 | 9 | type scopeCtxKey struct{} 10 | 11 | func GetCtxWithScope(ctx context.Context, scope string) context.Context { 12 | return context.WithValue(ctx, scopeCtxKey{}, scope) 13 | } 14 | 15 | func GetScopeFromCtx(ctx context.Context) (string, bool) { 16 | if scope, ok := ctx.Value(scopeCtxKey{}).(string); ok { 17 | return scope, true 18 | } 19 | return "", false 20 | } 21 | 22 | type traceIdCtxKey struct{} 23 | 24 | func GetCtxWithTraceId(ctx context.Context) context.Context { 25 | return context.WithValue(ctx, traceIdCtxKey{}, generateTraceId()) 26 | } 27 | 28 | func GetTraceIdFromCtx(ctx context.Context) (string, bool) { 29 | if traceId, ok := ctx.Value(traceIdCtxKey{}).(string); ok { 30 | return traceId, true 31 | } 32 | return "", false 33 | } 34 | 35 | func generateTraceId() string { 36 | sb := strings.Builder{} 37 | sb.Grow(35) 38 | 39 | var q uint64 40 | var r uint8 41 | for i := 0; i < 32; i++ { 42 | if i%15 == 0 { 43 | q = rand.Uint64() 44 | } 45 | q, r = q>>4, uint8(q&0xF) 46 | if r > 9 { 47 | r += 0x27 48 | } 49 | sb.WriteByte(r + 0x30) 50 | if i&7 == 7 && i != 31 { 51 | sb.WriteByte(0x2D) 52 | } 53 | } 54 | return sb.String() 55 | } 56 | -------------------------------------------------------------------------------- /util/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/xvzc/SpoofDPI/util" 11 | ) 12 | 13 | const ( 14 | scopeFieldName = "scope" 15 | traceIdFieldName = "trace_id" 16 | ) 17 | 18 | var logger zerolog.Logger 19 | 20 | func GetCtxLogger(ctx context.Context) zerolog.Logger { 21 | return logger.With().Ctx(ctx).Logger() 22 | } 23 | 24 | func InitLogger(cfg *util.Config) { 25 | partsOrder := []string{ 26 | zerolog.LevelFieldName, 27 | zerolog.TimestampFieldName, 28 | traceIdFieldName, 29 | scopeFieldName, 30 | zerolog.MessageFieldName, 31 | } 32 | 33 | consoleWriter := zerolog.ConsoleWriter{ 34 | Out: os.Stdout, 35 | TimeFormat: time.RFC3339, 36 | PartsOrder: partsOrder, 37 | FormatPrepare: func(m map[string]any) error { 38 | formatFieldValue[string](m, "%s", traceIdFieldName) 39 | formatFieldValue[string](m, "[%s]", scopeFieldName) 40 | return nil 41 | }, 42 | FieldsExclude: []string{traceIdFieldName, scopeFieldName}, 43 | } 44 | 45 | logger = zerolog.New(consoleWriter).Hook(ctxHook{}) 46 | if cfg.Debug { 47 | logger = logger.Level(zerolog.DebugLevel) 48 | } else { 49 | logger = logger.Level(zerolog.InfoLevel) 50 | } 51 | logger = logger.With().Timestamp().Logger() 52 | } 53 | 54 | func formatFieldValue[T any](vs map[string]any, format string, field string) { 55 | if v, ok := vs[field].(T); ok { 56 | vs[field] = fmt.Sprintf(format, v) 57 | } else { 58 | vs[field] = "" 59 | } 60 | } 61 | 62 | type ctxHook struct{} 63 | 64 | func (h ctxHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { 65 | if scope, ok := util.GetScopeFromCtx(e.GetCtx()); ok { 66 | e.Str(scopeFieldName, scope) 67 | } 68 | if traceId, ok := util.GetTraceIdFromCtx(e.GetCtx()); ok { 69 | e.Str(traceIdFieldName, traceId) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /util/os.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | getDefaultNetworkCMD = "networksetup -listnetworkserviceorder | grep" + 14 | " `(route -n get default | grep 'interface' || route -n get -inet6 default | grep 'interface') | cut -d ':' -f2`" + 15 | " -B 1 | head -n 1 | cut -d ' ' -f 2-" 16 | darwinOS = "darwin" 17 | permissionErrorHelpTextMacOS = "By default SpoofDPI tries to set itself up as a system-wide proxy server.\n" + 18 | "Doing so may require root access on machines with\n" + 19 | "'Settings > Privacy & Security > Advanced > Require" + 20 | " an administrator password to access system-wide settings' enabled.\n" + 21 | "If you do not want SpoofDPI to act as a system-wide proxy, provide" + 22 | " -system-proxy=false." 23 | ) 24 | 25 | func SetOsProxy(port uint16) error { 26 | if runtime.GOOS != darwinOS { 27 | return nil 28 | } 29 | 30 | network, err := getDefaultNetwork() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return setProxy(getProxyTypes(), network, "127.0.0.1", port) 36 | } 37 | 38 | func UnsetOsProxy() error { 39 | if runtime.GOOS != darwinOS { 40 | return nil 41 | } 42 | 43 | network, err := getDefaultNetwork() 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return unsetProxy(getProxyTypes(), network) 49 | } 50 | 51 | func getDefaultNetwork() (string, error) { 52 | network, err := exec.Command("sh", "-c", getDefaultNetworkCMD).Output() 53 | if err != nil { 54 | return "", err 55 | } else if len(network) == 0 { 56 | return "", errors.New("no available networks") 57 | } 58 | return strings.TrimSpace(string(network)), nil 59 | } 60 | 61 | func getProxyTypes() []string { 62 | return []string{"webproxy", "securewebproxy"} 63 | } 64 | 65 | func setProxy(proxyTypes []string, network, domain string, port uint16) error { 66 | args := []string{"", network, domain, strconv.FormatUint(uint64(port), 10)} 67 | 68 | for _, proxyType := range proxyTypes { 69 | args[0] = "-set" + proxyType 70 | if err := networkSetup(args); err != nil { 71 | return fmt.Errorf("setting %s: %w", proxyType, err) 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | func unsetProxy(proxyTypes []string, network string) error { 78 | args := []string{"", network, "off"} 79 | 80 | for _, proxyType := range proxyTypes { 81 | args[0] = "-set" + proxyType + "state" 82 | if err := networkSetup(args); err != nil { 83 | return fmt.Errorf("unsetting %s: %w", proxyType, err) 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | func networkSetup(args []string) error { 90 | cmd := exec.Command("networksetup", args...) 91 | out, err := cmd.CombinedOutput() 92 | if err != nil { 93 | msg := string(out) 94 | if isMacOSPermissionError(err) { 95 | msg += permissionErrorHelpTextMacOS 96 | } 97 | return fmt.Errorf("%s: %s", cmd.String(), msg) 98 | } 99 | return nil 100 | } 101 | 102 | func isMacOSPermissionError(err error) bool { 103 | if runtime.GOOS != darwinOS { 104 | return false 105 | } 106 | 107 | var exitErr *exec.ExitError 108 | ok := errors.As(err, &exitErr) 109 | return ok && exitErr.ExitCode() == 14 110 | } 111 | -------------------------------------------------------------------------------- /version/VERSION: -------------------------------------------------------------------------------- 1 | 0.12.0 2 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import _ "embed" 4 | 5 | //go:embed VERSION 6 | var VERSION string 7 | 8 | func PrintVersion() { 9 | println("spoofdpi", "v" + VERSION) 10 | println("A simple and fast anti-censorship tool written in Go.") 11 | println("https://github.com/xvzc/SpoofDPI") 12 | } 13 | --------------------------------------------------------------------------------