Compare commits
190 Commits
Author | SHA1 | Date | |
---|---|---|---|
3c54f99fea | |||
824f5bd731 | |||
3f3db8476e | |||
f375f080da | |||
e19e9ef5a4 | |||
682e65cb54 | |||
16a6d409d9 | |||
4186bcf1b2 | |||
df5112175f | |||
d9341a49ea | |||
4e9e4b6cde | |||
936b7012ba | |||
a9cbd9ec98 | |||
c9943fb857 | |||
a40274e2a2 | |||
b59d45c660 | |||
7b01e103c2 | |||
93a8acecce | |||
586bb91c0c | |||
baf03b81e3 | |||
9807e1189c | |||
3d5a0d9f73 | |||
cc96187f58 | |||
3aefa1d924 | |||
42e21b3733 | |||
0a35237915 | |||
a1f3a5ea26 | |||
e63f995258 | |||
d0c829c578 | |||
4ad9761b32 | |||
1f593d37fb | |||
109bfcb0f9 | |||
7ee49f5171 | |||
d759d16944 | |||
807d53c1e7 | |||
1355196b7c | |||
573316bcde | |||
784c28266c | |||
5da1b2a8aa | |||
0976d27cb1 | |||
6c83ff3496 | |||
f7f97ef625 | |||
5acdd72a1d | |||
f53686103d | |||
f63c9eb22f | |||
a37243cf30 | |||
b3c1b4a840 | |||
14bbf6eedc | |||
aa81193d5b | |||
9eb98e399d | |||
d48cfecf60 | |||
6036fb63ba | |||
cd48f69b1f | |||
fcc594ae26 | |||
f4de055aa1 | |||
35925cb3da | |||
ff430df845 | |||
e4cdea2111 | |||
b6ee47a541 | |||
b25009cde7 | |||
6fedd7ec84 | |||
9619c3fb20 | |||
02d029dd2d | |||
09c28e0355 | |||
3600077f3b | |||
de7656a787 | |||
5dfe7f8561 | |||
ed27898a33 | |||
532396d25c | |||
4b1b494164 | |||
0d33dc3eb9 | |||
994cbff215 | |||
bea2ee8bf2 | |||
1e5593f1a9 | |||
34febc4579 | |||
97581148b5 | |||
0402878daa | |||
4735f61fd1 | |||
16ae107e70 | |||
83efe2ae57 | |||
87e4d94290 | |||
b98e9ea202 | |||
9a62b1081d | |||
2cd1b890ce | |||
ba060bd0ee | |||
b1795b1e3d | |||
76c9820065 | |||
2db4ce57ef | |||
50b3d497f6 | |||
2321e9139d | |||
baabf21340 | |||
d3bb4c65a8 | |||
8c3e2a7559 | |||
bc52f8e4fd | |||
d3b14c325f | |||
4859b158b4 | |||
d65b51c62b | |||
a6444bb449 | |||
e09931dcf7 | |||
5bd189f2d0 | |||
8766287e72 | |||
10f9571c9e | |||
96a8259c42 | |||
68dd0622b8 | |||
558ac6b965 | |||
e773f95f21 | |||
314ce1c249 | |||
13275b1aa6 | |||
02d9169b5d | |||
7631bcc99e | |||
a32ee13fc9 | |||
b8ed738238 | |||
687c2a21cf | |||
ad18064e6b | |||
c9735ef75b | |||
b70882f01a | |||
5805334ccd | |||
c1b4382fe8 | |||
008743f20b | |||
50d778da3c | |||
8b7c731fd6 | |||
0b7918de9c | |||
4f61c04519 | |||
89cf06036d | |||
4ba6f248bc | |||
83a684c551 | |||
92a23f1eab | |||
622ac45258 | |||
791d203b5f | |||
77d6f9ae6f | |||
b1d9dfd6bf | |||
6532947e71 | |||
6c5f23f552 | |||
78c3034158 | |||
8f0098092d | |||
33a6579a3a | |||
b4221d4b74 | |||
0e4b9daaad | |||
ee72865f48 | |||
6521acf8f1 | |||
4f73410618 | |||
20eff200b1 | |||
ae1e1dc9f6 | |||
cf9e1545a4 | |||
6c7a8fffe0 | |||
3a3e2c05af | |||
02c7fd8d70 | |||
e6aa452b51 | |||
35449bfa17 | |||
acd51bbc90 | |||
f44cd9180c | |||
93c987a6cb | |||
3f0584ac09 | |||
59968fff1c | |||
7c62fe41b4 | |||
2781090405 | |||
14c9cf1b97 | |||
3dfff84cc3 | |||
5f3db72422 | |||
18bb285a90 | |||
60bad66bc3 | |||
99b34e8d8b | |||
9f1d85ab6e | |||
4323dd24d0 | |||
59bda1d547 | |||
1c760935f4 | |||
4f674755ce | |||
f1b792bd26 | |||
58c077b45e | |||
1854199c47 | |||
ecac8eb8e5 | |||
48cff50a4c | |||
fb628e9c62 | |||
2dece02df6 | |||
8f32e6a60f | |||
98614a1f3f | |||
c1b4c94b9c | |||
7ddbc12cdb | |||
1a217e21e9 | |||
147a7ce779 | |||
fb0289bb4c | |||
3e7970612a | |||
46244a6496 | |||
71d30e6654 | |||
008731c249 | |||
5628f97da1 | |||
8d0c6c6e66 | |||
5073c3cde8 | |||
3a27cfc4a1 | |||
3638b077cd |
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -7,48 +7,54 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- The English version is available. -->
|
<!--
|
||||||
感谢你向 Clash Core 提交 issue!
|
感谢你向 Clash Core 提交 issue!
|
||||||
在提交之前,请确认:
|
在提交之前,请确认:
|
||||||
|
|
||||||
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
|
||||||
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 Openclash、Koolclash 等)的特定问题
|
|
||||||
- [ ] 我已经使用 Clash core 的 dev 分支版本测试过,问题依旧存在
|
|
||||||
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
|
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
|
||||||
|
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
||||||
|
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
|
||||||
|
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
||||||
|
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
|
||||||
|
|
||||||
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
||||||
|
|
||||||
<!--
|
Thanks for opening an issue towards the Clash core!
|
||||||
Thanks for submitting an issue towards the Clash core!
|
|
||||||
But before so, please do the following checklist:
|
But before so, please do the following checklist:
|
||||||
|
|
||||||
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
||||||
- [ ] Your issue may already be reported! Please search on the [issue tracker](……/) before creating one.
|
- [ ] I have searched on the [issue tracker](……/) for a related issue.
|
||||||
- [ ] I have tested using the dev branch, and the issue still exists.
|
- [ ] I have tested using the dev branch, and the issue still exists.
|
||||||
- [ ] This is an issue related to the Clash core, not to the derivatives of Clash, like Openclash or Koolclash
|
- [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
|
||||||
|
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
|
||||||
|
|
||||||
Please understand that we close issues that fail to follow the issue template.
|
Please understand that we close issues that fail to follow this issue template.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
我都确认过了,我要继续提交。
|
|
||||||
<!-- None of the above, create a bug report -->
|
|
||||||
------------------------------------------------------------------
|
------------------------------------------------------------------
|
||||||
|
|
||||||
|
<!--
|
||||||
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
|
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
|
||||||
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
|
Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
|
||||||
|
-->
|
||||||
|
|
||||||
### clash core config
|
### Clash config
|
||||||
<!--
|
<!--
|
||||||
在下方附上 Clash core 脱敏后配置文件的内容
|
在下方附上 Clash core 脱敏后配置文件的内容
|
||||||
Paste the Clash core configuration below.
|
Paste the Clash core configuration below.
|
||||||
-->
|
-->
|
||||||
```
|
<details>
|
||||||
|
<summary>config.yaml</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
……
|
……
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
### Clash log
|
### Clash log
|
||||||
<!--
|
<!--
|
||||||
在下方附上 Clash Core 的日志,log level 最好使用 DEBUG
|
在下方附上 Clash Core 的日志,log level 使用 DEBUG
|
||||||
Paste the Clash core log below with the log level set to `DEBUG`.
|
Paste the Clash core log below with the log level set to `DEBUG`.
|
||||||
-->
|
-->
|
||||||
```
|
```
|
||||||
@ -57,9 +63,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
|
|||||||
|
|
||||||
### 环境 Environment
|
### 环境 Environment
|
||||||
|
|
||||||
* Clash Core 的操作系统 (the OS that the Clash core is running on)
|
* 操作系统 (the OS that the Clash core is running on)
|
||||||
……
|
|
||||||
* 使用者的操作系统 (the OS running on the client)
|
|
||||||
……
|
……
|
||||||
* 网路环境或拓扑 (network conditions/topology)
|
* 网路环境或拓扑 (network conditions/topology)
|
||||||
……
|
……
|
||||||
@ -67,7 +71,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
|
|||||||
……
|
……
|
||||||
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
||||||
……
|
……
|
||||||
* 其他
|
* 其他 (any other information that would be useful)
|
||||||
……
|
……
|
||||||
|
|
||||||
### 说明 Description
|
### 说明 Description
|
||||||
@ -85,7 +89,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
|
|||||||
**我预期会发生……?**
|
**我预期会发生……?**
|
||||||
<!-- **Expected behavior:** [What you expected to happen] -->
|
<!-- **Expected behavior:** [What you expected to happen] -->
|
||||||
|
|
||||||
**实际上发生了什麽?**
|
**实际上发生了什么?**
|
||||||
<!-- **Actual behavior:** [What actually happened] -->
|
<!-- **Actual behavior:** [What actually happened] -->
|
||||||
|
|
||||||
### 可能的解决方案 Possible Solution
|
### 可能的解决方案 Possible Solution
|
||||||
|
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
76
.github/workflows/docker.yml
vendored
Normal file
76
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: Publish Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Set up docker buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to Github Package
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: Dreamacro
|
||||||
|
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build dev branch and push
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
|
||||||
|
|
||||||
|
- name: Get all docker tags
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: actions/github-script@v3
|
||||||
|
id: tags
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
|
||||||
|
const tags = [
|
||||||
|
'dreamacro/clash:latest',
|
||||||
|
`dreamacro/clash:${ref}`,
|
||||||
|
'ghcr.io/dreamacro/clash:latest',
|
||||||
|
`ghcr.io/dreamacro/clash:${ref}`
|
||||||
|
]
|
||||||
|
return tags.join(',')
|
||||||
|
result-encoding: string
|
||||||
|
|
||||||
|
- name: Build release and push
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{steps.tags.outputs.result}}
|
16
.github/workflows/go.yml
vendored
16
.github/workflows/go.yml
vendored
@ -7,24 +7,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: 1.14.x
|
go-version: 1.16
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Get dependencies and run test
|
- name: Get dependencies, run test and static check
|
||||||
run: |
|
run: |
|
||||||
go test ./...
|
go test ./...
|
||||||
|
go vet ./...
|
||||||
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
staticcheck -- $(go list ./...)
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
@ -41,4 +44,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: bin/*
|
files: bin/*
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
|
||||||
|
19
.github/workflows/stale.yml
vendored
Normal file
19
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-close: 5
|
@ -3,12 +3,14 @@ FROM golang:alpine as builder
|
|||||||
RUN apk add --no-cache make git && \
|
RUN apk add --no-cache make git && \
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
||||||
WORKDIR /clash-src
|
WORKDIR /clash-src
|
||||||
|
COPY --from=tonistiigi/xx:golang / /
|
||||||
COPY . /clash-src
|
COPY . /clash-src
|
||||||
RUN go mod download && \
|
RUN go mod download && \
|
||||||
make linux-amd64 && \
|
make docker && \
|
||||||
mv ./bin/clash-linux-amd64 /clash
|
mv ./bin/clash-docker /clash
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
FROM golang:alpine as builder
|
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
|
|
||||||
wget -O /qemu-arm-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-arm-static && \
|
|
||||||
chmod +x /qemu-arm-static
|
|
||||||
|
|
||||||
WORKDIR /clash-src
|
|
||||||
COPY . /clash-src
|
|
||||||
RUN go mod download && \
|
|
||||||
make linux-armv7 && \
|
|
||||||
mv ./bin/clash-linux-armv7 /clash
|
|
||||||
|
|
||||||
FROM arm32v7/alpine:latest
|
|
||||||
|
|
||||||
COPY --from=builder /qemu-arm-static /usr/bin/
|
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
|
||||||
COPY --from=builder /clash /
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
ENTRYPOINT ["/clash"]
|
|
@ -1,20 +0,0 @@
|
|||||||
FROM golang:alpine as builder
|
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
|
|
||||||
wget -O /qemu-aarch64-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-aarch64-static && \
|
|
||||||
chmod +x /qemu-aarch64-static
|
|
||||||
|
|
||||||
WORKDIR /clash-src
|
|
||||||
COPY . /clash-src
|
|
||||||
RUN go mod download && \
|
|
||||||
make linux-armv8 && \
|
|
||||||
mv ./bin/clash-linux-armv8 /clash
|
|
||||||
|
|
||||||
FROM arm64v8/alpine:latest
|
|
||||||
|
|
||||||
COPY --from=builder /qemu-aarch64-static /usr/bin/
|
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
|
||||||
COPY --from=builder /clash /
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
ENTRYPOINT ["/clash"]
|
|
23
Makefile
23
Makefile
@ -2,12 +2,13 @@ NAME=clash
|
|||||||
BINDIR=bin
|
BINDIR=bin
|
||||||
VERSION=$(shell git describe --tags || echo "unknown version")
|
VERSION=$(shell git describe --tags || echo "unknown version")
|
||||||
BUILDTIME=$(shell date -u)
|
BUILDTIME=$(shell date -u)
|
||||||
GOBUILD=CGO_ENABLED=0 go build -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
|
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
|
||||||
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
||||||
-w -s'
|
-w -s -buildid='
|
||||||
|
|
||||||
PLATFORM_LIST = \
|
PLATFORM_LIST = \
|
||||||
darwin-amd64 \
|
darwin-amd64 \
|
||||||
|
darwin-arm64 \
|
||||||
linux-386 \
|
linux-386 \
|
||||||
linux-amd64 \
|
linux-amd64 \
|
||||||
linux-armv5 \
|
linux-armv5 \
|
||||||
@ -21,17 +22,25 @@ PLATFORM_LIST = \
|
|||||||
linux-mips64 \
|
linux-mips64 \
|
||||||
linux-mips64le \
|
linux-mips64le \
|
||||||
freebsd-386 \
|
freebsd-386 \
|
||||||
freebsd-amd64
|
freebsd-amd64 \
|
||||||
|
freebsd-arm64
|
||||||
|
|
||||||
WINDOWS_ARCH_LIST = \
|
WINDOWS_ARCH_LIST = \
|
||||||
windows-386 \
|
windows-386 \
|
||||||
windows-amd64
|
windows-amd64 \
|
||||||
|
windows-arm32v7
|
||||||
|
|
||||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||||
|
|
||||||
|
docker:
|
||||||
|
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
darwin-amd64:
|
darwin-amd64:
|
||||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
darwin-arm64:
|
||||||
|
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-386:
|
linux-386:
|
||||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -74,11 +83,17 @@ freebsd-386:
|
|||||||
freebsd-amd64:
|
freebsd-amd64:
|
||||||
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
freebsd-arm64:
|
||||||
|
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
windows-386:
|
windows-386:
|
||||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-amd64:
|
windows-amd64:
|
||||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
|
windows-arm32v7:
|
||||||
|
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
||||||
|
368
README.md
368
README.md
@ -19,369 +19,36 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Local HTTP/HTTPS/SOCKS server with/without authentication
|
- Local HTTP/HTTPS/SOCKS server with authentication support
|
||||||
- VMess, Shadowsocks, Trojan (experimental), Snell protocol support for remote connections. UDP is supported.
|
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
|
||||||
- Built-in DNS server that aims to minimize DNS pollution attacks, supports DoH/DoT upstream. Fake IP is also supported.
|
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
|
||||||
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
|
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
|
||||||
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
|
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
|
||||||
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
|
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
|
||||||
- Netfilter TCP redirecting. You can deploy Clash on your Internet gateway with `iptables`.
|
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||||
- Comprehensive HTTP API controller
|
- Comprehensive HTTP RESTful API controller
|
||||||
|
|
||||||
## Install
|
## Premium Features
|
||||||
|
|
||||||
Clash requires Go >= 1.13. You can build it from source:
|
- TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
|
||||||
|
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
|
||||||
|
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
|
||||||
|
|
||||||
```sh
|
## Getting Started
|
||||||
$ go get -u -v github.com/Dreamacro/clash
|
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||||
```
|
|
||||||
|
|
||||||
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
|
## Premium Release
|
||||||
Pre-built TUN mode binaries are available here: [TUN release](https://github.com/Dreamacro/clash/releases/tag/TUN). Source is not currently available.
|
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
|
||||||
|
|
||||||
Check Clash version with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ clash -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Daemonize Clash
|
|
||||||
|
|
||||||
Unfortunately, there is no native or elegant way to implement daemons on Golang. We recommend using third-party daemon management tools like PM2, Supervisor or the like to keep Clash running as a service.
|
|
||||||
|
|
||||||
In the case of [pm2](https://github.com/Unitech/pm2), start the daemon this way:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ pm2 start clash
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have Docker installed, it's recommended to deploy Clash directly using `docker-compose`: [run Clash in Docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
The default configuration directory is `$HOME/.config/clash`.
|
|
||||||
|
|
||||||
The name of the configuration file is `config.yaml`.
|
|
||||||
|
|
||||||
If you want to use another directory, use `-d` to control the configuration directory.
|
|
||||||
|
|
||||||
For example, you can use the current directory as the configuration directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ clash -d .
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>This is an example configuration file (click to expand)</summary>
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# port of HTTP
|
|
||||||
port: 7890
|
|
||||||
|
|
||||||
# port of SOCKS5
|
|
||||||
socks-port: 7891
|
|
||||||
|
|
||||||
# redir port for Linux and macOS
|
|
||||||
# redir-port: 7892
|
|
||||||
|
|
||||||
allow-lan: false
|
|
||||||
|
|
||||||
# Only applicable when setting allow-lan to true
|
|
||||||
# "*": bind all IP addresses
|
|
||||||
# 192.168.122.11: bind a single IPv4 address
|
|
||||||
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
|
|
||||||
# bind-address: "*"
|
|
||||||
|
|
||||||
# Rule / Global / Direct (default is Rule)
|
|
||||||
mode: Rule
|
|
||||||
|
|
||||||
# set log level to stdout (default is info)
|
|
||||||
# info / warning / error / debug / silent
|
|
||||||
log-level: info
|
|
||||||
|
|
||||||
# RESTful API for clash
|
|
||||||
external-controller: 127.0.0.1:9090
|
|
||||||
|
|
||||||
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
|
|
||||||
# input is a relative path to the configuration directory or an absolute path
|
|
||||||
# external-ui: folder
|
|
||||||
|
|
||||||
# Secret for RESTful API (Optional)
|
|
||||||
# secret: ""
|
|
||||||
|
|
||||||
# experimental feature
|
|
||||||
experimental:
|
|
||||||
ignore-resolve-fail: true # ignore dns resolve fail, default value is true
|
|
||||||
# interface-name: en0 # outbound interface name
|
|
||||||
|
|
||||||
# authentication of local SOCKS5/HTTP(S) server
|
|
||||||
# authentication:
|
|
||||||
# - "user1:pass1"
|
|
||||||
# - "user2:pass2"
|
|
||||||
|
|
||||||
# # experimental hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
|
|
||||||
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com > .example.com)
|
|
||||||
# hosts:
|
|
||||||
# '*.clash.dev': 127.0.0.1
|
|
||||||
# '.dev': 127.0.0.1
|
|
||||||
# 'alpha.clash.dev': '::1'
|
|
||||||
|
|
||||||
# dns:
|
|
||||||
# enable: true # set true to enable dns (default is false)
|
|
||||||
# ipv6: false # default is false
|
|
||||||
# listen: 0.0.0.0:53
|
|
||||||
# # default-nameserver: # resolve dns nameserver host, should fill pure IP
|
|
||||||
# # - 114.114.114.114
|
|
||||||
# # - 8.8.8.8
|
|
||||||
# enhanced-mode: redir-host # or fake-ip
|
|
||||||
# # fake-ip-range: 198.18.0.1/16 # if you don't know what it is, don't change it
|
|
||||||
# fake-ip-filter: # fake ip white domain list
|
|
||||||
# - '*.lan'
|
|
||||||
# - localhost.ptlogin2.qq.com
|
|
||||||
# nameserver:
|
|
||||||
# - 114.114.114.114
|
|
||||||
# - tls://dns.rubyfish.cn:853 # dns over tls
|
|
||||||
# - https://1.1.1.1/dns-query # dns over https
|
|
||||||
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
|
|
||||||
# - tcp://1.1.1.1
|
|
||||||
# fallback-filter:
|
|
||||||
# geoip: true # default
|
|
||||||
# ipcidr: # ips in these subnets will be considered polluted
|
|
||||||
# - 240.0.0.0/4
|
|
||||||
|
|
||||||
proxies:
|
|
||||||
# shadowsocks
|
|
||||||
# The supported ciphers(encrypt methods):
|
|
||||||
# aes-128-gcm aes-192-gcm aes-256-gcm
|
|
||||||
# aes-128-cfb aes-192-cfb aes-256-cfb
|
|
||||||
# aes-128-ctr aes-192-ctr aes-256-ctr
|
|
||||||
# rc4-md5 chacha20-ietf xchacha20
|
|
||||||
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
|
|
||||||
- name: "ss1"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
# udp: true
|
|
||||||
|
|
||||||
# old obfs configuration format remove after prerelease
|
|
||||||
- name: "ss2"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
plugin: obfs
|
|
||||||
plugin-opts:
|
|
||||||
mode: tls # or http
|
|
||||||
# host: bing.com
|
|
||||||
|
|
||||||
- name: "ss3"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
plugin: v2ray-plugin
|
|
||||||
plugin-opts:
|
|
||||||
mode: websocket # no QUIC now
|
|
||||||
# tls: true # wss
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# host: bing.com
|
|
||||||
# path: "/"
|
|
||||||
# mux: true
|
|
||||||
# headers:
|
|
||||||
# custom: value
|
|
||||||
|
|
||||||
# vmess
|
|
||||||
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
|
|
||||||
- name: "vmess"
|
|
||||||
type: vmess
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
alterId: 32
|
|
||||||
cipher: auto
|
|
||||||
# udp: true
|
|
||||||
# tls: true
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# network: ws
|
|
||||||
# ws-path: /path
|
|
||||||
# ws-headers:
|
|
||||||
# Host: v2ray.com
|
|
||||||
|
|
||||||
- name: "vmess-http"
|
|
||||||
type: vmess
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
alterId: 32
|
|
||||||
cipher: auto
|
|
||||||
# udp: true
|
|
||||||
# network: http
|
|
||||||
# http-opts:
|
|
||||||
# # method: "GET"
|
|
||||||
# # path:
|
|
||||||
# # - '/'
|
|
||||||
# # - '/video'
|
|
||||||
# # headers:
|
|
||||||
# # Connection:
|
|
||||||
# # - keep-alive
|
|
||||||
|
|
||||||
# socks5
|
|
||||||
- name: "socks"
|
|
||||||
type: socks5
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
# username: username
|
|
||||||
# password: password
|
|
||||||
# tls: true
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# udp: true
|
|
||||||
|
|
||||||
# http
|
|
||||||
- name: "http"
|
|
||||||
type: http
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
# username: username
|
|
||||||
# password: password
|
|
||||||
# tls: true # https
|
|
||||||
# skip-cert-verify: true
|
|
||||||
|
|
||||||
# snell
|
|
||||||
- name: "snell"
|
|
||||||
type: snell
|
|
||||||
server: server
|
|
||||||
port: 44046
|
|
||||||
psk: yourpsk
|
|
||||||
# obfs-opts:
|
|
||||||
# mode: http # or tls
|
|
||||||
# host: bing.com
|
|
||||||
|
|
||||||
# trojan
|
|
||||||
- name: "trojan"
|
|
||||||
type: trojan
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
password: yourpsk
|
|
||||||
# udp: true
|
|
||||||
# sni: example.com # aka server name
|
|
||||||
# alpn:
|
|
||||||
# - h2
|
|
||||||
# - http/1.1
|
|
||||||
# skip-cert-verify: true
|
|
||||||
|
|
||||||
proxy-groups:
|
|
||||||
# relay chains the proxies. proxies shall not contain a relay. No UDP support.
|
|
||||||
# Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet
|
|
||||||
- name: "relay"
|
|
||||||
type: relay
|
|
||||||
proxies:
|
|
||||||
- http
|
|
||||||
- vmess
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
|
|
||||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
|
||||||
- name: "auto"
|
|
||||||
type: url-test
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
|
|
||||||
- name: "fallback-auto"
|
|
||||||
type: fallback
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# load-balance: The request of the same eTLD will be dial on the same proxy.
|
|
||||||
- name: "load-balance"
|
|
||||||
type: load-balance
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# select is used for selecting proxy or proxy group
|
|
||||||
# you can use RESTful API to switch proxy, is recommended for use in GUI.
|
|
||||||
- name: Proxy
|
|
||||||
type: select
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
- auto
|
|
||||||
|
|
||||||
- name: UseProvider
|
|
||||||
type: select
|
|
||||||
use:
|
|
||||||
- provider1
|
|
||||||
proxies:
|
|
||||||
- Proxy
|
|
||||||
- DIRECT
|
|
||||||
|
|
||||||
proxy-providers:
|
|
||||||
provider1:
|
|
||||||
type: http
|
|
||||||
url: "url"
|
|
||||||
interval: 3600
|
|
||||||
path: ./hk.yaml
|
|
||||||
health-check:
|
|
||||||
enable: true
|
|
||||||
interval: 600
|
|
||||||
url: http://www.gstatic.com/generate_204
|
|
||||||
test:
|
|
||||||
type: file
|
|
||||||
path: /test.yaml
|
|
||||||
health-check:
|
|
||||||
enable: true
|
|
||||||
interval: 36000
|
|
||||||
url: http://www.gstatic.com/generate_204
|
|
||||||
|
|
||||||
rules:
|
|
||||||
- DOMAIN-SUFFIX,google.com,auto
|
|
||||||
- DOMAIN-KEYWORD,google,auto
|
|
||||||
- DOMAIN,google.com,auto
|
|
||||||
- DOMAIN-SUFFIX,ad.com,REJECT
|
|
||||||
# rename SOURCE-IP-CIDR and would remove after prerelease
|
|
||||||
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
|
|
||||||
# optional param "no-resolve" for IP rules (GEOIP IP-CIDR)
|
|
||||||
- IP-CIDR,127.0.0.0/8,DIRECT
|
|
||||||
- GEOIP,CN,DIRECT
|
|
||||||
- DST-PORT,80,DIRECT
|
|
||||||
- SRC-PORT,7777,DIRECT
|
|
||||||
# FINAL would remove after prerelease
|
|
||||||
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
|
|
||||||
- MATCH,auto
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Advanced
|
|
||||||
[Provider](https://github.com/Dreamacro/clash/wiki/Provider)
|
|
||||||
|
|
||||||
## Documentations
|
|
||||||
https://clash.gitbook.io/
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||||
|
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||||
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
This software is released under the GPL-3.0 license.
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
@ -390,4 +57,3 @@ https://clash.gitbook.io/
|
|||||||
- [x] Redir proxy
|
- [x] Redir proxy
|
||||||
- [x] UDP support
|
- [x] UDP support
|
||||||
- [x] Connection manager
|
- [x] Connection manager
|
||||||
- [ ] Event API
|
|
||||||
|
@ -6,33 +6,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPAdapter is a adapter for HTTP connection
|
// NewHTTP receive normal http request and return HTTPContext
|
||||||
type HTTPAdapter struct {
|
func NewHTTP(request *http.Request, conn net.Conn) *context.HTTPContext {
|
||||||
net.Conn
|
|
||||||
metadata *C.Metadata
|
|
||||||
R *http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata return destination metadata
|
|
||||||
func (h *HTTPAdapter) Metadata() *C.Metadata {
|
|
||||||
return h.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHTTP is HTTPAdapter generator
|
|
||||||
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
|
|
||||||
metadata := parseHTTPAddr(request)
|
metadata := parseHTTPAddr(request)
|
||||||
metadata.Type = C.HTTP
|
metadata.Type = C.HTTP
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
return &HTTPAdapter{
|
return context.NewHTTPContext(conn, request, metadata)
|
||||||
metadata: metadata,
|
|
||||||
R: request,
|
|
||||||
Conn: conn,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHopByHopHeaders remove hop-by-hop header
|
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||||
@ -58,3 +43,19 @@ func RemoveHopByHopHeaders(header http.Header) {
|
|||||||
header.Del(strings.TrimSpace(h))
|
header.Del(strings.TrimSpace(h))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||||
|
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
||||||
|
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||||
|
host := req.Host
|
||||||
|
if host == "" {
|
||||||
|
host = req.URL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
|
||||||
|
host = pHost
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Host = host
|
||||||
|
req.URL.Host = host
|
||||||
|
}
|
||||||
|
@ -5,18 +5,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHTTPS is HTTPAdapter generator
|
// NewHTTPS receive CONNECT request and return ConnContext
|
||||||
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
|
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
||||||
metadata := parseHTTPAddr(request)
|
metadata := parseHTTPAddr(request)
|
||||||
metadata.Type = C.HTTPCONNECT
|
metadata.Type = C.HTTPCONNECT
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
return &SocketAdapter{
|
return context.NewConnContext(conn, metadata)
|
||||||
metadata: metadata,
|
|
||||||
Conn: conn,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,11 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
"github.com/Dreamacro/clash/component/socks5"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SocketAdapter is a adapter for socks and redir connection
|
// NewSocket receive TCP inbound and return ConnContext
|
||||||
type SocketAdapter struct {
|
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
|
||||||
net.Conn
|
|
||||||
metadata *C.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata return destination metadata
|
|
||||||
func (s *SocketAdapter) Metadata() *C.Metadata {
|
|
||||||
return s.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSocket is SocketAdapter generator
|
|
||||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter {
|
|
||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.TCP
|
metadata.NetWork = C.TCP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
@ -28,8 +18,5 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter
|
|||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SocketAdapter{
|
return context.NewConnContext(conn, metadata)
|
||||||
Conn: conn,
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
"github.com/Dreamacro/clash/component/socks5"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
@ -16,7 +17,8 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
|||||||
|
|
||||||
switch target[0] {
|
switch target[0] {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
metadata.Host = string(target[2 : 2+target[1]])
|
// trim for FQDN
|
||||||
|
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
|
||||||
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
||||||
case socks5.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
ip := net.IP(target[1 : 1+net.IPv4len])
|
ip := net.IP(target[1 : 1+net.IPv4len])
|
||||||
@ -38,6 +40,9 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
port = "80"
|
port = "80"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim FQDN (#737)
|
||||||
|
host = strings.TrimRight(host, ".")
|
||||||
|
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{
|
||||||
NetWork: C.TCP,
|
NetWork: C.TCP,
|
||||||
AddrType: C.AtypDomainName,
|
AddrType: C.AtypDomainName,
|
||||||
|
@ -10,10 +10,8 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
"go.uber.org/atomic"
|
||||||
defaultURLTestTimeout = time.Second * 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
@ -23,36 +21,44 @@ type Base struct {
|
|||||||
udp bool
|
udp bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name implements C.ProxyAdapter
|
||||||
func (b *Base) Name() string {
|
func (b *Base) Name() string {
|
||||||
return b.name
|
return b.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type implements C.ProxyAdapter
|
||||||
func (b *Base) Type() C.AdapterType {
|
func (b *Base) Type() C.AdapterType {
|
||||||
return b.tp
|
return b.tp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
return c, errors.New("no support")
|
return c, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return nil, errors.New("no support")
|
return nil, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (b *Base) SupportUDP() bool {
|
func (b *Base) SupportUDP() bool {
|
||||||
return b.udp
|
return b.udp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]string{
|
return json.Marshal(map[string]string{
|
||||||
"type": b.Type().String(),
|
"type": b.Type().String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Addr implements C.ProxyAdapter
|
||||||
func (b *Base) Addr() string {
|
func (b *Base) Addr() string {
|
||||||
return b.addr
|
return b.addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -66,10 +72,12 @@ type conn struct {
|
|||||||
chain C.Chain
|
chain C.Chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chains implements C.Connection
|
||||||
func (c *conn) Chains() C.Chain {
|
func (c *conn) Chains() C.Chain {
|
||||||
return c.chain
|
return c.chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendToChains implements C.Connection
|
||||||
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
||||||
c.chain = append(c.chain, a.Name())
|
c.chain = append(c.chain, a.Name())
|
||||||
}
|
}
|
||||||
@ -78,52 +86,53 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
|||||||
return &conn{c, []string{a.Name()}}
|
return &conn{c, []string{a.Name()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketConn interface {
|
|
||||||
net.PacketConn
|
|
||||||
WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type packetConn struct {
|
type packetConn struct {
|
||||||
PacketConn
|
net.PacketConn
|
||||||
chain C.Chain
|
chain C.Chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chains implements C.Connection
|
||||||
func (c *packetConn) Chains() C.Chain {
|
func (c *packetConn) Chains() C.Chain {
|
||||||
return c.chain
|
return c.chain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendToChains implements C.Connection
|
||||||
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
||||||
c.chain = append(c.chain, a.Name())
|
c.chain = append(c.chain, a.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPacketConn(pc PacketConn, a C.ProxyAdapter) C.PacketConn {
|
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||||
return &packetConn{pc, []string{a.Name()}}
|
return &packetConn{pc, []string{a.Name()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
history *queue.Queue
|
history *queue.Queue
|
||||||
alive bool
|
alive *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alive implements C.Proxy
|
||||||
func (p *Proxy) Alive() bool {
|
func (p *Proxy) Alive() bool {
|
||||||
return p.alive
|
return p.alive.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dial implements C.Proxy
|
||||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return p.DialContext(ctx, metadata)
|
return p.DialContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.alive = false
|
p.alive.Store(false)
|
||||||
}
|
}
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DelayHistory implements C.Proxy
|
||||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||||
queue := p.history.Copy()
|
queue := p.history.Copy()
|
||||||
histories := []C.DelayHistory{}
|
histories := []C.DelayHistory{}
|
||||||
@ -134,9 +143,10 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||||
|
// implements C.Proxy
|
||||||
func (p *Proxy) LastDelay() (delay uint16) {
|
func (p *Proxy) LastDelay() (delay uint16) {
|
||||||
var max uint16 = 0xffff
|
var max uint16 = 0xffff
|
||||||
if !p.alive {
|
if !p.alive.Load() {
|
||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +161,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
|||||||
return history.Delay
|
return history.Delay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -165,9 +176,10 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// URLTest get the delay for the specified URL
|
// URLTest get the delay for the specified URL
|
||||||
|
// implements C.Proxy
|
||||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.alive = err == nil
|
p.alive.Store(err == nil)
|
||||||
record := C.DelayHistory{Time: time.Now()}
|
record := C.DelayHistory{Time: time.Now()}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
record.Delay = t
|
record.Delay = t
|
||||||
@ -223,5 +235,5 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||||
return &Proxy{adapter, queue.New(10), true}
|
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,6 +12,7 @@ type Direct struct {
|
|||||||
*Base
|
*Base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
return NewConn(c, d), nil
|
return NewConn(c, d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := dialer.ListenPacket("udp", "")
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -36,17 +37,6 @@ type directPacketConn struct {
|
|||||||
net.PacketConn
|
net.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dp *directPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
return dp.WriteTo(p, metadata.UDPAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDirect() *Direct {
|
func NewDirect() *Direct {
|
||||||
return &Direct{
|
return &Direct{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
|
@ -31,9 +31,11 @@ type HttpOption struct {
|
|||||||
UserName string `proxy:"username,omitempty"`
|
UserName string `proxy:"username,omitempty"`
|
||||||
Password string `proxy:"password,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if h.tlsConfig != nil {
|
if h.tlsConfig != nil {
|
||||||
cc := tls.Client(c, h.tlsConfig)
|
cc := tls.Client(c, h.tlsConfig)
|
||||||
@ -50,13 +52,16 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
c, err := dialer.DialContext(ctx, "tcp", h.addr)
|
c, err := dialer.DialContext(ctx, "tcp", h.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = h.StreamConn(c, metadata)
|
c, err = h.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -114,10 +119,14 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
func NewHttp(option HttpOption) *Http {
|
func NewHttp(option HttpOption) *Http {
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
|
sni := option.Server
|
||||||
|
if option.SNI != "" {
|
||||||
|
sni = option.SNI
|
||||||
|
}
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
InsecureSkipVerify: option.SkipCertVerify,
|
||||||
ClientSessionCache: getClientSessionCache(),
|
ClientSessionCache: getClientSessionCache(),
|
||||||
ServerName: option.Server,
|
ServerName: sni,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,11 +11,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|||||||
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||||
proxyType, existType := mapping["type"].(string)
|
proxyType, existType := mapping["type"].(string)
|
||||||
if !existType {
|
if !existType {
|
||||||
return nil, fmt.Errorf("Missing type")
|
return nil, fmt.Errorf("missing type")
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy C.ProxyAdapter
|
var (
|
||||||
err := fmt.Errorf("Cannot parse")
|
proxy C.ProxyAdapter
|
||||||
|
err error
|
||||||
|
)
|
||||||
switch proxyType {
|
switch proxyType {
|
||||||
case "ss":
|
case "ss":
|
||||||
ssOption := &ShadowSocksOption{}
|
ssOption := &ShadowSocksOption{}
|
||||||
@ -24,6 +26,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = NewShadowSocks(*ssOption)
|
proxy, err = NewShadowSocks(*ssOption)
|
||||||
|
case "ssr":
|
||||||
|
ssrOption := &ShadowSocksROption{}
|
||||||
|
err = decoder.Decode(mapping, ssrOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = NewShadowSocksR(*ssrOption)
|
||||||
case "socks5":
|
case "socks5":
|
||||||
socksOption := &Socks5Option{}
|
socksOption := &Socks5Option{}
|
||||||
err = decoder.Decode(mapping, socksOption)
|
err = decoder.Decode(mapping, socksOption)
|
||||||
@ -65,7 +74,7 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|||||||
}
|
}
|
||||||
proxy, err = NewTrojan(*trojanOption)
|
proxy, err = NewTrojan(*trojanOption)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -14,10 +14,12 @@ type Reject struct {
|
|||||||
*Base
|
*Base
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
return NewConn(&NopConn{}, r), nil
|
return NewConn(&NopConn{}, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
return nil, errors.New("match reject rule")
|
return nil, errors.New("match reject rule")
|
||||||
}
|
}
|
||||||
|
@ -37,14 +37,10 @@ type ShadowSocksOption struct {
|
|||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Plugin string `proxy:"plugin,omitempty"`
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
||||||
|
|
||||||
// deprecated when bump to 1.0
|
|
||||||
Obfs string `proxy:"obfs,omitempty"`
|
|
||||||
ObfsHost string `proxy:"obfs-host,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type simpleObfsOption struct {
|
type simpleObfsOption struct {
|
||||||
Mode string `obfs:"mode"`
|
Mode string `obfs:"mode,omitempty"`
|
||||||
Host string `obfs:"host,omitempty"`
|
Host string `obfs:"host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +54,7 @@ type v2rayObfsOption struct {
|
|||||||
Mux bool `obfs:"mux,omitempty"`
|
Mux bool `obfs:"mux,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
switch ss.obfsMode {
|
switch ss.obfsMode {
|
||||||
case "tls":
|
case "tls":
|
||||||
@ -77,17 +74,21 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
return NewConn(c, ss), err
|
return NewConn(c, ss), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := dialer.ListenPacket("udp", "")
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -96,6 +97,7 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
|
|
||||||
addr, err := resolveUDPAddr("udp", ss.addr)
|
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
pc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +105,7 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]string{
|
return json.Marshal(map[string]string{
|
||||||
"type": ss.Type().String(),
|
"type": ss.Type().String(),
|
||||||
@ -122,17 +125,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
var obfsOption *simpleObfsOption
|
var obfsOption *simpleObfsOption
|
||||||
obfsMode := ""
|
obfsMode := ""
|
||||||
|
|
||||||
// forward compatibility before 1.0
|
|
||||||
if option.Obfs != "" {
|
|
||||||
obfsMode = option.Obfs
|
|
||||||
obfsOption = &simpleObfsOption{
|
|
||||||
Host: "bing.com",
|
|
||||||
}
|
|
||||||
if option.ObfsHost != "" {
|
|
||||||
obfsOption.Host = option.ObfsHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
if option.Plugin == "obfs" {
|
if option.Plugin == "obfs" {
|
||||||
opts := simpleObfsOption{Host: "bing.com"}
|
opts := simpleObfsOption{Host: "bing.com"}
|
||||||
@ -197,14 +189,6 @@ func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|||||||
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (spc *ssPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, _, e := spc.PacketConn.ReadFrom(b)
|
n, _, e := spc.PacketConn.ReadFrom(b)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
156
adapters/outbound/shadowsocksr.go
Normal file
156
adapters/outbound/shadowsocksr.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/obfs"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/protocol"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShadowSocksR struct {
|
||||||
|
*Base
|
||||||
|
cipher core.Cipher
|
||||||
|
obfs obfs.Obfs
|
||||||
|
protocol protocol.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShadowSocksROption struct {
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
Password string `proxy:"password"`
|
||||||
|
Cipher string `proxy:"cipher"`
|
||||||
|
Obfs string `proxy:"obfs"`
|
||||||
|
ObfsParam string `proxy:"obfs-param,omitempty"`
|
||||||
|
Protocol string `proxy:"protocol"`
|
||||||
|
ProtocolParam string `proxy:"protocol-param,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
c = ssr.obfs.StreamConn(c)
|
||||||
|
c = ssr.cipher.StreamConn(c)
|
||||||
|
var (
|
||||||
|
iv []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch conn := c.(type) {
|
||||||
|
case *shadowstream.Conn:
|
||||||
|
iv, err = conn.ObtainWriteIV()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case *shadowaead.Conn:
|
||||||
|
return nil, fmt.Errorf("invalid connection type")
|
||||||
|
}
|
||||||
|
c = ssr.protocol.StreamConn(c, iv)
|
||||||
|
_, err = c.Write(serializesSocksAddr(metadata))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
|
c, err = ssr.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, ssr), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := resolveUDPAddr("udp", ssr.addr)
|
||||||
|
if err != nil {
|
||||||
|
pc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = ssr.cipher.PacketConn(pc)
|
||||||
|
pc = ssr.protocol.PacketConn(pc)
|
||||||
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]string{
|
||||||
|
"type": ssr.Type().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
cipher := option.Cipher
|
||||||
|
password := option.Password
|
||||||
|
coreCiph, err := core.PickCipher(cipher, nil, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
ivSize int
|
||||||
|
key []byte
|
||||||
|
)
|
||||||
|
if option.Cipher == "dummy" {
|
||||||
|
ivSize = 0
|
||||||
|
key = core.Kdf(option.Password, 16)
|
||||||
|
} else {
|
||||||
|
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
|
||||||
|
}
|
||||||
|
ivSize = ciph.IVSize()
|
||||||
|
key = ciph.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
|
||||||
|
Host: option.Server,
|
||||||
|
Port: option.Port,
|
||||||
|
Key: key,
|
||||||
|
IVSize: ivSize,
|
||||||
|
Param: option.ObfsParam,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
|
||||||
|
Key: key,
|
||||||
|
Overhead: obfsOverhead,
|
||||||
|
Param: option.ProtocolParam,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ShadowSocksR{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.ShadowsocksR,
|
||||||
|
udp: option.UDP,
|
||||||
|
},
|
||||||
|
cipher: coreCiph,
|
||||||
|
obfs: obfs,
|
||||||
|
protocol: protocol,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -16,7 +16,9 @@ import (
|
|||||||
type Snell struct {
|
type Snell struct {
|
||||||
*Base
|
*Base
|
||||||
psk []byte
|
psk []byte
|
||||||
|
pool *snell.Pool
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
|
version int
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnellOption struct {
|
type SnellOption struct {
|
||||||
@ -24,30 +26,60 @@ type SnellOption struct {
|
|||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Psk string `proxy:"psk"`
|
Psk string `proxy:"psk"`
|
||||||
|
Version int `proxy:"version,omitempty"`
|
||||||
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
|
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
type streamOption struct {
|
||||||
switch s.obfsOption.Mode {
|
psk []byte
|
||||||
|
version int
|
||||||
|
addr string
|
||||||
|
obfsOption *simpleObfsOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
||||||
|
switch option.obfsOption.Mode {
|
||||||
case "tls":
|
case "tls":
|
||||||
c = obfs.NewTLSObfs(c, s.obfsOption.Host)
|
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
|
||||||
case "http":
|
case "http":
|
||||||
_, port, _ := net.SplitHostPort(s.addr)
|
_, port, _ := net.SplitHostPort(option.addr)
|
||||||
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
|
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
|
||||||
}
|
}
|
||||||
c = snell.StreamConn(c, s.psk)
|
return snell.StreamConn(c, option.psk, option.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||||
port, _ := strconv.Atoi(metadata.DstPort)
|
port, _ := strconv.Atoi(metadata.DstPort)
|
||||||
err := snell.WriteHeader(c, metadata.String(), uint(port))
|
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
if s.version == snell.Version2 {
|
||||||
|
c, err := s.pool.Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := strconv.Atoi(metadata.DstPort)
|
||||||
|
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c, s), err
|
||||||
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = s.StreamConn(c, metadata)
|
c, err = s.StreamConn(c, metadata)
|
||||||
return NewConn(c, s), err
|
return NewConn(c, s), err
|
||||||
}
|
}
|
||||||
@ -62,11 +94,22 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
switch obfsOption.Mode {
|
||||||
|
case "tls", "http", "":
|
||||||
|
break
|
||||||
|
default:
|
||||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Snell{
|
// backward compatible
|
||||||
|
if option.Version == 0 {
|
||||||
|
option.Version = snell.DefaultSnellVersion
|
||||||
|
}
|
||||||
|
if option.Version != snell.Version1 && option.Version != snell.Version2 {
|
||||||
|
return nil, fmt.Errorf("snell version error: %d", option.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Snell{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
@ -74,5 +117,19 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
},
|
},
|
||||||
psk: psk,
|
psk: psk,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
}, nil
|
version: option.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.Version == snell.Version2 {
|
||||||
|
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ type Socks5Option struct {
|
|||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if ss.tls {
|
if ss.tls {
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
@ -58,13 +59,16 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -73,6 +77,7 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
|
|||||||
return NewConn(c, ss), nil
|
return NewConn(c, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -88,11 +93,7 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
c = cc
|
c = cc
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer safeConnClose(c, err)
|
||||||
if err != nil {
|
|
||||||
c.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
var user *socks5.User
|
var user *socks5.User
|
||||||
@ -122,7 +123,21 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
pc.Close()
|
pc.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, ss), nil
|
// Support unspecified UDP bind address.
|
||||||
|
bindUDPAddr := bindAddr.UDPAddr()
|
||||||
|
if bindUDPAddr == nil {
|
||||||
|
err = errors.New("invalid UDP bind address")
|
||||||
|
return
|
||||||
|
} else if bindUDPAddr.IP.IsUnspecified() {
|
||||||
|
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bindUDPAddr.IP = serverAddr.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSocks5(option Socks5Option) *Socks5 {
|
func NewSocks5(option Socks5Option) *Socks5 {
|
||||||
@ -164,14 +179,6 @@ func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|||||||
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *socksPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, _, e := uc.PacketConn.ReadFrom(b)
|
n, _, e := uc.PacketConn.ReadFrom(b)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
|
@ -2,34 +2,52 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/gun"
|
||||||
"github.com/Dreamacro/clash/component/trojan"
|
"github.com/Dreamacro/clash/component/trojan"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Trojan struct {
|
type Trojan struct {
|
||||||
*Base
|
*Base
|
||||||
instance *trojan.Trojan
|
instance *trojan.Trojan
|
||||||
|
|
||||||
|
// for gun mux
|
||||||
|
gunTLSConfig *tls.Config
|
||||||
|
gunConfig *gun.Config
|
||||||
|
transport *http2.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrojanOption struct {
|
type TrojanOption struct {
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Password string `proxy:"password"`
|
Password string `proxy:"password"`
|
||||||
ALPN []string `proxy:"alpn,omitempty"`
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
Network string `proxy:"network,omitempty"`
|
||||||
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
c, err := t.instance.StreamConn(c)
|
var err error
|
||||||
|
if t.transport != nil {
|
||||||
|
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
||||||
|
} else {
|
||||||
|
c, err = t.instance.StreamConn(c)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
@ -38,12 +56,31 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
// gun transport
|
||||||
|
if t.transport != nil {
|
||||||
|
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, t), nil
|
||||||
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = t.StreamConn(c, metadata)
|
c, err = t.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -52,17 +89,30 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
return NewConn(c, t), err
|
return NewConn(c, t), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
// DialUDP implements C.ProxyAdapter
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
defer cancel()
|
var c net.Conn
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
// grpc transport
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
if t.transport != nil {
|
||||||
}
|
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
tcpKeepAlive(c)
|
if err != nil {
|
||||||
c, err = t.instance.StreamConn(c)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
if err != nil {
|
}
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
defer safeConnClose(c, err)
|
||||||
|
} else {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||||
|
defer cancel()
|
||||||
|
c, err = dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
c, err = t.instance.StreamConn(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
||||||
@ -71,7 +121,7 @@ func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pc := t.instance.PacketConn(c)
|
pc := t.instance.PacketConn(c)
|
||||||
return newPacketConn(&trojanPacketConn{pc, c}, t), err
|
return newPacketConn(pc, t), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trojan) MarshalJSON() ([]byte, error) {
|
func (t *Trojan) MarshalJSON() ([]byte, error) {
|
||||||
@ -95,7 +145,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
tOption.ServerName = option.SNI
|
tOption.ServerName = option.SNI
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Trojan{
|
t := &Trojan{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
@ -103,14 +153,33 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
instance: trojan.New(tOption),
|
instance: trojan.New(tOption),
|
||||||
}, nil
|
}
|
||||||
}
|
|
||||||
|
|
||||||
type trojanPacketConn struct {
|
if option.Network == "grpc" {
|
||||||
net.PacketConn
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
conn net.Conn
|
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tpc *trojanPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
tlsConfig := &tls.Config{
|
||||||
return trojan.WritePacket(tpc.conn, serializesSocksAddr(metadata), p)
|
NextProtos: option.ALPN,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
InsecureSkipVerify: tOption.SkipCertVerify,
|
||||||
|
ServerName: tOption.ServerName,
|
||||||
|
ClientSessionCache: getClientSessionCache(),
|
||||||
|
}
|
||||||
|
|
||||||
|
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
t.gunTLSConfig = tlsConfig
|
||||||
|
t.gunConfig = &gun.Config{
|
||||||
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
|
Host: tOption.ServerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
}
|
}
|
||||||
|
@ -98,3 +98,9 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
|||||||
}
|
}
|
||||||
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func safeConnClose(c net.Conn, err error) {
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -10,15 +11,23 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/gun"
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
"github.com/Dreamacro/clash/component/vmess"
|
"github.com/Dreamacro/clash/component/vmess"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Vmess struct {
|
type Vmess struct {
|
||||||
*Base
|
*Base
|
||||||
client *vmess.Client
|
client *vmess.Client
|
||||||
option *VmessOption
|
option *VmessOption
|
||||||
|
|
||||||
|
// for gun mux
|
||||||
|
gunTLSConfig *tls.Config
|
||||||
|
gunConfig *gun.Config
|
||||||
|
transport *http2.Transport
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmessOption struct {
|
type VmessOption struct {
|
||||||
@ -32,9 +41,12 @@ type VmessOption struct {
|
|||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
WSPath string `proxy:"ws-path,omitempty"`
|
WSPath string `proxy:"ws-path,omitempty"`
|
||||||
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
ServerName string `proxy:"servername,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPOptions struct {
|
type HTTPOptions struct {
|
||||||
@ -43,6 +55,16 @@ type HTTPOptions struct {
|
|||||||
Headers map[string][]string `proxy:"headers,omitempty"`
|
Headers map[string][]string `proxy:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTP2Options struct {
|
||||||
|
Host []string `proxy:"host,omitempty"`
|
||||||
|
Path string `proxy:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrpcOptions struct {
|
||||||
|
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
var err error
|
var err error
|
||||||
switch v.option.Network {
|
switch v.option.Network {
|
||||||
@ -66,9 +88,29 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
wsOpts.TLS = true
|
wsOpts.TLS = true
|
||||||
wsOpts.SessionCache = getClientSessionCache()
|
wsOpts.SessionCache = getClientSessionCache()
|
||||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
||||||
|
wsOpts.ServerName = v.option.ServerName
|
||||||
}
|
}
|
||||||
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
case "http":
|
case "http":
|
||||||
|
// readability first, so just copy default TLS logic
|
||||||
|
if v.option.TLS {
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsOpts := &vmess.TLSConfig{
|
||||||
|
Host: host,
|
||||||
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
|
SessionCache: getClientSessionCache(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
httpOpts := &vmess.HTTPConfig{
|
httpOpts := &vmess.HTTPConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
@ -78,6 +120,32 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c = vmess.StreamHTTPConn(c, httpOpts)
|
c = vmess.StreamHTTPConn(c, httpOpts)
|
||||||
|
case "h2":
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsOpts := vmess.TLSConfig{
|
||||||
|
Host: host,
|
||||||
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
|
SessionCache: getClientSessionCache(),
|
||||||
|
NextProtos: []string{"h2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamTLSConn(c, &tlsOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h2Opts := &vmess.H2Config{
|
||||||
|
Hosts: v.option.HTTP2Opts.Host,
|
||||||
|
Path: v.option.HTTP2Opts.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamH2Conn(c, h2Opts)
|
||||||
|
case "grpc":
|
||||||
|
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
|
||||||
default:
|
default:
|
||||||
// handle TLS
|
// handle TLS
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
@ -87,6 +155,11 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
SkipCertVerify: v.option.SkipCertVerify,
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
SessionCache: getClientSessionCache(),
|
SessionCache: getClientSessionCache(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,19 +171,38 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
// gun transport
|
||||||
|
if v.transport != nil {
|
||||||
|
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, v), nil
|
||||||
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error", v.addr)
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
c, err = v.StreamConn(c, metadata)
|
||||||
return NewConn(c, v), err
|
return NewConn(c, v), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
// DialUDP implements C.ProxyAdapter
|
||||||
// vmess use stream-oriented udp, so clash needs a net.UDPAddr
|
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
|
||||||
if !metadata.Resolved() {
|
if !metadata.Resolved() {
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
ip, err := resolver.ResolveIP(metadata.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -119,17 +211,33 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
metadata.DstIP = ip
|
metadata.DstIP = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
var c net.Conn
|
||||||
defer cancel()
|
// gun transport
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
if v.transport != nil {
|
||||||
if err != nil {
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
return nil, fmt.Errorf("%s connect error", v.addr)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
|
} else {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||||
|
defer cancel()
|
||||||
|
c, err = dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
defer safeConnClose(c, err)
|
||||||
|
|
||||||
|
c, err = v.StreamConn(c, metadata)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,21 +249,66 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
Security: security,
|
Security: security,
|
||||||
HostName: option.Server,
|
HostName: option.Server,
|
||||||
Port: strconv.Itoa(option.Port),
|
Port: strconv.Itoa(option.Port),
|
||||||
|
IsAead: option.AlterID == 0,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Vmess{
|
switch option.Network {
|
||||||
|
case "h2", "grpc":
|
||||||
|
if !option.TLS {
|
||||||
|
return nil, fmt.Errorf("TLS must be true with h2/grpc network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := &Vmess{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Vmess,
|
tp: C.Vmess,
|
||||||
udp: true,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
client: client,
|
client: client,
|
||||||
option: &option,
|
option: &option,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
switch option.Network {
|
||||||
|
case "h2":
|
||||||
|
if len(option.HTTP2Opts.Host) == 0 {
|
||||||
|
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
||||||
|
}
|
||||||
|
case "grpc":
|
||||||
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gunConfig := &gun.Config{
|
||||||
|
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
||||||
|
Host: v.option.ServerName,
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||||
|
ServerName: v.option.ServerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName == "" {
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsConfig.ServerName = host
|
||||||
|
gunConfig.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
v.gunTLSConfig = tlsConfig
|
||||||
|
v.gunConfig = gunConfig
|
||||||
|
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||||
@ -195,10 +348,6 @@ func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
|||||||
return uc.Conn.Write(b)
|
return uc.Conn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *vmessPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
return uc.Conn.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, err := uc.Conn.Read(b)
|
n, err := uc.Conn.Read(b)
|
||||||
return n, uc.rAddr, err
|
return n, uc.rAddr, err
|
||||||
|
@ -11,10 +11,14 @@ const (
|
|||||||
defaultGetProxiesDuration = time.Second * 5
|
defaultGetProxiesDuration = time.Second * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy {
|
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
||||||
proxies := []C.Proxy{}
|
proxies := []C.Proxy{}
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
proxies = append(proxies, provider.Proxies()...)
|
if touch {
|
||||||
|
proxies = append(proxies, provider.ProxiesWithTouch()...)
|
||||||
|
} else {
|
||||||
|
proxies = append(proxies, provider.Proxies()...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
@ -12,17 +12,19 @@ import (
|
|||||||
|
|
||||||
type Fallback struct {
|
type Fallback struct {
|
||||||
*outbound.Base
|
*outbound.Base
|
||||||
single *singledo.Single
|
disableUDP bool
|
||||||
providers []provider.ProxyProvider
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) Now() string {
|
func (f *Fallback) Now() string {
|
||||||
proxy := f.findAliveProxy()
|
proxy := f.findAliveProxy(false)
|
||||||
return proxy.Name()
|
return proxy.Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
proxy := f.findAliveProxy()
|
proxy := f.findAliveProxy(true)
|
||||||
c, err := proxy.DialContext(ctx, metadata)
|
c, err := proxy.DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(f)
|
c.AppendToChains(f)
|
||||||
@ -30,8 +32,9 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
proxy := f.findAliveProxy()
|
proxy := f.findAliveProxy(true)
|
||||||
pc, err := proxy.DialUDP(metadata)
|
pc, err := proxy.DialUDP(metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(f)
|
pc.AppendToChains(f)
|
||||||
@ -39,14 +42,20 @@ func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (f *Fallback) SupportUDP() bool {
|
func (f *Fallback) SupportUDP() bool {
|
||||||
proxy := f.findAliveProxy()
|
if f.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := f.findAliveProxy(false)
|
||||||
return proxy.SupportUDP()
|
return proxy.SupportUDP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range f.proxies() {
|
for _, proxy := range f.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
@ -56,34 +65,36 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
proxy := f.findAliveProxy()
|
proxy := f.findAliveProxy(true)
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) proxies() []C.Proxy {
|
func (f *Fallback) proxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := f.single.Do(func() (interface{}, error) {
|
elm, _, _ := f.single.Do(func() (interface{}, error) {
|
||||||
return getProvidersProxies(f.providers), nil
|
return getProvidersProxies(f.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) findAliveProxy() C.Proxy {
|
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||||
proxies := f.proxies()
|
proxies := f.proxies(touch)
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if proxy.Alive() {
|
if proxy.Alive() {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.proxies()[0]
|
return proxies[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFallback(name string, providers []provider.ProxyProvider) *Fallback {
|
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||||
return &Fallback{
|
return &Fallback{
|
||||||
Base: outbound.NewBase(name, "", C.Fallback, false),
|
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
|
disableUDP: options.DisableUDP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
@ -14,11 +16,25 @@ import (
|
|||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
|
||||||
|
|
||||||
type LoadBalance struct {
|
type LoadBalance struct {
|
||||||
*outbound.Base
|
*outbound.Base
|
||||||
single *singledo.Single
|
disableUDP bool
|
||||||
maxRetry int
|
single *singledo.Single
|
||||||
providers []provider.ProxyProvider
|
providers []provider.ProxyProvider
|
||||||
|
strategyFn strategyFn
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
|
||||||
|
func parseStrategy(config map[string]interface{}) string {
|
||||||
|
if elm, ok := config["strategy"]; ok {
|
||||||
|
if strategy, ok := elm.(string); ok {
|
||||||
|
return strategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "consistent-hashing"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKey(metadata *C.Metadata) string {
|
func getKey(metadata *C.Metadata) string {
|
||||||
@ -52,6 +68,7 @@ func jumpHash(key uint64, buckets int32) int32 {
|
|||||||
return int32(b)
|
return int32(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -65,6 +82,7 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -77,36 +95,62 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
|
|||||||
return proxy.DialUDP(metadata)
|
return proxy.DialUDP(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) SupportUDP() bool {
|
func (lb *LoadBalance) SupportUDP() bool {
|
||||||
return true
|
return !lb.disableUDP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
func strategyRoundRobin() strategyFn {
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
idx := 0
|
||||||
proxies := lb.proxies()
|
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||||
buckets := int32(len(proxies))
|
length := len(proxies)
|
||||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
for i := 0; i < length; i++ {
|
||||||
idx := jumpHash(key, buckets)
|
idx = (idx + 1) % length
|
||||||
proxy := proxies[idx]
|
proxy := proxies[idx]
|
||||||
if proxy.Alive() {
|
if proxy.Alive() {
|
||||||
return proxy
|
return proxy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return proxies[0]
|
return proxies[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) proxies() []C.Proxy {
|
func strategyConsistentHashing() strategyFn {
|
||||||
|
maxRetry := 5
|
||||||
|
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||||
|
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||||
|
buckets := int32(len(proxies))
|
||||||
|
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
||||||
|
idx := jumpHash(key, buckets)
|
||||||
|
proxy := proxies[idx]
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
proxies := lb.proxies(true)
|
||||||
|
return lb.strategyFn(proxies, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := lb.single.Do(func() (interface{}, error) {
|
elm, _, _ := lb.single.Do(func() (interface{}, error) {
|
||||||
return getProvidersProxies(lb.providers), nil
|
return getProvidersProxies(lb.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range lb.proxies() {
|
for _, proxy := range lb.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
@ -115,11 +159,21 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
|
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||||
return &LoadBalance{
|
var strategyFn strategyFn
|
||||||
Base: outbound.NewBase(name, "", C.LoadBalance, false),
|
switch strategy {
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
case "consistent-hashing":
|
||||||
maxRetry: 3,
|
strategyFn = strategyConsistentHashing()
|
||||||
providers: providers,
|
case "round-robin":
|
||||||
|
strategyFn = strategyRoundRobin()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||||
}
|
}
|
||||||
|
return &LoadBalance{
|
||||||
|
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
strategyFn: strategyFn,
|
||||||
|
disableUDP: options.DisableUDP,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -12,25 +12,28 @@ import (
|
|||||||
var (
|
var (
|
||||||
errFormat = errors.New("format error")
|
errFormat = errors.New("format error")
|
||||||
errType = errors.New("unsupport type")
|
errType = errors.New("unsupport type")
|
||||||
errMissUse = errors.New("`use` field should not be empty")
|
|
||||||
errMissProxy = errors.New("`use` or `proxies` missing")
|
errMissProxy = errors.New("`use` or `proxies` missing")
|
||||||
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
||||||
errDuplicateProvider = errors.New("`duplicate provider name")
|
errDuplicateProvider = errors.New("`duplicate provider name")
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupCommonOption struct {
|
type GroupCommonOption struct {
|
||||||
Name string `group:"name"`
|
Name string `group:"name"`
|
||||||
Type string `group:"type"`
|
Type string `group:"type"`
|
||||||
Proxies []string `group:"proxies,omitempty"`
|
Proxies []string `group:"proxies,omitempty"`
|
||||||
Use []string `group:"use,omitempty"`
|
Use []string `group:"use,omitempty"`
|
||||||
URL string `group:"url,omitempty"`
|
URL string `group:"url,omitempty"`
|
||||||
Interval int `group:"interval,omitempty"`
|
Interval int `group:"interval,omitempty"`
|
||||||
|
Lazy bool `group:"lazy,omitempty"`
|
||||||
|
DisableUDP bool `group:"disable-udp,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
|
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
||||||
|
|
||||||
groupOption := &GroupCommonOption{}
|
groupOption := &GroupCommonOption{
|
||||||
|
Lazy: true,
|
||||||
|
}
|
||||||
if err := decoder.Decode(config, groupOption); err != nil {
|
if err := decoder.Decode(config, groupOption); err != nil {
|
||||||
return nil, errFormat
|
return nil, errFormat
|
||||||
}
|
}
|
||||||
@ -55,7 +58,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
|
|
||||||
// if Use not empty, drop health check options
|
// if Use not empty, drop health check options
|
||||||
if len(groupOption.Use) != 0 {
|
if len(groupOption.Use) != 0 {
|
||||||
hc := provider.NewHealthCheck(ps, "", 0)
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -63,9 +66,13 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
|
|
||||||
providers = append(providers, pd)
|
providers = append(providers, pd)
|
||||||
} else {
|
} else {
|
||||||
|
if _, ok := providersMap[groupName]; ok {
|
||||||
|
return nil, errDuplicateProvider
|
||||||
|
}
|
||||||
|
|
||||||
// select don't need health check
|
// select don't need health check
|
||||||
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
||||||
hc := provider.NewHealthCheck(ps, "", 0)
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -78,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
return nil, errMissHealthCheck
|
return nil, errMissHealthCheck
|
||||||
}
|
}
|
||||||
|
|
||||||
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval))
|
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -101,15 +108,17 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
var group C.ProxyAdapter
|
var group C.ProxyAdapter
|
||||||
switch groupOption.Type {
|
switch groupOption.Type {
|
||||||
case "url-test":
|
case "url-test":
|
||||||
group = NewURLTest(groupName, providers)
|
opts := parseURLTestOption(config)
|
||||||
|
group = NewURLTest(groupOption, providers, opts...)
|
||||||
case "select":
|
case "select":
|
||||||
group = NewSelector(groupName, providers)
|
group = NewSelector(groupOption, providers)
|
||||||
case "fallback":
|
case "fallback":
|
||||||
group = NewFallback(groupName, providers)
|
group = NewFallback(groupOption, providers)
|
||||||
case "load-balance":
|
case "load-balance":
|
||||||
group = NewLoadBalance(groupName, providers)
|
strategy := parseStrategy(config)
|
||||||
|
return NewLoadBalance(groupOption, providers, strategy)
|
||||||
case "relay":
|
case "relay":
|
||||||
group = NewRelay(groupName, providers)
|
group = NewRelay(groupOption, providers)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
||||||
}
|
}
|
||||||
|
@ -19,10 +19,11 @@ type Relay struct {
|
|||||||
providers []provider.ProxyProvider
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
proxies := r.proxies(metadata)
|
proxies := r.proxies(metadata, true)
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return nil, errors.New("Proxy does not exist")
|
return nil, errors.New("proxy does not exist")
|
||||||
}
|
}
|
||||||
first := proxies[0]
|
first := proxies[0]
|
||||||
last := proxies[len(proxies)-1]
|
last := proxies[len(proxies)-1]
|
||||||
@ -56,9 +57,10 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
return outbound.NewConn(c, r), nil
|
return outbound.NewConn(c, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (r *Relay) MarshalJSON() ([]byte, error) {
|
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range r.rawProxies() {
|
for _, proxy := range r.rawProxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
@ -67,16 +69,16 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) rawProxies() []C.Proxy {
|
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
||||||
return getProvidersProxies(r.providers), nil
|
return getProvidersProxies(r.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
|
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||||
proxies := r.rawProxies()
|
proxies := r.rawProxies(touch)
|
||||||
|
|
||||||
for n, proxy := range proxies {
|
for n, proxy := range proxies {
|
||||||
subproxy := proxy.Unwrap(metadata)
|
subproxy := proxy.Unwrap(metadata)
|
||||||
@ -89,9 +91,9 @@ func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
|
|||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelay(name string, providers []provider.ProxyProvider) *Relay {
|
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||||
return &Relay{
|
return &Relay{
|
||||||
Base: outbound.NewBase(name, "", C.Relay, false),
|
Base: outbound.NewBase(options.Name, "", C.Relay, false),
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
}
|
}
|
||||||
|
@ -13,34 +13,43 @@ import (
|
|||||||
|
|
||||||
type Selector struct {
|
type Selector struct {
|
||||||
*outbound.Base
|
*outbound.Base
|
||||||
single *singledo.Single
|
disableUDP bool
|
||||||
selected string
|
single *singledo.Single
|
||||||
providers []provider.ProxyProvider
|
selected string
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
c, err := s.selectedProxy().DialContext(ctx, metadata)
|
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(s)
|
c.AppendToChains(s)
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := s.selectedProxy().DialUDP(metadata)
|
pc, err := s.selectedProxy(true).DialUDP(metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(s)
|
pc.AppendToChains(s)
|
||||||
}
|
}
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (s *Selector) SupportUDP() bool {
|
func (s *Selector) SupportUDP() bool {
|
||||||
return s.selectedProxy().SupportUDP()
|
if s.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.selectedProxy(false).SupportUDP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range getProvidersProxies(s.providers) {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,11 +61,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Now() string {
|
func (s *Selector) Now() string {
|
||||||
return s.selectedProxy().Name()
|
return s.selectedProxy(false).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Set(name string) error {
|
func (s *Selector) Set(name string) error {
|
||||||
for _, proxy := range getProvidersProxies(s.providers) {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
if proxy.Name() == name {
|
if proxy.Name() == name {
|
||||||
s.selected = name
|
s.selected = name
|
||||||
s.single.Reset()
|
s.single.Reset()
|
||||||
@ -64,16 +73,17 @@ func (s *Selector) Set(name string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("Proxy does not exist")
|
return errors.New("proxy not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return s.selectedProxy()
|
return s.selectedProxy(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) selectedProxy() C.Proxy {
|
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||||
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
||||||
proxies := getProvidersProxies(s.providers)
|
proxies := getProvidersProxies(s.providers, touch)
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if proxy.Name() == s.selected {
|
if proxy.Name() == s.selected {
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
@ -86,12 +96,13 @@ func (s *Selector) selectedProxy() C.Proxy {
|
|||||||
return elm.(C.Proxy)
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSelector(name string, providers []provider.ProxyProvider) *Selector {
|
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||||
selected := providers[0].Proxies()[0].Name()
|
selected := providers[0].Proxies()[0].Name()
|
||||||
return &Selector{
|
return &Selector{
|
||||||
Base: outbound.NewBase(name, "", C.Selector, false),
|
Base: outbound.NewBase(options.Name, "", C.Selector, false),
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
|
disableUDP: options.DisableUDP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,51 +11,71 @@ import (
|
|||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type urlTestOption func(*URLTest)
|
||||||
|
|
||||||
|
func urlTestWithTolerance(tolerance uint16) urlTestOption {
|
||||||
|
return func(u *URLTest) {
|
||||||
|
u.tolerance = tolerance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type URLTest struct {
|
type URLTest struct {
|
||||||
*outbound.Base
|
*outbound.Base
|
||||||
|
tolerance uint16
|
||||||
|
disableUDP bool
|
||||||
|
fastNode C.Proxy
|
||||||
single *singledo.Single
|
single *singledo.Single
|
||||||
fastSingle *singledo.Single
|
fastSingle *singledo.Single
|
||||||
providers []provider.ProxyProvider
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) Now() string {
|
func (u *URLTest) Now() string {
|
||||||
return u.fast().Name()
|
return u.fast(false).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||||
c, err = u.fast().DialContext(ctx, metadata)
|
c, err = u.fast(true).DialContext(ctx, metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(u)
|
c.AppendToChains(u)
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := u.fast().DialUDP(metadata)
|
pc, err := u.fast(true).DialUDP(metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(u)
|
pc.AppendToChains(u)
|
||||||
}
|
}
|
||||||
return pc, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
return u.fast()
|
return u.fast(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) proxies() []C.Proxy {
|
func (u *URLTest) proxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := u.single.Do(func() (interface{}, error) {
|
elm, _, _ := u.single.Do(func() (interface{}, error) {
|
||||||
return getProvidersProxies(u.providers), nil
|
return getProvidersProxies(u.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) fast() C.Proxy {
|
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
|
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
|
||||||
proxies := u.proxies()
|
proxies := u.proxies(touch)
|
||||||
fast := proxies[0]
|
fast := proxies[0]
|
||||||
min := fast.LastDelay()
|
min := fast.LastDelay()
|
||||||
|
fastNotExist := true
|
||||||
|
|
||||||
for _, proxy := range proxies[1:] {
|
for _, proxy := range proxies[1:] {
|
||||||
|
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
|
||||||
|
fastNotExist = false
|
||||||
|
}
|
||||||
|
|
||||||
if !proxy.Alive() {
|
if !proxy.Alive() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -66,19 +86,31 @@ func (u *URLTest) fast() C.Proxy {
|
|||||||
min = delay
|
min = delay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fast, nil
|
|
||||||
|
// tolerance
|
||||||
|
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
|
||||||
|
u.fastNode = fast
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fastNode, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.(C.Proxy)
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
func (u *URLTest) SupportUDP() bool {
|
func (u *URLTest) SupportUDP() bool {
|
||||||
return u.fast().SupportUDP()
|
if u.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fast(false).SupportUDP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range u.proxies() {
|
for _, proxy := range u.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
@ -88,11 +120,31 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewURLTest(name string, providers []provider.ProxyProvider) *URLTest {
|
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
|
||||||
return &URLTest{
|
opts := []urlTestOption{}
|
||||||
Base: outbound.NewBase(name, "", C.URLTest, false),
|
|
||||||
|
// tolerance
|
||||||
|
if elm, ok := config["tolerance"]; ok {
|
||||||
|
if tolerance, ok := elm.(int); ok {
|
||||||
|
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||||
|
urlTest := &URLTest{
|
||||||
|
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
fastSingle: singledo.NewSingle(time.Second * 10),
|
fastSingle: singledo.NewSingle(time.Second * 10),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
|
disableUDP: commonOptions.DisableUDP,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(urlTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlTest
|
||||||
}
|
}
|
||||||
|
@ -16,25 +16,7 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip == nil {
|
||||||
if ip.To4() != nil {
|
|
||||||
addr = &C.Metadata{
|
|
||||||
AddrType: C.AtypIPv4,
|
|
||||||
Host: "",
|
|
||||||
DstIP: ip,
|
|
||||||
DstPort: port,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
addr = &C.Metadata{
|
|
||||||
AddrType: C.AtypIPv6,
|
|
||||||
Host: "",
|
|
||||||
DstIP: ip,
|
|
||||||
DstPort: port,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypDomainName,
|
AddrType: C.AtypDomainName,
|
||||||
Host: host,
|
Host: host,
|
||||||
@ -42,7 +24,23 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
DstPort: port,
|
DstPort: port,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
} else if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
addr = &C.Metadata{
|
||||||
|
AddrType: C.AtypIPv4,
|
||||||
|
Host: "",
|
||||||
|
DstIP: ip4,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr = &C.Metadata{
|
||||||
|
AddrType: C.AtypIPv6,
|
||||||
|
Host: "",
|
||||||
|
DstIP: ip,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
func tcpKeepAlive(c net.Conn) {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
@ -12,6 +13,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
fileMode os.FileMode = 0666
|
fileMode os.FileMode = 0666
|
||||||
|
dirMode os.FileMode = 0755
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser = func([]byte) (interface{}, error)
|
type parser = func([]byte) (interface{}, error)
|
||||||
@ -21,6 +23,7 @@ type fetcher struct {
|
|||||||
vehicle Vehicle
|
vehicle Vehicle
|
||||||
updatedAt *time.Time
|
updatedAt *time.Time
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
hash [16]byte
|
hash [16]byte
|
||||||
parser parser
|
parser parser
|
||||||
onUpdate func(interface{})
|
onUpdate func(interface{})
|
||||||
@ -35,10 +38,12 @@ func (f *fetcher) VehicleType() VehicleType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Initial() (interface{}, error) {
|
func (f *fetcher) Initial() (interface{}, error) {
|
||||||
var buf []byte
|
var (
|
||||||
var err error
|
buf []byte
|
||||||
var isLocal bool
|
err error
|
||||||
if stat, err := os.Stat(f.vehicle.Path()); err == nil {
|
isLocal bool
|
||||||
|
)
|
||||||
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
||||||
modTime := stat.ModTime()
|
modTime := stat.ModTime()
|
||||||
f.updatedAt = &modTime
|
f.updatedAt = &modTime
|
||||||
@ -67,10 +72,14 @@ func (f *fetcher) Initial() (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLocal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
|
if f.vehicle.Type() != File && !isLocal {
|
||||||
return nil, err
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.hash = md5.Sum(buf)
|
f.hash = md5.Sum(buf)
|
||||||
@ -101,8 +110,10 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
|
if f.vehicle.Type() != File {
|
||||||
return nil, false, err
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.updatedAt = &now
|
f.updatedAt = &now
|
||||||
@ -113,31 +124,49 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
|
|
||||||
func (f *fetcher) Destroy() error {
|
func (f *fetcher) Destroy() error {
|
||||||
if f.ticker != nil {
|
if f.ticker != nil {
|
||||||
f.ticker.Stop()
|
f.done <- struct{}{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) pullLoop() {
|
func (f *fetcher) pullLoop() {
|
||||||
for range f.ticker.C {
|
for {
|
||||||
elm, same, err := f.Update()
|
select {
|
||||||
if err != nil {
|
case <-f.ticker.C:
|
||||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
elm, same, err := f.Update()
|
||||||
continue
|
if err != nil {
|
||||||
}
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if same {
|
if same {
|
||||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
if f.onUpdate != nil {
|
if f.onUpdate != nil {
|
||||||
f.onUpdate(elm)
|
f.onUpdate(elm)
|
||||||
|
}
|
||||||
|
case <-f.done:
|
||||||
|
f.ticker.Stop()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func safeWrite(path string, buf []byte) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(dir, dirMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(path, buf, fileMode)
|
||||||
|
}
|
||||||
|
|
||||||
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
|
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
|
||||||
var ticker *time.Ticker
|
var ticker *time.Ticker
|
||||||
if interval != 0 {
|
if interval != 0 {
|
||||||
@ -149,6 +178,7 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
|
|||||||
ticker: ticker,
|
ticker: ticker,
|
||||||
vehicle: vehicle,
|
vehicle: vehicle,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,12 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -17,10 +20,12 @@ type HealthCheckOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HealthCheck struct {
|
type HealthCheck struct {
|
||||||
url string
|
url string
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
interval uint
|
interval uint
|
||||||
done chan struct{}
|
lazy bool
|
||||||
|
lastTouch *atomic.Int64
|
||||||
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) process() {
|
func (hc *HealthCheck) process() {
|
||||||
@ -30,7 +35,10 @@ func (hc *HealthCheck) process() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
hc.check()
|
now := time.Now().Unix()
|
||||||
|
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||||
|
hc.check()
|
||||||
|
}
|
||||||
case <-hc.done:
|
case <-hc.done:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
return
|
return
|
||||||
@ -46,13 +54,24 @@ func (hc *HealthCheck) auto() bool {
|
|||||||
return hc.interval != 0
|
return hc.interval != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) touch() {
|
||||||
|
hc.lastTouch.Store(time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
func (hc *HealthCheck) check() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
|
||||||
for _, proxy := range hc.proxies {
|
for _, proxy := range hc.proxies {
|
||||||
go proxy.URLTest(ctx, hc.url)
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(p C.Proxy) {
|
||||||
|
p.URLTest(ctx, hc.url)
|
||||||
|
wg.Done()
|
||||||
|
}(proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
<-ctx.Done()
|
wg.Wait()
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,11 +79,13 @@ func (hc *HealthCheck) close() {
|
|||||||
hc.done <- struct{}{}
|
hc.done <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck {
|
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
|
||||||
return &HealthCheck{
|
return &HealthCheck{
|
||||||
proxies: proxies,
|
proxies: proxies,
|
||||||
url: url,
|
url: url,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
done: make(chan struct{}, 1),
|
lazy: lazy,
|
||||||
|
lastTouch: atomic.NewInt64(0),
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ type healthCheckSchema struct {
|
|||||||
Enable bool `provider:"enable"`
|
Enable bool `provider:"enable"`
|
||||||
URL string `provider:"url"`
|
URL string `provider:"url"`
|
||||||
Interval int `provider:"interval"`
|
Interval int `provider:"interval"`
|
||||||
|
Lazy bool `provider:"lazy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxyProviderSchema struct {
|
type proxyProviderSchema struct {
|
||||||
@ -30,16 +31,20 @@ type proxyProviderSchema struct {
|
|||||||
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
|
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||||
|
|
||||||
schema := &proxyProviderSchema{}
|
schema := &proxyProviderSchema{
|
||||||
|
HealthCheck: healthCheckSchema{
|
||||||
|
Lazy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
if err := decoder.Decode(mapping, schema); err != nil {
|
if err := decoder.Decode(mapping, schema); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var hcInterval uint = 0
|
var hcInterval uint
|
||||||
if schema.HealthCheck.Enable {
|
if schema.HealthCheck.Enable {
|
||||||
hcInterval = uint(schema.HealthCheck.Interval)
|
hcInterval = uint(schema.HealthCheck.Interval)
|
||||||
}
|
}
|
||||||
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval)
|
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
|
||||||
|
|
||||||
path := C.Path.Resolve(schema.Path)
|
path := C.Path.Resolve(schema.Path)
|
||||||
|
|
||||||
|
@ -50,6 +50,9 @@ type Provider interface {
|
|||||||
type ProxyProvider interface {
|
type ProxyProvider interface {
|
||||||
Provider
|
Provider
|
||||||
Proxies() []C.Proxy
|
Proxies() []C.Proxy
|
||||||
|
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
||||||
|
// Commonly used in Dial and DialUDP
|
||||||
|
ProxiesWithTouch() []C.Proxy
|
||||||
HealthCheck()
|
HealthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
|
|||||||
return pp.proxies
|
return pp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
|
pp.healthCheck.touch()
|
||||||
|
return pp.Proxies()
|
||||||
|
}
|
||||||
|
|
||||||
func proxiesParse(buf []byte) (interface{}, error) {
|
func proxiesParse(buf []byte) (interface{}, error) {
|
||||||
schema := &ProxySchema{}
|
schema := &ProxySchema{}
|
||||||
|
|
||||||
@ -120,20 +128,20 @@ func proxiesParse(buf []byte) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if schema.Proxies == nil {
|
if schema.Proxies == nil {
|
||||||
return nil, errors.New("File must have a `proxies` field")
|
return nil, errors.New("file must have a `proxies` field")
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies := []C.Proxy{}
|
proxies := []C.Proxy{}
|
||||||
for idx, mapping := range schema.Proxies {
|
for idx, mapping := range schema.Proxies {
|
||||||
proxy, err := outbound.ParseProxy(mapping)
|
proxy, err := outbound.ParseProxy(mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Proxy %d error: %w", idx, err)
|
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
|
||||||
}
|
}
|
||||||
proxies = append(proxies, proxy)
|
proxies = append(proxies, proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return nil, errors.New("File doesn't have any valid proxy")
|
return nil, errors.New("file doesn't have any valid proxy")
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
@ -142,7 +150,9 @@ func proxiesParse(buf []byte) (interface{}, error) {
|
|||||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||||
pp.proxies = proxies
|
pp.proxies = proxies
|
||||||
pp.healthCheck.setProxy(proxies)
|
pp.healthCheck.setProxy(proxies)
|
||||||
go pp.healthCheck.check()
|
if pp.healthCheck.auto() {
|
||||||
|
go pp.healthCheck.check()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopProxyProvider(pd *ProxySetProvider) {
|
func stopProxyProvider(pd *ProxySetProvider) {
|
||||||
@ -221,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
|
|||||||
return cp.proxies
|
return cp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
|
cp.healthCheck.touch()
|
||||||
|
return cp.Proxies()
|
||||||
|
}
|
||||||
|
|
||||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
pd.healthCheck.close()
|
pd.healthCheck.close()
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(resp.Body)
|
buf, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
21
common/cache/lrucache.go
vendored
21
common/cache/lrucache.go
vendored
@ -121,7 +121,7 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
|
|||||||
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWithExpire stores the interface{} representation of a response for a given key and given exires.
|
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
|
||||||
// The expires time will round to second.
|
// The expires time will round to second.
|
||||||
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@ -146,6 +146,23 @@ func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires tim
|
|||||||
c.maybeDeleteOldest()
|
c.maybeDeleteOldest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloneTo clone and overwrite elements to another LruCache
|
||||||
|
func (c *LruCache) CloneTo(n *LruCache) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
n.lru = list.New()
|
||||||
|
n.cache = make(map[interface{}]*list.Element)
|
||||||
|
|
||||||
|
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||||
|
elm := e.Value.(*entry)
|
||||||
|
n.cache[elm.key] = n.lru.PushBack(elm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *LruCache) get(key interface{}) *entry {
|
func (c *LruCache) get(key interface{}) *entry {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
@ -171,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the value associated with a key.
|
// Delete removes the value associated with a key.
|
||||||
func (c *LruCache) Delete(key string) {
|
func (c *LruCache) Delete(key interface{}) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
|
18
common/cache/lrucache_test.go
vendored
18
common/cache/lrucache_test.go
vendored
@ -164,3 +164,21 @@ func TestStale(t *testing.T) {
|
|||||||
assert.Equal(t, tenSecBefore, expires)
|
assert.Equal(t, tenSecBefore, expires)
|
||||||
assert.Equal(t, true, exist)
|
assert.Equal(t, true, exist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloneTo(t *testing.T) {
|
||||||
|
o := NewLRUCache(WithSize(10))
|
||||||
|
o.Set("1", 1)
|
||||||
|
o.Set("2", 2)
|
||||||
|
|
||||||
|
n := NewLRUCache(WithSize(2))
|
||||||
|
n.Set("3", 3)
|
||||||
|
n.Set("4", 4)
|
||||||
|
|
||||||
|
o.CloneTo(n)
|
||||||
|
|
||||||
|
assert.False(t, n.Exist("3"))
|
||||||
|
assert.True(t, n.Exist("1"))
|
||||||
|
|
||||||
|
n.Set("5", 5)
|
||||||
|
assert.False(t, n.Exist("1"))
|
||||||
|
}
|
||||||
|
11
common/net/io.go
Normal file
11
common/net/io.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type ReadOnlyReader struct {
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriteOnlyWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
package observable
|
package observable
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func iterator(item []interface{}) chan interface{} {
|
func iterator(item []interface{}) chan interface{} {
|
||||||
@ -33,25 +33,25 @@ func TestObservable(t *testing.T) {
|
|||||||
assert.Equal(t, count, 5)
|
assert.Equal(t, count, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_MutilSubscribe(t *testing.T) {
|
func TestObservable_MultiSubscribe(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
ch1, _ := src.Subscribe()
|
ch1, _ := src.Subscribe()
|
||||||
ch2, _ := src.Subscribe()
|
ch2, _ := src.Subscribe()
|
||||||
count := 0
|
var count = atomic.NewInt32(0)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
waitCh := func(ch <-chan interface{}) {
|
waitCh := func(ch <-chan interface{}) {
|
||||||
for range ch {
|
for range ch {
|
||||||
count++
|
count.Inc()
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
go waitCh(ch1)
|
go waitCh(ch1)
|
||||||
go waitCh(ch2)
|
go waitCh(ch2)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Equal(t, count, 10)
|
assert.Equal(t, int32(10), count.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribe(t *testing.T) {
|
func TestObservable_UnSubscribe(t *testing.T) {
|
||||||
@ -82,9 +82,6 @@ func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||||
// waiting for other goroutine recycle
|
|
||||||
time.Sleep(120 * time.Millisecond)
|
|
||||||
init := runtime.NumGoroutine()
|
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
max := 100
|
max := 100
|
||||||
@ -107,6 +104,43 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
go waitCh(ch)
|
go waitCh(ch)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
now := runtime.NumGoroutine()
|
|
||||||
assert.Equal(t, init, now)
|
for _, sub := range list {
|
||||||
|
_, more := <-sub
|
||||||
|
assert.False(t, more)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, more := <-list[0]
|
||||||
|
assert.False(t, more)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Benchmark_Observable_1000(b *testing.B) {
|
||||||
|
ch := make(chan interface{})
|
||||||
|
o := NewObservable(ch)
|
||||||
|
num := 1000
|
||||||
|
|
||||||
|
subs := []Subscription{}
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
sub, _ := o.Subscribe()
|
||||||
|
subs = append(subs, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(num)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for _, sub := range subs {
|
||||||
|
go func(s Subscription) {
|
||||||
|
for range s {
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ch <- i
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ch)
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
@ -2,34 +2,32 @@ package observable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/eapache/channels.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Subscription <-chan interface{}
|
type Subscription <-chan interface{}
|
||||||
|
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
buffer *channels.InfiniteChannel
|
buffer chan interface{}
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Emit(item interface{}) {
|
func (s *Subscriber) Emit(item interface{}) {
|
||||||
s.buffer.In() <- item
|
s.buffer <- item
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Out() Subscription {
|
func (s *Subscriber) Out() Subscription {
|
||||||
return s.buffer.Out()
|
return s.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Close() {
|
func (s *Subscriber) Close() {
|
||||||
s.once.Do(func() {
|
s.once.Do(func() {
|
||||||
s.buffer.Close()
|
close(s.buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSubscriber() *Subscriber {
|
func newSubscriber() *Subscriber {
|
||||||
sub := &Subscriber{
|
sub := &Subscriber{
|
||||||
buffer: channels.NewInfiniteChannel(),
|
buffer: make(chan interface{}, 200),
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
@ -55,11 +55,13 @@ func (alloc *Allocator) Put(buf []byte) error {
|
|||||||
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
||||||
return errors.New("allocator Put() incorrect buffer size")
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//lint:ignore SA6002 ignore temporarily
|
||||||
alloc.buffers[bits].Put(buf)
|
alloc.buffers[bits].Put(buf)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// msb return the pos of most significiant bit
|
// msb return the pos of most significant bit
|
||||||
func msb(size int) uint16 {
|
func msb(size int) uint16 {
|
||||||
return uint16(bits.Len32(uint32(size)) - 1)
|
return uint16(bits.Len32(uint32(size)) - 1)
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,11 @@ func TestAllocGet(t *testing.T) {
|
|||||||
func TestAllocPut(t *testing.T) {
|
func TestAllocPut(t *testing.T) {
|
||||||
alloc := NewAllocator()
|
alloc := NewAllocator()
|
||||||
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
|
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 3, 3)), "put elem:3 []bytes misbehavior")
|
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 4, 4)), "put elem:4 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 65536, 65536)), "put elem:65536 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 65537, 65537)), "put elem:65537 []bytes misbehavior")
|
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocPutThenGet(t *testing.T) {
|
func TestAllocPutThenGet(t *testing.T) {
|
||||||
|
@ -24,6 +24,8 @@ type Result struct {
|
|||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do single.Do likes sync.singleFlight
|
||||||
|
//lint:ignore ST1008 it likes sync.singleFlight
|
||||||
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
|
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -44,9 +46,12 @@ func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, s
|
|||||||
s.mux.Unlock()
|
s.mux.Unlock()
|
||||||
call.val, call.err = fn()
|
call.val, call.err = fn()
|
||||||
call.wg.Done()
|
call.wg.Done()
|
||||||
|
|
||||||
|
s.mux.Lock()
|
||||||
s.call = nil
|
s.call = nil
|
||||||
s.result = &Result{call.val, call.err}
|
s.result = &Result{call.val, call.err}
|
||||||
s.last = now
|
s.last = now
|
||||||
|
s.mux.Unlock()
|
||||||
return call.val, call.err, false
|
return call.val, call.err, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,12 +6,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
shardCount := 0
|
var shardCount = atomic.NewInt32(0)
|
||||||
call := func() (interface{}, error) {
|
call := func() (interface{}, error) {
|
||||||
foo++
|
foo++
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
@ -25,7 +26,7 @@ func TestBasic(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
_, _, shard := single.Do(call)
|
_, _, shard := single.Do(call)
|
||||||
if shard {
|
if shard {
|
||||||
shardCount++
|
shardCount.Inc()
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@ -33,7 +34,7 @@ func TestBasic(t *testing.T) {
|
|||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Equal(t, 1, foo)
|
assert.Equal(t, 1, foo)
|
||||||
assert.Equal(t, 4, shardCount)
|
assert.Equal(t, int32(4), shardCount.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimer(t *testing.T) {
|
func TestTimer(t *testing.T) {
|
||||||
|
118
component/dialer/bind.go
Normal file
118
component/dialer/bind.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In some OS, such as Windows, it takes a little longer to get interface information
|
||||||
|
var ifaceSingle = singledo.NewSingle(time.Second * 20)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errPlatformNotSupport = errors.New("unsupport platform")
|
||||||
|
)
|
||||||
|
|
||||||
|
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
|
||||||
|
ipv4 := ip.To4() != nil
|
||||||
|
|
||||||
|
for _, elm := range addrs {
|
||||||
|
addr, ok := elm.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrV4 := addr.IP.To4() != nil
|
||||||
|
|
||||||
|
if addrV4 && ipv4 {
|
||||||
|
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
||||||
|
} else if !addrV4 && !ipv4 {
|
||||||
|
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrAddrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
|
||||||
|
ipv4 := ip.To4() != nil
|
||||||
|
|
||||||
|
for _, elm := range addrs {
|
||||||
|
addr, ok := elm.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addrV4 := addr.IP.To4() != nil
|
||||||
|
|
||||||
|
if addrV4 && ipv4 {
|
||||||
|
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
||||||
|
} else if !addrV4 && !ipv4 {
|
||||||
|
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrAddrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
|
||||||
|
return net.InterfaceByName(name)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.(*net.Interface).Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch network {
|
||||||
|
case "tcp", "tcp4", "tcp6":
|
||||||
|
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
|
||||||
|
dialer.LocalAddr = addr
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "udp", "udp4", "udp6":
|
||||||
|
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
|
||||||
|
dialer.LocalAddr = addr
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fallbackBindToListenConfig(name string) (string, error) {
|
||||||
|
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
|
||||||
|
return net.InterfaceByName(name)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.(*net.Interface).Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, elm := range addrs {
|
||||||
|
addr, ok := elm.(*net.IPNet)
|
||||||
|
if !ok || addr.IP.To4() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return net.JoinHostPort(addr.IP.String(), "0"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrAddrNotFound
|
||||||
|
}
|
53
component/dialer/bind_darwin.go
Normal file
53
component/dialer/bind_darwin.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||||
|
|
||||||
|
func bindControl(ifaceIdx int) controlFn {
|
||||||
|
return func(network, address string, c syscall.RawConn) error {
|
||||||
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
|
if err == nil {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Control(func(fd uintptr) {
|
||||||
|
switch network {
|
||||||
|
case "tcp4", "udp4":
|
||||||
|
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx)
|
||||||
|
case "tcp6", "udp6":
|
||||||
|
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||||
|
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
|
||||||
|
return net.InterfaceByName(ifaceName)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer.Control = bindControl(iface.(*net.Interface).Index)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||||
|
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
|
||||||
|
return net.InterfaceByName(ifaceName)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.Control = bindControl(iface.(*net.Interface).Index)
|
||||||
|
return nil
|
||||||
|
}
|
36
component/dialer/bind_linux.go
Normal file
36
component/dialer/bind_linux.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||||
|
|
||||||
|
func bindControl(ifaceName string) controlFn {
|
||||||
|
return func(network, address string, c syscall.RawConn) error {
|
||||||
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
|
if err == nil {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Control(func(fd uintptr) {
|
||||||
|
syscall.BindToDevice(int(fd), ifaceName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||||
|
dialer.Control = bindControl(ifaceName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||||
|
lc.Control = bindControl(ifaceName)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
13
component/dialer/bind_others.go
Normal file
13
component/dialer/bind_others.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// +build !linux,!darwin
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||||
|
return errPlatformNotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||||
|
return errPlatformNotSupport
|
||||||
|
}
|
@ -19,17 +19,6 @@ func Dialer() (*net.Dialer, error) {
|
|||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenConfig() (*net.ListenConfig, error) {
|
|
||||||
cfg := &net.ListenConfig{}
|
|
||||||
if ListenConfigHook != nil {
|
|
||||||
if err := ListenConfigHook(cfg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Dial(network, address string) (net.Conn, error) {
|
func Dial(network, address string) (net.Conn, error) {
|
||||||
return DialContext(context.Background(), network, address)
|
return DialContext(context.Background(), network, address)
|
||||||
}
|
}
|
||||||
@ -66,29 +55,26 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|||||||
}
|
}
|
||||||
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||||
case "tcp", "udp":
|
case "tcp", "udp":
|
||||||
return dualStackDailContext(ctx, network, address)
|
return dualStackDialContext(ctx, network, address)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("network invalid")
|
return nil, errors.New("network invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenPacket(network, address string) (net.PacketConn, error) {
|
func ListenPacket(network, address string) (net.PacketConn, error) {
|
||||||
lc, err := ListenConfig()
|
cfg := &net.ListenConfig{}
|
||||||
if err != nil {
|
if ListenPacketHook != nil {
|
||||||
return nil, err
|
var err error
|
||||||
}
|
address, err = ListenPacketHook(cfg, address)
|
||||||
|
|
||||||
if ListenPacketHook != nil && address == "" {
|
|
||||||
ip, err := ListenPacketHook()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
address = net.JoinHostPort(ip.String(), "0")
|
|
||||||
}
|
}
|
||||||
return lc.ListenPacket(context.Background(), network, address)
|
|
||||||
|
return cfg.ListenPacket(context.Background(), network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dualStackDailContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
host, port, err := net.SplitHostPort(address)
|
host, port, err := net.SplitHostPort(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -147,28 +133,27 @@ func dualStackDailContext(ctx context.Context, network, address string) (net.Con
|
|||||||
go startRacer(ctx, network+"4", host, false)
|
go startRacer(ctx, network+"4", host, false)
|
||||||
go startRacer(ctx, network+"6", host, true)
|
go startRacer(ctx, network+"6", host, true)
|
||||||
|
|
||||||
for {
|
for res := range results {
|
||||||
select {
|
if res.error == nil {
|
||||||
case res := <-results:
|
return res.Conn, nil
|
||||||
if res.error == nil {
|
}
|
||||||
return res.Conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !res.ipv6 {
|
if !res.ipv6 {
|
||||||
primary = res
|
primary = res
|
||||||
|
} else {
|
||||||
|
fallback = res
|
||||||
|
}
|
||||||
|
|
||||||
|
if primary.done && fallback.done {
|
||||||
|
if primary.resolved {
|
||||||
|
return nil, primary.error
|
||||||
|
} else if fallback.resolved {
|
||||||
|
return nil, fallback.error
|
||||||
} else {
|
} else {
|
||||||
fallback = res
|
return nil, primary.error
|
||||||
}
|
|
||||||
|
|
||||||
if primary.done && fallback.done {
|
|
||||||
if primary.resolved {
|
|
||||||
return nil, primary.error
|
|
||||||
} else if fallback.resolved {
|
|
||||||
return nil, fallback.error
|
|
||||||
} else {
|
|
||||||
return nil, primary.error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("never touched")
|
||||||
}
|
}
|
||||||
|
@ -3,20 +3,15 @@ package dialer
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DialerHookFunc = func(dialer *net.Dialer) error
|
type DialerHookFunc = func(dialer *net.Dialer) error
|
||||||
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
|
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
|
||||||
type ListenConfigHookFunc = func(*net.ListenConfig) error
|
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
|
||||||
type ListenPacketHookFunc = func() (net.IP, error)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DialerHook DialerHookFunc
|
DialerHook DialerHookFunc
|
||||||
DialHook DialHookFunc
|
DialHook DialHookFunc
|
||||||
ListenConfigHook ListenConfigHookFunc
|
|
||||||
ListenPacketHook ListenPacketHookFunc
|
ListenPacketHook ListenPacketHookFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,124 +20,24 @@ var (
|
|||||||
ErrNetworkNotSupport = errors.New("network not support")
|
ErrNetworkNotSupport = errors.New("network not support")
|
||||||
)
|
)
|
||||||
|
|
||||||
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
|
|
||||||
ipv4 := ip.To4() != nil
|
|
||||||
|
|
||||||
for _, elm := range addrs {
|
|
||||||
addr, ok := elm.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrV4 := addr.IP.To4() != nil
|
|
||||||
|
|
||||||
if addrV4 && ipv4 {
|
|
||||||
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
|
||||||
} else if !addrV4 && !ipv4 {
|
|
||||||
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrAddrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
|
|
||||||
ipv4 := ip.To4() != nil
|
|
||||||
|
|
||||||
for _, elm := range addrs {
|
|
||||||
addr, ok := elm.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrV4 := addr.IP.To4() != nil
|
|
||||||
|
|
||||||
if addrV4 && ipv4 {
|
|
||||||
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
|
||||||
} else if !addrV4 && !ipv4 {
|
|
||||||
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrAddrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
|
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
|
||||||
single := singledo.NewSingle(5 * time.Second)
|
return func(lc *net.ListenConfig, address string) (string, error) {
|
||||||
|
err := bindIfaceToListenConfig(lc, name)
|
||||||
return func() (net.IP, error) {
|
if err == errPlatformNotSupport {
|
||||||
elm, err, _ := single.Do(func() (interface{}, error) {
|
address, err = fallbackBindToListenConfig(name)
|
||||||
iface, err := net.InterfaceByName(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addrs, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs := elm.([]net.Addr)
|
return address, err
|
||||||
|
|
||||||
for _, elm := range addrs {
|
|
||||||
addr, ok := elm.(*net.IPNet)
|
|
||||||
if !ok || addr.IP.To4() == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr.IP, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrAddrNotFound
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DialerWithInterface(name string) DialHookFunc {
|
func DialerWithInterface(name string) DialHookFunc {
|
||||||
single := singledo.NewSingle(5 * time.Second)
|
|
||||||
|
|
||||||
return func(dialer *net.Dialer, network string, ip net.IP) error {
|
return func(dialer *net.Dialer, network string, ip net.IP) error {
|
||||||
elm, err, _ := single.Do(func() (interface{}, error) {
|
err := bindIfaceToDialer(dialer, name)
|
||||||
iface, err := net.InterfaceByName(name)
|
if err == errPlatformNotSupport {
|
||||||
if err != nil {
|
err = fallbackBindToDialer(dialer, network, ip, name)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := iface.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addrs, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs := elm.([]net.Addr)
|
return err
|
||||||
|
|
||||||
switch network {
|
|
||||||
case "tcp", "tcp4", "tcp6":
|
|
||||||
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
|
|
||||||
dialer.LocalAddr = addr
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case "udp", "udp4", "udp6":
|
|
||||||
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
|
|
||||||
dialer.LocalAddr = addr
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pool is a implementation about fake ip generator without storage
|
// Pool is a implementation about fake ip generator without storage
|
||||||
@ -16,7 +16,8 @@ type Pool struct {
|
|||||||
gateway uint32
|
gateway uint32
|
||||||
offset uint32
|
offset uint32
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
host *trie.Trie
|
host *trie.DomainTrie
|
||||||
|
ipnet *net.IPNet
|
||||||
cache *cache.LruCache
|
cache *cache.LruCache
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +90,16 @@ func (p *Pool) Gateway() net.IP {
|
|||||||
return uintToIP(p.gateway)
|
return uintToIP(p.gateway)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPNet return raw ipnet
|
||||||
|
func (p *Pool) IPNet() *net.IPNet {
|
||||||
|
return p.ipnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatchFrom clone cache from old pool
|
||||||
|
func (p *Pool) PatchFrom(o *Pool) {
|
||||||
|
o.cache.CloneTo(p.cache)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pool) get(host string) net.IP {
|
func (p *Pool) get(host string) net.IP {
|
||||||
current := p.offset
|
current := p.offset
|
||||||
for {
|
for {
|
||||||
@ -116,11 +127,11 @@ func ipToUint(ip net.IP) uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func uintToIP(v uint32) net.IP {
|
func uintToIP(v uint32) net.IP {
|
||||||
return net.IPv4(byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
|
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New return Pool instance
|
// New return Pool instance
|
||||||
func New(ipnet *net.IPNet, size int, host *trie.Trie) (*Pool, error) {
|
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
|
||||||
min := ipToUint(ipnet.IP) + 2
|
min := ipToUint(ipnet.IP) + 2
|
||||||
|
|
||||||
ones, bits := ipnet.Mask.Size()
|
ones, bits := ipnet.Mask.Size()
|
||||||
@ -136,6 +147,7 @@ func New(ipnet *net.IPNet, size int, host *trie.Trie) (*Pool, error) {
|
|||||||
max: max,
|
max: max,
|
||||||
gateway: min - 1,
|
gateway: min - 1,
|
||||||
host: host,
|
host: host,
|
||||||
|
ipnet: ipnet,
|
||||||
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
|
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
239
component/gun/gun.go
Normal file
239
component/gun/gun.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
// Modified from: https://github.com/Qv2ray/gun-lite
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
package gun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidLength = errors.New("invalid length")
|
||||||
|
ErrSmallBuffer = errors.New("buffer too small")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultHeader = http.Header{
|
||||||
|
"content-type": []string{"application/grpc"},
|
||||||
|
"user-agent": []string{"grpc-go/1.36.0"},
|
||||||
|
}
|
||||||
|
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
||||||
|
)
|
||||||
|
|
||||||
|
type DialFn = func(network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
response *http.Response
|
||||||
|
request *http.Request
|
||||||
|
transport *http2.Transport
|
||||||
|
writer *io.PipeWriter
|
||||||
|
once sync.Once
|
||||||
|
close *atomic.Bool
|
||||||
|
err error
|
||||||
|
remain int
|
||||||
|
br *bufio.Reader
|
||||||
|
|
||||||
|
// deadlines
|
||||||
|
deadline *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ServiceName string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Conn) initRequest() {
|
||||||
|
response, err := g.transport.RoundTrip(g.request)
|
||||||
|
if err != nil {
|
||||||
|
g.err = err
|
||||||
|
g.writer.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !g.close.Load() {
|
||||||
|
g.response = response
|
||||||
|
g.br = bufio.NewReader(response.Body)
|
||||||
|
} else {
|
||||||
|
response.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Conn) Read(b []byte) (n int, err error) {
|
||||||
|
g.once.Do(g.initRequest)
|
||||||
|
if g.err != nil {
|
||||||
|
return 0, g.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.remain > 0 {
|
||||||
|
size := g.remain
|
||||||
|
if len(b) < size {
|
||||||
|
size = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = io.ReadFull(g.br, b[:size])
|
||||||
|
g.remain -= n
|
||||||
|
return
|
||||||
|
} else if g.response == nil {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0x00 grpclength(uint32) 0x0A uleb128 payload
|
||||||
|
_, err = g.br.Discard(6)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
protobufPayloadLen, err := binary.ReadUvarint(g.br)
|
||||||
|
if err != nil {
|
||||||
|
return 0, ErrInvalidLength
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int(protobufPayloadLen)
|
||||||
|
if len(b) < size {
|
||||||
|
size = len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = io.ReadFull(g.br, b[:size])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remain := int(protobufPayloadLen) - n
|
||||||
|
if remain > 0 {
|
||||||
|
g.remain = remain
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Conn) Write(b []byte) (n int, err error) {
|
||||||
|
protobufHeader := [binary.MaxVarintLen64 + 1]byte{0x0A}
|
||||||
|
varuintSize := binary.PutUvarint(protobufHeader[1:], uint64(len(b)))
|
||||||
|
grpcHeader := make([]byte, 5)
|
||||||
|
grpcPayloadLen := uint32(varuintSize + 1 + len(b))
|
||||||
|
binary.BigEndian.PutUint32(grpcHeader[1:5], grpcPayloadLen)
|
||||||
|
|
||||||
|
buf := bufferPool.Get().(*bytes.Buffer)
|
||||||
|
defer bufferPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
buf.Write(grpcHeader)
|
||||||
|
buf.Write(protobufHeader[:varuintSize+1])
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
_, err = g.writer.Write(buf.Bytes())
|
||||||
|
if err == io.ErrClosedPipe && g.err != nil {
|
||||||
|
err = g.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Conn) Close() error {
|
||||||
|
g.close.Store(true)
|
||||||
|
if r := g.response; r != nil {
|
||||||
|
r.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.writer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Conn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
|
||||||
|
func (g *Conn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4zero, Port: 0} }
|
||||||
|
func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) }
|
||||||
|
func (g *Conn) SetWriteDeadline(t time.Time) error { return g.SetDeadline(t) }
|
||||||
|
|
||||||
|
func (g *Conn) SetDeadline(t time.Time) error {
|
||||||
|
d := time.Until(t)
|
||||||
|
if g.deadline != nil {
|
||||||
|
g.deadline.Reset(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
g.deadline = time.AfterFunc(d, func() {
|
||||||
|
g.Close()
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config) *http2.Transport {
|
||||||
|
dialFunc := func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
pconn, err := dialFn(network, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cn := tls.Client(pconn, cfg)
|
||||||
|
if err := cn.Handshake(); err != nil {
|
||||||
|
pconn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
state := cn.ConnectionState()
|
||||||
|
if p := state.NegotiatedProtocol; p != http2.NextProtoTLS {
|
||||||
|
cn.Close()
|
||||||
|
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http2.NextProtoTLS)
|
||||||
|
}
|
||||||
|
return cn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http2.Transport{
|
||||||
|
DialTLS: dialFunc,
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
AllowHTTP: false,
|
||||||
|
DisableCompression: true,
|
||||||
|
PingTimeout: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamGunWithTransport(transport *http2.Transport, cfg *Config) (net.Conn, error) {
|
||||||
|
serviceName := "GunService"
|
||||||
|
if cfg.ServiceName != "" {
|
||||||
|
serviceName = cfg.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, writer := io.Pipe()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Body: reader,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: cfg.Host,
|
||||||
|
Path: fmt.Sprintf("/%s/Tun", serviceName),
|
||||||
|
},
|
||||||
|
Proto: "HTTP/2",
|
||||||
|
ProtoMajor: 2,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Header: defaultHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := &Conn{
|
||||||
|
request: request,
|
||||||
|
transport: transport,
|
||||||
|
writer: writer,
|
||||||
|
close: atomic.NewBool(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
go conn.once.Do(conn.initRequest)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config) (net.Conn, error) {
|
||||||
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
return StreamGunWithTransport(transport, cfg)
|
||||||
|
}
|
@ -22,9 +22,9 @@ func (t *Table) Get(key string) C.PacketConn {
|
|||||||
return item.(C.PacketConn)
|
return item.(C.PacketConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) GetOrCreateLock(key string) (*sync.WaitGroup, bool) {
|
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
|
||||||
item, loaded := t.mapping.LoadOrStore(key, &sync.WaitGroup{})
|
item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
|
||||||
return item.(*sync.WaitGroup), loaded
|
return item.(*sync.Cond), loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) Delete(key string) {
|
func (t *Table) Delete(key string) {
|
||||||
|
114
component/pool/pool.go
Normal file
114
component/pool/pool.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Factory = func(context.Context) (interface{}, error)
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
elm interface{}
|
||||||
|
time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*pool)
|
||||||
|
|
||||||
|
// WithEvict set the evict callback
|
||||||
|
func WithEvict(cb func(interface{})) Option {
|
||||||
|
return func(p *pool) {
|
||||||
|
p.evict = cb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAge defined element max age (millisecond)
|
||||||
|
func WithAge(maxAge int64) Option {
|
||||||
|
return func(p *pool) {
|
||||||
|
p.maxAge = maxAge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSize defined max size of Pool
|
||||||
|
func WithSize(maxSize int) Option {
|
||||||
|
return func(p *pool) {
|
||||||
|
p.ch = make(chan interface{}, maxSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool is for GC, see New for detail
|
||||||
|
type Pool struct {
|
||||||
|
*pool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pool struct {
|
||||||
|
ch chan interface{}
|
||||||
|
factory Factory
|
||||||
|
evict func(interface{})
|
||||||
|
maxAge int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
|
||||||
|
now := time.Now()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case item := <-p.ch:
|
||||||
|
elm := item.(*entry)
|
||||||
|
if p.maxAge != 0 && now.Sub(item.(*entry).time).Milliseconds() > p.maxAge {
|
||||||
|
if p.evict != nil {
|
||||||
|
p.evict(elm.elm)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return elm.elm, nil
|
||||||
|
default:
|
||||||
|
return p.factory(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) Get() (interface{}, error) {
|
||||||
|
return p.GetContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) Put(item interface{}) {
|
||||||
|
e := &entry{
|
||||||
|
elm: item,
|
||||||
|
time: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case p.ch <- e:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// pool is full
|
||||||
|
if p.evict != nil {
|
||||||
|
p.evict(item)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recycle(p *Pool) {
|
||||||
|
for item := range p.pool.ch {
|
||||||
|
if p.pool.evict != nil {
|
||||||
|
p.pool.evict(item.(*entry).elm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(factory Factory, options ...Option) *Pool {
|
||||||
|
p := &pool{
|
||||||
|
ch: make(chan interface{}, 10),
|
||||||
|
factory: factory,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
P := &Pool{p}
|
||||||
|
runtime.SetFinalizer(P, recycle)
|
||||||
|
return P
|
||||||
|
}
|
73
component/pool/pool_test.go
Normal file
73
component/pool/pool_test.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lg() Factory {
|
||||||
|
initial := -1
|
||||||
|
return func(context.Context) (interface{}, error) {
|
||||||
|
initial++
|
||||||
|
return initial, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPool_Basic(t *testing.T) {
|
||||||
|
g := lg()
|
||||||
|
pool := New(g)
|
||||||
|
|
||||||
|
elm, _ := pool.Get()
|
||||||
|
assert.Equal(t, 0, elm.(int))
|
||||||
|
pool.Put(elm)
|
||||||
|
elm, _ = pool.Get()
|
||||||
|
assert.Equal(t, 0, elm.(int))
|
||||||
|
elm, _ = pool.Get()
|
||||||
|
assert.Equal(t, 1, elm.(int))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPool_MaxSize(t *testing.T) {
|
||||||
|
g := lg()
|
||||||
|
size := 5
|
||||||
|
pool := New(g, WithSize(size))
|
||||||
|
|
||||||
|
items := []interface{}{}
|
||||||
|
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
item, _ := pool.Get()
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
extra, _ := pool.Get()
|
||||||
|
assert.Equal(t, size, extra.(int))
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
pool.Put(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Put(extra)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
elm, _ := pool.Get()
|
||||||
|
assert.Equal(t, item.(int), elm.(int))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPool_MaxAge(t *testing.T) {
|
||||||
|
g := lg()
|
||||||
|
pool := New(g, WithAge(20))
|
||||||
|
|
||||||
|
elm, _ := pool.Get()
|
||||||
|
pool.Put(elm)
|
||||||
|
|
||||||
|
elm, _ = pool.Get()
|
||||||
|
assert.Equal(t, 0, elm.(int))
|
||||||
|
pool.Put(elm)
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 22)
|
||||||
|
elm, _ = pool.Get()
|
||||||
|
assert.Equal(t, 1, elm.(int))
|
||||||
|
}
|
21
component/process/process.go
Normal file
21
component/process/process.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidNetwork = errors.New("invalid network")
|
||||||
|
ErrPlatformNotSupport = errors.New("not support on this platform")
|
||||||
|
ErrNotFound = errors.New("process not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TCP = "tcp"
|
||||||
|
UDP = "udp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
|
||||||
|
return findProcessName(network, srcIP, srcPort)
|
||||||
|
}
|
107
component/process/process_darwin.go
Normal file
107
component/process/process_darwin.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
procpidpathinfo = 0xb
|
||||||
|
procpidpathinfosize = 1024
|
||||||
|
proccallnumpidinfo = 0x2
|
||||||
|
)
|
||||||
|
|
||||||
|
func findProcessName(network string, ip net.IP, port int) (string, error) {
|
||||||
|
var spath string
|
||||||
|
switch network {
|
||||||
|
case TCP:
|
||||||
|
spath = "net.inet.tcp.pcblist_n"
|
||||||
|
case UDP:
|
||||||
|
spath = "net.inet.udp.pcblist_n"
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
isIPv4 := ip.To4() != nil
|
||||||
|
|
||||||
|
value, err := syscall.Sysctl(spath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := []byte(value)
|
||||||
|
|
||||||
|
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||||
|
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||||
|
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||||
|
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||||
|
itemSize := 384
|
||||||
|
if network == TCP {
|
||||||
|
// rup8(sizeof(xtcpcb_n))
|
||||||
|
itemSize += 208
|
||||||
|
}
|
||||||
|
// skip the first xinpgen(24 bytes) block
|
||||||
|
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||||
|
// offset of xinpcb_n and xsocket_n
|
||||||
|
inp, so := i, i+104
|
||||||
|
|
||||||
|
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
||||||
|
if uint16(port) != srcPort {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// xinpcb_n.inp_vflag
|
||||||
|
flag := buf[inp+44]
|
||||||
|
|
||||||
|
var srcIP net.IP
|
||||||
|
switch {
|
||||||
|
case flag&0x1 > 0 && isIPv4:
|
||||||
|
// ipv4
|
||||||
|
srcIP = net.IP(buf[inp+76 : inp+80])
|
||||||
|
case flag&0x2 > 0 && !isIPv4:
|
||||||
|
// ipv6
|
||||||
|
srcIP = net.IP(buf[inp+64 : inp+80])
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ip.Equal(srcIP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// xsocket_n.so_last_pid
|
||||||
|
pid := readNativeUint32(buf[so+68 : so+72])
|
||||||
|
return getExecPathFromPID(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
|
buf := make([]byte, procpidpathinfosize)
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_PROC_INFO,
|
||||||
|
proccallnumpidinfo,
|
||||||
|
uintptr(pid),
|
||||||
|
procpidpathinfo,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
procpidpathinfosize)
|
||||||
|
if errno != 0 {
|
||||||
|
return "", errno
|
||||||
|
}
|
||||||
|
firstZero := bytes.IndexByte(buf, 0)
|
||||||
|
if firstZero <= 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Base(string(buf[:firstZero])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNativeUint32(b []byte) uint32 {
|
||||||
|
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||||
|
}
|
234
component/process/process_freebsd_amd64.go
Normal file
234
component/process/process_freebsd_amd64.go
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// store process name for when dealing with multiple PROCESS-NAME rules
|
||||||
|
var (
|
||||||
|
defaultSearcher *searcher
|
||||||
|
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
||||||
|
once.Do(func() {
|
||||||
|
if err := initSearcher(); err != nil {
|
||||||
|
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
|
||||||
|
log.Warnln("All PROCESS-NAME rules will be skipped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if defaultSearcher == nil {
|
||||||
|
return "", ErrPlatformNotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
var spath string
|
||||||
|
isTCP := network == TCP
|
||||||
|
switch network {
|
||||||
|
case TCP:
|
||||||
|
spath = "net.inet.tcp.pcblist"
|
||||||
|
case UDP:
|
||||||
|
spath = "net.inet.udp.pcblist"
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := syscall.Sysctl(spath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := []byte(value)
|
||||||
|
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return getExecPathFromPID(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
size := uint64(len(buf))
|
||||||
|
// CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid
|
||||||
|
mib := [4]uint32{1, 14, 12, pid}
|
||||||
|
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS___SYSCTL,
|
||||||
|
uintptr(unsafe.Pointer(&mib[0])),
|
||||||
|
uintptr(len(mib)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0,
|
||||||
|
0)
|
||||||
|
if errno != 0 || size == 0 {
|
||||||
|
return "", errno
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Base(string(buf[:size-1])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNativeUint32(b []byte) uint32 {
|
||||||
|
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
type searcher struct {
|
||||||
|
// sizeof(struct xinpgen)
|
||||||
|
headSize int
|
||||||
|
// sizeof(struct xtcpcb)
|
||||||
|
tcpItemSize int
|
||||||
|
// sizeof(struct xinpcb)
|
||||||
|
udpItemSize int
|
||||||
|
udpInpOffset int
|
||||||
|
port int
|
||||||
|
ip int
|
||||||
|
vflag int
|
||||||
|
socket int
|
||||||
|
|
||||||
|
// sizeof(struct xfile)
|
||||||
|
fileItemSize int
|
||||||
|
data int
|
||||||
|
pid int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
|
||||||
|
var itemSize int
|
||||||
|
var inpOffset int
|
||||||
|
|
||||||
|
if isTCP {
|
||||||
|
// struct xtcpcb
|
||||||
|
itemSize = s.tcpItemSize
|
||||||
|
inpOffset = 8
|
||||||
|
} else {
|
||||||
|
// struct xinpcb
|
||||||
|
itemSize = s.udpItemSize
|
||||||
|
inpOffset = s.udpInpOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
isIPv4 := ip.To4() != nil
|
||||||
|
// skip the first xinpgen block
|
||||||
|
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
|
||||||
|
inp := i + inpOffset
|
||||||
|
|
||||||
|
srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2])
|
||||||
|
|
||||||
|
if port != srcPort {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// xinpcb.inp_vflag
|
||||||
|
flag := buf[inp+s.vflag]
|
||||||
|
|
||||||
|
var srcIP net.IP
|
||||||
|
switch {
|
||||||
|
case flag&0x1 > 0 && isIPv4:
|
||||||
|
// ipv4
|
||||||
|
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
|
||||||
|
case flag&0x2 > 0 && !isIPv4:
|
||||||
|
// ipv6
|
||||||
|
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ip.Equal(srcIP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison
|
||||||
|
socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8])
|
||||||
|
return s.searchSocketPid(socket)
|
||||||
|
}
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searcher) searchSocketPid(socket uint64) (uint32, error) {
|
||||||
|
value, err := syscall.Sysctl("kern.file")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := []byte(value)
|
||||||
|
|
||||||
|
// struct xfile
|
||||||
|
itemSize := s.fileItemSize
|
||||||
|
for i := 0; i+itemSize <= len(buf); i += itemSize {
|
||||||
|
// xfile.xf_data
|
||||||
|
data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8])
|
||||||
|
if data == socket {
|
||||||
|
// xfile.xf_pid
|
||||||
|
pid := readNativeUint32(buf[i+s.pid : i+s.pid+4])
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearcher(major int) *searcher {
|
||||||
|
var s *searcher
|
||||||
|
switch major {
|
||||||
|
case 11:
|
||||||
|
s = &searcher{
|
||||||
|
headSize: 32,
|
||||||
|
tcpItemSize: 1304,
|
||||||
|
udpItemSize: 632,
|
||||||
|
port: 198,
|
||||||
|
ip: 228,
|
||||||
|
vflag: 116,
|
||||||
|
socket: 88,
|
||||||
|
fileItemSize: 80,
|
||||||
|
data: 56,
|
||||||
|
pid: 8,
|
||||||
|
udpInpOffset: 8,
|
||||||
|
}
|
||||||
|
case 12:
|
||||||
|
fallthrough
|
||||||
|
case 13:
|
||||||
|
s = &searcher{
|
||||||
|
headSize: 64,
|
||||||
|
tcpItemSize: 744,
|
||||||
|
udpItemSize: 400,
|
||||||
|
port: 254,
|
||||||
|
ip: 284,
|
||||||
|
vflag: 392,
|
||||||
|
socket: 16,
|
||||||
|
fileItemSize: 128,
|
||||||
|
data: 56,
|
||||||
|
pid: 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSearcher() error {
|
||||||
|
osRelease, err := syscall.Sysctl("kern.osrelease")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dot := strings.Index(osRelease, ".")
|
||||||
|
if dot != -1 {
|
||||||
|
osRelease = osRelease[:dot]
|
||||||
|
}
|
||||||
|
major, err := strconv.Atoi(osRelease)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defaultSearcher = newSearcher(major)
|
||||||
|
if defaultSearcher == nil {
|
||||||
|
return fmt.Errorf("unsupported freebsd version %d", major)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
238
component/process/process_linux.go
Normal file
238
component/process/process_linux.go
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
|
||||||
|
func init() {
|
||||||
|
var x uint32 = 0x01020304
|
||||||
|
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||||
|
nativeEndian = binary.BigEndian
|
||||||
|
} else {
|
||||||
|
nativeEndian = binary.LittleEndian
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
|
||||||
|
type ProcessNameResolver func(inode, uid int) (name string, err error)
|
||||||
|
|
||||||
|
// export for android
|
||||||
|
var (
|
||||||
|
DefaultSocketResolver SocketResolver = resolveSocketByNetlink
|
||||||
|
DefaultProcessNameResolver ProcessNameResolver = resolveProcessNameByProcSearch
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
||||||
|
socketDiagByFamily = 20
|
||||||
|
pathProc = "/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nativeEndian binary.ByteOrder = binary.LittleEndian
|
||||||
|
|
||||||
|
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
||||||
|
inode, uid, err := DefaultSocketResolver(network, ip, srcPort)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultProcessNameResolver(inode, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, error) {
|
||||||
|
var family byte
|
||||||
|
var protocol byte
|
||||||
|
|
||||||
|
switch network {
|
||||||
|
case TCP:
|
||||||
|
protocol = syscall.IPPROTO_TCP
|
||||||
|
case UDP:
|
||||||
|
protocol = syscall.IPPROTO_UDP
|
||||||
|
default:
|
||||||
|
return 0, 0, ErrInvalidNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.To4() != nil {
|
||||||
|
family = syscall.AF_INET
|
||||||
|
} else {
|
||||||
|
family = syscall.AF_INET6
|
||||||
|
}
|
||||||
|
|
||||||
|
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
|
||||||
|
|
||||||
|
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
defer syscall.Close(socket)
|
||||||
|
|
||||||
|
syscall.SetNonblock(socket, true)
|
||||||
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 50})
|
||||||
|
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 50})
|
||||||
|
|
||||||
|
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||||
|
Family: syscall.AF_NETLINK,
|
||||||
|
Pad: 0,
|
||||||
|
Pid: 0,
|
||||||
|
Groups: 0,
|
||||||
|
}); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := syscall.Write(socket, req); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rb := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(rb)
|
||||||
|
|
||||||
|
n, err := syscall.Read(socket, rb)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
messages, err := syscall.ParseNetlinkMessage(rb[:n])
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
} else if len(messages) == 0 {
|
||||||
|
return 0, 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
message := messages[0]
|
||||||
|
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
|
||||||
|
return 0, 0, syscall.ESRCH
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, inode := unpackSocketDiagResponse(&messages[0])
|
||||||
|
|
||||||
|
return int(uid), int(inode), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
|
||||||
|
s := make([]byte, 16)
|
||||||
|
|
||||||
|
if v4 := source.To4(); v4 != nil {
|
||||||
|
copy(s, v4)
|
||||||
|
} else {
|
||||||
|
copy(s, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||||
|
|
||||||
|
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
||||||
|
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
||||||
|
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
||||||
|
nativeEndian.PutUint32(buf[8:12], 0)
|
||||||
|
nativeEndian.PutUint32(buf[12:16], 0)
|
||||||
|
|
||||||
|
buf[16] = family
|
||||||
|
buf[17] = protocol
|
||||||
|
buf[18] = 0
|
||||||
|
buf[19] = 0
|
||||||
|
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
|
||||||
|
binary.BigEndian.PutUint16(buf[26:28], 0)
|
||||||
|
|
||||||
|
copy(buf[28:44], s)
|
||||||
|
copy(buf[44:60], net.IPv6zero)
|
||||||
|
|
||||||
|
nativeEndian.PutUint32(buf[60:64], 0)
|
||||||
|
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||||
|
if len(msg.Data) < 72 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
data := msg.Data
|
||||||
|
|
||||||
|
uid = nativeEndian.Uint32(data[64:68])
|
||||||
|
inode = nativeEndian.Uint32(data[68:72])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
|
||||||
|
files, err := ioutil.ReadDir(pathProc)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]byte, syscall.PathMax)
|
||||||
|
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() || !isPid(f.Name()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
processPath := path.Join(pathProc, f.Name())
|
||||||
|
fdPath := path.Join(processPath, "fd")
|
||||||
|
|
||||||
|
fds, err := ioutil.ReadDir(fdPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fd := range fds {
|
||||||
|
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(buffer[:n], socket) {
|
||||||
|
cmdline, err := ioutil.ReadFile(path.Join(processPath, "cmdline"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitCmdline(cmdline), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", syscall.ESRCH
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCmdline(cmdline []byte) string {
|
||||||
|
indexOfEndOfString := len(cmdline)
|
||||||
|
|
||||||
|
for i, c := range cmdline {
|
||||||
|
if c == 0 {
|
||||||
|
indexOfEndOfString = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Base(string(cmdline[:indexOfEndOfString]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPid(s string) bool {
|
||||||
|
for _, s := range s {
|
||||||
|
if s < '0' || s > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
10
component/process/process_other.go
Normal file
10
component/process/process_other.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// +build !darwin,!linux,!windows
|
||||||
|
// +build !freebsd !amd64
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
||||||
|
return "", ErrPlatformNotSupport
|
||||||
|
}
|
224
component/process/process_windows.go
Normal file
224
component/process/process_windows.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tcpTableFunc = "GetExtendedTcpTable"
|
||||||
|
tcpTablePidConn = 4
|
||||||
|
udpTableFunc = "GetExtendedUdpTable"
|
||||||
|
udpTablePid = 1
|
||||||
|
queryProcNameFunc = "QueryFullProcessImageNameW"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
getExTCPTable uintptr
|
||||||
|
getExUDPTable uintptr
|
||||||
|
queryProcName uintptr
|
||||||
|
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initWin32API() error {
|
||||||
|
h, err := windows.LoadLibrary("iphlpapi.dll")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err = windows.LoadLibrary("kernel32.dll")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
||||||
|
once.Do(func() {
|
||||||
|
err := initWin32API()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
|
||||||
|
log.Warnln("All PROCESS-NAMES rules will be skiped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
family := windows.AF_INET
|
||||||
|
if ip.To4() == nil {
|
||||||
|
family = windows.AF_INET6
|
||||||
|
}
|
||||||
|
|
||||||
|
var class int
|
||||||
|
var fn uintptr
|
||||||
|
switch network {
|
||||||
|
case TCP:
|
||||||
|
fn = getExTCPTable
|
||||||
|
class = tcpTablePidConn
|
||||||
|
case UDP:
|
||||||
|
fn = getExUDPTable
|
||||||
|
class = udpTablePid
|
||||||
|
default:
|
||||||
|
return "", ErrInvalidNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := getTransportTable(fn, family, class)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := newSearcher(family == windows.AF_INET, network == TCP)
|
||||||
|
|
||||||
|
pid, err := s.Search(buf, ip, uint16(srcPort))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return getExecPathFromPID(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
type searcher struct {
|
||||||
|
itemSize int
|
||||||
|
port int
|
||||||
|
ip int
|
||||||
|
ipSize int
|
||||||
|
pid int
|
||||||
|
tcpState int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
|
||||||
|
n := int(readNativeUint32(b[:4]))
|
||||||
|
itemSize := s.itemSize
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
||||||
|
|
||||||
|
if s.tcpState >= 0 {
|
||||||
|
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
||||||
|
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
||||||
|
if tcpState != 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
||||||
|
// this field can be illustrated as follows depends on different machine endianess:
|
||||||
|
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
||||||
|
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
||||||
|
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
||||||
|
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
||||||
|
if srcPort != port {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
|
||||||
|
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
||||||
|
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid := readNativeUint32(row[s.pid : s.pid+4])
|
||||||
|
return pid, nil
|
||||||
|
}
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSearcher(isV4, isTCP bool) *searcher {
|
||||||
|
var itemSize, port, ip, ipSize, pid int
|
||||||
|
tcpState := -1
|
||||||
|
switch {
|
||||||
|
case isV4 && isTCP:
|
||||||
|
// struct MIB_TCPROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
||||||
|
case isV4 && !isTCP:
|
||||||
|
// struct MIB_UDPROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
||||||
|
case !isV4 && isTCP:
|
||||||
|
// struct MIB_TCP6ROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
||||||
|
case !isV4 && !isTCP:
|
||||||
|
// struct MIB_UDP6ROW_OWNER_PID
|
||||||
|
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
||||||
|
}
|
||||||
|
|
||||||
|
return &searcher{
|
||||||
|
itemSize: itemSize,
|
||||||
|
port: port,
|
||||||
|
ip: ip,
|
||||||
|
ipSize: ipSize,
|
||||||
|
pid: pid,
|
||||||
|
tcpState: tcpState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
||||||
|
for size, buf := uint32(8), make([]byte, 8); ; {
|
||||||
|
ptr := unsafe.Pointer(&buf[0])
|
||||||
|
err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case 0:
|
||||||
|
return buf, nil
|
||||||
|
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
||||||
|
buf = make([]byte, size)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("syscall error: %d", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNativeUint32(b []byte) uint32 {
|
||||||
|
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
|
// kernel process starts with a colon in order to distinguish with normal processes
|
||||||
|
switch pid {
|
||||||
|
case 0:
|
||||||
|
// reserved pid for system idle process
|
||||||
|
return ":System Idle Process", nil
|
||||||
|
case 4:
|
||||||
|
// reserved pid for windows kernel image
|
||||||
|
return ":System", nil
|
||||||
|
}
|
||||||
|
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(h)
|
||||||
|
|
||||||
|
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
||||||
|
size := uint32(len(buf))
|
||||||
|
r1, _, err := syscall.Syscall6(
|
||||||
|
queryProcName, 4,
|
||||||
|
uintptr(h),
|
||||||
|
uintptr(1),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0, 0)
|
||||||
|
if r1 == 0 {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
|
||||||
|
}
|
101
component/profile/cachefile/cache.go
Normal file
101
component/profile/cachefile/cache.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package cachefile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/profile"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
initOnce sync.Once
|
||||||
|
fileMode os.FileMode = 0666
|
||||||
|
defaultCache *CacheFile
|
||||||
|
)
|
||||||
|
|
||||||
|
type cache struct {
|
||||||
|
Selected map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheFile store and update the cache file
|
||||||
|
type CacheFile struct {
|
||||||
|
path string
|
||||||
|
model *cache
|
||||||
|
buf *bytes.Buffer
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SetSelected(group, selected string) {
|
||||||
|
if !profile.StoreSelected.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
|
||||||
|
model := c.element()
|
||||||
|
|
||||||
|
model.Selected[group] = selected
|
||||||
|
c.buf.Reset()
|
||||||
|
if err := gob.NewEncoder(c.buf).Encode(model); err != nil {
|
||||||
|
log.Warnln("[CacheFile] encode gob failed: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(c.path, c.buf.Bytes(), fileMode); err != nil {
|
||||||
|
log.Warnln("[CacheFile] write cache to %s failed: %s", c.path, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SelectedMap() map[string]string {
|
||||||
|
if !profile.StoreSelected.Load() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
|
||||||
|
model := c.element()
|
||||||
|
|
||||||
|
mapping := map[string]string{}
|
||||||
|
for k, v := range model.Selected {
|
||||||
|
mapping[k] = v
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) element() *cache {
|
||||||
|
if c.model != nil {
|
||||||
|
return c.model
|
||||||
|
}
|
||||||
|
|
||||||
|
model := &cache{
|
||||||
|
Selected: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf, err := ioutil.ReadFile(c.path); err == nil {
|
||||||
|
bufReader := bytes.NewBuffer(buf)
|
||||||
|
gob.NewDecoder(bufReader).Decode(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.model = model
|
||||||
|
return c.model
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache return singleton of CacheFile
|
||||||
|
func Cache() *CacheFile {
|
||||||
|
initOnce.Do(func() {
|
||||||
|
defaultCache = &CacheFile{
|
||||||
|
path: C.Path.Cache(),
|
||||||
|
buf: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return defaultCache
|
||||||
|
}
|
10
component/profile/profile.go
Normal file
10
component/profile/profile.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package profile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// StoreSelected is a global switch for storing selected proxy to cache
|
||||||
|
StoreSelected = atomic.NewBool(true)
|
||||||
|
)
|
55
component/resolver/enhancer.go
Normal file
55
component/resolver/enhancer.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DefaultHostMapper Enhancer
|
||||||
|
|
||||||
|
type Enhancer interface {
|
||||||
|
FakeIPEnabled() bool
|
||||||
|
MappingEnabled() bool
|
||||||
|
IsFakeIP(net.IP) bool
|
||||||
|
IsExistFakeIP(net.IP) bool
|
||||||
|
FindHostByIP(net.IP) (string, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FakeIPEnabled() bool {
|
||||||
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
|
return mapper.FakeIPEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func MappingEnabled() bool {
|
||||||
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
|
return mapper.MappingEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFakeIP(ip net.IP) bool {
|
||||||
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
|
return mapper.IsFakeIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsExistFakeIP(ip net.IP) bool {
|
||||||
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
|
return mapper.IsExistFakeIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindHostByIP(ip net.IP) (string, bool) {
|
||||||
|
if mapper := DefaultHostMapper; mapper != nil {
|
||||||
|
return mapper.FindHostByIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
@ -1,24 +1,35 @@
|
|||||||
package resolver
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
trie "github.com/Dreamacro/clash/component/domain-trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultResolver aim to resolve ip
|
// DefaultResolver aim to resolve ip
|
||||||
DefaultResolver Resolver
|
DefaultResolver Resolver
|
||||||
|
|
||||||
|
// DisableIPv6 means don't resolve ipv6 host
|
||||||
|
// default value is true
|
||||||
|
DisableIPv6 = true
|
||||||
|
|
||||||
// DefaultHosts aim to resolve hosts
|
// DefaultHosts aim to resolve hosts
|
||||||
DefaultHosts = trie.New()
|
DefaultHosts = trie.New()
|
||||||
|
|
||||||
|
// DefaultDNSTimeout defined the default dns request timeout
|
||||||
|
DefaultDNSTimeout = time.Second * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrIPNotFound = errors.New("couldn't find ip")
|
ErrIPNotFound = errors.New("couldn't find ip")
|
||||||
ErrIPVersion = errors.New("ip version error")
|
ErrIPVersion = errors.New("ip version error")
|
||||||
|
ErrIPv6Disabled = errors.New("ipv6 disabled")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resolver interface {
|
type Resolver interface {
|
||||||
@ -47,22 +58,24 @@ func ResolveIPv4(host string) (net.IP, error) {
|
|||||||
return DefaultResolver.ResolveIPv4(host)
|
return DefaultResolver.ResolveIPv4(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddrs, err := net.LookupIP(host)
|
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if len(ipAddrs) == 0 {
|
||||||
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range ipAddrs {
|
return ipAddrs[rand.Intn(len(ipAddrs))], nil
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
return ip4, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrIPNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIPv6 with a host, return ipv6
|
// ResolveIPv6 with a host, return ipv6
|
||||||
func ResolveIPv6(host string) (net.IP, error) {
|
func ResolveIPv6(host string) (net.IP, error) {
|
||||||
|
if DisableIPv6 {
|
||||||
|
return nil, ErrIPv6Disabled
|
||||||
|
}
|
||||||
|
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
if ip := node.Data.(net.IP).To16(); ip != nil {
|
if ip := node.Data.(net.IP).To16(); ip != nil {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
@ -81,28 +94,31 @@ func ResolveIPv6(host string) (net.IP, error) {
|
|||||||
return DefaultResolver.ResolveIPv6(host)
|
return DefaultResolver.ResolveIPv6(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddrs, err := net.LookupIP(host)
|
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if len(ipAddrs) == 0 {
|
||||||
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range ipAddrs {
|
return ipAddrs[rand.Intn(len(ipAddrs))], nil
|
||||||
if ip.To4() == nil {
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrIPNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIP with a host, return ip
|
// ResolveIPWithResolver same as ResolveIP, but with a resolver
|
||||||
func ResolveIP(host string) (net.IP, error) {
|
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
return node.Data.(net.IP), nil
|
return node.Data.(net.IP), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if r != nil {
|
||||||
return DefaultResolver.ResolveIP(host)
|
if DisableIPv6 {
|
||||||
|
return r.ResolveIPv4(host)
|
||||||
|
}
|
||||||
|
return r.ResolveIP(host)
|
||||||
|
} else if DisableIPv6 {
|
||||||
|
return ResolveIPv4(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
@ -117,3 +133,8 @@ func ResolveIP(host string) (net.IP, error) {
|
|||||||
|
|
||||||
return ipAddr.IP, nil
|
return ipAddr.IP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveIP with a host, return ip
|
||||||
|
func ResolveIP(host string) (net.IP, error) {
|
||||||
|
return ResolveIPWithResolver(host, DefaultResolver)
|
||||||
|
}
|
||||||
|
@ -28,6 +28,7 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
|
|||||||
n := copy(b, ho.buf[ho.offset:])
|
n := copy(b, ho.buf[ho.offset:])
|
||||||
ho.offset += n
|
ho.offset += n
|
||||||
if ho.offset == len(ho.buf) {
|
if ho.offset == len(ho.buf) {
|
||||||
|
pool.Put(ho.buf)
|
||||||
ho.buf = nil
|
ho.buf = nil
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
@ -67,7 +68,10 @@ func (ho *HTTPObfs) Write(b []byte) (int, error) {
|
|||||||
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2))
|
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2))
|
||||||
req.Header.Set("Upgrade", "websocket")
|
req.Header.Set("Upgrade", "websocket")
|
||||||
req.Header.Set("Connection", "Upgrade")
|
req.Header.Set("Connection", "Upgrade")
|
||||||
req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
|
req.Host = ho.host
|
||||||
|
if ho.port != "80" {
|
||||||
|
req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
|
||||||
|
}
|
||||||
req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
|
req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
|
||||||
req.ContentLength = int64(len(b))
|
req.ContentLength = int64(len(b))
|
||||||
err := req.Write(ho.Conn)
|
err := req.Write(ho.Conn)
|
||||||
|
@ -1,21 +1,54 @@
|
|||||||
package snell
|
package snell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
)
|
)
|
||||||
|
|
||||||
type snellCipher struct {
|
type snellCipher struct {
|
||||||
psk []byte
|
psk []byte
|
||||||
|
keySize int
|
||||||
makeAEAD func(key []byte) (cipher.AEAD, error)
|
makeAEAD func(key []byte) (cipher.AEAD, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *snellCipher) KeySize() int { return 32 }
|
func (sc *snellCipher) KeySize() int { return sc.keySize }
|
||||||
func (sc *snellCipher) SaltSize() int { return 16 }
|
func (sc *snellCipher) SaltSize() int { return 16 }
|
||||||
func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
|
func (sc *snellCipher) Encrypter(salt []byte) (cipher.AEAD, error) {
|
||||||
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
|
return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize()))
|
||||||
}
|
}
|
||||||
func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
|
func (sc *snellCipher) Decrypter(salt []byte) (cipher.AEAD, error) {
|
||||||
return sc.makeAEAD(argon2.IDKey(sc.psk, salt, 3, 8, 1, uint32(sc.KeySize())))
|
return sc.makeAEAD(snellKDF(sc.psk, salt, sc.KeySize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func snellKDF(psk, salt []byte, keySize int) []byte {
|
||||||
|
// snell use a special kdf function
|
||||||
|
return argon2.IDKey(psk, salt, 3, 8, 1, 32)[:keySize]
|
||||||
|
}
|
||||||
|
|
||||||
|
func aesGCM(key []byte) (cipher.AEAD, error) {
|
||||||
|
blk, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cipher.NewGCM(blk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAES128GCM(psk []byte) shadowaead.Cipher {
|
||||||
|
return &snellCipher{
|
||||||
|
psk: psk,
|
||||||
|
keySize: 16,
|
||||||
|
makeAEAD: aesGCM,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChacha20Poly1305(psk []byte) shadowaead.Cipher {
|
||||||
|
return &snellCipher{
|
||||||
|
psk: psk,
|
||||||
|
keySize: 32,
|
||||||
|
makeAEAD: chacha20poly1305.New,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
85
component/snell/pool.go
Normal file
85
component/snell/pool.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package snell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/pool"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pool struct {
|
||||||
|
pool *pool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Get() (net.Conn, error) {
|
||||||
|
return p.GetContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) GetContext(ctx context.Context) (net.Conn, error) {
|
||||||
|
elm, err := p.pool.GetContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PoolConn{elm.(*Snell), p}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Put(conn net.Conn) {
|
||||||
|
if err := HalfClose(conn); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.pool.Put(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoolConn struct {
|
||||||
|
*Snell
|
||||||
|
pool *Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PoolConn) Read(b []byte) (int, error) {
|
||||||
|
// save old status of reply (it mutable by Read)
|
||||||
|
reply := pc.Snell.reply
|
||||||
|
|
||||||
|
n, err := pc.Snell.Read(b)
|
||||||
|
if err == shadowaead.ErrZeroChunk {
|
||||||
|
// if reply is false, it should be client halfclose.
|
||||||
|
// ignore error and read data again.
|
||||||
|
if !reply {
|
||||||
|
pc.Snell.reply = false
|
||||||
|
return pc.Snell.Read(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PoolConn) Write(b []byte) (int, error) {
|
||||||
|
return pc.Snell.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PoolConn) Close() error {
|
||||||
|
// clash use SetReadDeadline to break bidirectional copy between client and server.
|
||||||
|
// reset it before reuse connection to avoid io timeout error.
|
||||||
|
pc.Snell.Conn.SetReadDeadline(time.Time{})
|
||||||
|
pc.pool.Put(pc.Snell)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPool(factory func(context.Context) (*Snell, error)) *Pool {
|
||||||
|
p := pool.New(
|
||||||
|
func(ctx context.Context) (interface{}, error) {
|
||||||
|
return factory(ctx)
|
||||||
|
},
|
||||||
|
pool.WithAge(15000),
|
||||||
|
pool.WithSize(10),
|
||||||
|
pool.WithEvict(func(item interface{}) {
|
||||||
|
item.(*Snell).Close()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &Pool{p}
|
||||||
|
}
|
@ -4,19 +4,27 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CommandPing byte = 0
|
Version1 = 1
|
||||||
CommandConnect byte = 1
|
Version2 = 2
|
||||||
|
DefaultSnellVersion = Version1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandPing byte = 0
|
||||||
|
CommandConnect byte = 1
|
||||||
|
CommandConnectV2 byte = 5
|
||||||
|
|
||||||
CommandTunnel byte = 0
|
CommandTunnel byte = 0
|
||||||
|
CommandPong byte = 1
|
||||||
CommandError byte = 2
|
CommandError byte = 2
|
||||||
|
|
||||||
Version byte = 1
|
Version byte = 1
|
||||||
@ -24,6 +32,7 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
||||||
|
endSignal = []byte{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Snell struct {
|
type Snell struct {
|
||||||
@ -45,14 +54,20 @@ func (s *Snell) Read(b []byte) (int, error) {
|
|||||||
if s.buffer[0] == CommandTunnel {
|
if s.buffer[0] == CommandTunnel {
|
||||||
return s.Conn.Read(b)
|
return s.Conn.Read(b)
|
||||||
} else if s.buffer[0] != CommandError {
|
} else if s.buffer[0] != CommandError {
|
||||||
return 0, errors.New("Command not support")
|
return 0, errors.New("command not support")
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommandError
|
// CommandError
|
||||||
|
// 1 byte error code
|
||||||
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
|
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
errcode := int(s.buffer[0])
|
||||||
|
|
||||||
|
// 1 byte error message length
|
||||||
|
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
length := int(s.buffer[0])
|
length := int(s.buffer[0])
|
||||||
msg := make([]byte, length)
|
msg := make([]byte, length)
|
||||||
|
|
||||||
@ -60,15 +75,19 @@ func (s *Snell) Read(b []byte) (int, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, errors.New(string(msg))
|
return 0, fmt.Errorf("server reported code: %d, message: %s", errcode, string(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteHeader(conn net.Conn, host string, port uint) error {
|
func WriteHeader(conn net.Conn, host string, port uint, version int) error {
|
||||||
buf := bufferPool.Get().(*bytes.Buffer)
|
buf := bufferPool.Get().(*bytes.Buffer)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufferPool.Put(buf)
|
defer bufferPool.Put(buf)
|
||||||
buf.WriteByte(Version)
|
buf.WriteByte(Version)
|
||||||
buf.WriteByte(CommandConnect)
|
if version == Version2 {
|
||||||
|
buf.WriteByte(CommandConnectV2)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(CommandConnect)
|
||||||
|
}
|
||||||
|
|
||||||
// clientID length & id
|
// clientID length & id
|
||||||
buf.WriteByte(0)
|
buf.WriteByte(0)
|
||||||
@ -85,7 +104,24 @@ func WriteHeader(conn net.Conn, host string, port uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StreamConn(conn net.Conn, psk []byte) net.Conn {
|
// HalfClose works only on version2
|
||||||
cipher := &snellCipher{psk, chacha20poly1305.New}
|
func HalfClose(conn net.Conn) error {
|
||||||
|
if _, err := conn.Write(endSignal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, ok := conn.(*Snell); ok {
|
||||||
|
s.reply = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamConn(conn net.Conn, psk []byte, version int) *Snell {
|
||||||
|
var cipher shadowaead.Cipher
|
||||||
|
if version == Version2 {
|
||||||
|
cipher = NewAES128GCM(psk)
|
||||||
|
} else {
|
||||||
|
cipher = NewChacha20Poly1305(psk)
|
||||||
|
}
|
||||||
return &Snell{Conn: shadowaead.NewConn(conn, cipher)}
|
return &Snell{Conn: shadowaead.NewConn(conn, cipher)}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ func (err Error) Error() string {
|
|||||||
// Command is request commands as defined in RFC 1928 section 4.
|
// Command is request commands as defined in RFC 1928 section 4.
|
||||||
type Command = uint8
|
type Command = uint8
|
||||||
|
|
||||||
|
const Version = 5
|
||||||
|
|
||||||
// SOCKS request commands as defined in RFC 1928 section 4.
|
// SOCKS request commands as defined in RFC 1928 section 4.
|
||||||
const (
|
const (
|
||||||
CmdConnect Command = 1
|
CmdConnect Command = 1
|
||||||
@ -227,6 +229,10 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if buf[1] == 2 {
|
if buf[1] == 2 {
|
||||||
|
if user == nil {
|
||||||
|
return nil, ErrAuth
|
||||||
|
}
|
||||||
|
|
||||||
// password protocol version
|
// password protocol version
|
||||||
authMsg := &bytes.Buffer{}
|
authMsg := &bytes.Buffer{}
|
||||||
authMsg.WriteByte(1)
|
authMsg.WriteByte(1)
|
||||||
|
9
component/ssr/obfs/base.go
Normal file
9
component/ssr/obfs/base.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
type Base struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Key []byte
|
||||||
|
IVSize int
|
||||||
|
Param string
|
||||||
|
}
|
9
component/ssr/obfs/http_post.go
Normal file
9
component/ssr/obfs/http_post.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("http_post", newHTTPPost, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPPost(b *Base) Obfs {
|
||||||
|
return &httpObfs{Base: b, post: true}
|
||||||
|
}
|
407
component/ssr/obfs/http_simple.go
Normal file
407
component/ssr/obfs/http_simple.go
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("http_simple", newHTTPSimple, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpObfs struct {
|
||||||
|
*Base
|
||||||
|
post bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPSimple(b *Base) Obfs {
|
||||||
|
return &httpObfs{Base: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpConn struct {
|
||||||
|
net.Conn
|
||||||
|
*httpObfs
|
||||||
|
hasSentHeader bool
|
||||||
|
hasRecvHeader bool
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpObfs) StreamConn(c net.Conn) net.Conn {
|
||||||
|
return &httpConn{Conn: c, httpObfs: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpConn) Read(b []byte) (int, error) {
|
||||||
|
if c.buf != nil {
|
||||||
|
n := copy(b, c.buf)
|
||||||
|
if n == len(c.buf) {
|
||||||
|
c.buf = nil
|
||||||
|
} else {
|
||||||
|
c.buf = c.buf[n:]
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.hasRecvHeader {
|
||||||
|
return c.Conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
n, err := c.Conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
pos := bytes.Index(buf[:n], []byte("\r\n\r\n"))
|
||||||
|
if pos == -1 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
c.hasRecvHeader = true
|
||||||
|
dataLength := n - pos - 4
|
||||||
|
n = copy(b, buf[4+pos:n])
|
||||||
|
if dataLength > n {
|
||||||
|
c.buf = append(c.buf, buf[4+pos+n:4+pos+dataLength]...)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpConn) Write(b []byte) (int, error) {
|
||||||
|
if c.hasSentHeader {
|
||||||
|
return c.Conn.Write(b)
|
||||||
|
}
|
||||||
|
// 30: head length
|
||||||
|
headLength := c.IVSize + 30
|
||||||
|
|
||||||
|
bLength := len(b)
|
||||||
|
headDataLength := bLength
|
||||||
|
if bLength-headLength > 64 {
|
||||||
|
headDataLength = headLength + rand.Intn(65)
|
||||||
|
}
|
||||||
|
headData := b[:headDataLength]
|
||||||
|
b = b[headDataLength:]
|
||||||
|
|
||||||
|
var body string
|
||||||
|
host := c.Host
|
||||||
|
if len(c.Param) > 0 {
|
||||||
|
pos := strings.Index(c.Param, "#")
|
||||||
|
if pos != -1 {
|
||||||
|
body = strings.ReplaceAll(c.Param[pos+1:], "\n", "\r\n")
|
||||||
|
body = strings.ReplaceAll(body, "\\n", "\r\n")
|
||||||
|
host = c.Param[:pos]
|
||||||
|
} else {
|
||||||
|
host = c.Param
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hosts := strings.Split(host, ",")
|
||||||
|
host = hosts[rand.Intn(len(hosts))]
|
||||||
|
|
||||||
|
buf := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
if c.post {
|
||||||
|
buf.WriteString("POST /")
|
||||||
|
} else {
|
||||||
|
buf.WriteString("GET /")
|
||||||
|
}
|
||||||
|
packURLEncodedHeadData(buf, headData)
|
||||||
|
buf.WriteString(" HTTP/1.1\r\nHost: " + host)
|
||||||
|
if c.Port != 80 {
|
||||||
|
buf.WriteString(":" + strconv.Itoa(c.Port))
|
||||||
|
}
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
if len(body) > 0 {
|
||||||
|
buf.WriteString(body + "\r\n\r\n")
|
||||||
|
} else {
|
||||||
|
buf.WriteString("User-Agent: ")
|
||||||
|
buf.WriteString(userAgent[rand.Intn(len(userAgent))])
|
||||||
|
buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n")
|
||||||
|
if c.post {
|
||||||
|
packBoundary(buf)
|
||||||
|
}
|
||||||
|
buf.WriteString("DNT: 1\r\nConnection: keep-alive\r\n\r\n")
|
||||||
|
}
|
||||||
|
buf.Write(b)
|
||||||
|
_, err := c.Conn.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
c.hasSentHeader = true
|
||||||
|
return bLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packURLEncodedHeadData(buf *bytes.Buffer, data []byte) {
|
||||||
|
dataLength := len(data)
|
||||||
|
for i := 0; i < dataLength; i++ {
|
||||||
|
buf.WriteRune('%')
|
||||||
|
buf.WriteString(hex.EncodeToString(data[i : i+1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func packBoundary(buf *bytes.Buffer) {
|
||||||
|
buf.WriteString("Content-Type: multipart/form-data; boundary=")
|
||||||
|
set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
buf.WriteByte(set[rand.Intn(62)])
|
||||||
|
}
|
||||||
|
buf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgent = []string{
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
|
||||||
|
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
|
||||||
|
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
|
||||||
|
}
|
42
component/ssr/obfs/obfs.go
Normal file
42
component/ssr/obfs/obfs.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errTLS12TicketAuthIncorrectMagicNumber = errors.New("tls1.2_ticket_auth incorrect magic number")
|
||||||
|
errTLS12TicketAuthTooShortData = errors.New("tls1.2_ticket_auth too short data")
|
||||||
|
errTLS12TicketAuthHMACError = errors.New("tls1.2_ticket_auth hmac verifying failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type authData struct {
|
||||||
|
clientID [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Obfs interface {
|
||||||
|
StreamConn(net.Conn) net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type obfsCreator func(b *Base) Obfs
|
||||||
|
|
||||||
|
var obfsList = make(map[string]struct {
|
||||||
|
overhead int
|
||||||
|
new obfsCreator
|
||||||
|
})
|
||||||
|
|
||||||
|
func register(name string, c obfsCreator, o int) {
|
||||||
|
obfsList[name] = struct {
|
||||||
|
overhead int
|
||||||
|
new obfsCreator
|
||||||
|
}{overhead: o, new: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PickObfs(name string, b *Base) (Obfs, int, error) {
|
||||||
|
if choice, ok := obfsList[name]; ok {
|
||||||
|
return choice.new(b), choice.overhead, nil
|
||||||
|
}
|
||||||
|
return nil, 0, fmt.Errorf("Obfs %s not supported", name)
|
||||||
|
}
|
15
component/ssr/obfs/plain.go
Normal file
15
component/ssr/obfs/plain.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type plain struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("plain", newPlain, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlain(b *Base) Obfs {
|
||||||
|
return &plain{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plain) StreamConn(c net.Conn) net.Conn { return c }
|
71
component/ssr/obfs/random_head.go
Normal file
71
component/ssr/obfs/random_head.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"hash/crc32"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("random_head", newRandomHead, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type randomHead struct {
|
||||||
|
*Base
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRandomHead(b *Base) Obfs {
|
||||||
|
return &randomHead{Base: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
type randomHeadConn struct {
|
||||||
|
net.Conn
|
||||||
|
*randomHead
|
||||||
|
hasSentHeader bool
|
||||||
|
rawTransSent bool
|
||||||
|
rawTransRecv bool
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *randomHead) StreamConn(c net.Conn) net.Conn {
|
||||||
|
return &randomHeadConn{Conn: c, randomHead: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randomHeadConn) Read(b []byte) (int, error) {
|
||||||
|
if c.rawTransRecv {
|
||||||
|
return c.Conn.Read(b)
|
||||||
|
}
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
c.Conn.Read(buf)
|
||||||
|
c.rawTransRecv = true
|
||||||
|
c.Write(nil)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randomHeadConn) Write(b []byte) (int, error) {
|
||||||
|
if c.rawTransSent {
|
||||||
|
return c.Conn.Write(b)
|
||||||
|
}
|
||||||
|
c.buf = append(c.buf, b...)
|
||||||
|
if !c.hasSentHeader {
|
||||||
|
c.hasSentHeader = true
|
||||||
|
dataLength := rand.Intn(96) + 4
|
||||||
|
buf := pool.Get(dataLength + 4)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
rand.Read(buf[:dataLength])
|
||||||
|
binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength]))
|
||||||
|
_, err := c.Conn.Write(buf)
|
||||||
|
return len(b), err
|
||||||
|
}
|
||||||
|
if c.rawTransRecv {
|
||||||
|
_, err := c.Conn.Write(c.buf)
|
||||||
|
c.buf = nil
|
||||||
|
c.rawTransSent = true
|
||||||
|
return len(b), err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
231
component/ssr/obfs/tls1.2_ticket_auth.go
Normal file
231
component/ssr/obfs/tls1.2_ticket_auth.go
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("tls1.2_ticket_auth", newTLS12Ticket, 5)
|
||||||
|
register("tls1.2_ticket_fastauth", newTLS12Ticket, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tls12Ticket struct {
|
||||||
|
*Base
|
||||||
|
*authData
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTLS12Ticket(b *Base) Obfs {
|
||||||
|
r := &tls12Ticket{Base: b, authData: &authData{}}
|
||||||
|
rand.Read(r.clientID[:])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type tls12TicketConn struct {
|
||||||
|
net.Conn
|
||||||
|
*tls12Ticket
|
||||||
|
handshakeStatus int
|
||||||
|
decoded bytes.Buffer
|
||||||
|
underDecoded bytes.Buffer
|
||||||
|
sendBuf bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) StreamConn(c net.Conn) net.Conn {
|
||||||
|
return &tls12TicketConn{Conn: c, tls12Ticket: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tls12TicketConn) Read(b []byte) (int, error) {
|
||||||
|
if c.decoded.Len() > 0 {
|
||||||
|
return c.decoded.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
n, err := c.Conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.handshakeStatus == 8 {
|
||||||
|
c.underDecoded.Write(buf[:n])
|
||||||
|
for c.underDecoded.Len() > 5 {
|
||||||
|
if !bytes.Equal(c.underDecoded.Bytes()[:3], []byte{0x17, 3, 3}) {
|
||||||
|
c.underDecoded.Reset()
|
||||||
|
return 0, errTLS12TicketAuthIncorrectMagicNumber
|
||||||
|
}
|
||||||
|
size := int(binary.BigEndian.Uint16(c.underDecoded.Bytes()[3:5]))
|
||||||
|
if c.underDecoded.Len() < 5+size {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c.underDecoded.Next(5)
|
||||||
|
c.decoded.Write(c.underDecoded.Next(size))
|
||||||
|
}
|
||||||
|
n, _ = c.decoded.Read(b)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 11+32+1+32 {
|
||||||
|
return 0, errTLS12TicketAuthTooShortData
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hmac.Equal(buf[33:43], c.hmacSHA1(buf[11:33])[:10]) || !hmac.Equal(buf[n-10:n], c.hmacSHA1(buf[:n-10])[:10]) {
|
||||||
|
return 0, errTLS12TicketAuthHMACError
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Write(nil)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tls12TicketConn) Write(b []byte) (int, error) {
|
||||||
|
length := len(b)
|
||||||
|
if c.handshakeStatus == 8 {
|
||||||
|
buf := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
for len(b) > 2048 {
|
||||||
|
size := rand.Intn(4096) + 100
|
||||||
|
if len(b) < size {
|
||||||
|
size = len(b)
|
||||||
|
}
|
||||||
|
packData(buf, b[:size])
|
||||||
|
b = b[size:]
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
packData(buf, b)
|
||||||
|
}
|
||||||
|
_, err := c.Conn.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) > 0 {
|
||||||
|
packData(&c.sendBuf, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.handshakeStatus == 0 {
|
||||||
|
c.handshakeStatus = 1
|
||||||
|
|
||||||
|
data := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(data)
|
||||||
|
defer data.Reset()
|
||||||
|
|
||||||
|
data.Write([]byte{3, 3})
|
||||||
|
c.packAuthData(data)
|
||||||
|
data.WriteByte(0x20)
|
||||||
|
data.Write(c.clientID[:])
|
||||||
|
data.Write([]byte{0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a})
|
||||||
|
data.Write([]byte{0x1, 0x0})
|
||||||
|
|
||||||
|
ext := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(ext)
|
||||||
|
defer ext.Reset()
|
||||||
|
|
||||||
|
host := c.getHost()
|
||||||
|
ext.Write([]byte{0xff, 0x01, 0x00, 0x01, 0x00})
|
||||||
|
packSNIData(ext, host)
|
||||||
|
ext.Write([]byte{0, 0x17, 0, 0})
|
||||||
|
c.packTicketBuf(ext, host)
|
||||||
|
ext.Write([]byte{0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03})
|
||||||
|
ext.Write([]byte{0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00})
|
||||||
|
ext.Write([]byte{0x00, 0x12, 0x00, 0x00})
|
||||||
|
ext.Write([]byte{0x75, 0x50, 0x00, 0x00})
|
||||||
|
ext.Write([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00})
|
||||||
|
ext.Write([]byte{0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18})
|
||||||
|
|
||||||
|
binary.Write(data, binary.BigEndian, uint16(ext.Len()))
|
||||||
|
data.ReadFrom(ext)
|
||||||
|
|
||||||
|
ret := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(ret)
|
||||||
|
defer ret.Reset()
|
||||||
|
|
||||||
|
ret.Write([]byte{0x16, 3, 1})
|
||||||
|
binary.Write(ret, binary.BigEndian, uint16(data.Len()+4))
|
||||||
|
ret.Write([]byte{1, 0})
|
||||||
|
binary.Write(ret, binary.BigEndian, uint16(data.Len()))
|
||||||
|
ret.ReadFrom(data)
|
||||||
|
|
||||||
|
_, err := c.Conn.Write(ret.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return length, nil
|
||||||
|
} else if c.handshakeStatus == 1 && len(b) == 0 {
|
||||||
|
buf := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
|
buf.Write([]byte{0x14, 3, 3, 0, 1, 1, 0x16, 3, 3, 0, 0x20})
|
||||||
|
tools.AppendRandBytes(buf, 22)
|
||||||
|
buf.Write(c.hmacSHA1(buf.Bytes())[:10])
|
||||||
|
buf.ReadFrom(&c.sendBuf)
|
||||||
|
|
||||||
|
c.handshakeStatus = 8
|
||||||
|
|
||||||
|
_, err := c.Conn.Write(buf.Bytes())
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func packData(buf *bytes.Buffer, data []byte) {
|
||||||
|
buf.Write([]byte{0x17, 3, 3})
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(len(data)))
|
||||||
|
buf.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) packAuthData(buf *bytes.Buffer) {
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
|
||||||
|
tools.AppendRandBytes(buf, 18)
|
||||||
|
buf.Write(t.hmacSHA1(buf.Bytes()[buf.Len()-22:])[:10])
|
||||||
|
}
|
||||||
|
|
||||||
|
func packSNIData(buf *bytes.Buffer, u string) {
|
||||||
|
len := uint16(len(u))
|
||||||
|
buf.Write([]byte{0, 0})
|
||||||
|
binary.Write(buf, binary.BigEndian, len+5)
|
||||||
|
binary.Write(buf, binary.BigEndian, len+3)
|
||||||
|
buf.WriteByte(0)
|
||||||
|
binary.Write(buf, binary.BigEndian, len)
|
||||||
|
buf.WriteString(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) {
|
||||||
|
length := 16 * (rand.Intn(17) + 8)
|
||||||
|
buf.Write([]byte{0, 0x23})
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(length))
|
||||||
|
tools.AppendRandBytes(buf, length)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) hmacSHA1(data []byte) []byte {
|
||||||
|
key := pool.Get(len(t.Key) + 32)
|
||||||
|
defer pool.Put(key)
|
||||||
|
copy(key, t.Key)
|
||||||
|
copy(key[len(t.Key):], t.clientID[:])
|
||||||
|
|
||||||
|
sha1Data := tools.HmacSHA1(key, data)
|
||||||
|
return sha1Data[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) getHost() string {
|
||||||
|
host := t.Param
|
||||||
|
if len(host) == 0 {
|
||||||
|
host = t.Host
|
||||||
|
}
|
||||||
|
if len(host) > 0 && host[len(host)-1] >= '0' && host[len(host)-1] <= '9' {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
hosts := strings.Split(host, ",")
|
||||||
|
host = hosts[rand.Intn(len(hosts))]
|
||||||
|
return host
|
||||||
|
}
|
18
component/ssr/protocol/auth_aes128_md5.go
Normal file
18
component/ssr/protocol/auth_aes128_md5.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import "github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_aes128_md5", newAuthAES128MD5, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthAES128MD5(b *Base) Protocol {
|
||||||
|
a := &authAES128{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
authAES128Function: &authAES128Function{salt: "auth_aes128_md5", hmac: tools.HmacMD5, hashDigest: tools.MD5Sum},
|
||||||
|
userData: &userData{},
|
||||||
|
}
|
||||||
|
a.initUserData()
|
||||||
|
return a
|
||||||
|
}
|
275
component/ssr/protocol/auth_aes128_sha1.go
Normal file
275
component/ssr/protocol/auth_aes128_sha1.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hmacMethod func(key, data []byte) []byte
|
||||||
|
type hashDigestMethod func([]byte) []byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_aes128_sha1", newAuthAES128SHA1, 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authAES128Function struct {
|
||||||
|
salt string
|
||||||
|
hmac hmacMethod
|
||||||
|
hashDigest hashDigestMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
type authAES128 struct {
|
||||||
|
*Base
|
||||||
|
*authData
|
||||||
|
*authAES128Function
|
||||||
|
*userData
|
||||||
|
iv []byte
|
||||||
|
hasSentHeader bool
|
||||||
|
rawTrans bool
|
||||||
|
packID uint32
|
||||||
|
recvID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthAES128SHA1(b *Base) Protocol {
|
||||||
|
a := &authAES128{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
authAES128Function: &authAES128Function{salt: "auth_aes128_sha1", hmac: tools.HmacSHA1, hashDigest: tools.SHA1Sum},
|
||||||
|
userData: &userData{},
|
||||||
|
}
|
||||||
|
a.initUserData()
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) initUserData() {
|
||||||
|
params := strings.Split(a.Param, ":")
|
||||||
|
if len(params) > 1 {
|
||||||
|
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
|
||||||
|
binary.LittleEndian.PutUint32(a.userID[:], uint32(userID))
|
||||||
|
a.userKey = a.hashDigest([]byte(params[1]))
|
||||||
|
} else {
|
||||||
|
log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(a.userKey) == 0 {
|
||||||
|
a.userKey = a.Key
|
||||||
|
rand.Read(a.userID[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) StreamConn(c net.Conn, iv []byte) net.Conn {
|
||||||
|
p := &authAES128{
|
||||||
|
Base: a.Base,
|
||||||
|
authData: a.next(),
|
||||||
|
authAES128Function: a.authAES128Function,
|
||||||
|
userData: a.userData,
|
||||||
|
packID: 1,
|
||||||
|
recvID: 1,
|
||||||
|
}
|
||||||
|
p.iv = iv
|
||||||
|
return &Conn{Conn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) PacketConn(c net.PacketConn) net.PacketConn {
|
||||||
|
p := &authAES128{
|
||||||
|
Base: a.Base,
|
||||||
|
authAES128Function: a.authAES128Function,
|
||||||
|
userData: a.userData,
|
||||||
|
}
|
||||||
|
return &PacketConn{PacketConn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) Decode(dst, src *bytes.Buffer) error {
|
||||||
|
if a.rawTrans {
|
||||||
|
dst.ReadFrom(src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for src.Len() > 4 {
|
||||||
|
macKey := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.userKey)
|
||||||
|
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID)
|
||||||
|
if !bytes.Equal(a.hmac(macKey, src.Bytes()[:2])[:2], src.Bytes()[2:4]) {
|
||||||
|
src.Reset()
|
||||||
|
return errAuthAES128MACError
|
||||||
|
}
|
||||||
|
|
||||||
|
length := int(binary.LittleEndian.Uint16(src.Bytes()[:2]))
|
||||||
|
if length >= 8192 || length < 7 {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthAES128LengthError
|
||||||
|
}
|
||||||
|
if length > src.Len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(a.hmac(macKey, src.Bytes()[:length-4])[:4], src.Bytes()[length-4:length]) {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthAES128ChksumError
|
||||||
|
}
|
||||||
|
|
||||||
|
a.recvID++
|
||||||
|
|
||||||
|
pos := int(src.Bytes()[4])
|
||||||
|
if pos < 255 {
|
||||||
|
pos += 4
|
||||||
|
} else {
|
||||||
|
pos = int(binary.LittleEndian.Uint16(src.Bytes()[5:7])) + 4
|
||||||
|
}
|
||||||
|
dst.Write(src.Bytes()[pos : length-4])
|
||||||
|
src.Next(length)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) Encode(buf *bytes.Buffer, b []byte) error {
|
||||||
|
fullDataLength := len(b)
|
||||||
|
if !a.hasSentHeader {
|
||||||
|
dataLength := getDataLength(b)
|
||||||
|
a.packAuthData(buf, b[:dataLength])
|
||||||
|
b = b[dataLength:]
|
||||||
|
a.hasSentHeader = true
|
||||||
|
}
|
||||||
|
for len(b) > 8100 {
|
||||||
|
a.packData(buf, b[:8100], fullDataLength)
|
||||||
|
b = b[8100:]
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
a.packData(buf, b, fullDataLength)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) DecodePacket(b []byte) ([]byte, error) {
|
||||||
|
if !bytes.Equal(a.hmac(a.Key, b[:len(b)-4])[:4], b[len(b)-4:]) {
|
||||||
|
return nil, errAuthAES128ChksumError
|
||||||
|
}
|
||||||
|
return b[:len(b)-4], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) EncodePacket(buf *bytes.Buffer, b []byte) error {
|
||||||
|
buf.Write(b)
|
||||||
|
buf.Write(a.userID[:])
|
||||||
|
buf.Write(a.hmac(a.userKey, buf.Bytes())[:4])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength int) {
|
||||||
|
dataLength := len(data)
|
||||||
|
randDataLength := a.getRandDataLengthForPackData(dataLength, fullDataLength)
|
||||||
|
/*
|
||||||
|
2: uint16 LittleEndian packedDataLength
|
||||||
|
2: hmac of packedDataLength
|
||||||
|
3: maxRandDataLengthPrefix (min:1)
|
||||||
|
4: hmac of packedData except the last 4 bytes
|
||||||
|
*/
|
||||||
|
packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4
|
||||||
|
if randDataLength < 128 {
|
||||||
|
packedDataLength -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
macKey := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.userKey)
|
||||||
|
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID)
|
||||||
|
a.packID++
|
||||||
|
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, uint16(packedDataLength))
|
||||||
|
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-2:])[:2])
|
||||||
|
a.packRandData(poolBuf, randDataLength)
|
||||||
|
poolBuf.Write(data)
|
||||||
|
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func trapezoidRandom(max int, d float64) int {
|
||||||
|
base := rand.Float64()
|
||||||
|
if d-0 > 1e-6 {
|
||||||
|
a := 1 - d
|
||||||
|
base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d)
|
||||||
|
}
|
||||||
|
return int(base * float64(max))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int) int {
|
||||||
|
if fullDataLength >= 32*1024-a.Overhead {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// 1460: tcp_mss
|
||||||
|
revLength := 1460 - dataLength - 9
|
||||||
|
if revLength == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if revLength < 0 {
|
||||||
|
if revLength > -1460 {
|
||||||
|
return trapezoidRandom(revLength+1460, -0.3)
|
||||||
|
}
|
||||||
|
return rand.Intn(32)
|
||||||
|
}
|
||||||
|
if dataLength > 900 {
|
||||||
|
return rand.Intn(revLength)
|
||||||
|
}
|
||||||
|
return trapezoidRandom(revLength, -0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dataLength := len(data)
|
||||||
|
randDataLength := a.getRandDataLengthForPackAuthData(dataLength)
|
||||||
|
/*
|
||||||
|
7: checkHead(1) and hmac of checkHead(6)
|
||||||
|
4: userID
|
||||||
|
16: encrypted data of authdata(12), uint16 BigEndian packedDataLength(2) and uint16 BigEndian randDataLength(2)
|
||||||
|
4: hmac of userID and encrypted data
|
||||||
|
4: hmac of packedAuthData except the last 4 bytes
|
||||||
|
*/
|
||||||
|
packedAuthDataLength := 7 + 4 + 16 + 4 + randDataLength + dataLength + 4
|
||||||
|
|
||||||
|
macKey := pool.Get(len(a.iv) + len(a.Key))
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.iv)
|
||||||
|
copy(macKey[len(a.iv):], a.Key)
|
||||||
|
|
||||||
|
poolBuf.WriteByte(byte(rand.Intn(256)))
|
||||||
|
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6])
|
||||||
|
poolBuf.Write(a.userID[:])
|
||||||
|
err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt)
|
||||||
|
if err != nil {
|
||||||
|
poolBuf.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[7:])[:4])
|
||||||
|
tools.AppendRandBytes(poolBuf, randDataLength)
|
||||||
|
poolBuf.Write(data)
|
||||||
|
poolBuf.Write(a.hmac(a.userKey, poolBuf.Bytes())[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) getRandDataLengthForPackAuthData(size int) int {
|
||||||
|
if size > 400 {
|
||||||
|
return rand.Intn(512)
|
||||||
|
}
|
||||||
|
return rand.Intn(1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) {
|
||||||
|
if size < 128 {
|
||||||
|
poolBuf.WriteByte(byte(size + 1))
|
||||||
|
tools.AppendRandBytes(poolBuf, size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolBuf.WriteByte(255)
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, uint16(size+3))
|
||||||
|
tools.AppendRandBytes(poolBuf, size)
|
||||||
|
}
|
310
component/ssr/protocol/auth_chain_a.go
Normal file
310
component/ssr/protocol/auth_chain_a.go
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rc4"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_chain_a", newAuthChainA, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
type randDataLengthMethod func(int, []byte, *tools.XorShift128Plus) int
|
||||||
|
|
||||||
|
type authChainA struct {
|
||||||
|
*Base
|
||||||
|
*authData
|
||||||
|
*userData
|
||||||
|
iv []byte
|
||||||
|
salt string
|
||||||
|
hasSentHeader bool
|
||||||
|
rawTrans bool
|
||||||
|
lastClientHash []byte
|
||||||
|
lastServerHash []byte
|
||||||
|
encrypter cipher.Stream
|
||||||
|
decrypter cipher.Stream
|
||||||
|
randomClient tools.XorShift128Plus
|
||||||
|
randomServer tools.XorShift128Plus
|
||||||
|
randDataLength randDataLengthMethod
|
||||||
|
packID uint32
|
||||||
|
recvID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthChainA(b *Base) Protocol {
|
||||||
|
a := &authChainA{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
userData: &userData{},
|
||||||
|
salt: "auth_chain_a",
|
||||||
|
}
|
||||||
|
a.initUserData()
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) initUserData() {
|
||||||
|
params := strings.Split(a.Param, ":")
|
||||||
|
if len(params) > 1 {
|
||||||
|
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
|
||||||
|
binary.LittleEndian.PutUint32(a.userID[:], uint32(userID))
|
||||||
|
a.userKey = []byte(params[1])
|
||||||
|
} else {
|
||||||
|
log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(a.userKey) == 0 {
|
||||||
|
a.userKey = a.Key
|
||||||
|
rand.Read(a.userID[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) StreamConn(c net.Conn, iv []byte) net.Conn {
|
||||||
|
p := &authChainA{
|
||||||
|
Base: a.Base,
|
||||||
|
authData: a.next(),
|
||||||
|
userData: a.userData,
|
||||||
|
salt: a.salt,
|
||||||
|
packID: 1,
|
||||||
|
recvID: 1,
|
||||||
|
}
|
||||||
|
p.iv = iv
|
||||||
|
p.randDataLength = p.getRandLength
|
||||||
|
return &Conn{Conn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) PacketConn(c net.PacketConn) net.PacketConn {
|
||||||
|
p := &authChainA{
|
||||||
|
Base: a.Base,
|
||||||
|
salt: a.salt,
|
||||||
|
userData: a.userData,
|
||||||
|
}
|
||||||
|
return &PacketConn{PacketConn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) Decode(dst, src *bytes.Buffer) error {
|
||||||
|
if a.rawTrans {
|
||||||
|
dst.ReadFrom(src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for src.Len() > 4 {
|
||||||
|
macKey := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.userKey)
|
||||||
|
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID)
|
||||||
|
|
||||||
|
dataLength := int(binary.LittleEndian.Uint16(src.Bytes()[:2]) ^ binary.LittleEndian.Uint16(a.lastServerHash[14:16]))
|
||||||
|
randDataLength := a.randDataLength(dataLength, a.lastServerHash, &a.randomServer)
|
||||||
|
length := dataLength + randDataLength
|
||||||
|
|
||||||
|
if length >= 4096 {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthChainLengthError
|
||||||
|
}
|
||||||
|
|
||||||
|
if 4+length > src.Len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
serverHash := tools.HmacMD5(macKey, src.Bytes()[:length+2])
|
||||||
|
if !bytes.Equal(serverHash[:2], src.Bytes()[length+2:length+4]) {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthChainChksumError
|
||||||
|
}
|
||||||
|
a.lastServerHash = serverHash
|
||||||
|
|
||||||
|
pos := 2
|
||||||
|
if dataLength > 0 && randDataLength > 0 {
|
||||||
|
pos += getRandStartPos(randDataLength, &a.randomServer)
|
||||||
|
}
|
||||||
|
wantedData := src.Bytes()[pos : pos+dataLength]
|
||||||
|
a.decrypter.XORKeyStream(wantedData, wantedData)
|
||||||
|
if a.recvID == 1 {
|
||||||
|
dst.Write(wantedData[2:])
|
||||||
|
} else {
|
||||||
|
dst.Write(wantedData)
|
||||||
|
}
|
||||||
|
a.recvID++
|
||||||
|
src.Next(length + 4)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) Encode(buf *bytes.Buffer, b []byte) error {
|
||||||
|
if !a.hasSentHeader {
|
||||||
|
dataLength := getDataLength(b)
|
||||||
|
a.packAuthData(buf, b[:dataLength])
|
||||||
|
b = b[dataLength:]
|
||||||
|
a.hasSentHeader = true
|
||||||
|
}
|
||||||
|
for len(b) > 2800 {
|
||||||
|
a.packData(buf, b[:2800])
|
||||||
|
b = b[2800:]
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
a.packData(buf, b)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) DecodePacket(b []byte) ([]byte, error) {
|
||||||
|
if len(b) < 9 {
|
||||||
|
return nil, errAuthChainLengthError
|
||||||
|
}
|
||||||
|
if !bytes.Equal(tools.HmacMD5(a.userKey, b[:len(b)-1])[:1], b[len(b)-1:]) {
|
||||||
|
return nil, errAuthChainChksumError
|
||||||
|
}
|
||||||
|
md5Data := tools.HmacMD5(a.Key, b[len(b)-8:len(b)-1])
|
||||||
|
|
||||||
|
randDataLength := udpGetRandLength(md5Data, &a.randomServer)
|
||||||
|
|
||||||
|
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16)
|
||||||
|
rc4Cipher, err := rc4.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wantedData := b[:len(b)-8-randDataLength]
|
||||||
|
rc4Cipher.XORKeyStream(wantedData, wantedData)
|
||||||
|
return wantedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) EncodePacket(buf *bytes.Buffer, b []byte) error {
|
||||||
|
authData := pool.Get(3)
|
||||||
|
defer pool.Put(authData)
|
||||||
|
rand.Read(authData)
|
||||||
|
|
||||||
|
md5Data := tools.HmacMD5(a.Key, authData)
|
||||||
|
|
||||||
|
randDataLength := udpGetRandLength(md5Data, &a.randomClient)
|
||||||
|
|
||||||
|
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16)
|
||||||
|
rc4Cipher, err := rc4.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rc4Cipher.XORKeyStream(b, b)
|
||||||
|
|
||||||
|
buf.Write(b)
|
||||||
|
tools.AppendRandBytes(buf, randDataLength)
|
||||||
|
buf.Write(authData)
|
||||||
|
binary.Write(buf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(md5Data[:4]))
|
||||||
|
buf.Write(tools.HmacMD5(a.userKey, buf.Bytes())[:1])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) packAuthData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
/*
|
||||||
|
dataLength := len(data)
|
||||||
|
12: checkHead(4) and hmac of checkHead(8)
|
||||||
|
4: uint32 LittleEndian uid (uid = userID ^ last client hash)
|
||||||
|
16: encrypted data of authdata(12), uint16 LittleEndian overhead(2) and uint16 LittleEndian number zero(2)
|
||||||
|
4: last server hash(4)
|
||||||
|
packedAuthDataLength := 12 + 4 + 16 + 4 + dataLength
|
||||||
|
*/
|
||||||
|
|
||||||
|
macKey := pool.Get(len(a.iv) + len(a.Key))
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.iv)
|
||||||
|
copy(macKey[len(a.iv):], a.Key)
|
||||||
|
|
||||||
|
// check head
|
||||||
|
tools.AppendRandBytes(poolBuf, 4)
|
||||||
|
a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes())
|
||||||
|
a.initRC4Cipher()
|
||||||
|
poolBuf.Write(a.lastClientHash[:8])
|
||||||
|
// uid
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(a.lastClientHash[8:12]))
|
||||||
|
// encrypted data
|
||||||
|
err := a.putEncryptedData(poolBuf, a.userKey, [2]int{a.Overhead, 0}, a.salt)
|
||||||
|
if err != nil {
|
||||||
|
poolBuf.Reset()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// last server hash
|
||||||
|
a.lastServerHash = tools.HmacMD5(a.userKey, poolBuf.Bytes()[12:])
|
||||||
|
poolBuf.Write(a.lastServerHash[:4])
|
||||||
|
// packed data
|
||||||
|
a.packData(poolBuf, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) packData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
a.encrypter.XORKeyStream(data, data)
|
||||||
|
|
||||||
|
macKey := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(macKey)
|
||||||
|
copy(macKey, a.userKey)
|
||||||
|
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID)
|
||||||
|
a.packID++
|
||||||
|
|
||||||
|
length := uint16(len(data)) ^ binary.LittleEndian.Uint16(a.lastClientHash[14:16])
|
||||||
|
|
||||||
|
originalLength := poolBuf.Len()
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, length)
|
||||||
|
a.putMixedRandDataAndData(poolBuf, data)
|
||||||
|
a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()[originalLength:])
|
||||||
|
poolBuf.Write(a.lastClientHash[:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) putMixedRandDataAndData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
randDataLength := a.randDataLength(len(data), a.lastClientHash, &a.randomClient)
|
||||||
|
if len(data) == 0 {
|
||||||
|
tools.AppendRandBytes(poolBuf, randDataLength)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if randDataLength > 0 {
|
||||||
|
startPos := getRandStartPos(randDataLength, &a.randomClient)
|
||||||
|
tools.AppendRandBytes(poolBuf, startPos)
|
||||||
|
poolBuf.Write(data)
|
||||||
|
tools.AppendRandBytes(poolBuf, randDataLength-startPos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolBuf.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandStartPos(length int, random *tools.XorShift128Plus) int {
|
||||||
|
if length == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(random.Next()%8589934609) % length
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) getRandLength(length int, lastHash []byte, random *tools.XorShift128Plus) int {
|
||||||
|
if length > 1440 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
random.InitFromBinAndLength(lastHash, length)
|
||||||
|
if length > 1300 {
|
||||||
|
return int(random.Next() % 31)
|
||||||
|
}
|
||||||
|
if length > 900 {
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
||||||
|
if length > 400 {
|
||||||
|
return int(random.Next() % 521)
|
||||||
|
}
|
||||||
|
return int(random.Next() % 1021)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainA) initRC4Cipher() {
|
||||||
|
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(a.lastClientHash), 16)
|
||||||
|
a.encrypter, _ = rc4.NewCipher(key)
|
||||||
|
a.decrypter, _ = rc4.NewCipher(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpGetRandLength(lastHash []byte, random *tools.XorShift128Plus) int {
|
||||||
|
random.InitFromBin(lastHash)
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
97
component/ssr/protocol/auth_chain_b.go
Normal file
97
component/ssr/protocol/auth_chain_b.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_chain_b", newAuthChainB, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authChainB struct {
|
||||||
|
*authChainA
|
||||||
|
dataSizeList []int
|
||||||
|
dataSizeList2 []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthChainB(b *Base) Protocol {
|
||||||
|
a := &authChainB{
|
||||||
|
authChainA: &authChainA{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
userData: &userData{},
|
||||||
|
salt: "auth_chain_b",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
a.initUserData()
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainB) StreamConn(c net.Conn, iv []byte) net.Conn {
|
||||||
|
p := &authChainB{
|
||||||
|
authChainA: &authChainA{
|
||||||
|
Base: a.Base,
|
||||||
|
authData: a.next(),
|
||||||
|
userData: a.userData,
|
||||||
|
salt: a.salt,
|
||||||
|
packID: 1,
|
||||||
|
recvID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.iv = iv
|
||||||
|
p.randDataLength = p.getRandLength
|
||||||
|
p.initDataSize()
|
||||||
|
return &Conn{Conn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainB) initDataSize() {
|
||||||
|
a.dataSizeList = a.dataSizeList[:0]
|
||||||
|
a.dataSizeList2 = a.dataSizeList2[:0]
|
||||||
|
|
||||||
|
a.randomServer.InitFromBin(a.Key)
|
||||||
|
length := a.randomServer.Next()%8 + 4
|
||||||
|
for ; length > 0; length-- {
|
||||||
|
a.dataSizeList = append(a.dataSizeList, int(a.randomServer.Next()%2340%2040%1440))
|
||||||
|
}
|
||||||
|
sort.Ints(a.dataSizeList)
|
||||||
|
|
||||||
|
length = a.randomServer.Next()%16 + 8
|
||||||
|
for ; length > 0; length-- {
|
||||||
|
a.dataSizeList2 = append(a.dataSizeList2, int(a.randomServer.Next()%2340%2040%1440))
|
||||||
|
}
|
||||||
|
sort.Ints(a.dataSizeList2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChainB) getRandLength(length int, lashHash []byte, random *tools.XorShift128Plus) int {
|
||||||
|
if length >= 1440 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
random.InitFromBinAndLength(lashHash, length)
|
||||||
|
pos := sort.Search(len(a.dataSizeList), func(i int) bool { return a.dataSizeList[i] >= length+a.Overhead })
|
||||||
|
finalPos := pos + int(random.Next()%uint64(len(a.dataSizeList)))
|
||||||
|
if finalPos < len(a.dataSizeList) {
|
||||||
|
return a.dataSizeList[finalPos] - length - a.Overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = sort.Search(len(a.dataSizeList2), func(i int) bool { return a.dataSizeList2[i] >= length+a.Overhead })
|
||||||
|
finalPos = pos + int(random.Next()%uint64(len(a.dataSizeList2)))
|
||||||
|
if finalPos < len(a.dataSizeList2) {
|
||||||
|
return a.dataSizeList2[finalPos] - length - a.Overhead
|
||||||
|
}
|
||||||
|
if finalPos < pos+len(a.dataSizeList2)-1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if length > 1300 {
|
||||||
|
return int(random.Next() % 31)
|
||||||
|
}
|
||||||
|
if length > 900 {
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
||||||
|
if length > 400 {
|
||||||
|
return int(random.Next() % 521)
|
||||||
|
}
|
||||||
|
return int(random.Next() % 1021)
|
||||||
|
}
|
182
component/ssr/protocol/auth_sha1_v4.go
Normal file
182
component/ssr/protocol/auth_sha1_v4.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"hash/adler32"
|
||||||
|
"hash/crc32"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_sha1_v4", newAuthSHA1V4, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
type authSHA1V4 struct {
|
||||||
|
*Base
|
||||||
|
*authData
|
||||||
|
iv []byte
|
||||||
|
hasSentHeader bool
|
||||||
|
rawTrans bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthSHA1V4(b *Base) Protocol {
|
||||||
|
return &authSHA1V4{Base: b, authData: &authData{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) StreamConn(c net.Conn, iv []byte) net.Conn {
|
||||||
|
p := &authSHA1V4{Base: a.Base, authData: a.next()}
|
||||||
|
p.iv = iv
|
||||||
|
return &Conn{Conn: c, Protocol: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) PacketConn(c net.PacketConn) net.PacketConn {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) Decode(dst, src *bytes.Buffer) error {
|
||||||
|
if a.rawTrans {
|
||||||
|
dst.ReadFrom(src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for src.Len() > 4 {
|
||||||
|
if uint16(crc32.ChecksumIEEE(src.Bytes()[:2])&0xffff) != binary.LittleEndian.Uint16(src.Bytes()[2:4]) {
|
||||||
|
src.Reset()
|
||||||
|
return errAuthSHA1V4CRC32Error
|
||||||
|
}
|
||||||
|
|
||||||
|
length := int(binary.BigEndian.Uint16(src.Bytes()[:2]))
|
||||||
|
if length >= 8192 || length < 7 {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthSHA1V4LengthError
|
||||||
|
}
|
||||||
|
if length > src.Len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if adler32.Checksum(src.Bytes()[:length-4]) != binary.LittleEndian.Uint32(src.Bytes()[length-4:length]) {
|
||||||
|
a.rawTrans = true
|
||||||
|
src.Reset()
|
||||||
|
return errAuthSHA1V4Adler32Error
|
||||||
|
}
|
||||||
|
|
||||||
|
pos := int(src.Bytes()[4])
|
||||||
|
if pos < 255 {
|
||||||
|
pos += 4
|
||||||
|
} else {
|
||||||
|
pos = int(binary.BigEndian.Uint16(src.Bytes()[5:7])) + 4
|
||||||
|
}
|
||||||
|
dst.Write(src.Bytes()[pos : length-4])
|
||||||
|
src.Next(length)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) Encode(buf *bytes.Buffer, b []byte) error {
|
||||||
|
if !a.hasSentHeader {
|
||||||
|
dataLength := getDataLength(b)
|
||||||
|
|
||||||
|
a.packAuthData(buf, b[:dataLength])
|
||||||
|
b = b[dataLength:]
|
||||||
|
|
||||||
|
a.hasSentHeader = true
|
||||||
|
}
|
||||||
|
for len(b) > 8100 {
|
||||||
|
a.packData(buf, b[:8100])
|
||||||
|
b = b[8100:]
|
||||||
|
}
|
||||||
|
if len(b) > 0 {
|
||||||
|
a.packData(buf, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, error) { return b, nil }
|
||||||
|
|
||||||
|
func (a *authSHA1V4) EncodePacket(buf *bytes.Buffer, b []byte) error {
|
||||||
|
buf.Write(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
dataLength := len(data)
|
||||||
|
randDataLength := a.getRandDataLength(dataLength)
|
||||||
|
/*
|
||||||
|
2: uint16 BigEndian packedDataLength
|
||||||
|
2: uint16 LittleEndian crc32Data & 0xffff
|
||||||
|
3: maxRandDataLengthPrefix (min:1)
|
||||||
|
4: adler32Data
|
||||||
|
*/
|
||||||
|
packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4
|
||||||
|
if randDataLength < 128 {
|
||||||
|
packedDataLength -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.Write(poolBuf, binary.BigEndian, uint16(packedDataLength))
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, uint16(crc32.ChecksumIEEE(poolBuf.Bytes()[poolBuf.Len()-2:])&0xffff))
|
||||||
|
a.packRandData(poolBuf, randDataLength)
|
||||||
|
poolBuf.Write(data)
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, adler32.Checksum(poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packAuthData(poolBuf *bytes.Buffer, data []byte) {
|
||||||
|
dataLength := len(data)
|
||||||
|
randDataLength := a.getRandDataLength(12 + dataLength)
|
||||||
|
/*
|
||||||
|
2: uint16 BigEndian packedAuthDataLength
|
||||||
|
4: uint32 LittleEndian crc32Data
|
||||||
|
3: maxRandDataLengthPrefix (min: 1)
|
||||||
|
12: authDataLength
|
||||||
|
10: hmacSHA1DataLength
|
||||||
|
*/
|
||||||
|
packedAuthDataLength := 2 + 4 + 3 + randDataLength + 12 + dataLength + 10
|
||||||
|
if randDataLength < 128 {
|
||||||
|
packedAuthDataLength -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
salt := []byte("auth_sha1_v4")
|
||||||
|
crcData := pool.Get(len(salt) + len(a.Key) + 2)
|
||||||
|
defer pool.Put(crcData)
|
||||||
|
binary.BigEndian.PutUint16(crcData, uint16(packedAuthDataLength))
|
||||||
|
copy(crcData[2:], salt)
|
||||||
|
copy(crcData[2+len(salt):], a.Key)
|
||||||
|
|
||||||
|
key := pool.Get(len(a.iv) + len(a.Key))
|
||||||
|
defer pool.Put(key)
|
||||||
|
copy(key, a.iv)
|
||||||
|
copy(key[len(a.iv):], a.Key)
|
||||||
|
|
||||||
|
poolBuf.Write(crcData[:2])
|
||||||
|
binary.Write(poolBuf, binary.LittleEndian, crc32.ChecksumIEEE(crcData))
|
||||||
|
a.packRandData(poolBuf, randDataLength)
|
||||||
|
a.putAuthData(poolBuf)
|
||||||
|
poolBuf.Write(data)
|
||||||
|
poolBuf.Write(tools.HmacSHA1(key, poolBuf.Bytes()[poolBuf.Len()-packedAuthDataLength+10:])[:10])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packRandData(poolBuf *bytes.Buffer, size int) {
|
||||||
|
if size < 128 {
|
||||||
|
poolBuf.WriteByte(byte(size + 1))
|
||||||
|
tools.AppendRandBytes(poolBuf, size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolBuf.WriteByte(255)
|
||||||
|
binary.Write(poolBuf, binary.BigEndian, uint16(size+3))
|
||||||
|
tools.AppendRandBytes(poolBuf, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) getRandDataLength(size int) int {
|
||||||
|
if size > 1200 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if size > 400 {
|
||||||
|
return rand.Intn(256)
|
||||||
|
}
|
||||||
|
return rand.Intn(512)
|
||||||
|
}
|
78
component/ssr/protocol/base.go
Normal file
78
component/ssr/protocol/base.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Base struct {
|
||||||
|
Key []byte
|
||||||
|
Overhead int
|
||||||
|
Param string
|
||||||
|
}
|
||||||
|
|
||||||
|
type userData struct {
|
||||||
|
userKey []byte
|
||||||
|
userID [4]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type authData struct {
|
||||||
|
clientID [4]byte
|
||||||
|
connectionID uint32
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authData) next() *authData {
|
||||||
|
r := &authData{}
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
if a.connectionID > 0xff000000 || a.connectionID == 0 {
|
||||||
|
rand.Read(a.clientID[:])
|
||||||
|
a.connectionID = rand.Uint32() & 0xffffff
|
||||||
|
}
|
||||||
|
a.connectionID++
|
||||||
|
copy(r.clientID[:], a.clientID[:])
|
||||||
|
r.connectionID = a.connectionID
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authData) putAuthData(buf *bytes.Buffer) {
|
||||||
|
binary.Write(buf, binary.LittleEndian, uint32(time.Now().Unix()))
|
||||||
|
buf.Write(a.clientID[:])
|
||||||
|
binary.Write(buf, binary.LittleEndian, a.connectionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authData) putEncryptedData(b *bytes.Buffer, userKey []byte, paddings [2]int, salt string) error {
|
||||||
|
encrypt := pool.Get(16)
|
||||||
|
defer pool.Put(encrypt)
|
||||||
|
binary.LittleEndian.PutUint32(encrypt, uint32(time.Now().Unix()))
|
||||||
|
copy(encrypt[4:], a.clientID[:])
|
||||||
|
binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID)
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[12:], uint16(paddings[0]))
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[14:], uint16(paddings[1]))
|
||||||
|
|
||||||
|
cipherKey := core.Kdf(base64.StdEncoding.EncodeToString(userKey)+salt, 16)
|
||||||
|
block, err := aes.NewCipher(cipherKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("New cipher error: %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iv := bytes.Repeat([]byte{0}, 16)
|
||||||
|
cbcCipher := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
|
||||||
|
cbcCipher.CryptBlocks(encrypt, encrypt)
|
||||||
|
|
||||||
|
b.Write(encrypt)
|
||||||
|
return nil
|
||||||
|
}
|
33
component/ssr/protocol/origin.go
Normal file
33
component/ssr/protocol/origin.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type origin struct{}
|
||||||
|
|
||||||
|
func init() { register("origin", newOrigin, 0) }
|
||||||
|
|
||||||
|
func newOrigin(b *Base) Protocol { return &origin{} }
|
||||||
|
|
||||||
|
func (o *origin) StreamConn(c net.Conn, iv []byte) net.Conn { return c }
|
||||||
|
|
||||||
|
func (o *origin) PacketConn(c net.PacketConn) net.PacketConn { return c }
|
||||||
|
|
||||||
|
func (o *origin) Decode(dst, src *bytes.Buffer) error {
|
||||||
|
dst.ReadFrom(src)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) Encode(buf *bytes.Buffer, b []byte) error {
|
||||||
|
buf.Write(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) DecodePacket(b []byte) ([]byte, error) { return b, nil }
|
||||||
|
|
||||||
|
func (o *origin) EncodePacket(buf *bytes.Buffer, b []byte) error {
|
||||||
|
buf.Write(b)
|
||||||
|
return nil
|
||||||
|
}
|
38
component/ssr/protocol/packet.go
Normal file
38
component/ssr/protocol/packet.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
buf := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
err := c.EncodePacket(buf, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = c.PacketConn.WriteTo(buf.Bytes(), addr)
|
||||||
|
return len(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
|
n, addr, err := c.PacketConn.ReadFrom(b)
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
decoded, err := c.DecodePacket(b[:n])
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
copy(b, decoded)
|
||||||
|
return len(decoded), addr, nil
|
||||||
|
}
|
76
component/ssr/protocol/protocol.go
Normal file
76
component/ssr/protocol/protocol.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errAuthSHA1V4CRC32Error = errors.New("auth_sha1_v4 decode data wrong crc32")
|
||||||
|
errAuthSHA1V4LengthError = errors.New("auth_sha1_v4 decode data wrong length")
|
||||||
|
errAuthSHA1V4Adler32Error = errors.New("auth_sha1_v4 decode data wrong adler32")
|
||||||
|
errAuthAES128MACError = errors.New("auth_aes128 decode data wrong mac")
|
||||||
|
errAuthAES128LengthError = errors.New("auth_aes128 decode data wrong length")
|
||||||
|
errAuthAES128ChksumError = errors.New("auth_aes128 decode data wrong checksum")
|
||||||
|
errAuthChainLengthError = errors.New("auth_chain decode data wrong length")
|
||||||
|
errAuthChainChksumError = errors.New("auth_chain decode data wrong checksum")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Protocol interface {
|
||||||
|
StreamConn(net.Conn, []byte) net.Conn
|
||||||
|
PacketConn(net.PacketConn) net.PacketConn
|
||||||
|
Decode(dst, src *bytes.Buffer) error
|
||||||
|
Encode(buf *bytes.Buffer, b []byte) error
|
||||||
|
DecodePacket([]byte) ([]byte, error)
|
||||||
|
EncodePacket(buf *bytes.Buffer, b []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolCreator func(b *Base) Protocol
|
||||||
|
|
||||||
|
var protocolList = make(map[string]struct {
|
||||||
|
overhead int
|
||||||
|
new protocolCreator
|
||||||
|
})
|
||||||
|
|
||||||
|
func register(name string, c protocolCreator, o int) {
|
||||||
|
protocolList[name] = struct {
|
||||||
|
overhead int
|
||||||
|
new protocolCreator
|
||||||
|
}{overhead: o, new: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PickProtocol(name string, b *Base) (Protocol, error) {
|
||||||
|
if choice, ok := protocolList[name]; ok {
|
||||||
|
b.Overhead += choice.overhead
|
||||||
|
return choice.new(b), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("protocol %s not supported", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHeadSize(b []byte, defaultValue int) int {
|
||||||
|
if len(b) < 2 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
headType := b[0] & 7
|
||||||
|
switch headType {
|
||||||
|
case 1:
|
||||||
|
return 7
|
||||||
|
case 4:
|
||||||
|
return 19
|
||||||
|
case 3:
|
||||||
|
return 4 + int(b[1])
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDataLength(b []byte) int {
|
||||||
|
bLength := len(b)
|
||||||
|
dataLength := getHeadSize(b, 30) + rand.Intn(32)
|
||||||
|
if bLength < dataLength {
|
||||||
|
return bLength
|
||||||
|
}
|
||||||
|
return dataLength
|
||||||
|
}
|
52
component/ssr/protocol/stream.go
Normal file
52
component/ssr/protocol/stream.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
Protocol
|
||||||
|
decoded bytes.Buffer
|
||||||
|
underDecoded bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
|
if c.decoded.Len() > 0 {
|
||||||
|
return c.decoded.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
n, err := c.Conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
c.underDecoded.Write(buf[:n])
|
||||||
|
err = c.Decode(&c.decoded, &c.underDecoded)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
n, _ = c.decoded.Read(b)
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (int, error) {
|
||||||
|
bLength := len(b)
|
||||||
|
buf := tools.BufPool.Get().(*bytes.Buffer)
|
||||||
|
defer tools.BufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
err := c.Encode(buf, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = c.Conn.Write(buf.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return bLength, nil
|
||||||
|
}
|
18
component/ssr/tools/bufPool.go
Normal file
18
component/ssr/tools/bufPool.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var BufPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
||||||
|
|
||||||
|
func AppendRandBytes(b *bytes.Buffer, length int) {
|
||||||
|
randBytes := pool.Get(length)
|
||||||
|
defer pool.Put(randBytes)
|
||||||
|
rand.Read(randBytes)
|
||||||
|
b.Write(randBytes)
|
||||||
|
}
|
33
component/ssr/tools/crypto.go
Normal file
33
component/ssr/tools/crypto.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HmacSHA1Len = 10
|
||||||
|
|
||||||
|
func HmacMD5(key, data []byte) []byte {
|
||||||
|
hmacMD5 := hmac.New(md5.New, key)
|
||||||
|
hmacMD5.Write(data)
|
||||||
|
return hmacMD5.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HmacSHA1(key, data []byte) []byte {
|
||||||
|
hmacSHA1 := hmac.New(sha1.New, key)
|
||||||
|
hmacSHA1.Write(data)
|
||||||
|
return hmacSHA1.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MD5Sum(b []byte) []byte {
|
||||||
|
h := md5.New()
|
||||||
|
h.Write(b)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SHA1Sum(b []byte) []byte {
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write(b)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
57
component/ssr/tools/random.go
Normal file
57
component/ssr/tools/random.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// XorShift128Plus - a pseudorandom number generator
|
||||||
|
type XorShift128Plus struct {
|
||||||
|
s [2]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XorShift128Plus) Next() uint64 {
|
||||||
|
x := r.s[0]
|
||||||
|
y := r.s[1]
|
||||||
|
r.s[0] = y
|
||||||
|
x ^= x << 23
|
||||||
|
x ^= y ^ (x >> 17) ^ (y >> 26)
|
||||||
|
r.s[1] = x
|
||||||
|
return x + y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XorShift128Plus) InitFromBin(bin []byte) {
|
||||||
|
var full []byte
|
||||||
|
if len(bin) < 16 {
|
||||||
|
full := pool.Get(16)[:0]
|
||||||
|
defer pool.Put(full)
|
||||||
|
full = append(full, bin...)
|
||||||
|
for len(full) < 16 {
|
||||||
|
full = append(full, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
full = bin
|
||||||
|
}
|
||||||
|
r.s[0] = binary.LittleEndian.Uint64(full[:8])
|
||||||
|
r.s[1] = binary.LittleEndian.Uint64(full[8:16])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *XorShift128Plus) InitFromBinAndLength(bin []byte, length int) {
|
||||||
|
var full []byte
|
||||||
|
if len(bin) < 16 {
|
||||||
|
full := pool.Get(16)[:0]
|
||||||
|
defer pool.Put(full)
|
||||||
|
full = append(full, bin...)
|
||||||
|
for len(full) < 16 {
|
||||||
|
full = append(full, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
full = bin
|
||||||
|
binary.LittleEndian.PutUint16(full, uint16(length))
|
||||||
|
r.s[0] = binary.LittleEndian.Uint64(full[:8])
|
||||||
|
r.s[1] = binary.LittleEndian.Uint64(full[8:16])
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
r.Next()
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
wildcard = "*"
|
wildcard = "*"
|
||||||
dotWildcard = ""
|
dotWildcard = ""
|
||||||
domainStep = "."
|
complexWildcard = "+"
|
||||||
|
domainStep = "."
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -16,9 +17,9 @@ var (
|
|||||||
ErrInvalidDomain = errors.New("invalid domain")
|
ErrInvalidDomain = errors.New("invalid domain")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Trie contains the main logic for adding and searching nodes for domain segments.
|
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
|
||||||
// support wildcard domain (e.g *.google.com)
|
// support wildcard domain (e.g *.google.com)
|
||||||
type Trie struct {
|
type DomainTrie struct {
|
||||||
root *Node
|
root *Node
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +30,11 @@ func validAndSplitDomain(domain string) ([]string, bool) {
|
|||||||
|
|
||||||
parts := strings.Split(domain, domainStep)
|
parts := strings.Split(domain, domainStep)
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
return nil, false
|
if parts[0] == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts, true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, part := range parts[1:] {
|
for _, part := range parts[1:] {
|
||||||
@ -47,12 +52,25 @@ func validAndSplitDomain(domain string) ([]string, bool) {
|
|||||||
// 2. *.example.com
|
// 2. *.example.com
|
||||||
// 3. subdomain.*.example.com
|
// 3. subdomain.*.example.com
|
||||||
// 4. .example.com
|
// 4. .example.com
|
||||||
func (t *Trie) Insert(domain string, data interface{}) error {
|
// 5. +.example.com
|
||||||
|
func (t *DomainTrie) Insert(domain string, data interface{}) error {
|
||||||
parts, valid := validAndSplitDomain(domain)
|
parts, valid := validAndSplitDomain(domain)
|
||||||
if !valid {
|
if !valid {
|
||||||
return ErrInvalidDomain
|
return ErrInvalidDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parts[0] == complexWildcard {
|
||||||
|
t.insert(parts[1:], data)
|
||||||
|
parts[0] = dotWildcard
|
||||||
|
t.insert(parts, data)
|
||||||
|
} else {
|
||||||
|
t.insert(parts, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DomainTrie) insert(parts []string, data interface{}) {
|
||||||
node := t.root
|
node := t.root
|
||||||
// reverse storage domain part to save space
|
// reverse storage domain part to save space
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
@ -65,7 +83,6 @@ func (t *Trie) Insert(domain string, data interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
node.Data = data
|
node.Data = data
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search is the most important part of the Trie.
|
// Search is the most important part of the Trie.
|
||||||
@ -73,54 +90,42 @@ func (t *Trie) Insert(domain string, data interface{}) error {
|
|||||||
// 1. static part
|
// 1. static part
|
||||||
// 2. wildcard domain
|
// 2. wildcard domain
|
||||||
// 2. dot wildcard domain
|
// 2. dot wildcard domain
|
||||||
func (t *Trie) Search(domain string) *Node {
|
func (t *DomainTrie) Search(domain string) *Node {
|
||||||
parts, valid := validAndSplitDomain(domain)
|
parts, valid := validAndSplitDomain(domain)
|
||||||
if !valid || parts[0] == "" {
|
if !valid || parts[0] == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n := t.root
|
n := t.search(t.root, parts)
|
||||||
var dotWildcardNode *Node
|
|
||||||
var wildcardNode *Node
|
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
|
||||||
part := parts[i]
|
|
||||||
|
|
||||||
if node := n.getChild(dotWildcard); node != nil {
|
if n == nil || n.Data == nil {
|
||||||
dotWildcardNode = node
|
|
||||||
}
|
|
||||||
|
|
||||||
child := n.getChild(part)
|
|
||||||
if child == nil && wildcardNode != nil {
|
|
||||||
child = wildcardNode.getChild(part)
|
|
||||||
}
|
|
||||||
wildcardNode = n.getChild(wildcard)
|
|
||||||
|
|
||||||
n = child
|
|
||||||
if n == nil {
|
|
||||||
n = wildcardNode
|
|
||||||
wildcardNode = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == nil {
|
|
||||||
if dotWildcardNode != nil {
|
|
||||||
return dotWildcardNode
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if n.Data == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new, empty Trie.
|
func (t *DomainTrie) search(node *Node, parts []string) *Node {
|
||||||
func New() *Trie {
|
if len(parts) == 0 {
|
||||||
return &Trie{root: newNode(nil)}
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := node.getChild(parts[len(parts)-1]); c != nil {
|
||||||
|
if n := t.search(c, parts[:len(parts)-1]); n != nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := node.getChild(wildcard); c != nil {
|
||||||
|
if n := t.search(c, parts[:len(parts)-1]); n != nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.getChild(dotWildcard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new, empty Trie.
|
||||||
|
func New() *DomainTrie {
|
||||||
|
return &DomainTrie{root: newNode(nil)}
|
||||||
}
|
}
|
@ -14,6 +14,7 @@ func TestTrie_Basic(t *testing.T) {
|
|||||||
domains := []string{
|
domains := []string{
|
||||||
"example.com",
|
"example.com",
|
||||||
"google.com",
|
"google.com",
|
||||||
|
"localhost",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@ -24,6 +25,9 @@ func TestTrie_Basic(t *testing.T) {
|
|||||||
assert.NotNil(t, node)
|
assert.NotNil(t, node)
|
||||||
assert.True(t, node.Data.(net.IP).Equal(localIP))
|
assert.True(t, node.Data.(net.IP).Equal(localIP))
|
||||||
assert.NotNil(t, tree.Insert("", localIP))
|
assert.NotNil(t, tree.Insert("", localIP))
|
||||||
|
assert.Nil(t, tree.Search(""))
|
||||||
|
assert.NotNil(t, tree.Search("localhost"))
|
||||||
|
assert.Nil(t, tree.Search("www.google.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTrie_Wildcard(t *testing.T) {
|
func TestTrie_Wildcard(t *testing.T) {
|
||||||
@ -35,6 +39,11 @@ func TestTrie_Wildcard(t *testing.T) {
|
|||||||
".org",
|
".org",
|
||||||
".example.net",
|
".example.net",
|
||||||
".apple.*",
|
".apple.*",
|
||||||
|
"+.foo.com",
|
||||||
|
"+.stun.*.*",
|
||||||
|
"+.stun.*.*.*",
|
||||||
|
"+.stun.*.*.*.*",
|
||||||
|
"stun.l.google.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@ -46,6 +55,9 @@ func TestTrie_Wildcard(t *testing.T) {
|
|||||||
assert.NotNil(t, tree.Search("test.org"))
|
assert.NotNil(t, tree.Search("test.org"))
|
||||||
assert.NotNil(t, tree.Search("test.example.net"))
|
assert.NotNil(t, tree.Search("test.example.net"))
|
||||||
assert.NotNil(t, tree.Search("test.apple.com"))
|
assert.NotNil(t, tree.Search("test.apple.com"))
|
||||||
|
assert.NotNil(t, tree.Search("test.foo.com"))
|
||||||
|
assert.NotNil(t, tree.Search("foo.com"))
|
||||||
|
assert.NotNil(t, tree.Search("global.stun.website.com"))
|
||||||
assert.Nil(t, tree.Search("foo.sub.example.com"))
|
assert.Nil(t, tree.Search("foo.sub.example.com"))
|
||||||
assert.Nil(t, tree.Search("foo.example.dev"))
|
assert.Nil(t, tree.Search("foo.example.dev"))
|
||||||
assert.Nil(t, tree.Search("example.com"))
|
assert.Nil(t, tree.Search("example.com"))
|
@ -70,8 +70,8 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
|
|||||||
|
|
||||||
func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
|
func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
|
||||||
buf := bufPool.Get().(*bytes.Buffer)
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
defer buf.Reset()
|
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
buf.Write(t.hexPassword)
|
buf.Write(t.hexPassword)
|
||||||
buf.Write(crlf)
|
buf.Write(crlf)
|
||||||
@ -92,8 +92,8 @@ func (t *Trojan) PacketConn(conn net.Conn) net.PacketConn {
|
|||||||
|
|
||||||
func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) {
|
func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) {
|
||||||
buf := bufPool.Get().(*bytes.Buffer)
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
defer buf.Reset()
|
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
buf.Write(socks5Addr)
|
buf.Write(socks5Addr)
|
||||||
binary.Write(buf, binary.BigEndian, uint16(len(payload)))
|
binary.Write(buf, binary.BigEndian, uint16(len(payload)))
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/pool"
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
)
|
)
|
||||||
@ -15,6 +16,8 @@ type aeadWriter struct {
|
|||||||
nonce [32]byte
|
nonce [32]byte
|
||||||
count uint16
|
count uint16
|
||||||
iv []byte
|
iv []byte
|
||||||
|
|
||||||
|
writeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
|
func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
|
||||||
@ -22,8 +25,12 @@ func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *aeadWriter) Write(b []byte) (n int, err error) {
|
func (w *aeadWriter) Write(b []byte) (n int, err error) {
|
||||||
|
w.writeLock.Lock()
|
||||||
buf := pool.Get(pool.RelayBufferSize)
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
defer pool.Put(buf)
|
defer func() {
|
||||||
|
w.writeLock.Unlock()
|
||||||
|
pool.Put(buf)
|
||||||
|
}()
|
||||||
length := len(b)
|
length := len(b)
|
||||||
for {
|
for {
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
@ -86,7 +93,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
|
|||||||
|
|
||||||
size := int(binary.BigEndian.Uint16(r.sizeBuf))
|
size := int(binary.BigEndian.Uint16(r.sizeBuf))
|
||||||
if size > maxSize {
|
if size > maxSize {
|
||||||
return 0, errors.New("Buffer is larger than standard")
|
return 0, errors.New("buffer is larger than standard")
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := pool.Get(size)
|
buf := pool.Get(size)
|
||||||
|
@ -47,7 +47,7 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
|
|||||||
|
|
||||||
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
|
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
|
||||||
if size > maxSize {
|
if size > maxSize {
|
||||||
return 0, errors.New("Buffer is larger than standard")
|
return 0, errors.New("buffer is larger than standard")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(b) >= size {
|
if len(b) >= size {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user