├── .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 | 
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 |
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 | 
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 |
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 | 
8 |
9 | # Installation
10 | SpoofDPI의 설치과정은 [여기](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md)를 참고바랍니다.
11 |
12 |
13 |
14 |
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 | 
8 |
9 | # Installation
10 | Инструкции по установке SpoofDPI вы можете найти [здесь](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md).
11 |
12 |
13 |
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 | 
10 |
11 | # Installation
12 | See the installation guide for SpoofDPI [here](https://github.com/xvzc/SpoofDPI/blob/main/_docs/INSTALL.md).
13 |
14 |
15 |
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 |
--------------------------------------------------------------------------------