Compare commits
206 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
646bd4eeb4 | |||
752f87a8dc | |||
b979ff0bc2 | |||
b085addbb0 | |||
94e0e4b000 | |||
7d51ab5846 | |||
41a9488cfa | |||
51b6b8521b | |||
e5379558f6 | |||
d1fd57c432 | |||
18603c9a46 | |||
5036f62a9c | |||
2047b8eda1 | |||
0e56c195bb | |||
2b33bfae6b | |||
3fc6d55003 | |||
8eddcd77bf | |||
27dd1d7944 | |||
b1cf2ec837 | |||
84f627f302 | |||
5c03613858 | |||
1825535abd | |||
2750c7ead0 | |||
3ccd7def86 | |||
65dab4e34f | |||
5591e15452 | |||
19f809b1c8 | |||
206767247e | |||
518354e7eb | |||
86dfb6562c | |||
c0a2473160 | |||
70a19b999d | |||
e54f51af81 | |||
b068466108 | |||
b562f28c1b | |||
230e01f078 | |||
082847b403 | |||
9471d80785 | |||
b263095533 | |||
14d5137703 | |||
d8a771916a | |||
f7f30d3406 | |||
b2c9cbb43e | |||
c733f80793 | |||
88d8f93793 | |||
e57a13ed7a | |||
23525ecc15 | |||
814bd05315 | |||
e81b88fb94 | |||
c4994d6429 | |||
0740d20ba0 | |||
9eaca6e4ab | |||
609869bf5a | |||
d68339cea7 | |||
0f4cdbf187 | |||
f3f8e7e52f | |||
8d07c1eb3e | |||
46edae9896 | |||
df0ab6aa8e | |||
7b48138ad0 | |||
e9032c55fa | |||
d75cb069d9 | |||
f69f635e0b | |||
8b5e511426 | |||
6641bf7c07 | |||
afc9f3f59a | |||
a55be58c01 | |||
dcf97ff5b4 | |||
72c0af9739 | |||
b0f9c6afa8 | |||
19bb0b655c | |||
26ce3e8814 | |||
aa207ec664 | |||
c626b988a6 | |||
82c387e92b | |||
14fb789002 | |||
9071351022 | |||
f688eda2c2 | |||
2810533df4 | |||
6b7144acce | |||
e68c0d088b | |||
2c0cc374d3 | |||
86d3d77a7f | |||
14a3ff32f6 | |||
50704eaeeb | |||
38458cc4d0 | |||
b19a49335f | |||
9dda932494 | |||
6ce7b6ef83 |
100
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
100
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[Bug]"
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
感谢你向 Clash Core 提交 issue!
|
||||||
|
在提交之前,请确认:
|
||||||
|
|
||||||
|
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
|
||||||
|
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
||||||
|
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
|
||||||
|
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
||||||
|
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
|
||||||
|
|
||||||
|
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
||||||
|
|
||||||
|
Thanks for opening an issue towards the Clash core!
|
||||||
|
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.
|
||||||
|
- [ ] 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 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 this issue template.
|
||||||
|
-->
|
||||||
|
|
||||||
|
------------------------------------------------------------------
|
||||||
|
|
||||||
|
<!--
|
||||||
|
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Clash config
|
||||||
|
<!--
|
||||||
|
在下方附上 Clash core 脱敏后配置文件的内容
|
||||||
|
Paste the Clash core configuration below.
|
||||||
|
-->
|
||||||
|
<details>
|
||||||
|
<summary>config.yaml</summary>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
……
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### Clash log
|
||||||
|
<!--
|
||||||
|
在下方附上 Clash Core 的日志,log level 使用 DEBUG
|
||||||
|
Paste the Clash core log below with the log level set to `DEBUG`.
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
……
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境 Environment
|
||||||
|
|
||||||
|
* 操作系统 (the OS that the Clash core is running on)
|
||||||
|
……
|
||||||
|
* 网路环境或拓扑 (network conditions/topology)
|
||||||
|
……
|
||||||
|
* iptables,如果适用 (if applicable)
|
||||||
|
……
|
||||||
|
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
||||||
|
……
|
||||||
|
* 其他 (any other information that would be useful)
|
||||||
|
……
|
||||||
|
|
||||||
|
### 说明 Description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
|
||||||
|
-->
|
||||||
|
|
||||||
|
### 重现问题的具体布骤 Steps to Reproduce
|
||||||
|
|
||||||
|
1. [First Step]
|
||||||
|
2. [Second Step]
|
||||||
|
3. ……
|
||||||
|
|
||||||
|
**我预期会发生……?**
|
||||||
|
<!-- **Expected behavior:** [What you expected to happen] -->
|
||||||
|
|
||||||
|
**实际上发生了什么?**
|
||||||
|
<!-- **Actual behavior:** [What actually happened] -->
|
||||||
|
|
||||||
|
### 可能的解决方案 Possible Solution
|
||||||
|
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
|
||||||
|
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||||
|
<!-- or ideas how to implement the addition or change -->
|
||||||
|
|
||||||
|
### 更多信息 More Information
|
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- The English version is available. -->
|
||||||
|
感谢你向 Clash Core 提交 Feature Request!
|
||||||
|
在提交之前,请确认:
|
||||||
|
|
||||||
|
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求
|
||||||
|
|
||||||
|
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thanks for submitting a feature request towards the Clash core!
|
||||||
|
But before so, please do the following checklist:
|
||||||
|
|
||||||
|
- [ ] I have searched on the [issue tracker](……/) before creating the issue.
|
||||||
|
|
||||||
|
Please understand that we close issues that fail to follow the issue template.
|
||||||
|
-->
|
||||||
|
|
||||||
|
我都确认过了,我要继续提交。
|
||||||
|
<!-- None of the above, create a feature request -->
|
||||||
|
------------------------------------------------------------------
|
||||||
|
|
||||||
|
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 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. -->
|
||||||
|
|
||||||
|
### Clash core config
|
||||||
|
<!--
|
||||||
|
在下方附上 Clash Core 脱敏后的配置内容
|
||||||
|
Paste the Clash core configuration below.
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
……
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clash log
|
||||||
|
<!--
|
||||||
|
在下方附上 Clash Core 的日志,log level 请使用 DEBUG
|
||||||
|
Paste the Clash core log below with the log level set to `DEBUG`.
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
……
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境 Environment
|
||||||
|
|
||||||
|
* Clash Core 的操作系统 (the OS that the Clash core is running on)
|
||||||
|
……
|
||||||
|
* 使用者的操作系统 (the OS running on the client)
|
||||||
|
……
|
||||||
|
* 网路环境或拓扑 (network conditions/topology)
|
||||||
|
……
|
||||||
|
* iptables,如果适用 (if applicable)
|
||||||
|
……
|
||||||
|
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
||||||
|
……
|
||||||
|
* 其他
|
||||||
|
……
|
||||||
|
|
||||||
|
### 说明 Description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
|
||||||
|
-->
|
||||||
|
|
||||||
|
### 可能的解决方案 Possible Solution
|
||||||
|
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
|
||||||
|
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||||
|
<!-- or ideas how to implement the addition or change -->
|
||||||
|
|
||||||
|
### 更多信息 More Information
|
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}}
|
15
.github/workflows/go.yml
vendored
15
.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.13.x
|
go-version: 1.15.x
|
||||||
|
|
||||||
- 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 get -u honnef.co/go/tools/cmd/staticcheck
|
||||||
|
staticcheck -- $(go list ./...)
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
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
|
10
Dockerfile
10
Dockerfile
@ -1,16 +1,16 @@
|
|||||||
FROM golang:alpine as builder
|
FROM golang:alpine as builder
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
RUN apk add --no-cache make git && \
|
||||||
wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
|
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
||||||
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
|
|
||||||
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /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/
|
||||||
|
13
Makefile
13
Makefile
@ -2,9 +2,9 @@ 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 \
|
||||||
@ -25,10 +25,14 @@ PLATFORM_LIST = \
|
|||||||
|
|
||||||
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)-$@
|
||||||
|
|
||||||
@ -79,6 +83,9 @@ windows-386:
|
|||||||
|
|
||||||
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))
|
||||||
|
302
README.md
302
README.md
@ -19,297 +19,27 @@
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Local HTTP/HTTPS/SOCKS server
|
- Local HTTP/HTTPS/SOCKS server with authentication support
|
||||||
- GeoIP rule support
|
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
|
||||||
- Supports Vmess, Shadowsocks, Snell and SOCKS5 protocol
|
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
|
||||||
- Supports Netfilter TCP redirecting
|
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
|
||||||
- Comprehensive HTTP API
|
- 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
|
||||||
|
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||||
|
- Comprehensive HTTP RESTful API controller
|
||||||
|
|
||||||
## Install
|
## Getting Started
|
||||||
|
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||||
|
|
||||||
Clash Requires Go >= 1.13. You can build it from source:
|
## Credits
|
||||||
|
|
||||||
```sh
|
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||||
$ go get -u -v github.com/Dreamacro/clash
|
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||||
```
|
|
||||||
|
|
||||||
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
|
|
||||||
|
|
||||||
Check Clash version with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ clash -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Daemon
|
|
||||||
|
|
||||||
Unfortunately, there is no native and elegant way to implement daemons on Golang.
|
|
||||||
|
|
||||||
So we can use third-party daemon tools like PM2, Supervisor or the like.
|
|
||||||
|
|
||||||
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ pm2 start clash
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have Docker installed, you can run 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
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
# hosts:
|
|
||||||
# '*.clash.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
|
|
||||||
# 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
|
|
||||||
|
|
||||||
Proxy:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
Proxy Group:
|
|
||||||
# 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
|
|
||||||
|
|
||||||
Rule:
|
|
||||||
- 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/
|
|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
|
||||||
|
|
||||||
[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
|
||||||
@ -318,4 +48,4 @@ https://clash.gitbook.io/
|
|||||||
- [x] Redir proxy
|
- [x] Redir proxy
|
||||||
- [x] UDP support
|
- [x] UDP support
|
||||||
- [x] Connection manager
|
- [x] Connection manager
|
||||||
- [ ] Event API
|
- [ ] ~~Event API~~
|
||||||
|
@ -17,9 +17,9 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPacket is PacketAdapter generator
|
// NewPacket is PacketAdapter generator
|
||||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, netType C.NetWork) *PacketAdapter {
|
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = netType
|
metadata.NetWork = C.UDP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
|
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
|
||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
|
@ -19,9 +19,9 @@ func (s *SocketAdapter) Metadata() *C.Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSocket is SocketAdapter generator
|
// NewSocket is SocketAdapter generator
|
||||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, netType C.NetWork) *SocketAdapter {
|
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter {
|
||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = netType
|
metadata.NetWork = C.TCP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
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
|
||||||
|
@ -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,14 +10,13 @@ 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 {
|
||||||
name string
|
name string
|
||||||
|
addr string
|
||||||
tp C.AdapterType
|
tp C.AdapterType
|
||||||
udp bool
|
udp bool
|
||||||
}
|
}
|
||||||
@ -30,8 +29,12 @@ func (b *Base) Type() C.AdapterType {
|
|||||||
return b.tp
|
return b.tp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
return nil, nil, errors.New("no support")
|
return c, errors.New("no support")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
return nil, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Base) SupportUDP() bool {
|
func (b *Base) SupportUDP() bool {
|
||||||
@ -44,8 +47,16 @@ func (b *Base) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBase(name string, tp C.AdapterType, udp bool) *Base {
|
func (b *Base) Addr() string {
|
||||||
return &Base{name, tp, udp}
|
return b.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
|
||||||
|
return &Base{name, addr, tp, udp}
|
||||||
}
|
}
|
||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
@ -61,7 +72,7 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
|||||||
c.chain = append(c.chain, a.Name())
|
c.chain = append(c.chain, a.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||||
return &conn{c, []string{a.Name()}}
|
return &conn{c, []string{a.Name()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,18 +89,18 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
|||||||
c.chain = append(c.chain, a.Name())
|
c.chain = append(c.chain, a.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPacketConn(c net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||||
return &packetConn{c, []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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) Alive() bool {
|
func (p *Proxy) Alive() bool {
|
||||||
return p.alive
|
return p.alive.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||||
@ -101,7 +112,7 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
@ -118,7 +129,7 @@ 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.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +160,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
// URLTest get the delay for the specified URL
|
// URLTest get the delay for the specified URL
|
||||||
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
|
||||||
@ -189,7 +200,12 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{Transport: transport}
|
client := http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -200,5 +216,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)}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,30 +13,26 @@ type Direct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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.Host, metadata.DstPort)
|
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
||||||
if metadata.DstIP != nil {
|
|
||||||
address = net.JoinHostPort(metadata.DstIP.String(), metadata.DstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := dialContext(ctx, "tcp", address)
|
c, err := dialer.DialContext(ctx, "tcp", address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
return newConn(c, d), nil
|
return NewConn(c, d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, err := net.ListenPacket("udp", "")
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return newPacketConn(&directPacketConn{pc}, d), nil
|
||||||
|
}
|
||||||
|
|
||||||
addr, err := resolveUDPAddr("udp", metadata.RemoteAddress())
|
type directPacketConn struct {
|
||||||
if err != nil {
|
net.PacketConn
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return newPacketConn(pc, d), addr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDirect() *Direct {
|
func NewDirect() *Direct {
|
||||||
|
@ -13,12 +13,12 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Http struct {
|
type Http struct {
|
||||||
*Base
|
*Base
|
||||||
addr string
|
|
||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
@ -31,26 +31,39 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
c, err := dialContext(ctx, "tcp", h.addr)
|
if h.tlsConfig != nil {
|
||||||
if err == nil && h.tlsConfig != nil {
|
|
||||||
cc := tls.Client(c, h.tlsConfig)
|
cc := tls.Client(c, h.tlsConfig)
|
||||||
err = cc.Handshake()
|
err := cc.Handshake()
|
||||||
c = cc
|
c = cc
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := h.shakeHand(metadata, c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
|
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)
|
||||||
if err := h.shakeHand(metadata, c); err != nil {
|
|
||||||
|
c, err = h.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return newConn(c, h), nil
|
return NewConn(c, h), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||||
@ -102,19 +115,23 @@ 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Http{
|
return &Http{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Http,
|
tp: C.Http,
|
||||||
},
|
},
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
@ -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)
|
||||||
@ -39,7 +48,12 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|||||||
}
|
}
|
||||||
proxy = NewHttp(*httpOption)
|
proxy = NewHttp(*httpOption)
|
||||||
case "vmess":
|
case "vmess":
|
||||||
vmessOption := &VmessOption{}
|
vmessOption := &VmessOption{
|
||||||
|
HTTPOpts: HTTPOptions{
|
||||||
|
Method: "GET",
|
||||||
|
Path: []string{"/"},
|
||||||
|
},
|
||||||
|
}
|
||||||
err = decoder.Decode(mapping, vmessOption)
|
err = decoder.Decode(mapping, vmessOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@ -52,8 +66,15 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = NewSnell(*snellOption)
|
proxy, err = NewSnell(*snellOption)
|
||||||
|
case "trojan":
|
||||||
|
trojanOption := &TrojanOption{}
|
||||||
|
err = decoder.Decode(mapping, trojanOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
@ -2,6 +2,7 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
@ -14,7 +15,11 @@ type Reject struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
return nil, errors.New("match reject rule")
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReject() *Reject {
|
func NewReject() *Reject {
|
||||||
@ -22,6 +27,7 @@ func NewReject() *Reject {
|
|||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: "REJECT",
|
name: "REJECT",
|
||||||
tp: C.Reject,
|
tp: C.Reject,
|
||||||
|
udp: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,14 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
"github.com/Dreamacro/clash/component/socks5"
|
||||||
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
||||||
@ -19,7 +20,6 @@ import (
|
|||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
*Base
|
*Base
|
||||||
server string
|
|
||||||
cipher core.Cipher
|
cipher core.Cipher
|
||||||
|
|
||||||
// obfs
|
// obfs
|
||||||
@ -37,10 +37,6 @@ 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 {
|
||||||
@ -58,48 +54,49 @@ type v2rayObfsOption struct {
|
|||||||
Mux bool `obfs:"mux,omitempty"`
|
Mux bool `obfs:"mux,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
c, err := dialContext(ctx, "tcp", ss.server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.server, err)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
switch ss.obfsMode {
|
switch ss.obfsMode {
|
||||||
case "tls":
|
case "tls":
|
||||||
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
||||||
case "http":
|
case "http":
|
||||||
_, port, _ := net.SplitHostPort(ss.server)
|
_, port, _ := net.SplitHostPort(ss.addr)
|
||||||
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||||
case "websocket":
|
case "websocket":
|
||||||
var err error
|
var err error
|
||||||
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
|
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.server, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c = ss.cipher.StreamConn(c)
|
c = ss.cipher.StreamConn(c)
|
||||||
_, err = c.Write(serializesSocksAddr(metadata))
|
_, err := c.Write(serializesSocksAddr(metadata))
|
||||||
return newConn(c, ss), err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
pc, err := net.ListenPacket("udp", "")
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
c, err = ss.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, ss), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := resolveUDPAddr("udp", ss.server)
|
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
|
|
||||||
if targetAddr == nil {
|
|
||||||
return nil, nil, fmt.Errorf("parse address %s error: %s", metadata.String(), metadata.DstPort)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pc = ss.cipher.PacketConn(pc)
|
pc = ss.cipher.PacketConn(pc)
|
||||||
return newPacketConn(&ssUDPConn{PacketConn: pc, rAddr: targetAddr}, ss), addr, nil
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
||||||
@ -109,76 +106,61 @@ func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
cipher := option.Cipher
|
cipher := option.Cipher
|
||||||
password := option.Password
|
password := option.Password
|
||||||
ciph, err := core.PickCipher(cipher, nil, password)
|
ciph, err := core.PickCipher(cipher, nil, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ss %s initialize error: %w", server, err)
|
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var v2rayOption *v2rayObfs.Option
|
var v2rayOption *v2rayObfs.Option
|
||||||
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"}
|
||||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||||
return nil, fmt.Errorf("ss %s initialize obfs error: %w", server, err)
|
return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Mode != "tls" && opts.Mode != "http" {
|
if opts.Mode != "tls" && opts.Mode != "http" {
|
||||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode)
|
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||||
}
|
}
|
||||||
obfsMode = opts.Mode
|
obfsMode = opts.Mode
|
||||||
obfsOption = &opts
|
obfsOption = &opts
|
||||||
} else if option.Plugin == "v2ray-plugin" {
|
} else if option.Plugin == "v2ray-plugin" {
|
||||||
opts := v2rayObfsOption{Host: "bing.com", Mux: true}
|
opts := v2rayObfsOption{Host: "bing.com", Mux: true}
|
||||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||||
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", server, err)
|
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Mode != "websocket" {
|
if opts.Mode != "websocket" {
|
||||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", server, opts.Mode)
|
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||||
}
|
}
|
||||||
obfsMode = opts.Mode
|
obfsMode = opts.Mode
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
|
||||||
if opts.TLS {
|
|
||||||
tlsConfig = &tls.Config{
|
|
||||||
ServerName: opts.Host,
|
|
||||||
InsecureSkipVerify: opts.SkipCertVerify,
|
|
||||||
ClientSessionCache: getClientSessionCache(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v2rayOption = &v2rayObfs.Option{
|
v2rayOption = &v2rayObfs.Option{
|
||||||
Host: opts.Host,
|
Host: opts.Host,
|
||||||
Path: opts.Path,
|
Path: opts.Path,
|
||||||
Headers: opts.Headers,
|
Headers: opts.Headers,
|
||||||
TLSConfig: tlsConfig,
|
Mux: opts.Mux,
|
||||||
Mux: opts.Mux,
|
}
|
||||||
|
|
||||||
|
if opts.TLS {
|
||||||
|
v2rayOption.TLS = true
|
||||||
|
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||||
|
v2rayOption.SessionCache = getClientSessionCache()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ShadowSocks{
|
return &ShadowSocks{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
tp: C.Shadowsocks,
|
tp: C.Shadowsocks,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
server: server,
|
|
||||||
cipher: ciph,
|
cipher: ciph,
|
||||||
|
|
||||||
obfsMode: obfsMode,
|
obfsMode: obfsMode,
|
||||||
@ -187,27 +169,35 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ssUDPConn struct {
|
type ssPacketConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
rAddr socks5.Addr
|
rAddr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *ssUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||||
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
|
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return uc.PacketConn.WriteTo(packet[3:], addr)
|
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *ssUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, _, e := uc.PacketConn.ReadFrom(b)
|
n, _, e := spc.PacketConn.ReadFrom(b)
|
||||||
addr := socks5.SplitAddr(b[:n])
|
if e != nil {
|
||||||
var from net.Addr
|
return 0, nil, e
|
||||||
if e == nil {
|
|
||||||
// Get the source IP/Port of packet.
|
|
||||||
from = addr.UDPAddr()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr := socks5.SplitAddr(b[:n])
|
||||||
|
if addr == nil {
|
||||||
|
return 0, nil, errors.New("parse addr error")
|
||||||
|
}
|
||||||
|
|
||||||
|
udpAddr := addr.UDPAddr()
|
||||||
|
if udpAddr == nil {
|
||||||
|
return 0, nil, errors.New("parse addr error")
|
||||||
|
}
|
||||||
|
|
||||||
copy(b, b[len(addr):])
|
copy(b, b[len(addr):])
|
||||||
return n - len(addr), from, e
|
return n - len(addr), udpAddr, e
|
||||||
}
|
}
|
||||||
|
134
adapters/outbound/shadowsocksr.go
Normal file
134
adapters/outbound/shadowsocksr.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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/shadowstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShadowSocksR struct {
|
||||||
|
*Base
|
||||||
|
cipher *core.StreamCipher
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
c = obfs.NewConn(c, ssr.obfs)
|
||||||
|
c = ssr.cipher.StreamConn(c)
|
||||||
|
conn, ok := c.(*shadowstream.Conn)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid connection type")
|
||||||
|
}
|
||||||
|
iv, err := conn.ObtainWriteIV()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c = protocol.NewConn(c, ssr.protocol, iv)
|
||||||
|
_, err = c.Write(serializesSocksAddr(metadata))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, 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)
|
||||||
|
|
||||||
|
c, err = ssr.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, ssr), err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = ssr.cipher.PacketConn(pc)
|
||||||
|
pc = protocol.NewPacketConn(pc, ssr.protocol)
|
||||||
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cipher error: %w", addr, err)
|
||||||
|
}
|
||||||
|
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s is not a supported stream cipher in ssr", cipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
obfs, err := obfs.PickObfs(option.Obfs, &obfs.Base{
|
||||||
|
IVSize: ciph.IVSize(),
|
||||||
|
Key: ciph.Key,
|
||||||
|
HeadLen: 30,
|
||||||
|
Host: option.Server,
|
||||||
|
Port: option.Port,
|
||||||
|
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{
|
||||||
|
IV: nil,
|
||||||
|
Key: ciph.Key,
|
||||||
|
TCPMss: 1460,
|
||||||
|
Param: option.ProtocolParam,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
|
||||||
|
}
|
||||||
|
protocol.SetOverhead(obfs.GetObfsOverhead() + protocol.GetProtocolOverhead())
|
||||||
|
|
||||||
|
return &ShadowSocksR{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.ShadowsocksR,
|
||||||
|
udp: option.UDP,
|
||||||
|
},
|
||||||
|
cipher: ciph,
|
||||||
|
obfs: obfs,
|
||||||
|
protocol: protocol,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||||
"github.com/Dreamacro/clash/component/snell"
|
"github.com/Dreamacro/clash/component/snell"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
@ -14,9 +15,10 @@ import (
|
|||||||
|
|
||||||
type Snell struct {
|
type Snell struct {
|
||||||
*Base
|
*Base
|
||||||
server string
|
|
||||||
psk []byte
|
psk []byte
|
||||||
|
pool *snell.Pool
|
||||||
obfsOption *simpleObfsOption
|
obfsOption *simpleObfsOption
|
||||||
|
version int
|
||||||
}
|
}
|
||||||
|
|
||||||
type SnellOption struct {
|
type SnellOption struct {
|
||||||
@ -24,49 +26,100 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type streamOption struct {
|
||||||
|
psk []byte
|
||||||
|
version int
|
||||||
|
addr string
|
||||||
|
obfsOption *simpleObfsOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
||||||
|
switch option.obfsOption.Mode {
|
||||||
|
case "tls":
|
||||||
|
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
|
||||||
|
case "http":
|
||||||
|
_, port, _ := net.SplitHostPort(option.addr)
|
||||||
|
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
|
||||||
|
}
|
||||||
|
return snell.StreamConn(c, option.psk, option.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
c, err := dialContext(ctx, "tcp", s.server)
|
if s.version == snell.Version2 {
|
||||||
|
c, err := s.pool.Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := strconv.Atoi(metadata.DstPort)
|
||||||
|
err = snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
|
return NewConn(c, s), err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.server, err)
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
switch s.obfsOption.Mode {
|
|
||||||
case "tls":
|
c, err = s.StreamConn(c, metadata)
|
||||||
c = obfs.NewTLSObfs(c, s.obfsOption.Host)
|
return NewConn(c, s), err
|
||||||
case "http":
|
|
||||||
_, port, _ := net.SplitHostPort(s.server)
|
|
||||||
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
|
|
||||||
}
|
|
||||||
c = snell.StreamConn(c, s.psk)
|
|
||||||
port, _ := strconv.Atoi(metadata.DstPort)
|
|
||||||
err = snell.WriteHeader(c, metadata.String(), uint(port))
|
|
||||||
return newConn(c, s), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSnell(option SnellOption) (*Snell, error) {
|
func NewSnell(option SnellOption) (*Snell, error) {
|
||||||
server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
psk := []byte(option.Psk)
|
psk := []byte(option.Psk)
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
||||||
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
||||||
return nil, fmt.Errorf("snell %s initialize obfs error: %w", server, err)
|
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
||||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", server, 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,
|
||||||
tp: C.Snell,
|
tp: C.Snell,
|
||||||
},
|
},
|
||||||
server: server,
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -3,19 +3,20 @@ package outbound
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
"github.com/Dreamacro/clash/component/socks5"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Socks5 struct {
|
type Socks5 struct {
|
||||||
*Base
|
*Base
|
||||||
addr string
|
|
||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tls bool
|
tls bool
|
||||||
@ -34,19 +35,16 @@ type Socks5Option struct {
|
|||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
c, err := dialContext(ctx, "tcp", ss.addr)
|
if ss.tls {
|
||||||
|
|
||||||
if err == nil && ss.tls {
|
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
err = cc.Handshake()
|
err := cc.Handshake()
|
||||||
c = cc
|
c = cc
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
var user *socks5.User
|
var user *socks5.User
|
||||||
if ss.user != "" {
|
if ss.user != "" {
|
||||||
user = &socks5.User{
|
user = &socks5.User{
|
||||||
@ -57,13 +55,28 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
|
|||||||
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
|
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return newConn(c, ss), nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, _ net.Addr, err error) {
|
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
c, err = ss.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, ss), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
c, err := dialContext(ctx, "tcp", ss.addr)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
return
|
return
|
||||||
@ -96,17 +109,7 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, _ net.Addr, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := net.ResolveUDPAddr("udp", bindAddr.String())
|
pc, err := dialer.ListenPacket("udp", "")
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
targetAddr := socks5.ParseAddr(metadata.RemoteAddress())
|
|
||||||
if targetAddr == nil {
|
|
||||||
return nil, nil, fmt.Errorf("parse address %s error: %s", metadata.String(), metadata.DstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
pc, err := net.ListenPacket("udp", "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -119,7 +122,7 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, _ net.Addr, err
|
|||||||
pc.Close()
|
pc.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return newPacketConn(&socksUDPConn{PacketConn: pc, rAddr: targetAddr, tcpConn: c}, ss), addr, nil
|
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSocks5(option Socks5Option) *Socks5 {
|
func NewSocks5(option Socks5Option) *Socks5 {
|
||||||
@ -135,10 +138,10 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
|||||||
return &Socks5{
|
return &Socks5{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Socks5,
|
tp: C.Socks5,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tls: option.TLS,
|
tls: option.TLS,
|
||||||
@ -147,22 +150,22 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type socksUDPConn struct {
|
type socksPacketConn struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
rAddr socks5.Addr
|
rAddr net.Addr
|
||||||
tcpConn net.Conn
|
tcpConn net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *socksUDPConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||||
packet, err := socks5.EncodeUDPPacket(uc.rAddr, b)
|
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return uc.PacketConn.WriteTo(packet, addr)
|
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *socksUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, a, e := uc.PacketConn.ReadFrom(b)
|
n, _, e := uc.PacketConn.ReadFrom(b)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return 0, nil, e
|
return 0, nil, e
|
||||||
}
|
}
|
||||||
@ -170,13 +173,18 @@ func (uc *socksUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
udpAddr := addr.UDPAddr()
|
||||||
|
if udpAddr == nil {
|
||||||
|
return 0, nil, errors.New("parse udp addr error")
|
||||||
|
}
|
||||||
|
|
||||||
// due to DecodeUDPPacket is mutable, record addr length
|
// due to DecodeUDPPacket is mutable, record addr length
|
||||||
addrLength := len(addr)
|
|
||||||
copy(b, payload)
|
copy(b, payload)
|
||||||
return n - addrLength - 3, a, nil
|
return n - len(addr) - 3, udpAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *socksUDPConn) Close() error {
|
func (uc *socksPacketConn) Close() error {
|
||||||
uc.tcpConn.Close()
|
uc.tcpConn.Close()
|
||||||
return uc.PacketConn.Close()
|
return uc.PacketConn.Close()
|
||||||
}
|
}
|
||||||
|
107
adapters/outbound/trojan.go
Normal file
107
adapters/outbound/trojan.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/trojan"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Trojan struct {
|
||||||
|
*Base
|
||||||
|
instance *trojan.Trojan
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrojanOption struct {
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
Password string `proxy:"password"`
|
||||||
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
|
SNI string `proxy:"sni,omitempty"`
|
||||||
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
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.CommandTCP, serializesSocksAddr(metadata))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
c, err = t.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, t), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := t.instance.PacketConn(c)
|
||||||
|
return newPacketConn(pc, t), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]string{
|
||||||
|
"type": t.Type().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
|
tOption := &trojan.Option{
|
||||||
|
Password: option.Password,
|
||||||
|
ALPN: option.ALPN,
|
||||||
|
ServerName: option.Server,
|
||||||
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
|
ClientSessionCache: getClientSessionCache(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.SNI != "" {
|
||||||
|
tOption.ServerName = option.SNI
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Trojan{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.Trojan,
|
||||||
|
udp: option.UDP,
|
||||||
|
},
|
||||||
|
instance: trojan.New(tOption),
|
||||||
|
}, nil
|
||||||
|
}
|
@ -2,7 +2,6 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -11,9 +10,9 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
"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/dns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -87,92 +86,13 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
|
|||||||
return bytes.Join(buf, nil)
|
return bytes.Join(buf, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
returned := make(chan struct{})
|
|
||||||
defer close(returned)
|
|
||||||
|
|
||||||
type dialResult struct {
|
|
||||||
net.Conn
|
|
||||||
error
|
|
||||||
resolved bool
|
|
||||||
ipv6 bool
|
|
||||||
done bool
|
|
||||||
}
|
|
||||||
results := make(chan dialResult)
|
|
||||||
var primary, fallback dialResult
|
|
||||||
|
|
||||||
startRacer := func(ctx context.Context, host string, ipv6 bool) {
|
|
||||||
dialer := net.Dialer{}
|
|
||||||
result := dialResult{ipv6: ipv6, done: true}
|
|
||||||
defer func() {
|
|
||||||
select {
|
|
||||||
case results <- result:
|
|
||||||
case <-returned:
|
|
||||||
if result.Conn != nil {
|
|
||||||
result.Conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var ip net.IP
|
|
||||||
if ipv6 {
|
|
||||||
ip, result.error = dns.ResolveIPv6(host)
|
|
||||||
} else {
|
|
||||||
ip, result.error = dns.ResolveIPv4(host)
|
|
||||||
}
|
|
||||||
if result.error != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
result.resolved = true
|
|
||||||
|
|
||||||
if ipv6 {
|
|
||||||
result.Conn, result.error = dialer.DialContext(ctx, "tcp6", net.JoinHostPort(ip.String(), port))
|
|
||||||
} else {
|
|
||||||
result.Conn, result.error = dialer.DialContext(ctx, "tcp4", net.JoinHostPort(ip.String(), port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go startRacer(ctx, host, false)
|
|
||||||
go startRacer(ctx, host, true)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case res := <-results:
|
|
||||||
if res.error == nil {
|
|
||||||
return res.Conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !res.ipv6 {
|
|
||||||
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 {
|
|
||||||
return nil, primary.error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
func resolveUDPAddr(network, address string) (*net.UDPAddr, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := dns.ResolveIP(host)
|
ip, err := resolver.ResolveIP(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,23 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Vmess struct {
|
type Vmess struct {
|
||||||
*Base
|
*Base
|
||||||
server string
|
|
||||||
client *vmess.Client
|
client *vmess.Client
|
||||||
|
option *VmessOption
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmessOption struct {
|
type VmessOption struct {
|
||||||
@ -27,63 +31,189 @@ type VmessOption struct {
|
|||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
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"`
|
||||||
|
HTTP2Opts HTTP2Options `proxy:"h2-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 {
|
||||||
|
Method string `proxy:"method,omitempty"`
|
||||||
|
Path []string `proxy:"path,omitempty"`
|
||||||
|
Headers map[string][]string `proxy:"headers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP2Options struct {
|
||||||
|
Host []string `proxy:"host,omitempty"`
|
||||||
|
Path string `proxy:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
var err error
|
||||||
|
switch v.option.Network {
|
||||||
|
case "ws":
|
||||||
|
host, port, _ := net.SplitHostPort(v.addr)
|
||||||
|
wsOpts := &vmess.WebsocketConfig{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Path: v.option.WSPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.option.WSHeaders) != 0 {
|
||||||
|
header := http.Header{}
|
||||||
|
for key, value := range v.option.WSHeaders {
|
||||||
|
header.Add(key, value)
|
||||||
|
}
|
||||||
|
wsOpts.Headers = header
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.TLS {
|
||||||
|
wsOpts.TLS = true
|
||||||
|
wsOpts.SessionCache = getClientSessionCache()
|
||||||
|
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
||||||
|
wsOpts.ServerName = v.option.ServerName
|
||||||
|
}
|
||||||
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
|
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)
|
||||||
|
httpOpts := &vmess.HTTPConfig{
|
||||||
|
Host: host,
|
||||||
|
Method: v.option.HTTPOpts.Method,
|
||||||
|
Path: v.option.HTTPOpts.Path,
|
||||||
|
Headers: v.option.HTTPOpts.Headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
default:
|
||||||
|
// handle TLS
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
c, err := dialContext(ctx, "tcp", v.server)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error", v.server)
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
c, err = v.client.New(c, parseVmessAddr(metadata))
|
|
||||||
return newConn(c, v), err
|
c, err = v.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, v), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
// vmess use stream-oriented udp, so clash needs a net.UDPAddr
|
||||||
|
if !metadata.Resolved() {
|
||||||
|
ip, err := resolver.ResolveIP(metadata.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("can't resolve ip")
|
||||||
|
}
|
||||||
|
metadata.DstIP = ip
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
c, err := dialContext(ctx, "tcp", v.server)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("%s connect error", v.server)
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
c, err = v.client.New(c, parseVmessAddr(metadata))
|
c, err = v.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("new vmess client error: %v", err)
|
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||||
}
|
}
|
||||||
return newPacketConn(&vmessUDPConn{Conn: c}, v), c.RemoteAddr(), nil
|
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVmess(option VmessOption) (*Vmess, error) {
|
func NewVmess(option VmessOption) (*Vmess, error) {
|
||||||
security := strings.ToLower(option.Cipher)
|
security := strings.ToLower(option.Cipher)
|
||||||
client, err := vmess.NewClient(vmess.Config{
|
client, err := vmess.NewClient(vmess.Config{
|
||||||
UUID: option.UUID,
|
UUID: option.UUID,
|
||||||
AlterID: uint16(option.AlterID),
|
AlterID: uint16(option.AlterID),
|
||||||
Security: security,
|
Security: security,
|
||||||
TLS: option.TLS,
|
HostName: option.Server,
|
||||||
HostName: option.Server,
|
Port: strconv.Itoa(option.Port),
|
||||||
Port: strconv.Itoa(option.Port),
|
|
||||||
NetWork: option.Network,
|
|
||||||
WebSocketPath: option.WSPath,
|
|
||||||
WebSocketHeaders: option.WSHeaders,
|
|
||||||
SkipCertVerify: option.SkipCertVerify,
|
|
||||||
SessionCache: getClientSessionCache(),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if option.Network == "h2" && !option.TLS {
|
||||||
|
return nil, fmt.Errorf("TLS must be true with h2 network")
|
||||||
|
}
|
||||||
|
|
||||||
return &Vmess{
|
return &Vmess{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Vmess,
|
tp: C.Vmess,
|
||||||
udp: true,
|
udp: option.UDP,
|
||||||
},
|
},
|
||||||
server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
|
||||||
client: client,
|
client: client,
|
||||||
|
option: &option,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,15 +245,16 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type vmessUDPConn struct {
|
type vmessPacketConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
rAddr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *vmessUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
return uc.Conn.Write(b)
|
return uc.Conn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *vmessUDPConn) 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.RemoteAddr(), 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
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
@ -13,17 +12,18 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -31,23 +31,27 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
proxy := f.findAliveProxy()
|
proxy := f.findAliveProxy(true)
|
||||||
pc, addr, err := proxy.DialUDP(metadata)
|
pc, err := proxy.DialUDP(metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(f)
|
pc.AppendToChains(f)
|
||||||
}
|
}
|
||||||
return pc, addr, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
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{}{
|
||||||
@ -57,29 +61,35 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) proxies() []C.Proxy {
|
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
proxy := f.findAliveProxy(true)
|
||||||
|
return 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 {
|
||||||
@ -59,49 +75,69 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
proxy := lb.Unwrap(metadata)
|
||||||
proxies := lb.proxies()
|
|
||||||
buckets := int32(len(proxies))
|
c, err = proxy.DialContext(ctx, metadata)
|
||||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
|
||||||
idx := jumpHash(key, buckets)
|
|
||||||
proxy := proxies[idx]
|
|
||||||
if proxy.Alive() {
|
|
||||||
c, err = proxy.DialContext(ctx, metadata)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c, err = proxies[0].DialContext(ctx, metadata)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, addr net.Addr, err error) {
|
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(lb)
|
pc.AppendToChains(lb)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
proxy := lb.Unwrap(metadata)
|
||||||
proxies := lb.proxies()
|
|
||||||
buckets := int32(len(proxies))
|
|
||||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
|
||||||
idx := jumpHash(key, buckets)
|
|
||||||
proxy := proxies[idx]
|
|
||||||
if proxy.Alive() {
|
|
||||||
return proxy.DialUDP(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies[0].DialUDP(metadata)
|
return proxy.DialUDP(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) SupportUDP() bool {
|
func (lb *LoadBalance) SupportUDP() bool {
|
||||||
return true
|
return !lb.disableUDP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) proxies() []C.Proxy {
|
func strategyRoundRobin() strategyFn {
|
||||||
|
idx := 0
|
||||||
|
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||||
|
length := len(proxies)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
idx = (idx + 1) % length
|
||||||
|
proxy := proxies[idx]
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@ -109,7 +145,7 @@ func (lb *LoadBalance) proxies() []C.Proxy {
|
|||||||
|
|
||||||
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{}{
|
||||||
@ -118,11 +154,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,18 +58,22 @@ 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.NewCompatibleProvier(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
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" {
|
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
||||||
hc := provider.NewHealthCheck(ps, "", 0)
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
pd, err := provider.NewCompatibleProvier(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -78,8 +85,8 @@ 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.NewCompatibleProvier(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -101,13 +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":
|
||||||
|
group = NewRelay(groupOption, providers)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
||||||
}
|
}
|
||||||
|
98
adapters/outboundgroup/relay.go
Normal file
98
adapters/outboundgroup/relay.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Relay struct {
|
||||||
|
*outbound.Base
|
||||||
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||||
|
proxies := r.proxies(metadata, true)
|
||||||
|
if len(proxies) == 0 {
|
||||||
|
return nil, errors.New("proxy does not exist")
|
||||||
|
}
|
||||||
|
first := proxies[0]
|
||||||
|
last := proxies[len(proxies)-1]
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
var currentMeta *C.Metadata
|
||||||
|
for _, proxy := range proxies[1:] {
|
||||||
|
currentMeta, err = addrToMetadata(proxy.Addr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = first.StreamConn(c, currentMeta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
first = proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = last.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outbound.NewConn(c, r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||||
|
var all []string
|
||||||
|
for _, proxy := range r.rawProxies(false) {
|
||||||
|
all = append(all, proxy.Name())
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]interface{}{
|
||||||
|
"type": r.Type().String(),
|
||||||
|
"all": all,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
||||||
|
return getProvidersProxies(r.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||||
|
proxies := r.rawProxies(touch)
|
||||||
|
|
||||||
|
for n, proxy := range proxies {
|
||||||
|
subproxy := proxy.Unwrap(metadata)
|
||||||
|
for subproxy != nil {
|
||||||
|
proxies[n] = subproxy
|
||||||
|
subproxy = subproxy.Unwrap(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||||
|
return &Relay{
|
||||||
|
Base: outbound.NewBase(options.Name, "", C.Relay, false),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
"github.com/Dreamacro/clash/adapters/provider"
|
||||||
@ -14,34 +13,39 @@ import (
|
|||||||
|
|
||||||
type Selector struct {
|
type Selector struct {
|
||||||
*outbound.Base
|
*outbound.Base
|
||||||
single *singledo.Single
|
disableUDP bool
|
||||||
selected C.Proxy
|
single *singledo.Single
|
||||||
providers []provider.ProxyProvider
|
selected string
|
||||||
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
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.selected.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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, addr, err := s.selected.DialUDP(metadata)
|
pc, err := s.selectedProxy(true).DialUDP(metadata)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(s)
|
pc.AppendToChains(s)
|
||||||
}
|
}
|
||||||
return pc, addr, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) SupportUDP() bool {
|
func (s *Selector) SupportUDP() bool {
|
||||||
return s.selected.SupportUDP()
|
if s.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.selectedProxy(false).SupportUDP()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range s.proxies() {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,34 +57,47 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Now() string {
|
func (s *Selector) Now() string {
|
||||||
return s.selected.Name()
|
return s.selectedProxy(false).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) Set(name string) error {
|
func (s *Selector) Set(name string) error {
|
||||||
for _, proxy := range s.proxies() {
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
if proxy.Name() == name {
|
if proxy.Name() == name {
|
||||||
s.selected = proxy
|
s.selected = name
|
||||||
|
s.single.Reset()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("Proxy does not exist")
|
return errors.New("proxy not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) proxies() []C.Proxy {
|
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return s.selectedProxy(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||||
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
||||||
return getProvidersProxies(s.providers), nil
|
proxies := getProvidersProxies(s.providers, touch)
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if proxy.Name() == s.selected {
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0], nil
|
||||||
})
|
})
|
||||||
|
|
||||||
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]
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
@ -12,44 +11,59 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, net.Addr, error) {
|
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
pc, addr, 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, addr, err
|
return pc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) proxies() []C.Proxy {
|
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return u.fast(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
for _, proxy := range proxies[1:] {
|
for _, proxy := range proxies[1:] {
|
||||||
@ -63,19 +77,29 @@ func (u *URLTest) fast() C.Proxy {
|
|||||||
min = delay
|
min = delay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fast, nil
|
|
||||||
|
// tolerance
|
||||||
|
if u.fastNode == nil || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
|
||||||
|
u.fastNode = fast
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fastNode, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.(C.Proxy)
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) SupportUDP() bool {
|
func (u *URLTest) SupportUDP() bool {
|
||||||
return u.fast().SupportUDP()
|
if u.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fast(false).SupportUDP()
|
||||||
}
|
}
|
||||||
|
|
||||||
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{}{
|
||||||
@ -85,11 +109,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
|
||||||
}
|
}
|
||||||
|
53
adapters/outboundgroup/util.go
Normal file
53
adapters/outboundgroup/util.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
||||||
|
host, port, err := net.SplitHostPort(rawAddress)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("addrToMetadata failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
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{
|
||||||
|
AddrType: C.AtypDomainName,
|
||||||
|
Host: host,
|
||||||
|
DstIP: nil,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpKeepAlive(c net.Conn) {
|
||||||
|
if tcp, ok := c.(*net.TCPConn); ok {
|
||||||
|
tcp.SetKeepAlive(true)
|
||||||
|
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
180
adapters/provider/fetcher.go
Normal file
180
adapters/provider/fetcher.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileMode os.FileMode = 0666
|
||||||
|
dirMode os.FileMode = 0755
|
||||||
|
)
|
||||||
|
|
||||||
|
type parser = func([]byte) (interface{}, error)
|
||||||
|
|
||||||
|
type fetcher struct {
|
||||||
|
name string
|
||||||
|
vehicle Vehicle
|
||||||
|
updatedAt *time.Time
|
||||||
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
|
hash [16]byte
|
||||||
|
parser parser
|
||||||
|
onUpdate func(interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) VehicleType() VehicleType {
|
||||||
|
return f.vehicle.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Initial() (interface{}, error) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
err error
|
||||||
|
isLocal bool
|
||||||
|
)
|
||||||
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
|
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
||||||
|
modTime := stat.ModTime()
|
||||||
|
f.updatedAt = &modTime
|
||||||
|
isLocal = true
|
||||||
|
} else {
|
||||||
|
buf, err = f.vehicle.Read()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err := f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
if !isLocal {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse local file error, fallback to remote
|
||||||
|
buf, err = f.vehicle.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err = f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.vehicle.Type() != File {
|
||||||
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f.hash = md5.Sum(buf)
|
||||||
|
|
||||||
|
// pull proxies automatically
|
||||||
|
if f.ticker != nil {
|
||||||
|
go f.pullLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Update() (interface{}, bool, error) {
|
||||||
|
buf, err := f.vehicle.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
hash := md5.Sum(buf)
|
||||||
|
if bytes.Equal(f.hash[:], hash[:]) {
|
||||||
|
f.updatedAt = &now
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies, err := f.parser(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.updatedAt = &now
|
||||||
|
f.hash = hash
|
||||||
|
|
||||||
|
return proxies, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) Destroy() error {
|
||||||
|
if f.ticker != nil {
|
||||||
|
f.done <- struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fetcher) pullLoop() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.ticker.C:
|
||||||
|
elm, same, err := f.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if same {
|
||||||
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
|
if f.onUpdate != nil {
|
||||||
|
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 {
|
||||||
|
var ticker *time.Ticker
|
||||||
|
if interval != 0 {
|
||||||
|
ticker = time.NewTicker(interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fetcher{
|
||||||
|
name: name,
|
||||||
|
ticker: ticker,
|
||||||
|
vehicle: vehicle,
|
||||||
|
parser: parser,
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -17,10 +19,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 +34,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,6 +53,10 @@ 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)
|
||||||
for _, proxy := range hc.proxies {
|
for _, proxy := range hc.proxies {
|
||||||
@ -60,11 +71,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,7 +31,11 @@ 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
|
||||||
}
|
}
|
||||||
@ -39,9 +44,9 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
|
|||||||
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.Reslove(schema.Path)
|
path := C.Path.Resolve(schema.Path)
|
||||||
|
|
||||||
var vehicle Vehicle
|
var vehicle Vehicle
|
||||||
switch schema.Type {
|
switch schema.Type {
|
||||||
|
@ -1,26 +1,20 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"runtime"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapters/outbound"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ReservedName = "default"
|
ReservedName = "default"
|
||||||
|
|
||||||
fileMode = 0666
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Provider Type
|
// Provider Type
|
||||||
@ -49,33 +43,35 @@ type Provider interface {
|
|||||||
VehicleType() VehicleType
|
VehicleType() VehicleType
|
||||||
Type() ProviderType
|
Type() ProviderType
|
||||||
Initial() error
|
Initial() error
|
||||||
Reload() error
|
Update() error
|
||||||
Destroy() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyProvider interface
|
// ProxyProvider 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()
|
||||||
Update() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxySchema struct {
|
type ProxySchema struct {
|
||||||
Proxies []map[string]interface{} `yaml:"proxies"`
|
Proxies []map[string]interface{} `yaml:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for auto gc
|
||||||
type ProxySetProvider struct {
|
type ProxySetProvider struct {
|
||||||
name string
|
*proxySetProvider
|
||||||
vehicle Vehicle
|
|
||||||
hash [16]byte
|
|
||||||
proxies []C.Proxy
|
|
||||||
healthCheck *HealthCheck
|
|
||||||
ticker *time.Ticker
|
|
||||||
updatedAt *time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) MarshalJSON() ([]byte, error) {
|
type proxySetProvider struct {
|
||||||
|
*fetcher
|
||||||
|
proxies []C.Proxy
|
||||||
|
healthCheck *HealthCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
"name": pp.Name(),
|
"name": pp.Name(),
|
||||||
"type": pp.Type().String(),
|
"type": pp.Type().String(),
|
||||||
@ -85,119 +81,46 @@ func (pp *ProxySetProvider) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Name() string {
|
func (pp *proxySetProvider) Name() string {
|
||||||
return pp.name
|
return pp.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Reload() error {
|
func (pp *proxySetProvider) HealthCheck() {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *ProxySetProvider) HealthCheck() {
|
|
||||||
pp.healthCheck.check()
|
pp.healthCheck.check()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Update() error {
|
func (pp *proxySetProvider) Update() error {
|
||||||
return pp.pull()
|
elm, same, err := pp.fetcher.Update()
|
||||||
|
if err == nil && !same {
|
||||||
|
pp.onUpdate(elm)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Destroy() error {
|
func (pp *proxySetProvider) Initial() error {
|
||||||
pp.healthCheck.close()
|
elm, err := pp.fetcher.Initial()
|
||||||
|
|
||||||
if pp.ticker != nil {
|
|
||||||
pp.ticker.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Initial() error {
|
|
||||||
var buf []byte
|
|
||||||
var err error
|
|
||||||
if stat, err := os.Stat(pp.vehicle.Path()); err == nil {
|
|
||||||
buf, err = ioutil.ReadFile(pp.vehicle.Path())
|
|
||||||
modTime := stat.ModTime()
|
|
||||||
pp.updatedAt = &modTime
|
|
||||||
} else {
|
|
||||||
buf, err = pp.vehicle.Read()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies, err := pp.parse(buf)
|
pp.onUpdate(elm)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(pp.vehicle.Path(), buf, fileMode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.hash = md5.Sum(buf)
|
|
||||||
pp.setProxies(proxies)
|
|
||||||
|
|
||||||
// pull proxies automatically
|
|
||||||
if pp.ticker != nil {
|
|
||||||
go pp.pullLoop()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) VehicleType() VehicleType {
|
func (pp *proxySetProvider) Type() ProviderType {
|
||||||
return pp.vehicle.Type()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Type() ProviderType {
|
|
||||||
return Proxy
|
return Proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) Proxies() []C.Proxy {
|
func (pp *proxySetProvider) Proxies() []C.Proxy {
|
||||||
return pp.proxies
|
return pp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) pullLoop() {
|
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
for range pp.ticker.C {
|
pp.healthCheck.touch()
|
||||||
if err := pp.pull(); err != nil {
|
return pp.Proxies()
|
||||||
log.Warnln("[Provider] %s pull error: %s", pp.Name(), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *ProxySetProvider) pull() error {
|
func proxiesParse(buf []byte) (interface{}, error) {
|
||||||
buf, err := pp.vehicle.Read()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
hash := md5.Sum(buf)
|
|
||||||
if bytes.Equal(pp.hash[:], hash[:]) {
|
|
||||||
log.Debugln("[Provider] %s's proxies doesn't change", pp.Name())
|
|
||||||
pp.updatedAt = &now
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies, err := pp.parse(buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Infoln("[Provider] %s's proxies update", pp.Name())
|
|
||||||
|
|
||||||
if err := ioutil.WriteFile(pp.vehicle.Path(), buf, fileMode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.updatedAt = &now
|
|
||||||
pp.hash = hash
|
|
||||||
pp.setProxies(proxies)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *ProxySetProvider) parse(buf []byte) ([]C.Proxy, error) {
|
|
||||||
schema := &ProxySchema{}
|
schema := &ProxySchema{}
|
||||||
|
|
||||||
if err := yaml.Unmarshal(buf, schema); err != nil {
|
if err := yaml.Unmarshal(buf, schema); err != nil {
|
||||||
@ -205,57 +128,73 @@ func (pp *ProxySetProvider) parse(buf []byte) ([]C.Proxy, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
pd.healthCheck.close()
|
||||||
|
pd.fetcher.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
|
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
|
||||||
var ticker *time.Ticker
|
|
||||||
if interval != 0 {
|
|
||||||
ticker = time.NewTicker(interval)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hc.auto() {
|
if hc.auto() {
|
||||||
go hc.process()
|
go hc.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ProxySetProvider{
|
pd := &proxySetProvider{
|
||||||
name: name,
|
|
||||||
vehicle: vehicle,
|
|
||||||
proxies: []C.Proxy{},
|
proxies: []C.Proxy{},
|
||||||
healthCheck: hc,
|
healthCheck: hc,
|
||||||
ticker: ticker,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUpdate := func(elm interface{}) {
|
||||||
|
ret := elm.([]C.Proxy)
|
||||||
|
pd.setProxies(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate)
|
||||||
|
pd.fetcher = fetcher
|
||||||
|
|
||||||
|
wrapper := &ProxySetProvider{pd}
|
||||||
|
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
||||||
|
return wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompatibleProvier struct {
|
// for auto gc
|
||||||
|
type CompatibleProvider struct {
|
||||||
|
*compatibleProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
type compatibleProvider struct {
|
||||||
name string
|
name string
|
||||||
healthCheck *HealthCheck
|
healthCheck *HealthCheck
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) MarshalJSON() ([]byte, error) {
|
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
"name": cp.Name(),
|
"name": cp.Name(),
|
||||||
"type": cp.Type().String(),
|
"type": cp.Type().String(),
|
||||||
@ -264,44 +203,44 @@ func (cp *CompatibleProvier) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Name() string {
|
func (cp *compatibleProvider) Name() string {
|
||||||
return cp.name
|
return cp.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Reload() error {
|
func (cp *compatibleProvider) HealthCheck() {
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Destroy() error {
|
|
||||||
cp.healthCheck.close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *CompatibleProvier) HealthCheck() {
|
|
||||||
cp.healthCheck.check()
|
cp.healthCheck.check()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Update() error {
|
func (cp *compatibleProvider) Update() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Initial() error {
|
func (cp *compatibleProvider) Initial() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) VehicleType() VehicleType {
|
func (cp *compatibleProvider) VehicleType() VehicleType {
|
||||||
return Compatible
|
return Compatible
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Type() ProviderType {
|
func (cp *compatibleProvider) Type() ProviderType {
|
||||||
return Proxy
|
return Proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *CompatibleProvier) Proxies() []C.Proxy {
|
func (cp *compatibleProvider) Proxies() []C.Proxy {
|
||||||
return cp.proxies
|
return cp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompatibleProvier(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvier, error) {
|
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
||||||
|
cp.healthCheck.touch()
|
||||||
|
return cp.Proxies()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
|
pd.healthCheck.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return nil, errors.New("Provider need one proxy at least")
|
return nil, errors.New("Provider need one proxy at least")
|
||||||
}
|
}
|
||||||
@ -310,9 +249,13 @@ func NewCompatibleProvier(name string, proxies []C.Proxy, hc *HealthCheck) (*Com
|
|||||||
go hc.process()
|
go hc.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CompatibleProvier{
|
pd := &compatibleProvider{
|
||||||
name: name,
|
name: name,
|
||||||
proxies: proxies,
|
proxies: proxies,
|
||||||
healthCheck: hc,
|
healthCheck: hc,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
wrapper := &CompatibleProvider{pd}
|
||||||
|
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
||||||
|
return wrapper, nil
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vehicle Type
|
// Vehicle Type
|
||||||
@ -73,10 +76,21 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, h.url, nil)
|
uri, err := url.Parse(h.url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user := uri.User; user != nil {
|
||||||
|
password, _ := user.Password()
|
||||||
|
req.SetBasicAuth(user.Username(), password)
|
||||||
|
}
|
||||||
|
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
@ -85,6 +99,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
DialContext: dialer.DialContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{Transport: transport}
|
client := http.Client{Transport: transport}
|
||||||
@ -92,15 +107,12 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(resp.Body)
|
buf, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(h.path, buf, fileMode); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
common/cache/lrucache.go
vendored
104
common/cache/lrucache.go
vendored
@ -12,7 +12,7 @@ import (
|
|||||||
type Option func(*LruCache)
|
type Option func(*LruCache)
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||||
type EvictCallback func(key interface{}, value interface{})
|
type EvictCallback = func(key interface{}, value interface{})
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict(cb EvictCallback) Option {
|
func WithEvict(cb EvictCallback) Option {
|
||||||
@ -42,6 +42,14 @@ func WithSize(maxSize int) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithStale decide whether Stale return is enabled.
|
||||||
|
// If this feature is enabled, element will not get Evicted according to `WithAge`.
|
||||||
|
func WithStale(stale bool) Option {
|
||||||
|
return func(l *LruCache) {
|
||||||
|
l.staleReturn = stale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
// LruCache is a thread-safe, in-memory lru-cache that evicts the
|
||||||
// least recently used entries from memory when (if set) the entries are
|
// least recently used entries from memory when (if set) the entries are
|
||||||
// older than maxAge (in seconds). Use the New constructor to create one.
|
// older than maxAge (in seconds). Use the New constructor to create one.
|
||||||
@ -52,6 +60,7 @@ type LruCache struct {
|
|||||||
cache map[interface{}]*list.Element
|
cache map[interface{}]*list.Element
|
||||||
lru *list.List // Front is least-recent
|
lru *list.List // Front is least-recent
|
||||||
updateAgeOnGet bool
|
updateAgeOnGet bool
|
||||||
|
staleReturn bool
|
||||||
onEvict EvictCallback
|
onEvict EvictCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,31 +81,28 @@ func NewLRUCache(options ...Option) *LruCache {
|
|||||||
// Get returns the interface{} representation of a cached response and a bool
|
// Get returns the interface{} representation of a cached response and a bool
|
||||||
// set to true if the key was found.
|
// set to true if the key was found.
|
||||||
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
||||||
c.mu.Lock()
|
entry := c.get(key)
|
||||||
defer c.mu.Unlock()
|
if entry == nil {
|
||||||
|
|
||||||
le, ok := c.cache[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
|
|
||||||
c.deleteElement(le)
|
|
||||||
c.maybeDeleteOldest()
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
c.lru.MoveToBack(le)
|
|
||||||
entry := le.Value.(*entry)
|
|
||||||
if c.maxAge > 0 && c.updateAgeOnGet {
|
|
||||||
entry.expires = time.Now().Unix() + c.maxAge
|
|
||||||
}
|
|
||||||
value := entry.value
|
value := entry.value
|
||||||
|
|
||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWithExpire returns the interface{} representation of a cached response,
|
||||||
|
// a time.Time Give expected expires,
|
||||||
|
// and a bool set to true if the key was found.
|
||||||
|
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||||
|
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
|
||||||
|
entry := c.get(key)
|
||||||
|
if entry == nil {
|
||||||
|
return nil, time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.value, time.Unix(entry.expires, 0), true
|
||||||
|
}
|
||||||
|
|
||||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||||
func (c *LruCache) Exist(key interface{}) bool {
|
func (c *LruCache) Exist(key interface{}) bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
@ -108,21 +114,26 @@ func (c *LruCache) Exist(key interface{}) bool {
|
|||||||
|
|
||||||
// Set stores the interface{} representation of a response for a given key.
|
// Set stores the interface{} representation of a response for a given key.
|
||||||
func (c *LruCache) Set(key interface{}, value interface{}) {
|
func (c *LruCache) Set(key interface{}, value interface{}) {
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
expires := int64(0)
|
expires := int64(0)
|
||||||
if c.maxAge > 0 {
|
if c.maxAge > 0 {
|
||||||
expires = time.Now().Unix() + c.maxAge
|
expires = time.Now().Unix() + c.maxAge
|
||||||
}
|
}
|
||||||
|
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
|
||||||
|
// The expires time will round to second.
|
||||||
|
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
c.lru.MoveToBack(le)
|
c.lru.MoveToBack(le)
|
||||||
e := le.Value.(*entry)
|
e := le.Value.(*entry)
|
||||||
e.value = value
|
e.value = value
|
||||||
e.expires = expires
|
e.expires = expires.Unix()
|
||||||
} else {
|
} else {
|
||||||
e := &entry{key: key, value: value, expires: expires}
|
e := &entry{key: key, value: value, expires: expires.Unix()}
|
||||||
c.cache[key] = c.lru.PushBack(e)
|
c.cache[key] = c.lru.PushBack(e)
|
||||||
|
|
||||||
if c.maxSize > 0 {
|
if c.maxSize > 0 {
|
||||||
@ -135,8 +146,49 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
|
|||||||
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 {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
le, ok := c.cache[key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
|
||||||
|
c.deleteElement(le)
|
||||||
|
c.maybeDeleteOldest()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lru.MoveToBack(le)
|
||||||
|
entry := le.Value.(*entry)
|
||||||
|
if c.maxAge > 0 && c.updateAgeOnGet {
|
||||||
|
entry.expires = time.Now().Unix() + c.maxAge
|
||||||
|
}
|
||||||
|
return 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 {
|
||||||
@ -147,7 +199,7 @@ func (c *LruCache) Delete(key string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache) maybeDeleteOldest() {
|
func (c *LruCache) maybeDeleteOldest() {
|
||||||
if c.maxAge > 0 {
|
if !c.staleReturn && c.maxAge > 0 {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
|
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
|
||||||
c.deleteElement(le)
|
c.deleteElement(le)
|
||||||
|
46
common/cache/lrucache_test.go
vendored
46
common/cache/lrucache_test.go
vendored
@ -136,3 +136,49 @@ func TestEvict(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, temp, 3)
|
assert.Equal(t, temp, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetWithExpire(t *testing.T) {
|
||||||
|
c := NewLRUCache(WithAge(1))
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
|
c.SetWithExpire(1, 2, tenSecBefore)
|
||||||
|
|
||||||
|
// res is expected not to exist, and expires should be empty time.Time
|
||||||
|
res, expires, exist := c.GetWithExpire(1)
|
||||||
|
assert.Equal(t, nil, res)
|
||||||
|
assert.Equal(t, time.Time{}, expires)
|
||||||
|
assert.Equal(t, false, exist)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStale(t *testing.T) {
|
||||||
|
c := NewLRUCache(WithAge(1), WithStale(true))
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
|
c.SetWithExpire(1, 2, tenSecBefore)
|
||||||
|
|
||||||
|
res, expires, exist := c.GetWithExpire(1)
|
||||||
|
assert.Equal(t, 2, res)
|
||||||
|
assert.Equal(t, tenSecBefore, expires)
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -15,17 +15,16 @@ type Picker struct {
|
|||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
result interface{}
|
errOnce sync.Once
|
||||||
|
result interface{}
|
||||||
firstDone chan struct{}
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPicker(ctx context.Context, cancel func()) *Picker {
|
func newPicker(ctx context.Context, cancel func()) *Picker {
|
||||||
return &Picker{
|
return &Picker{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
firstDone: make(chan struct{}, 1),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,12 +41,6 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
|
|||||||
return newPicker(ctx, cancel), ctx
|
return newPicker(ctx, cancel), ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithoutAutoCancel returns a new Picker and an associated Context derived from ctx,
|
|
||||||
// but it wouldn't cancel context when the first element return.
|
|
||||||
func WithoutAutoCancel(ctx context.Context) *Picker {
|
|
||||||
return newPicker(ctx, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait blocks until all function calls from the Go method have returned,
|
// Wait blocks until all function calls from the Go method have returned,
|
||||||
// then returns the first nil error result (if any) from them.
|
// then returns the first nil error result (if any) from them.
|
||||||
func (p *Picker) Wait() interface{} {
|
func (p *Picker) Wait() interface{} {
|
||||||
@ -58,14 +51,9 @@ func (p *Picker) Wait() interface{} {
|
|||||||
return p.result
|
return p.result
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitWithoutCancel blocks until the first result return, if timeout will return nil.
|
// Error return the first error (if all success return nil)
|
||||||
func (p *Picker) WaitWithoutCancel() interface{} {
|
func (p *Picker) Error() error {
|
||||||
select {
|
return p.err
|
||||||
case <-p.firstDone:
|
|
||||||
return p.result
|
|
||||||
case <-p.ctx.Done():
|
|
||||||
return p.result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go calls the given function in a new goroutine.
|
// Go calls the given function in a new goroutine.
|
||||||
@ -79,11 +67,14 @@ func (p *Picker) Go(f func() (interface{}, error)) {
|
|||||||
if ret, err := f(); err == nil {
|
if ret, err := f(); err == nil {
|
||||||
p.once.Do(func() {
|
p.once.Do(func() {
|
||||||
p.result = ret
|
p.result = ret
|
||||||
p.firstDone <- struct{}{}
|
|
||||||
if p.cancel != nil {
|
if p.cancel != nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
p.errOnce.Do(func() {
|
||||||
|
p.err = err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -36,31 +36,5 @@ func TestPicker_Timeout(t *testing.T) {
|
|||||||
|
|
||||||
number := picker.Wait()
|
number := picker.Wait()
|
||||||
assert.Nil(t, number)
|
assert.Nil(t, number)
|
||||||
}
|
assert.NotNil(t, picker.Error())
|
||||||
|
|
||||||
func TestPicker_WaitWithoutAutoCancel(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*60)
|
|
||||||
defer cancel()
|
|
||||||
picker := WithoutAutoCancel(ctx)
|
|
||||||
|
|
||||||
trigger := false
|
|
||||||
picker.Go(sleepAndSend(ctx, 10, 1))
|
|
||||||
picker.Go(func() (interface{}, error) {
|
|
||||||
timer := time.NewTimer(time.Millisecond * time.Duration(30))
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
trigger = true
|
|
||||||
return 2, nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
elm := picker.WaitWithoutCancel()
|
|
||||||
|
|
||||||
assert.NotNil(t, elm)
|
|
||||||
assert.Equal(t, elm.(int), 1)
|
|
||||||
|
|
||||||
elm = picker.Wait()
|
|
||||||
assert.True(t, trigger)
|
|
||||||
assert.Equal(t, elm.(int), 1)
|
|
||||||
}
|
}
|
||||||
|
67
common/pool/alloc.go
Normal file
67
common/pool/alloc.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
// Inspired by https://github.com/xtaci/smux/blob/master/alloc.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultAllocator *Allocator
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultAllocator = NewAllocator()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||||
|
type Allocator struct {
|
||||||
|
buffers []sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAllocator initiates a []byte allocator for frames less than 65536 bytes,
|
||||||
|
// the waste(memory fragmentation) of space allocation is guaranteed to be
|
||||||
|
// no more than 50%.
|
||||||
|
func NewAllocator() *Allocator {
|
||||||
|
alloc := new(Allocator)
|
||||||
|
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
||||||
|
for k := range alloc.buffers {
|
||||||
|
i := k
|
||||||
|
alloc.buffers[k].New = func() interface{} {
|
||||||
|
return make([]byte, 1<<uint32(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alloc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a []byte from pool with most appropriate cap
|
||||||
|
func (alloc *Allocator) Get(size int) []byte {
|
||||||
|
if size <= 0 || size > 65536 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bits := msb(size)
|
||||||
|
if size == 1<<bits {
|
||||||
|
return alloc.buffers[bits].Get().([]byte)[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put returns a []byte to pool for future use,
|
||||||
|
// which the cap must be exactly 2^n
|
||||||
|
func (alloc *Allocator) Put(buf []byte) error {
|
||||||
|
bits := msb(cap(buf))
|
||||||
|
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
||||||
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore SA6002 ignore temporarily
|
||||||
|
alloc.buffers[bits].Put(buf)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// msb return the pos of most significant bit
|
||||||
|
func msb(size int) uint16 {
|
||||||
|
return uint16(bits.Len32(uint32(size)) - 1)
|
||||||
|
}
|
48
common/pool/alloc_test.go
Normal file
48
common/pool/alloc_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllocGet(t *testing.T) {
|
||||||
|
alloc := NewAllocator()
|
||||||
|
assert.Nil(t, alloc.Get(0))
|
||||||
|
assert.Equal(t, 1, len(alloc.Get(1)))
|
||||||
|
assert.Equal(t, 2, len(alloc.Get(2)))
|
||||||
|
assert.Equal(t, 3, len(alloc.Get(3)))
|
||||||
|
assert.Equal(t, 4, cap(alloc.Get(3)))
|
||||||
|
assert.Equal(t, 4, cap(alloc.Get(4)))
|
||||||
|
assert.Equal(t, 1023, len(alloc.Get(1023)))
|
||||||
|
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
||||||
|
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
||||||
|
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
||||||
|
assert.Nil(t, alloc.Get(65537))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllocPut(t *testing.T) {
|
||||||
|
alloc := NewAllocator()
|
||||||
|
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
|
||||||
|
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []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, 65536)), "put elem:65536 []bytes misbehavior")
|
||||||
|
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllocPutThenGet(t *testing.T) {
|
||||||
|
alloc := NewAllocator()
|
||||||
|
data := alloc.Get(4)
|
||||||
|
alloc.Put(data)
|
||||||
|
newData := alloc.Get(4)
|
||||||
|
|
||||||
|
assert.Equal(t, cap(data), cap(newData), "different cap while alloc.Get()")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMSB(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
msb(rand.Int())
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
package pool
|
package pool
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// io.Copy default buffer size is 32 KiB
|
// io.Copy default buffer size is 32 KiB
|
||||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
||||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
||||||
bufferSize = 20 * 1024
|
RelayBufferSize = 20 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// BufPool provide buffer for relay
|
func Get(size int) []byte {
|
||||||
var BufPool = sync.Pool{New: func() interface{} { return make([]byte, bufferSize) }}
|
return defaultAllocator.Get(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Put(buf []byte) error {
|
||||||
|
return defaultAllocator.Put(buf)
|
||||||
|
}
|
||||||
|
@ -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,12 +46,19 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Single) Reset() {
|
||||||
|
s.last = time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSingle(wait time.Duration) *Single {
|
func NewSingle(wait time.Duration) *Single {
|
||||||
return &Single{wait: wait}
|
return &Single{wait: wait}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
@ -19,13 +20,13 @@ func TestBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
const n = 10
|
const n = 5
|
||||||
wg.Add(n)
|
wg.Add(n)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
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, 9, shardCount)
|
assert.Equal(t, int32(4), shardCount.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimer(t *testing.T) {
|
func TestTimer(t *testing.T) {
|
||||||
@ -51,3 +52,18 @@ func TestTimer(t *testing.T) {
|
|||||||
assert.Equal(t, 1, foo)
|
assert.Equal(t, 1, foo)
|
||||||
assert.True(t, shard)
|
assert.True(t, shard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReset(t *testing.T) {
|
||||||
|
single := NewSingle(time.Millisecond * 30)
|
||||||
|
foo := 0
|
||||||
|
call := func() (interface{}, error) {
|
||||||
|
foo++
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
single.Do(call)
|
||||||
|
single.Reset()
|
||||||
|
single.Do(call)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, foo)
|
||||||
|
}
|
||||||
|
19
common/sockopt/reuseaddr_linux.go
Normal file
19
common/sockopt/reuseaddr_linux.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package sockopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UDPReuseaddr(c *net.UDPConn) (err error) {
|
||||||
|
rc, err := c.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rc.Control(func(fd uintptr) {
|
||||||
|
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
11
common/sockopt/reuseaddr_other.go
Normal file
11
common/sockopt/reuseaddr_other.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package sockopt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UDPReuseaddr(c *net.UDPConn) (err error) {
|
||||||
|
return
|
||||||
|
}
|
@ -216,6 +216,11 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
|
|||||||
}
|
}
|
||||||
|
|
||||||
v := dataVal.MapIndex(k).Interface()
|
v := dataVal.MapIndex(k).Interface()
|
||||||
|
if v == nil {
|
||||||
|
errors = append(errors, fmt.Sprintf("filed %s invalid", fieldName))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
currentVal := reflect.Indirect(reflect.New(valElemType))
|
currentVal := reflect.Indirect(reflect.New(valElemType))
|
||||||
if err := d.decode(fieldName, v, currentVal); err != nil {
|
if err := d.decode(fieldName, v, currentVal); err != nil {
|
||||||
errors = append(errors, err.Error())
|
errors = append(errors, err.Error())
|
||||||
|
104
component/dialer/bind.go
Normal file
104
component/dialer/bind.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
iface, err := net.InterfaceByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.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 := net.InterfaceByName(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.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
|
||||||
|
}
|
51
component/dialer/bind_darwin.go
Normal file
51
component/dialer/bind_darwin.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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 := net.InterfaceByName(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer.Control = bindControl(iface.Index)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||||
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.Control = bindControl(iface.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
|
||||||
|
}
|
159
component/dialer/dialer.go
Normal file
159
component/dialer/dialer.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Dialer() (*net.Dialer, error) {
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
if DialerHook != nil {
|
||||||
|
if err := DialerHook(dialer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Dial(network, address string) (net.Conn, error) {
|
||||||
|
return DialContext(context.Background(), network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
switch network {
|
||||||
|
case "tcp4", "tcp6", "udp4", "udp6":
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer, err := Dialer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip net.IP
|
||||||
|
switch network {
|
||||||
|
case "tcp4", "udp4":
|
||||||
|
ip, err = resolver.ResolveIPv4(host)
|
||||||
|
default:
|
||||||
|
ip, err = resolver.ResolveIPv6(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if DialHook != nil {
|
||||||
|
if err := DialHook(dialer, network, ip); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||||
|
case "tcp", "udp":
|
||||||
|
return dualStackDialContext(ctx, network, address)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("network invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenPacket(network, address string) (net.PacketConn, error) {
|
||||||
|
cfg := &net.ListenConfig{}
|
||||||
|
if ListenPacketHook != nil {
|
||||||
|
var err error
|
||||||
|
address, err = ListenPacketHook(cfg, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg.ListenPacket(context.Background(), network, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
returned := make(chan struct{})
|
||||||
|
defer close(returned)
|
||||||
|
|
||||||
|
type dialResult struct {
|
||||||
|
net.Conn
|
||||||
|
error
|
||||||
|
resolved bool
|
||||||
|
ipv6 bool
|
||||||
|
done bool
|
||||||
|
}
|
||||||
|
results := make(chan dialResult)
|
||||||
|
var primary, fallback dialResult
|
||||||
|
|
||||||
|
startRacer := func(ctx context.Context, network, host string, ipv6 bool) {
|
||||||
|
result := dialResult{ipv6: ipv6, done: true}
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case results <- result:
|
||||||
|
case <-returned:
|
||||||
|
if result.Conn != nil {
|
||||||
|
result.Conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dialer, err := Dialer()
|
||||||
|
if err != nil {
|
||||||
|
result.error = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip net.IP
|
||||||
|
if ipv6 {
|
||||||
|
ip, result.error = resolver.ResolveIPv6(host)
|
||||||
|
} else {
|
||||||
|
ip, result.error = resolver.ResolveIPv4(host)
|
||||||
|
}
|
||||||
|
if result.error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.resolved = true
|
||||||
|
|
||||||
|
if DialHook != nil {
|
||||||
|
if result.error = DialHook(dialer, network, ip); result.error != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Conn, result.error = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||||
|
}
|
||||||
|
|
||||||
|
go startRacer(ctx, network+"4", host, false)
|
||||||
|
go startRacer(ctx, network+"6", host, true)
|
||||||
|
|
||||||
|
for res := range results {
|
||||||
|
if res.error == nil {
|
||||||
|
return res.Conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res.ipv6 {
|
||||||
|
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 {
|
||||||
|
return nil, primary.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("never touched")
|
||||||
|
}
|
43
component/dialer/hook.go
Normal file
43
component/dialer/hook.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DialerHookFunc = func(dialer *net.Dialer) error
|
||||||
|
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
|
||||||
|
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DialerHook DialerHookFunc
|
||||||
|
DialHook DialHookFunc
|
||||||
|
ListenPacketHook ListenPacketHookFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAddrNotFound = errors.New("addr not found")
|
||||||
|
ErrNetworkNotSupport = errors.New("network not support")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
|
||||||
|
return func(lc *net.ListenConfig, address string) (string, error) {
|
||||||
|
err := bindIfaceToListenConfig(lc, name)
|
||||||
|
if err == errPlatformNotSupport {
|
||||||
|
address, err = fallbackBindToListenConfig(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return address, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DialerWithInterface(name string) DialHookFunc {
|
||||||
|
return func(dialer *net.Dialer, network string, ip net.IP) error {
|
||||||
|
err := bindIfaceToDialer(dialer, name)
|
||||||
|
if err == errPlatformNotSupport {
|
||||||
|
err = fallbackBindToDialer(dialer, network, ip, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
package trie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
wildcard = "*"
|
|
||||||
domainStep = "."
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrInvalidDomain means insert domain is invalid
|
|
||||||
ErrInvalidDomain = errors.New("invalid domain")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Trie contains the main logic for adding and searching nodes for domain segments.
|
|
||||||
// support wildcard domain (e.g *.google.com)
|
|
||||||
type Trie struct {
|
|
||||||
root *Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidDomain(domain string) bool {
|
|
||||||
return domain != "" && domain[0] != '.' && domain[len(domain)-1] != '.'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert adds a node to the trie.
|
|
||||||
// Support
|
|
||||||
// 1. www.example.com
|
|
||||||
// 2. *.example.com
|
|
||||||
// 3. subdomain.*.example.com
|
|
||||||
func (t *Trie) Insert(domain string, data interface{}) error {
|
|
||||||
if !isValidDomain(domain) {
|
|
||||||
return ErrInvalidDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(domain, domainStep)
|
|
||||||
node := t.root
|
|
||||||
// reverse storage domain part to save space
|
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
|
||||||
part := parts[i]
|
|
||||||
if !node.hasChild(part) {
|
|
||||||
node.addChild(part, newNode(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
node = node.getChild(part)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.Data = data
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search is the most important part of the Trie.
|
|
||||||
// Priority as:
|
|
||||||
// 1. static part
|
|
||||||
// 2. wildcard domain
|
|
||||||
func (t *Trie) Search(domain string) *Node {
|
|
||||||
if !isValidDomain(domain) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parts := strings.Split(domain, domainStep)
|
|
||||||
|
|
||||||
n := t.root
|
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
|
||||||
part := parts[i]
|
|
||||||
|
|
||||||
var child *Node
|
|
||||||
if !n.hasChild(part) {
|
|
||||||
if !n.hasChild(wildcard) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
child = n.getChild(wildcard)
|
|
||||||
} else {
|
|
||||||
child = n.getChild(part)
|
|
||||||
}
|
|
||||||
|
|
||||||
n = child
|
|
||||||
}
|
|
||||||
|
|
||||||
if n.Data == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new, empty Trie.
|
|
||||||
func New() *Trie {
|
|
||||||
return &Trie{root: newNode(nil)}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
package trie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var localIP = net.IP{127, 0, 0, 1}
|
|
||||||
|
|
||||||
func TestTrie_Basic(t *testing.T) {
|
|
||||||
tree := New()
|
|
||||||
domains := []string{
|
|
||||||
"example.com",
|
|
||||||
"google.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, domain := range domains {
|
|
||||||
tree.Insert(domain, localIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
node := tree.Search("example.com")
|
|
||||||
if node == nil {
|
|
||||||
t.Error("should not recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !node.Data.(net.IP).Equal(localIP) {
|
|
||||||
t.Error("should equal 127.0.0.1")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Insert("", localIP) == nil {
|
|
||||||
t.Error("should return error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrie_Wildcard(t *testing.T) {
|
|
||||||
tree := New()
|
|
||||||
domains := []string{
|
|
||||||
"*.example.com",
|
|
||||||
"sub.*.example.com",
|
|
||||||
"*.dev",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, domain := range domains {
|
|
||||||
tree.Insert(domain, localIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("sub.example.com") == nil {
|
|
||||||
t.Error("should not recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("sub.foo.example.com") == nil {
|
|
||||||
t.Error("should not recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("foo.sub.example.com") != nil {
|
|
||||||
t.Error("should recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("foo.example.dev") != nil {
|
|
||||||
t.Error("should recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("example.com") != nil {
|
|
||||||
t.Error("should recv nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrie_Boundary(t *testing.T) {
|
|
||||||
tree := New()
|
|
||||||
tree.Insert("*.dev", localIP)
|
|
||||||
|
|
||||||
if err := tree.Insert(".", localIP); err == nil {
|
|
||||||
t.Error("should recv err")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tree.Insert(".com", localIP); err == nil {
|
|
||||||
t.Error("should recv err")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search("dev") != nil {
|
|
||||||
t.Error("should recv nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Search(".dev") != nil {
|
|
||||||
t.Error("should recv 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
|
||||||
}
|
}
|
||||||
|
43
component/mmdb/mmdb.go
Normal file
43
component/mmdb/mmdb.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package mmdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mmdb *geoip2.Reader
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func LoadFromBytes(buffer []byte) {
|
||||||
|
once.Do(func() {
|
||||||
|
var err error
|
||||||
|
mmdb, err = geoip2.FromBytes(buffer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Can't load mmdb: %s", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify() bool {
|
||||||
|
instance, err := geoip2.Open(C.Path.MMDB())
|
||||||
|
if err == nil {
|
||||||
|
instance.Close()
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Instance() *geoip2.Reader {
|
||||||
|
once.Do(func() {
|
||||||
|
var err error
|
||||||
|
mmdb, err = geoip2.Open(C.Path.MMDB())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Can't load mmdb: %s", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mmdb
|
||||||
|
}
|
@ -1,39 +1,30 @@
|
|||||||
package nat
|
package nat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Table struct {
|
type Table struct {
|
||||||
mapping sync.Map
|
mapping sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
type element struct {
|
func (t *Table) Set(key string, pc C.PacketConn) {
|
||||||
RemoteAddr net.Addr
|
t.mapping.Store(key, pc)
|
||||||
RemoteConn net.PacketConn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) Set(key string, pc net.PacketConn, addr net.Addr) {
|
func (t *Table) Get(key string) C.PacketConn {
|
||||||
// set conn read timeout
|
|
||||||
t.mapping.Store(key, &element{
|
|
||||||
RemoteConn: pc,
|
|
||||||
RemoteAddr: addr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Table) Get(key string) (net.PacketConn, net.Addr) {
|
|
||||||
item, exist := t.mapping.Load(key)
|
item, exist := t.mapping.Load(key)
|
||||||
if !exist {
|
if !exist {
|
||||||
return nil, nil
|
return nil
|
||||||
}
|
}
|
||||||
elm := item.(*element)
|
return item.(C.PacketConn)
|
||||||
return elm.RemoteConn, elm.RemoteAddr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
}
|
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,16 +1,37 @@
|
|||||||
package dns
|
package resolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errIPNotFound = errors.New("couldn't find ip")
|
// DefaultResolver aim to resolve ip
|
||||||
errIPVersion = errors.New("ip version error")
|
DefaultResolver Resolver
|
||||||
|
|
||||||
|
// DisableIPv6 means don't resolve ipv6 host
|
||||||
|
// default value is true
|
||||||
|
DisableIPv6 = true
|
||||||
|
|
||||||
|
// DefaultHosts aim to resolve hosts
|
||||||
|
DefaultHosts = trie.New()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrIPNotFound = errors.New("couldn't find ip")
|
||||||
|
ErrIPVersion = errors.New("ip version error")
|
||||||
|
ErrIPv6Disabled = errors.New("ipv6 disabled")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver interface {
|
||||||
|
ResolveIP(host string) (ip net.IP, err error)
|
||||||
|
ResolveIPv4(host string) (ip net.IP, err error)
|
||||||
|
ResolveIPv6(host string) (ip net.IP, err error)
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveIPv4 with a host, return ipv4
|
// ResolveIPv4 with a host, return ipv4
|
||||||
func ResolveIPv4(host string) (net.IP, error) {
|
func ResolveIPv4(host string) (net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
@ -24,7 +45,7 @@ func ResolveIPv4(host string) (net.IP, error) {
|
|||||||
if !strings.Contains(host, ":") {
|
if !strings.Contains(host, ":") {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
return nil, errIPVersion
|
return nil, ErrIPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
@ -42,11 +63,15 @@ func ResolveIPv4(host string) (net.IP, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errIPNotFound
|
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
|
||||||
@ -58,7 +83,7 @@ func ResolveIPv6(host string) (net.IP, error) {
|
|||||||
if strings.Contains(host, ":") {
|
if strings.Contains(host, ":") {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
return nil, errIPVersion
|
return nil, ErrIPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
@ -76,7 +101,7 @@ func ResolveIPv6(host string) (net.IP, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errIPNotFound
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIP with a host, return ip
|
// ResolveIP with a host, return ip
|
||||||
@ -86,10 +111,12 @@ func ResolveIP(host string) (net.IP, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
if DefaultResolver.ipv6 {
|
if DisableIPv6 {
|
||||||
return DefaultResolver.ResolveIP(host)
|
return DefaultResolver.ResolveIPv4(host)
|
||||||
}
|
}
|
||||||
return DefaultResolver.ResolveIPv4(host)
|
return DefaultResolver.ResolveIP(host)
|
||||||
|
} else if DisableIPv6 {
|
||||||
|
return ResolveIPv4(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
@ -28,21 +28,22 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
if ho.firstResponse {
|
if ho.firstResponse {
|
||||||
buf := pool.BufPool.Get().([]byte)
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
n, err := ho.Conn.Read(buf)
|
n, err := ho.Conn.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
|
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
ho.firstResponse = false
|
ho.firstResponse = false
|
||||||
@ -52,7 +53,7 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
|
|||||||
ho.buf = buf[:idx+4+length]
|
ho.buf = buf[:idx+4+length]
|
||||||
ho.offset = idx + 4 + n
|
ho.offset = idx + 4 + n
|
||||||
} else {
|
} else {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
@ -29,12 +29,12 @@ type TLSObfs struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
|
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
|
||||||
buf := pool.BufPool.Get().([]byte)
|
buf := pool.Get(discardN)
|
||||||
_, err := io.ReadFull(to.Conn, buf[:discardN])
|
_, err := io.ReadFull(to.Conn, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
|
|
||||||
sizeBuf := make([]byte, 2)
|
sizeBuf := make([]byte, 2)
|
||||||
_, err = io.ReadFull(to.Conn, sizeBuf)
|
_, err = io.ReadFull(to.Conn, sizeBuf)
|
||||||
@ -102,15 +102,11 @@ func (to *TLSObfs) write(b []byte) (int, error) {
|
|||||||
return len(b), err
|
return len(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
size := pool.BufPool.Get().([]byte)
|
|
||||||
binary.BigEndian.PutUint16(size[:2], uint16(len(b)))
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
buf.Write([]byte{0x17, 0x03, 0x03})
|
buf.Write([]byte{0x17, 0x03, 0x03})
|
||||||
buf.Write(size[:2])
|
binary.Write(buf, binary.BigEndian, uint16(len(b)))
|
||||||
buf.Write(b)
|
buf.Write(b)
|
||||||
_, err := to.Conn.Write(buf.Bytes())
|
_, err := to.Conn.Write(buf.Bytes())
|
||||||
pool.BufPool.Put(size[:cap(size)])
|
|
||||||
return len(b), err
|
return len(b), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
80
component/snell/pool.go
Normal file
80
component/snell/pool.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package snell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
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
|
||||||
@ -178,7 +180,7 @@ func ServerHandshake(rw net.Conn, authenticator auth.Authenticator) (addr Addr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
command = buf[1]
|
command = buf[1]
|
||||||
addr, err = readAddr(rw, buf)
|
addr, err = ReadAddr(rw, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
@ -260,10 +266,10 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return readAddr(rw, buf)
|
return ReadAddr(rw, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAddr(r io.Reader, b []byte) (Addr, error) {
|
func ReadAddr(r io.Reader, b []byte) (Addr, error) {
|
||||||
if len(b) < MaxAddrLen {
|
if len(b) < MaxAddrLen {
|
||||||
return nil, io.ErrShortBuffer
|
return nil, io.ErrShortBuffer
|
||||||
}
|
}
|
||||||
|
11
component/ssr/obfs/base.go
Normal file
11
component/ssr/obfs/base.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
// Base information for obfs
|
||||||
|
type Base struct {
|
||||||
|
IVSize int
|
||||||
|
Key []byte
|
||||||
|
HeadLen int
|
||||||
|
Host string
|
||||||
|
Port 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPPost(b *Base) Obfs {
|
||||||
|
return &httpObfs{Base: b, post: true}
|
||||||
|
}
|
402
component/ssr/obfs/http_simple.go
Normal file
402
component/ssr/obfs/http_simple.go
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpObfs struct {
|
||||||
|
*Base
|
||||||
|
firstRequest bool
|
||||||
|
firstResponse bool
|
||||||
|
post bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("http_simple", newHTTPSimple)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPSimple(b *Base) Obfs {
|
||||||
|
return &httpObfs{Base: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpObfs) initForConn() Obfs {
|
||||||
|
return &httpObfs{
|
||||||
|
Base: h.Base,
|
||||||
|
firstRequest: true,
|
||||||
|
firstResponse: true,
|
||||||
|
post: h.post,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpObfs) GetObfsOverhead() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpObfs) Decode(b []byte) ([]byte, bool, error) {
|
||||||
|
if h.firstResponse {
|
||||||
|
idx := bytes.Index(b, []byte("\r\n\r\n"))
|
||||||
|
if idx == -1 {
|
||||||
|
return nil, false, io.EOF
|
||||||
|
}
|
||||||
|
h.firstResponse = false
|
||||||
|
return b[idx+4:], false, nil
|
||||||
|
}
|
||||||
|
return b, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpObfs) Encode(b []byte) ([]byte, error) {
|
||||||
|
if h.firstRequest {
|
||||||
|
bSize := len(b)
|
||||||
|
var headData []byte
|
||||||
|
|
||||||
|
if headSize := h.IVSize + h.HeadLen; bSize-headSize > 64 {
|
||||||
|
headData = make([]byte, headSize+rand.Intn(64))
|
||||||
|
} else {
|
||||||
|
headData = make([]byte, bSize)
|
||||||
|
}
|
||||||
|
copy(headData, b[:len(headData)])
|
||||||
|
host := h.Host
|
||||||
|
var customHead string
|
||||||
|
|
||||||
|
if len(h.Param) > 0 {
|
||||||
|
customHeads := strings.Split(h.Param, "#")
|
||||||
|
if len(customHeads) > 2 {
|
||||||
|
customHeads = customHeads[:2]
|
||||||
|
}
|
||||||
|
customHosts := h.Param
|
||||||
|
if len(customHeads) > 1 {
|
||||||
|
customHosts = customHeads[0]
|
||||||
|
customHead = customHeads[1]
|
||||||
|
}
|
||||||
|
hosts := strings.Split(customHosts, ",")
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
host = strings.TrimSpace(hosts[rand.Intn(len(hosts))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
method := "GET /"
|
||||||
|
if h.post {
|
||||||
|
method = "POST /"
|
||||||
|
}
|
||||||
|
requestPathIndex := rand.Intn(len(requestPath)/2) * 2
|
||||||
|
httpBuf := fmt.Sprintf("%s%s%s%s HTTP/1.1\r\nHost: %s:%d\r\n",
|
||||||
|
method,
|
||||||
|
requestPath[requestPathIndex],
|
||||||
|
data2URLEncode(headData),
|
||||||
|
requestPath[requestPathIndex+1],
|
||||||
|
host, h.Port)
|
||||||
|
if len(customHead) > 0 {
|
||||||
|
httpBuf = httpBuf + strings.Replace(customHead, "\\n", "\r\n", -1) + "\r\n\r\n"
|
||||||
|
} else {
|
||||||
|
var contentType string
|
||||||
|
if h.post {
|
||||||
|
contentType = "Content-Type: multipart/form-data; boundary=" + boundary() + "\r\n"
|
||||||
|
}
|
||||||
|
httpBuf = httpBuf + "User-agent: " + requestUserAgent[rand.Intn(len(requestUserAgent))] + "\r\n" +
|
||||||
|
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
|
||||||
|
"Accept-Language: en-US,en;q=0.8\r\n" +
|
||||||
|
"Accept-Encoding: gzip, deflate\r\n" +
|
||||||
|
contentType +
|
||||||
|
"DNT: 1\r\n" +
|
||||||
|
"Connection: keep-alive\r\n" +
|
||||||
|
"\r\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded []byte
|
||||||
|
if len(headData) < bSize {
|
||||||
|
encoded = make([]byte, len(httpBuf)+(bSize-len(headData)))
|
||||||
|
copy(encoded, []byte(httpBuf))
|
||||||
|
copy(encoded[len(httpBuf):], b[len(headData):])
|
||||||
|
} else {
|
||||||
|
encoded = []byte(httpBuf)
|
||||||
|
}
|
||||||
|
h.firstRequest = false
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func data2URLEncode(data []byte) (ret string) {
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
ret = fmt.Sprintf("%s%%%s", ret, hex.EncodeToString([]byte{data[i]}))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func boundary() (ret string) {
|
||||||
|
set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
ret = fmt.Sprintf("%s%c", ret, set[rand.Intn(len(set))])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
requestPath = []string{
|
||||||
|
"", "",
|
||||||
|
"login.php?redir=", "",
|
||||||
|
"register.php?code=", "",
|
||||||
|
"?keyword=", "",
|
||||||
|
"search?src=typd&q=", "&lang=en",
|
||||||
|
"s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&bar=&wd=", "&rn=",
|
||||||
|
"post.php?id=", "&goto=view.php",
|
||||||
|
}
|
||||||
|
requestUserAgent = []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",
|
||||||
|
}
|
||||||
|
)
|
37
component/ssr/obfs/obfs.go
Normal file
37
component/ssr/obfs/obfs.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Obfs provides methods for decoding and encoding
|
||||||
|
type Obfs interface {
|
||||||
|
initForConn() Obfs
|
||||||
|
GetObfsOverhead() int
|
||||||
|
Decode(b []byte) ([]byte, bool, error)
|
||||||
|
Encode(b []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type obfsCreator func(b *Base) Obfs
|
||||||
|
|
||||||
|
var obfsList = make(map[string]obfsCreator)
|
||||||
|
|
||||||
|
func register(name string, c obfsCreator) {
|
||||||
|
obfsList[name] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// PickObfs returns an obfs of the given name
|
||||||
|
func PickObfs(name string, b *Base) (Obfs, error) {
|
||||||
|
if obfsCreator, ok := obfsList[strings.ToLower(name)]; ok {
|
||||||
|
return obfsCreator(b), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Obfs %s not supported", name)
|
||||||
|
}
|
25
component/ssr/obfs/plain.go
Normal file
25
component/ssr/obfs/plain.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
type plain struct{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("plain", newPlain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlain(b *Base) Obfs {
|
||||||
|
return &plain{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plain) initForConn() Obfs { return &plain{} }
|
||||||
|
|
||||||
|
func (p *plain) GetObfsOverhead() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plain) Encode(b []byte) ([]byte, error) {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plain) Decode(b []byte) ([]byte, bool, error) {
|
||||||
|
return b, false, nil
|
||||||
|
}
|
75
component/ssr/obfs/random_head.go
Normal file
75
component/ssr/obfs/random_head.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"hash/crc32"
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
type randomHead struct {
|
||||||
|
*Base
|
||||||
|
firstRequest bool
|
||||||
|
firstResponse bool
|
||||||
|
headerSent bool
|
||||||
|
buffer []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("random_head", newRandomHead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRandomHead(b *Base) Obfs {
|
||||||
|
return &randomHead{Base: b}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *randomHead) initForConn() Obfs {
|
||||||
|
return &randomHead{
|
||||||
|
Base: r.Base,
|
||||||
|
firstRequest: true,
|
||||||
|
firstResponse: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *randomHead) GetObfsOverhead() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *randomHead) Encode(b []byte) (encoded []byte, err error) {
|
||||||
|
if !r.firstRequest {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bSize := len(b)
|
||||||
|
if r.headerSent {
|
||||||
|
if bSize > 0 {
|
||||||
|
d := make([]byte, len(r.buffer)+bSize)
|
||||||
|
copy(d, r.buffer)
|
||||||
|
copy(d[len(r.buffer):], b)
|
||||||
|
r.buffer = d
|
||||||
|
} else {
|
||||||
|
encoded = r.buffer
|
||||||
|
r.buffer = nil
|
||||||
|
r.firstRequest = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size := rand.Intn(96) + 8
|
||||||
|
encoded = make([]byte, size)
|
||||||
|
rand.Read(encoded)
|
||||||
|
crc := (0xFFFFFFFF - crc32.ChecksumIEEE(encoded[:size-4])) & 0xFFFFFFFF
|
||||||
|
binary.LittleEndian.PutUint32(encoded[size-4:], crc)
|
||||||
|
|
||||||
|
d := make([]byte, bSize)
|
||||||
|
copy(d, b)
|
||||||
|
r.buffer = d
|
||||||
|
}
|
||||||
|
r.headerSent = true
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *randomHead) Decode(b []byte) ([]byte, bool, error) {
|
||||||
|
if r.firstResponse {
|
||||||
|
r.firstResponse = false
|
||||||
|
return b, true, nil
|
||||||
|
}
|
||||||
|
return b, false, nil
|
||||||
|
}
|
72
component/ssr/obfs/stream.go
Normal file
72
component/ssr/obfs/stream.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewConn wraps a stream-oriented net.Conn with obfs decoding/encoding
|
||||||
|
func NewConn(c net.Conn, o Obfs) net.Conn {
|
||||||
|
return &Conn{Conn: c, Obfs: o.initForConn()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn represents an obfs connection
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
Obfs
|
||||||
|
buf []byte
|
||||||
|
offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
|
if c.buf != nil {
|
||||||
|
n := copy(b, c.buf[c.offset:])
|
||||||
|
c.offset += n
|
||||||
|
if c.offset == len(c.buf) {
|
||||||
|
pool.Put(c.buf)
|
||||||
|
c.buf = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
n, err := c.Conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
decoded, sendback, err := c.Decode(buf[:n])
|
||||||
|
// decoded may be part of buf
|
||||||
|
decodedData := pool.Get(len(decoded))
|
||||||
|
copy(decodedData, decoded)
|
||||||
|
if err != nil {
|
||||||
|
pool.Put(decodedData)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if sendback {
|
||||||
|
c.Write(nil)
|
||||||
|
pool.Put(decodedData)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
n = copy(b, decodedData)
|
||||||
|
if len(decodedData) > len(b) {
|
||||||
|
c.buf = decodedData
|
||||||
|
c.offset = n
|
||||||
|
} else {
|
||||||
|
pool.Put(decodedData)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (int, error) {
|
||||||
|
encoded, err := c.Encode(b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = c.Conn.Write(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
290
component/ssr/obfs/tls12_ticket_auth.go
Normal file
290
component/ssr/obfs/tls12_ticket_auth.go
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
package obfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tlsAuthData struct {
|
||||||
|
localClientID [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type tls12Ticket struct {
|
||||||
|
*Base
|
||||||
|
*tlsAuthData
|
||||||
|
handshakeStatus int
|
||||||
|
sendSaver bytes.Buffer
|
||||||
|
recvBuffer bytes.Buffer
|
||||||
|
buffer bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("tls1.2_ticket_auth", newTLS12Ticket)
|
||||||
|
register("tls1.2_ticket_fastauth", newTLS12Ticket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTLS12Ticket(b *Base) Obfs {
|
||||||
|
return &tls12Ticket{
|
||||||
|
Base: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) initForConn() Obfs {
|
||||||
|
r := &tls12Ticket{
|
||||||
|
Base: t.Base,
|
||||||
|
tlsAuthData: &tlsAuthData{},
|
||||||
|
}
|
||||||
|
rand.Read(r.localClientID[:])
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) GetObfsOverhead() int {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) Decode(b []byte) ([]byte, bool, error) {
|
||||||
|
if t.handshakeStatus == -1 {
|
||||||
|
return b, false, nil
|
||||||
|
}
|
||||||
|
t.buffer.Reset()
|
||||||
|
if t.handshakeStatus == 8 {
|
||||||
|
t.recvBuffer.Write(b)
|
||||||
|
for t.recvBuffer.Len() > 5 {
|
||||||
|
var h [5]byte
|
||||||
|
t.recvBuffer.Read(h[:])
|
||||||
|
if !bytes.Equal(h[:3], []byte{0x17, 0x3, 0x3}) {
|
||||||
|
log.Warnln("incorrect magic number %x, 0x170303 is expected", h[:3])
|
||||||
|
return nil, false, errTLS12TicketAuthIncorrectMagicNumber
|
||||||
|
}
|
||||||
|
size := int(binary.BigEndian.Uint16(h[3:5]))
|
||||||
|
if t.recvBuffer.Len() < size {
|
||||||
|
// 不够读,下回再读吧
|
||||||
|
unread := t.recvBuffer.Bytes()
|
||||||
|
t.recvBuffer.Reset()
|
||||||
|
t.recvBuffer.Write(h[:])
|
||||||
|
t.recvBuffer.Write(unread)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
d := pool.Get(size)
|
||||||
|
t.recvBuffer.Read(d)
|
||||||
|
t.buffer.Write(d)
|
||||||
|
pool.Put(d)
|
||||||
|
}
|
||||||
|
return t.buffer.Bytes(), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) < 11+32+1+32 {
|
||||||
|
return nil, false, errTLS12TicketAuthTooShortData
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := t.hmacSHA1(b[11 : 11+22])
|
||||||
|
|
||||||
|
if !hmac.Equal(b[33:33+tools.HmacSHA1Len], hash) {
|
||||||
|
return nil, false, errTLS12TicketAuthHMACError
|
||||||
|
}
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) Encode(b []byte) ([]byte, error) {
|
||||||
|
t.buffer.Reset()
|
||||||
|
switch t.handshakeStatus {
|
||||||
|
case 8:
|
||||||
|
if len(b) < 1024 {
|
||||||
|
d := []byte{0x17, 0x3, 0x3, 0, 0}
|
||||||
|
binary.BigEndian.PutUint16(d[3:5], uint16(len(b)&0xFFFF))
|
||||||
|
t.buffer.Write(d)
|
||||||
|
t.buffer.Write(b)
|
||||||
|
return t.buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
start := 0
|
||||||
|
var l int
|
||||||
|
for len(b)-start > 2048 {
|
||||||
|
l = rand.Intn(4096) + 100
|
||||||
|
if l > len(b)-start {
|
||||||
|
l = len(b) - start
|
||||||
|
}
|
||||||
|
packData(&t.buffer, b[start:start+l])
|
||||||
|
start += l
|
||||||
|
}
|
||||||
|
if len(b)-start > 0 {
|
||||||
|
l = len(b) - start
|
||||||
|
packData(&t.buffer, b[start:start+l])
|
||||||
|
}
|
||||||
|
return t.buffer.Bytes(), nil
|
||||||
|
case 1:
|
||||||
|
if len(b) > 0 {
|
||||||
|
if len(b) < 1024 {
|
||||||
|
packData(&t.sendSaver, b)
|
||||||
|
} else {
|
||||||
|
start := 0
|
||||||
|
var l int
|
||||||
|
for len(b)-start > 2048 {
|
||||||
|
l = rand.Intn(4096) + 100
|
||||||
|
if l > len(b)-start {
|
||||||
|
l = len(b) - start
|
||||||
|
}
|
||||||
|
packData(&t.buffer, b[start:start+l])
|
||||||
|
start += l
|
||||||
|
}
|
||||||
|
if len(b)-start > 0 {
|
||||||
|
l = len(b) - start
|
||||||
|
packData(&t.buffer, b[start:start+l])
|
||||||
|
}
|
||||||
|
io.Copy(&t.sendSaver, &t.buffer)
|
||||||
|
}
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
hmacData := make([]byte, 43)
|
||||||
|
handshakeFinish := []byte("\x14\x03\x03\x00\x01\x01\x16\x03\x03\x00\x20")
|
||||||
|
copy(hmacData, handshakeFinish)
|
||||||
|
rand.Read(hmacData[11:33])
|
||||||
|
h := t.hmacSHA1(hmacData[:33])
|
||||||
|
copy(hmacData[33:], h)
|
||||||
|
t.buffer.Write(hmacData)
|
||||||
|
io.Copy(&t.buffer, &t.sendSaver)
|
||||||
|
t.handshakeStatus = 8
|
||||||
|
return t.buffer.Bytes(), nil
|
||||||
|
case 0:
|
||||||
|
tlsData0 := []byte("\x00\x1c\xc0\x2b\xc0\x2f\xcc\xa9\xcc\xa8\xcc\x14\xcc\x13\xc0\x0a\xc0\x14\xc0\x09\xc0\x13\x00\x9c\x00\x35\x00\x2f\x00\x0a\x01\x00")
|
||||||
|
tlsData1 := []byte("\xff\x01\x00\x01\x00")
|
||||||
|
tlsData2 := []byte("\x00\x17\x00\x00\x00\x23\x00\xd0")
|
||||||
|
// tlsData3 := []byte("\x00\x0d\x00\x16\x00\x14\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x03\x01\x03\x03\x02\x01\x02\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x75\x50\x00\x00\x00\x0b\x00\x02\x01\x00\x00\x0a\x00\x06\x00\x04\x00\x17\x00\x18\x00\x15\x00\x66\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||||
|
tlsData3 := []byte("\x00\x0d\x00\x16\x00\x14\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x03\x01\x03\x03\x02\x01\x02\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x75\x50\x00\x00\x00\x0b\x00\x02\x01\x00\x00\x0a\x00\x06\x00\x04\x00\x17\x00\x18")
|
||||||
|
|
||||||
|
var tlsData [2048]byte
|
||||||
|
tlsDataLen := 0
|
||||||
|
copy(tlsData[0:], tlsData1)
|
||||||
|
tlsDataLen += len(tlsData1)
|
||||||
|
sni := t.sni(t.getHost())
|
||||||
|
copy(tlsData[tlsDataLen:], sni)
|
||||||
|
tlsDataLen += len(sni)
|
||||||
|
copy(tlsData[tlsDataLen:], tlsData2)
|
||||||
|
tlsDataLen += len(tlsData2)
|
||||||
|
ticketLen := rand.Intn(164)*2 + 64
|
||||||
|
tlsData[tlsDataLen-1] = uint8(ticketLen & 0xff)
|
||||||
|
tlsData[tlsDataLen-2] = uint8(ticketLen >> 8)
|
||||||
|
//ticketLen := 208
|
||||||
|
rand.Read(tlsData[tlsDataLen : tlsDataLen+ticketLen])
|
||||||
|
tlsDataLen += ticketLen
|
||||||
|
copy(tlsData[tlsDataLen:], tlsData3)
|
||||||
|
tlsDataLen += len(tlsData3)
|
||||||
|
|
||||||
|
length := 11 + 32 + 1 + 32 + len(tlsData0) + 2 + tlsDataLen
|
||||||
|
encodedData := make([]byte, length)
|
||||||
|
pdata := length - tlsDataLen
|
||||||
|
l := tlsDataLen
|
||||||
|
copy(encodedData[pdata:], tlsData[:tlsDataLen])
|
||||||
|
encodedData[pdata-1] = uint8(tlsDataLen)
|
||||||
|
encodedData[pdata-2] = uint8(tlsDataLen >> 8)
|
||||||
|
pdata -= 2
|
||||||
|
l += 2
|
||||||
|
copy(encodedData[pdata-len(tlsData0):], tlsData0)
|
||||||
|
pdata -= len(tlsData0)
|
||||||
|
l += len(tlsData0)
|
||||||
|
copy(encodedData[pdata-32:], t.localClientID[:])
|
||||||
|
pdata -= 32
|
||||||
|
l += 32
|
||||||
|
encodedData[pdata-1] = 0x20
|
||||||
|
pdata--
|
||||||
|
l++
|
||||||
|
copy(encodedData[pdata-32:], t.packAuthData())
|
||||||
|
pdata -= 32
|
||||||
|
l += 32
|
||||||
|
encodedData[pdata-1] = 0x3
|
||||||
|
encodedData[pdata-2] = 0x3 // tls version
|
||||||
|
pdata -= 2
|
||||||
|
l += 2
|
||||||
|
encodedData[pdata-1] = uint8(l)
|
||||||
|
encodedData[pdata-2] = uint8(l >> 8)
|
||||||
|
encodedData[pdata-3] = 0
|
||||||
|
encodedData[pdata-4] = 1
|
||||||
|
pdata -= 4
|
||||||
|
l += 4
|
||||||
|
encodedData[pdata-1] = uint8(l)
|
||||||
|
encodedData[pdata-2] = uint8(l >> 8)
|
||||||
|
pdata -= 2
|
||||||
|
l += 2
|
||||||
|
encodedData[pdata-1] = 0x1
|
||||||
|
encodedData[pdata-2] = 0x3 // tls version
|
||||||
|
pdata -= 2
|
||||||
|
l += 2
|
||||||
|
encodedData[pdata-1] = 0x16 // tls handshake
|
||||||
|
pdata--
|
||||||
|
l++
|
||||||
|
packData(&t.sendSaver, b)
|
||||||
|
t.handshakeStatus = 1
|
||||||
|
return encodedData, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected handshake status: %d", t.handshakeStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) hmacSHA1(data []byte) []byte {
|
||||||
|
key := make([]byte, len(t.Key)+32)
|
||||||
|
copy(key, t.Key)
|
||||||
|
copy(key[len(t.Key):], t.localClientID[:])
|
||||||
|
|
||||||
|
sha1Data := tools.HmacSHA1(key, data)
|
||||||
|
return sha1Data[:tools.HmacSHA1Len]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) sni(u string) []byte {
|
||||||
|
bURL := []byte(u)
|
||||||
|
length := len(bURL)
|
||||||
|
ret := make([]byte, length+9)
|
||||||
|
copy(ret[9:9+length], bURL)
|
||||||
|
binary.BigEndian.PutUint16(ret[7:], uint16(length&0xFFFF))
|
||||||
|
length += 3
|
||||||
|
binary.BigEndian.PutUint16(ret[4:], uint16(length&0xFFFF))
|
||||||
|
length += 2
|
||||||
|
binary.BigEndian.PutUint16(ret[2:], uint16(length&0xFFFF))
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) getHost() string {
|
||||||
|
host := t.Host
|
||||||
|
if len(t.Param) > 0 {
|
||||||
|
hosts := strings.Split(t.Param, ",")
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
|
||||||
|
host = hosts[rand.Intn(len(hosts))]
|
||||||
|
host = strings.TrimSpace(host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(host) > 0 && host[len(host)-1] >= byte('0') && host[len(host)-1] <= byte('9') && len(t.Param) == 0 {
|
||||||
|
host = ""
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tls12Ticket) packAuthData() (ret []byte) {
|
||||||
|
retSize := 32
|
||||||
|
ret = make([]byte, retSize)
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
binary.BigEndian.PutUint32(ret[:4], uint32(now))
|
||||||
|
|
||||||
|
rand.Read(ret[4 : 4+18])
|
||||||
|
|
||||||
|
hash := t.hmacSHA1(ret[:retSize-tools.HmacSHA1Len])
|
||||||
|
copy(ret[retSize-tools.HmacSHA1Len:], hash)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func packData(buffer *bytes.Buffer, suffix []byte) {
|
||||||
|
d := []byte{0x17, 0x3, 0x3, 0, 0}
|
||||||
|
binary.BigEndian.PutUint16(d[3:5], uint16(len(suffix)&0xFFFF))
|
||||||
|
buffer.Write(d)
|
||||||
|
buffer.Write(suffix)
|
||||||
|
}
|
310
component/ssr/protocol/auth_aes128_md5.go
Normal file
310
component/ssr/protocol/auth_aes128_md5.go
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authAES128 struct {
|
||||||
|
*Base
|
||||||
|
*recvInfo
|
||||||
|
*authData
|
||||||
|
hasSentHeader bool
|
||||||
|
packID uint32
|
||||||
|
userKey []byte
|
||||||
|
uid [4]byte
|
||||||
|
salt string
|
||||||
|
hmac hmacMethod
|
||||||
|
hashDigest hashDigestMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_aes128_md5", newAuthAES128MD5)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthAES128MD5(b *Base) Protocol {
|
||||||
|
return &authAES128{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
salt: "auth_aes128_md5",
|
||||||
|
hmac: tools.HmacMD5,
|
||||||
|
hashDigest: tools.MD5Sum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) initForConn(iv []byte) Protocol {
|
||||||
|
return &authAES128{
|
||||||
|
Base: &Base{
|
||||||
|
IV: iv,
|
||||||
|
Key: a.Key,
|
||||||
|
TCPMss: a.TCPMss,
|
||||||
|
Overhead: a.Overhead,
|
||||||
|
Param: a.Param,
|
||||||
|
},
|
||||||
|
recvInfo: &recvInfo{recvID: 1, buffer: new(bytes.Buffer)},
|
||||||
|
authData: a.authData,
|
||||||
|
packID: 1,
|
||||||
|
salt: a.salt,
|
||||||
|
hmac: a.hmac,
|
||||||
|
hashDigest: a.hashDigest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) GetProtocolOverhead() int {
|
||||||
|
return 9
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) SetOverhead(overhead int) {
|
||||||
|
a.Overhead = overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) Decode(b []byte) ([]byte, int, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
bSize := len(b)
|
||||||
|
readSize := 0
|
||||||
|
key := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(key)
|
||||||
|
copy(key, a.userKey)
|
||||||
|
for bSize > 4 {
|
||||||
|
binary.LittleEndian.PutUint32(key[len(key)-4:], a.recvID)
|
||||||
|
|
||||||
|
h := a.hmac(key, b[:2])
|
||||||
|
if !bytes.Equal(h[:2], b[2:4]) {
|
||||||
|
return nil, 0, errAuthAES128IncorrectMAC
|
||||||
|
}
|
||||||
|
|
||||||
|
length := int(binary.LittleEndian.Uint16(b[:2]))
|
||||||
|
if length >= 8192 || length < 8 {
|
||||||
|
return nil, 0, errAuthAES128DataLengthError
|
||||||
|
}
|
||||||
|
if length > bSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
h = a.hmac(key, b[:length-4])
|
||||||
|
if !bytes.Equal(h[:4], b[length-4:length]) {
|
||||||
|
return nil, 0, errAuthAES128IncorrectChecksum
|
||||||
|
}
|
||||||
|
|
||||||
|
a.recvID++
|
||||||
|
pos := int(b[4])
|
||||||
|
if pos < 255 {
|
||||||
|
pos += 4
|
||||||
|
} else {
|
||||||
|
pos = int(binary.LittleEndian.Uint16(b[5:7])) + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos > length-4 {
|
||||||
|
return nil, 0, errAuthAES128PositionTooLarge
|
||||||
|
}
|
||||||
|
a.buffer.Write(b[pos : length-4])
|
||||||
|
b = b[length:]
|
||||||
|
bSize -= length
|
||||||
|
readSize += length
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), readSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) Encode(b []byte) ([]byte, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
bSize := len(b)
|
||||||
|
offset := 0
|
||||||
|
if bSize > 0 && !a.hasSentHeader {
|
||||||
|
authSize := bSize
|
||||||
|
if authSize > 1200 {
|
||||||
|
authSize = 1200
|
||||||
|
}
|
||||||
|
a.hasSentHeader = true
|
||||||
|
a.buffer.Write(a.packAuthData(b[:authSize]))
|
||||||
|
bSize -= authSize
|
||||||
|
offset += authSize
|
||||||
|
}
|
||||||
|
const blockSize = 4096
|
||||||
|
for bSize > blockSize {
|
||||||
|
packSize, randSize := a.packedDataSize(b[offset : offset+blockSize])
|
||||||
|
pack := pool.Get(packSize)
|
||||||
|
a.packData(b[offset:offset+blockSize], pack, randSize)
|
||||||
|
a.buffer.Write(pack)
|
||||||
|
pool.Put(pack)
|
||||||
|
bSize -= blockSize
|
||||||
|
offset += blockSize
|
||||||
|
}
|
||||||
|
if bSize > 0 {
|
||||||
|
packSize, randSize := a.packedDataSize(b[offset:])
|
||||||
|
pack := pool.Get(packSize)
|
||||||
|
a.packData(b[offset:], pack, randSize)
|
||||||
|
a.buffer.Write(pack)
|
||||||
|
pool.Put(pack)
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) DecodePacket(b []byte) ([]byte, int, error) {
|
||||||
|
bSize := len(b)
|
||||||
|
h := a.hmac(a.Key, b[:bSize-4])
|
||||||
|
if !bytes.Equal(h[:4], b[bSize-4:]) {
|
||||||
|
return nil, 0, errAuthAES128IncorrectMAC
|
||||||
|
}
|
||||||
|
return b[:bSize-4], bSize - 4, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) EncodePacket(b []byte) ([]byte, error) {
|
||||||
|
a.initUserKeyAndID()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Write(b)
|
||||||
|
buf.Write(a.uid[:])
|
||||||
|
h := a.hmac(a.userKey, buf.Bytes())
|
||||||
|
buf.Write(h[:4])
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) initUserKeyAndID() {
|
||||||
|
if a.userKey == nil {
|
||||||
|
params := strings.Split(a.Param, ":")
|
||||||
|
if len(params) >= 2 {
|
||||||
|
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
|
||||||
|
binary.LittleEndian.PutUint32(a.uid[:], uint32(userID))
|
||||||
|
a.userKey = a.hashDigest([]byte(params[1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.userKey == nil {
|
||||||
|
rand.Read(a.uid[:])
|
||||||
|
a.userKey = make([]byte, len(a.Key))
|
||||||
|
copy(a.userKey, a.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packedDataSize(data []byte) (packSize, randSize int) {
|
||||||
|
dataSize := len(data)
|
||||||
|
randSize = 1
|
||||||
|
if dataSize <= 1200 {
|
||||||
|
if a.packID > 4 {
|
||||||
|
randSize += rand.Intn(32)
|
||||||
|
} else {
|
||||||
|
if dataSize > 900 {
|
||||||
|
randSize += rand.Intn(128)
|
||||||
|
} else {
|
||||||
|
randSize += rand.Intn(512)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packSize = randSize + dataSize + 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packData(data, ret []byte, randSize int) {
|
||||||
|
dataSize := len(data)
|
||||||
|
retSize := len(ret)
|
||||||
|
// 0~1, ret_size
|
||||||
|
binary.LittleEndian.PutUint16(ret[0:], uint16(retSize&0xFFFF))
|
||||||
|
// 2~3, hmac
|
||||||
|
key := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(key)
|
||||||
|
copy(key, a.userKey)
|
||||||
|
binary.LittleEndian.PutUint32(key[len(key)-4:], a.packID)
|
||||||
|
h := a.hmac(key, ret[:2])
|
||||||
|
copy(ret[2:4], h[:2])
|
||||||
|
// 4~rand_size+4, rand number
|
||||||
|
rand.Read(ret[4 : 4+randSize])
|
||||||
|
// 4, rand_size
|
||||||
|
if randSize < 128 {
|
||||||
|
ret[4] = byte(randSize & 0xFF)
|
||||||
|
} else {
|
||||||
|
// 4, magic number 0xFF
|
||||||
|
ret[4] = 0xFF
|
||||||
|
// 5~6, rand_size
|
||||||
|
binary.LittleEndian.PutUint16(ret[5:], uint16(randSize&0xFFFF))
|
||||||
|
}
|
||||||
|
// rand_size+4~ret_size-4, data
|
||||||
|
if dataSize > 0 {
|
||||||
|
copy(ret[randSize+4:], data)
|
||||||
|
}
|
||||||
|
a.packID++
|
||||||
|
h = a.hmac(key, ret[:retSize-4])
|
||||||
|
copy(ret[retSize-4:], h[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authAES128) packAuthData(data []byte) (ret []byte) {
|
||||||
|
dataSize := len(data)
|
||||||
|
var randSize int
|
||||||
|
|
||||||
|
if dataSize > 400 {
|
||||||
|
randSize = rand.Intn(512)
|
||||||
|
} else {
|
||||||
|
randSize = rand.Intn(1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataOffset := randSize + 16 + 4 + 4 + 7
|
||||||
|
retSize := dataOffset + dataSize + 4
|
||||||
|
ret = make([]byte, retSize)
|
||||||
|
encrypt := make([]byte, 24)
|
||||||
|
key := make([]byte, len(a.IV)+len(a.Key))
|
||||||
|
copy(key, a.IV)
|
||||||
|
copy(key[len(a.IV):], a.Key)
|
||||||
|
|
||||||
|
rand.Read(ret[dataOffset-randSize:])
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
a.connectionID++
|
||||||
|
if a.connectionID > 0xFF000000 {
|
||||||
|
a.clientID = nil
|
||||||
|
}
|
||||||
|
if len(a.clientID) == 0 {
|
||||||
|
a.clientID = make([]byte, 8)
|
||||||
|
rand.Read(a.clientID)
|
||||||
|
b := make([]byte, 4)
|
||||||
|
rand.Read(b)
|
||||||
|
a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF
|
||||||
|
}
|
||||||
|
copy(encrypt[4:], a.clientID)
|
||||||
|
binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID)
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
binary.LittleEndian.PutUint32(encrypt[:4], uint32(now))
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[12:], uint16(retSize&0xFFFF))
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[14:], uint16(randSize&0xFFFF))
|
||||||
|
|
||||||
|
a.initUserKeyAndID()
|
||||||
|
|
||||||
|
aesCipherKey := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+a.salt, 16)
|
||||||
|
block, err := aes.NewCipher(aesCipherKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
encryptData := make([]byte, 16)
|
||||||
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
cbc := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
cbc.CryptBlocks(encryptData, encrypt[:16])
|
||||||
|
copy(encrypt[:4], a.uid[:])
|
||||||
|
copy(encrypt[4:4+16], encryptData)
|
||||||
|
|
||||||
|
h := a.hmac(key, encrypt[:20])
|
||||||
|
copy(encrypt[20:], h[:4])
|
||||||
|
|
||||||
|
rand.Read(ret[:1])
|
||||||
|
h = a.hmac(key, ret[:1])
|
||||||
|
copy(ret[1:], h[:7-1])
|
||||||
|
|
||||||
|
copy(ret[7:], encrypt)
|
||||||
|
copy(ret[dataOffset:], data)
|
||||||
|
|
||||||
|
h = a.hmac(a.userKey, ret[:retSize-4])
|
||||||
|
copy(ret[retSize-4:], h[:4])
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
22
component/ssr/protocol/auth_aes128_sha1.go
Normal file
22
component/ssr/protocol/auth_aes128_sha1.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_aes128_sha1", newAuthAES128SHA1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthAES128SHA1(b *Base) Protocol {
|
||||||
|
return &authAES128{
|
||||||
|
Base: b,
|
||||||
|
recvInfo: &recvInfo{buffer: new(bytes.Buffer)},
|
||||||
|
authData: &authData{},
|
||||||
|
salt: "auth_aes128_sha1",
|
||||||
|
hmac: tools.HmacSHA1,
|
||||||
|
hashDigest: tools.SHA1Sum,
|
||||||
|
}
|
||||||
|
}
|
427
component/ssr/protocol/auth_chain_a.go
Normal file
427
component/ssr/protocol/auth_chain_a.go
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rc4"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authChain struct {
|
||||||
|
*Base
|
||||||
|
*recvInfo
|
||||||
|
*authData
|
||||||
|
randomClient shift128PlusContext
|
||||||
|
randomServer shift128PlusContext
|
||||||
|
enc cipher.Stream
|
||||||
|
dec cipher.Stream
|
||||||
|
headerSent bool
|
||||||
|
lastClientHash []byte
|
||||||
|
lastServerHash []byte
|
||||||
|
userKey []byte
|
||||||
|
uid [4]byte
|
||||||
|
salt string
|
||||||
|
hmac hmacMethod
|
||||||
|
hashDigest hashDigestMethod
|
||||||
|
rnd rndMethod
|
||||||
|
dataSizeList []int
|
||||||
|
dataSizeList2 []int
|
||||||
|
chunkID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_chain_a", newAuthChainA)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthChainA(b *Base) Protocol {
|
||||||
|
return &authChain{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
salt: "auth_chain_a",
|
||||||
|
hmac: tools.HmacMD5,
|
||||||
|
hashDigest: tools.SHA1Sum,
|
||||||
|
rnd: authChainAGetRandLen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) initForConn(iv []byte) Protocol {
|
||||||
|
r := &authChain{
|
||||||
|
Base: &Base{
|
||||||
|
IV: iv,
|
||||||
|
Key: a.Key,
|
||||||
|
TCPMss: a.TCPMss,
|
||||||
|
Overhead: a.Overhead,
|
||||||
|
Param: a.Param,
|
||||||
|
},
|
||||||
|
recvInfo: &recvInfo{recvID: 1, buffer: new(bytes.Buffer)},
|
||||||
|
authData: a.authData,
|
||||||
|
salt: a.salt,
|
||||||
|
hmac: a.hmac,
|
||||||
|
hashDigest: a.hashDigest,
|
||||||
|
rnd: a.rnd,
|
||||||
|
}
|
||||||
|
if r.salt == "auth_chain_b" {
|
||||||
|
initDataSize(r)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) GetProtocolOverhead() int {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) SetOverhead(overhead int) {
|
||||||
|
a.Overhead = overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) Decode(b []byte) ([]byte, int, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
key := pool.Get(len(a.userKey) + 4)
|
||||||
|
defer pool.Put(key)
|
||||||
|
readSize := 0
|
||||||
|
copy(key, a.userKey)
|
||||||
|
for len(b) > 4 {
|
||||||
|
binary.LittleEndian.PutUint32(key[len(a.userKey):], a.recvID)
|
||||||
|
dataLen := (int)((uint(b[1]^a.lastServerHash[15]) << 8) + uint(b[0]^a.lastServerHash[14]))
|
||||||
|
randLen := a.getServerRandLen(dataLen, a.Overhead)
|
||||||
|
length := randLen + dataLen
|
||||||
|
if length >= 4096 {
|
||||||
|
return nil, 0, errAuthChainDataLengthError
|
||||||
|
}
|
||||||
|
length += 4
|
||||||
|
if length > len(b) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := a.hmac(key, b[:length-2])
|
||||||
|
if !bytes.Equal(hash[:2], b[length-2:length]) {
|
||||||
|
return nil, 0, errAuthChainHMACError
|
||||||
|
}
|
||||||
|
var dataPos int
|
||||||
|
if dataLen > 0 && randLen > 0 {
|
||||||
|
dataPos = 2 + getRandStartPos(&a.randomServer, randLen)
|
||||||
|
} else {
|
||||||
|
dataPos = 2
|
||||||
|
}
|
||||||
|
d := pool.Get(dataLen)
|
||||||
|
a.dec.XORKeyStream(d, b[dataPos:dataPos+dataLen])
|
||||||
|
a.buffer.Write(d)
|
||||||
|
pool.Put(d)
|
||||||
|
if a.recvID == 1 {
|
||||||
|
a.TCPMss = int(binary.LittleEndian.Uint16(a.buffer.Next(2)))
|
||||||
|
}
|
||||||
|
a.lastServerHash = hash
|
||||||
|
a.recvID++
|
||||||
|
b = b[length:]
|
||||||
|
readSize += length
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), readSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) Encode(b []byte) ([]byte, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
bSize := len(b)
|
||||||
|
offset := 0
|
||||||
|
if bSize > 0 && !a.headerSent {
|
||||||
|
headSize := 1200
|
||||||
|
if headSize > bSize {
|
||||||
|
headSize = bSize
|
||||||
|
}
|
||||||
|
a.buffer.Write(a.packAuthData(b[:headSize]))
|
||||||
|
offset += headSize
|
||||||
|
bSize -= headSize
|
||||||
|
a.headerSent = true
|
||||||
|
}
|
||||||
|
var unitSize = a.TCPMss - a.Overhead
|
||||||
|
for bSize > unitSize {
|
||||||
|
dataLen, randLength := a.packedDataLen(b[offset : offset+unitSize])
|
||||||
|
d := pool.Get(dataLen)
|
||||||
|
a.packData(d, b[offset:offset+unitSize], randLength)
|
||||||
|
a.buffer.Write(d)
|
||||||
|
pool.Put(d)
|
||||||
|
bSize -= unitSize
|
||||||
|
offset += unitSize
|
||||||
|
}
|
||||||
|
if bSize > 0 {
|
||||||
|
dataLen, randLength := a.packedDataLen(b[offset:])
|
||||||
|
d := pool.Get(dataLen)
|
||||||
|
a.packData(d, b[offset:], randLength)
|
||||||
|
a.buffer.Write(d)
|
||||||
|
pool.Put(d)
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) DecodePacket(b []byte) ([]byte, int, error) {
|
||||||
|
bSize := len(b)
|
||||||
|
if bSize < 9 {
|
||||||
|
return nil, 0, errAuthChainDataLengthError
|
||||||
|
}
|
||||||
|
h := a.hmac(a.userKey, b[:bSize-1])
|
||||||
|
if h[0] != b[bSize-1] {
|
||||||
|
return nil, 0, errAuthChainHMACError
|
||||||
|
}
|
||||||
|
hash := a.hmac(a.Key, b[bSize-8:bSize-1])
|
||||||
|
cipherKey := a.getRC4CipherKey(hash)
|
||||||
|
dec, _ := rc4.NewCipher(cipherKey)
|
||||||
|
randLength := udpGetRandLen(&a.randomServer, hash)
|
||||||
|
bSize -= 8 + randLength
|
||||||
|
dec.XORKeyStream(b, b[:bSize])
|
||||||
|
return b, bSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) EncodePacket(b []byte) ([]byte, error) {
|
||||||
|
a.initUserKeyAndID()
|
||||||
|
authData := pool.Get(3)
|
||||||
|
defer pool.Put(authData)
|
||||||
|
rand.Read(authData)
|
||||||
|
hash := a.hmac(a.Key, authData)
|
||||||
|
uid := pool.Get(4)
|
||||||
|
defer pool.Put(uid)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
uid[i] = a.uid[i] ^ hash[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherKey := a.getRC4CipherKey(hash)
|
||||||
|
enc, _ := rc4.NewCipher(cipherKey)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc.XORKeyStream(b, b)
|
||||||
|
buf.Write(b)
|
||||||
|
|
||||||
|
randLength := udpGetRandLen(&a.randomClient, hash)
|
||||||
|
randBytes := pool.Get(randLength)
|
||||||
|
defer pool.Put(randBytes)
|
||||||
|
buf.Write(randBytes)
|
||||||
|
|
||||||
|
buf.Write(authData)
|
||||||
|
buf.Write(uid)
|
||||||
|
|
||||||
|
h := a.hmac(a.userKey, buf.Bytes())
|
||||||
|
buf.Write(h[:1])
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) getRC4CipherKey(hash []byte) []byte {
|
||||||
|
base64UserKey := base64.StdEncoding.EncodeToString(a.userKey)
|
||||||
|
return a.calcRC4CipherKey(hash, base64UserKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) calcRC4CipherKey(hash []byte, base64UserKey string) []byte {
|
||||||
|
password := pool.Get(len(base64UserKey) + base64.StdEncoding.EncodedLen(16))
|
||||||
|
defer pool.Put(password)
|
||||||
|
copy(password, base64UserKey)
|
||||||
|
base64.StdEncoding.Encode(password[len(base64UserKey):], hash[:16])
|
||||||
|
return core.Kdf(string(password), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) initUserKeyAndID() {
|
||||||
|
if a.userKey == nil {
|
||||||
|
params := strings.Split(a.Param, ":")
|
||||||
|
if len(params) >= 2 {
|
||||||
|
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
|
||||||
|
binary.LittleEndian.PutUint32(a.uid[:], uint32(userID))
|
||||||
|
a.userKey = []byte(params[1])[:len(a.userKey)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.userKey == nil {
|
||||||
|
rand.Read(a.uid[:])
|
||||||
|
a.userKey = make([]byte, len(a.Key))
|
||||||
|
copy(a.userKey, a.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) getClientRandLen(dataLength int, overhead int) int {
|
||||||
|
return a.rnd(dataLength, &a.randomClient, a.lastClientHash, a.dataSizeList, a.dataSizeList2, overhead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) getServerRandLen(dataLength int, overhead int) int {
|
||||||
|
return a.rnd(dataLength, &a.randomServer, a.lastServerHash, a.dataSizeList, a.dataSizeList2, overhead)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) packedDataLen(data []byte) (chunkLength, randLength int) {
|
||||||
|
dataLength := len(data)
|
||||||
|
randLength = a.getClientRandLen(dataLength, a.Overhead)
|
||||||
|
chunkLength = randLength + dataLength + 2 + 2
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authChain) packData(outData []byte, data []byte, randLength int) {
|
||||||
|
dataLength := len(data)
|
||||||
|
outLength := randLength + dataLength + 2
|
||||||
|
outData[0] = byte(dataLength) ^ a.lastClientHash[14]
|
||||||
|
outData[1] = byte(dataLength>>8) ^ a.lastClientHash[15]
|
||||||
|
|
||||||
|
{
|
||||||
|
if dataLength > 0 {
|
||||||
|
randPart1Length := getRandStartPos(&a.randomClient, randLength)
|
||||||
|
rand.Read(outData[2 : 2+randPart1Length])
|
||||||
|
a.enc.XORKeyStream(outData[2+randPart1Length:], data)
|
||||||
|
rand.Read(outData[2+randPart1Length+dataLength : outLength])
|
||||||
|
} else {
|
||||||
|
rand.Read(outData[2 : 2+randLength])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userKeyLen := uint8(len(a.userKey))
|
||||||
|
key := pool.Get(int(userKeyLen + 4))
|
||||||
|
defer pool.Put(key)
|
||||||
|
copy(key, a.userKey)
|
||||||
|
a.chunkID++
|
||||||
|
binary.LittleEndian.PutUint32(key[userKeyLen:], a.chunkID)
|
||||||
|
a.lastClientHash = a.hmac(key, outData[:outLength])
|
||||||
|
copy(outData[outLength:], a.lastClientHash[:2])
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeadLength = 4 + 8 + 4 + 16 + 4
|
||||||
|
|
||||||
|
func (a *authChain) packAuthData(data []byte) (outData []byte) {
|
||||||
|
outData = make([]byte, authHeadLength, authHeadLength+1500)
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
a.connectionID++
|
||||||
|
if a.connectionID > 0xFF000000 {
|
||||||
|
rand.Read(a.clientID)
|
||||||
|
b := make([]byte, 4)
|
||||||
|
rand.Read(b)
|
||||||
|
a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF
|
||||||
|
}
|
||||||
|
var key = make([]byte, len(a.IV)+len(a.Key))
|
||||||
|
copy(key, a.IV)
|
||||||
|
copy(key[len(a.IV):], a.Key)
|
||||||
|
|
||||||
|
encrypt := make([]byte, 20)
|
||||||
|
t := time.Now().Unix()
|
||||||
|
binary.LittleEndian.PutUint32(encrypt[:4], uint32(t))
|
||||||
|
copy(encrypt[4:8], a.clientID)
|
||||||
|
binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID)
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[12:], uint16(a.Overhead))
|
||||||
|
binary.LittleEndian.PutUint16(encrypt[14:], 0)
|
||||||
|
|
||||||
|
// first 12 bytes
|
||||||
|
{
|
||||||
|
rand.Read(outData[:4])
|
||||||
|
a.lastClientHash = a.hmac(key, outData[:4])
|
||||||
|
copy(outData[4:], a.lastClientHash[:8])
|
||||||
|
}
|
||||||
|
var base64UserKey string
|
||||||
|
// uid & 16 bytes auth data
|
||||||
|
{
|
||||||
|
a.initUserKeyAndID()
|
||||||
|
uid := make([]byte, 4)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
uid[i] = a.uid[i] ^ a.lastClientHash[8+i]
|
||||||
|
}
|
||||||
|
base64UserKey = base64.StdEncoding.EncodeToString(a.userKey)
|
||||||
|
aesCipherKey := core.Kdf(base64UserKey+a.salt, 16)
|
||||||
|
block, err := aes.NewCipher(aesCipherKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encryptData := make([]byte, 16)
|
||||||
|
iv := make([]byte, aes.BlockSize)
|
||||||
|
cbc := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
cbc.CryptBlocks(encryptData, encrypt[:16])
|
||||||
|
copy(encrypt[:4], uid[:])
|
||||||
|
copy(encrypt[4:4+16], encryptData)
|
||||||
|
}
|
||||||
|
// final HMAC
|
||||||
|
{
|
||||||
|
a.lastServerHash = a.hmac(a.userKey, encrypt[:20])
|
||||||
|
|
||||||
|
copy(outData[12:], encrypt)
|
||||||
|
copy(outData[12+20:], a.lastServerHash[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
// init cipher
|
||||||
|
cipherKey := a.calcRC4CipherKey(a.lastClientHash, base64UserKey)
|
||||||
|
a.enc, _ = rc4.NewCipher(cipherKey)
|
||||||
|
a.dec, _ = rc4.NewCipher(cipherKey)
|
||||||
|
|
||||||
|
// data
|
||||||
|
chunkLength, randLength := a.packedDataLen(data)
|
||||||
|
if chunkLength <= 1500 {
|
||||||
|
outData = outData[:authHeadLength+chunkLength]
|
||||||
|
} else {
|
||||||
|
newOutData := make([]byte, authHeadLength+chunkLength)
|
||||||
|
copy(newOutData, outData[:authHeadLength])
|
||||||
|
outData = newOutData
|
||||||
|
}
|
||||||
|
a.packData(outData[authHeadLength:], data, randLength)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandStartPos(random *shift128PlusContext, randLength int) int {
|
||||||
|
if randLength > 0 {
|
||||||
|
return int(random.Next() % 8589934609 % uint64(randLength))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func authChainAGetRandLen(dataLength int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int {
|
||||||
|
if dataLength > 1440 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
random.InitFromBinDatalen(lastHash[:16], dataLength)
|
||||||
|
if dataLength > 1300 {
|
||||||
|
return int(random.Next() % 31)
|
||||||
|
}
|
||||||
|
if dataLength > 900 {
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
||||||
|
if dataLength > 400 {
|
||||||
|
return int(random.Next() % 521)
|
||||||
|
}
|
||||||
|
return int(random.Next() % 1021)
|
||||||
|
}
|
||||||
|
|
||||||
|
func udpGetRandLen(random *shift128PlusContext, lastHash []byte) int {
|
||||||
|
random.InitFromBin(lastHash[:16])
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
||||||
|
|
||||||
|
type shift128PlusContext struct {
|
||||||
|
v [2]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *shift128PlusContext) InitFromBin(bin []byte) {
|
||||||
|
var fillBin [16]byte
|
||||||
|
copy(fillBin[:], bin)
|
||||||
|
|
||||||
|
ctx.v[0] = binary.LittleEndian.Uint64(fillBin[:8])
|
||||||
|
ctx.v[1] = binary.LittleEndian.Uint64(fillBin[8:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *shift128PlusContext) InitFromBinDatalen(bin []byte, datalen int) {
|
||||||
|
var fillBin [16]byte
|
||||||
|
copy(fillBin[:], bin)
|
||||||
|
binary.LittleEndian.PutUint16(fillBin[:2], uint16(datalen))
|
||||||
|
|
||||||
|
ctx.v[0] = binary.LittleEndian.Uint64(fillBin[:8])
|
||||||
|
ctx.v[1] = binary.LittleEndian.Uint64(fillBin[8:])
|
||||||
|
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *shift128PlusContext) Next() uint64 {
|
||||||
|
x := ctx.v[0]
|
||||||
|
y := ctx.v[1]
|
||||||
|
ctx.v[0] = y
|
||||||
|
x ^= x << 23
|
||||||
|
x ^= y ^ (x >> 17) ^ (y >> 26)
|
||||||
|
ctx.v[1] = x
|
||||||
|
return x + y
|
||||||
|
}
|
72
component/ssr/protocol/auth_chain_b.go
Normal file
72
component/ssr/protocol/auth_chain_b.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_chain_b", newAuthChainB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthChainB(b *Base) Protocol {
|
||||||
|
return &authChain{
|
||||||
|
Base: b,
|
||||||
|
authData: &authData{},
|
||||||
|
salt: "auth_chain_b",
|
||||||
|
hmac: tools.HmacMD5,
|
||||||
|
hashDigest: tools.SHA1Sum,
|
||||||
|
rnd: authChainBGetRandLen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDataSize(r *authChain) {
|
||||||
|
random := &r.randomServer
|
||||||
|
random.InitFromBin(r.Key)
|
||||||
|
len := random.Next()%8 + 4
|
||||||
|
r.dataSizeList = make([]int, len)
|
||||||
|
for i := 0; i < int(len); i++ {
|
||||||
|
r.dataSizeList[i] = int(random.Next() % 2340 % 2040 % 1440)
|
||||||
|
}
|
||||||
|
sort.Ints(r.dataSizeList)
|
||||||
|
|
||||||
|
len = random.Next()%16 + 8
|
||||||
|
r.dataSizeList2 = make([]int, len)
|
||||||
|
for i := 0; i < int(len); i++ {
|
||||||
|
r.dataSizeList2[i] = int(random.Next() % 2340 % 2040 % 1440)
|
||||||
|
}
|
||||||
|
sort.Ints(r.dataSizeList2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authChainBGetRandLen(dataLength int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int {
|
||||||
|
if dataLength > 1440 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
random.InitFromBinDatalen(lastHash[:16], dataLength)
|
||||||
|
pos := sort.Search(len(dataSizeList), func(i int) bool { return dataSizeList[i] > dataLength+overhead })
|
||||||
|
finalPos := uint64(pos) + random.Next()%uint64(len(dataSizeList))
|
||||||
|
if finalPos < uint64(len(dataSizeList)) {
|
||||||
|
return dataSizeList[finalPos] - dataLength - overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = sort.Search(len(dataSizeList2), func(i int) bool { return dataSizeList2[i] > dataLength+overhead })
|
||||||
|
finalPos = uint64(pos) + random.Next()%uint64(len(dataSizeList2))
|
||||||
|
if finalPos < uint64(len(dataSizeList2)) {
|
||||||
|
return dataSizeList2[finalPos] - dataLength - overhead
|
||||||
|
}
|
||||||
|
if finalPos < uint64(pos+len(dataSizeList2)-1) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataLength > 1300 {
|
||||||
|
return int(random.Next() % 31)
|
||||||
|
}
|
||||||
|
if dataLength > 900 {
|
||||||
|
return int(random.Next() % 127)
|
||||||
|
}
|
||||||
|
if dataLength > 400 {
|
||||||
|
return int(random.Next() % 521)
|
||||||
|
}
|
||||||
|
return int(random.Next() % 1021)
|
||||||
|
}
|
253
component/ssr/protocol/auth_sha1_v4.go
Normal file
253
component/ssr/protocol/auth_sha1_v4.go
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"hash/adler32"
|
||||||
|
"hash/crc32"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
"github.com/Dreamacro/clash/component/ssr/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authSHA1V4 struct {
|
||||||
|
*Base
|
||||||
|
*authData
|
||||||
|
headerSent bool
|
||||||
|
buffer bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("auth_sha1_v4", newAuthSHA1V4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthSHA1V4(b *Base) Protocol {
|
||||||
|
return &authSHA1V4{Base: b, authData: &authData{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) initForConn(iv []byte) Protocol {
|
||||||
|
return &authSHA1V4{
|
||||||
|
Base: &Base{
|
||||||
|
IV: iv,
|
||||||
|
Key: a.Key,
|
||||||
|
TCPMss: a.TCPMss,
|
||||||
|
Overhead: a.Overhead,
|
||||||
|
Param: a.Param,
|
||||||
|
},
|
||||||
|
authData: a.authData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) GetProtocolOverhead() int {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) SetOverhead(overhead int) {
|
||||||
|
a.Overhead = overhead
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) Decode(b []byte) ([]byte, int, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
bSize := len(b)
|
||||||
|
originalSize := bSize
|
||||||
|
for bSize > 4 {
|
||||||
|
crc := crc32.ChecksumIEEE(b[:2]) & 0xFFFF
|
||||||
|
if binary.LittleEndian.Uint16(b[2:4]) != uint16(crc) {
|
||||||
|
return nil, 0, errAuthSHA1v4CRC32Error
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(b[:2]))
|
||||||
|
if length >= 8192 || length < 8 {
|
||||||
|
return nil, 0, errAuthSHA1v4DataLengthError
|
||||||
|
}
|
||||||
|
if length > bSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if adler32.Checksum(b[:length-4]) == binary.LittleEndian.Uint32(b[length-4:]) {
|
||||||
|
pos := int(b[4])
|
||||||
|
if pos != 0xFF {
|
||||||
|
pos += 4
|
||||||
|
} else {
|
||||||
|
pos = int(binary.BigEndian.Uint16(b[5:5+2])) + 4
|
||||||
|
}
|
||||||
|
retSize := length - pos - 4
|
||||||
|
a.buffer.Write(b[pos : pos+retSize])
|
||||||
|
bSize -= length
|
||||||
|
b = b[length:]
|
||||||
|
} else {
|
||||||
|
return nil, 0, errAuthSHA1v4IncorrectChecksum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), originalSize - bSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) Encode(b []byte) ([]byte, error) {
|
||||||
|
a.buffer.Reset()
|
||||||
|
bSize := len(b)
|
||||||
|
offset := 0
|
||||||
|
if !a.headerSent && bSize > 0 {
|
||||||
|
headSize := getHeadSize(b, 30)
|
||||||
|
if headSize > bSize {
|
||||||
|
headSize = bSize
|
||||||
|
}
|
||||||
|
a.buffer.Write(a.packAuthData(b[:headSize]))
|
||||||
|
offset += headSize
|
||||||
|
bSize -= headSize
|
||||||
|
a.headerSent = true
|
||||||
|
}
|
||||||
|
const blockSize = 4096
|
||||||
|
for bSize > blockSize {
|
||||||
|
packSize, randSize := a.packedDataSize(b[offset : offset+blockSize])
|
||||||
|
pack := pool.Get(packSize)
|
||||||
|
a.packData(b[offset:offset+blockSize], pack, randSize)
|
||||||
|
a.buffer.Write(pack)
|
||||||
|
pool.Put(pack)
|
||||||
|
offset += blockSize
|
||||||
|
bSize -= blockSize
|
||||||
|
}
|
||||||
|
if bSize > 0 {
|
||||||
|
packSize, randSize := a.packedDataSize(b[offset:])
|
||||||
|
pack := pool.Get(packSize)
|
||||||
|
a.packData(b[offset:], pack, randSize)
|
||||||
|
a.buffer.Write(pack)
|
||||||
|
pool.Put(pack)
|
||||||
|
}
|
||||||
|
return a.buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, int, error) {
|
||||||
|
return b, len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) EncodePacket(b []byte) ([]byte, error) {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packedDataSize(data []byte) (packSize, randSize int) {
|
||||||
|
dataSize := len(data)
|
||||||
|
randSize = 1
|
||||||
|
if dataSize <= 1300 {
|
||||||
|
if dataSize > 400 {
|
||||||
|
randSize += rand.Intn(128)
|
||||||
|
} else {
|
||||||
|
randSize += rand.Intn(1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packSize = randSize + dataSize + 8
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packData(data, ret []byte, randSize int) {
|
||||||
|
dataSize := len(data)
|
||||||
|
retSize := len(ret)
|
||||||
|
// 0~1, ret size
|
||||||
|
binary.BigEndian.PutUint16(ret[:2], uint16(retSize&0xFFFF))
|
||||||
|
// 2~3, crc of ret size
|
||||||
|
crc := crc32.ChecksumIEEE(ret[:2]) & 0xFFFF
|
||||||
|
binary.LittleEndian.PutUint16(ret[2:4], uint16(crc))
|
||||||
|
// 4, rand size
|
||||||
|
if randSize < 128 {
|
||||||
|
ret[4] = uint8(randSize & 0xFF)
|
||||||
|
} else {
|
||||||
|
ret[4] = uint8(0xFF)
|
||||||
|
binary.BigEndian.PutUint16(ret[5:7], uint16(randSize&0xFFFF))
|
||||||
|
}
|
||||||
|
// (rand size+4)~(ret size-4), data
|
||||||
|
if dataSize > 0 {
|
||||||
|
copy(ret[randSize+4:], data)
|
||||||
|
}
|
||||||
|
// (ret size-4)~end, adler32 of full data
|
||||||
|
adler := adler32.Checksum(ret[:retSize-4])
|
||||||
|
binary.LittleEndian.PutUint32(ret[retSize-4:], adler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authSHA1V4) packAuthData(data []byte) (ret []byte) {
|
||||||
|
dataSize := len(data)
|
||||||
|
randSize := 1
|
||||||
|
if dataSize <= 1300 {
|
||||||
|
if dataSize > 400 {
|
||||||
|
randSize += rand.Intn(128)
|
||||||
|
} else {
|
||||||
|
randSize += rand.Intn(1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataOffset := randSize + 4 + 2
|
||||||
|
retSize := dataOffset + dataSize + 12 + tools.HmacSHA1Len
|
||||||
|
ret = make([]byte, retSize)
|
||||||
|
a.mutex.Lock()
|
||||||
|
defer a.mutex.Unlock()
|
||||||
|
a.connectionID++
|
||||||
|
if a.connectionID > 0xFF000000 {
|
||||||
|
a.clientID = nil
|
||||||
|
}
|
||||||
|
if len(a.clientID) == 0 {
|
||||||
|
a.clientID = make([]byte, 8)
|
||||||
|
rand.Read(a.clientID)
|
||||||
|
b := make([]byte, 4)
|
||||||
|
rand.Read(b)
|
||||||
|
a.connectionID = binary.LittleEndian.Uint32(b) & 0xFFFFFF
|
||||||
|
}
|
||||||
|
// 0~1, ret size
|
||||||
|
binary.BigEndian.PutUint16(ret[:2], uint16(retSize&0xFFFF))
|
||||||
|
|
||||||
|
// 2~6, crc of (ret size+salt+key)
|
||||||
|
salt := []byte("auth_sha1_v4")
|
||||||
|
crcData := make([]byte, len(salt)+len(a.Key)+2)
|
||||||
|
copy(crcData[:2], ret[:2])
|
||||||
|
copy(crcData[2:], salt)
|
||||||
|
copy(crcData[2+len(salt):], a.Key)
|
||||||
|
crc := crc32.ChecksumIEEE(crcData) & 0xFFFFFFFF
|
||||||
|
// 2~6, crc of (ret size+salt+key)
|
||||||
|
binary.LittleEndian.PutUint32(ret[2:], crc)
|
||||||
|
// 6~(rand size+6), rand numbers
|
||||||
|
rand.Read(ret[dataOffset-randSize : dataOffset])
|
||||||
|
// 6, rand size
|
||||||
|
if randSize < 128 {
|
||||||
|
ret[6] = byte(randSize & 0xFF)
|
||||||
|
} else {
|
||||||
|
// 6, magic number 0xFF
|
||||||
|
ret[6] = 0xFF
|
||||||
|
// 7~8, rand size
|
||||||
|
binary.BigEndian.PutUint16(ret[7:9], uint16(randSize&0xFFFF))
|
||||||
|
}
|
||||||
|
// rand size+6~(rand size+10), time stamp
|
||||||
|
now := time.Now().Unix()
|
||||||
|
binary.LittleEndian.PutUint32(ret[dataOffset:dataOffset+4], uint32(now))
|
||||||
|
// rand size+10~(rand size+14), client ID
|
||||||
|
copy(ret[dataOffset+4:dataOffset+4+4], a.clientID[:4])
|
||||||
|
// rand size+14~(rand size+18), connection ID
|
||||||
|
binary.LittleEndian.PutUint32(ret[dataOffset+8:dataOffset+8+4], a.connectionID)
|
||||||
|
// rand size+18~(rand size+18)+data length, data
|
||||||
|
copy(ret[dataOffset+12:], data)
|
||||||
|
|
||||||
|
key := make([]byte, len(a.IV)+len(a.Key))
|
||||||
|
copy(key, a.IV)
|
||||||
|
copy(key[len(a.IV):], a.Key)
|
||||||
|
|
||||||
|
h := tools.HmacSHA1(key, ret[:retSize-tools.HmacSHA1Len])
|
||||||
|
// (ret size-10)~(ret size)/(rand size)+18+data length~end, hmac
|
||||||
|
copy(ret[retSize-tools.HmacSHA1Len:], h[:tools.HmacSHA1Len])
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHeadSize(data []byte, defaultValue int) int {
|
||||||
|
if data == nil || len(data) < 2 {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
headType := data[0] & 0x07
|
||||||
|
switch headType {
|
||||||
|
case 1:
|
||||||
|
// IPv4 1+4+2
|
||||||
|
return 7
|
||||||
|
case 4:
|
||||||
|
// IPv6 1+16+2
|
||||||
|
return 19
|
||||||
|
case 3:
|
||||||
|
// domain name, variant length
|
||||||
|
return 4 + int(data[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
10
component/ssr/protocol/base.go
Normal file
10
component/ssr/protocol/base.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
// Base information for protocol
|
||||||
|
type Base struct {
|
||||||
|
IV []byte
|
||||||
|
Key []byte
|
||||||
|
TCPMss int
|
||||||
|
Overhead int
|
||||||
|
Param string
|
||||||
|
}
|
36
component/ssr/protocol/origin.go
Normal file
36
component/ssr/protocol/origin.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
type origin struct{ *Base }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("origin", newOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOrigin(b *Base) Protocol {
|
||||||
|
return &origin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) initForConn(iv []byte) Protocol { return &origin{} }
|
||||||
|
|
||||||
|
func (o *origin) GetProtocolOverhead() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) SetOverhead(overhead int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) Decode(b []byte) ([]byte, int, error) {
|
||||||
|
return b, len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) Encode(b []byte) ([]byte, error) {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) DecodePacket(b []byte) ([]byte, int, error) {
|
||||||
|
return b, len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *origin) EncodePacket(b []byte) ([]byte, error) {
|
||||||
|
return b, nil
|
||||||
|
}
|
42
component/ssr/protocol/packet.go
Normal file
42
component/ssr/protocol/packet.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPacketConn returns a net.NewPacketConn with protocol decoding/encoding
|
||||||
|
func NewPacketConn(pc net.PacketConn, p Protocol) net.PacketConn {
|
||||||
|
return &PacketConn{PacketConn: pc, Protocol: p.initForConn(nil)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketConn represents a protocol packet connection
|
||||||
|
type PacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
|
defer pool.Put(buf)
|
||||||
|
buf, err := c.EncodePacket(b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = c.PacketConn.WriteTo(buf, 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
|
||||||
|
}
|
||||||
|
bb, length, err := c.DecodePacket(b[:n])
|
||||||
|
if err != nil {
|
||||||
|
return n, addr, err
|
||||||
|
}
|
||||||
|
copy(b, bb)
|
||||||
|
return length, addr, err
|
||||||
|
}
|
63
component/ssr/protocol/protocol.go
Normal file
63
component/ssr/protocol/protocol.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errAuthAES128IncorrectMAC = errors.New("auth_aes128_* post decrypt incorrect mac")
|
||||||
|
errAuthAES128DataLengthError = errors.New("auth_aes128_* post decrypt length mismatch")
|
||||||
|
errAuthAES128IncorrectChecksum = errors.New("auth_aes128_* post decrypt incorrect checksum")
|
||||||
|
errAuthAES128PositionTooLarge = errors.New("auth_aes128_* post decrypt position is too large")
|
||||||
|
errAuthSHA1v4CRC32Error = errors.New("auth_sha1_v4 post decrypt data crc32 error")
|
||||||
|
errAuthSHA1v4DataLengthError = errors.New("auth_sha1_v4 post decrypt data length error")
|
||||||
|
errAuthSHA1v4IncorrectChecksum = errors.New("auth_sha1_v4 post decrypt incorrect checksum")
|
||||||
|
errAuthChainDataLengthError = errors.New("auth_chain_* post decrypt length mismatch")
|
||||||
|
errAuthChainHMACError = errors.New("auth_chain_* post decrypt hmac error")
|
||||||
|
)
|
||||||
|
|
||||||
|
type authData struct {
|
||||||
|
clientID []byte
|
||||||
|
connectionID uint32
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type recvInfo struct {
|
||||||
|
recvID uint32
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type hmacMethod func(key []byte, data []byte) []byte
|
||||||
|
type hashDigestMethod func(data []byte) []byte
|
||||||
|
type rndMethod func(dataSize int, random *shift128PlusContext, lastHash []byte, dataSizeList, dataSizeList2 []int, overhead int) int
|
||||||
|
|
||||||
|
// Protocol provides methods for decoding, encoding and iv setting
|
||||||
|
type Protocol interface {
|
||||||
|
initForConn(iv []byte) Protocol
|
||||||
|
GetProtocolOverhead() int
|
||||||
|
SetOverhead(int)
|
||||||
|
Decode([]byte) ([]byte, int, error)
|
||||||
|
Encode([]byte) ([]byte, error)
|
||||||
|
DecodePacket([]byte) ([]byte, int, error)
|
||||||
|
EncodePacket([]byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type protocolCreator func(b *Base) Protocol
|
||||||
|
|
||||||
|
var protocolList = make(map[string]protocolCreator)
|
||||||
|
|
||||||
|
func register(name string, c protocolCreator) {
|
||||||
|
protocolList[name] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// PickProtocol returns a protocol of the given name
|
||||||
|
func PickProtocol(name string, b *Base) (Protocol, error) {
|
||||||
|
if protocolCreator, ok := protocolList[strings.ToLower(name)]; ok {
|
||||||
|
return protocolCreator(b), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Protocol %s not supported", name)
|
||||||
|
}
|
68
component/ssr/protocol/stream.go
Normal file
68
component/ssr/protocol/stream.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewConn wraps a stream-oriented net.Conn with protocol decoding/encoding
|
||||||
|
func NewConn(c net.Conn, p Protocol, iv []byte) net.Conn {
|
||||||
|
return &Conn{Conn: c, Protocol: p.initForConn(iv)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn represents a protocol connection
|
||||||
|
type Conn struct {
|
||||||
|
net.Conn
|
||||||
|
Protocol
|
||||||
|
buf []byte
|
||||||
|
offset int
|
||||||
|
underDecoded bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Read(b []byte) (int, error) {
|
||||||
|
if c.buf != nil {
|
||||||
|
n := copy(b, c.buf[c.offset:])
|
||||||
|
c.offset += n
|
||||||
|
if c.offset == len(c.buf) {
|
||||||
|
c.buf = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
underDecoded := c.underDecoded.Bytes()
|
||||||
|
decoded, length, err := c.Decode(underDecoded)
|
||||||
|
if err != nil {
|
||||||
|
c.underDecoded.Reset()
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if length == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
c.underDecoded.Next(length)
|
||||||
|
n = copy(b, decoded)
|
||||||
|
if len(decoded) > len(b) {
|
||||||
|
c.buf = decoded
|
||||||
|
c.offset = n
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) Write(b []byte) (int, error) {
|
||||||
|
encoded, err := c.Encode(b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_, err = c.Conn.Write(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
33
component/ssr/tools/encrypt.go
Normal file
33
component/ssr/tools/encrypt.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)[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
func HmacSHA1(key, data []byte) []byte {
|
||||||
|
hmacSHA1 := hmac.New(sha1.New, key)
|
||||||
|
hmacSHA1.Write(data)
|
||||||
|
return hmacSHA1.Sum(nil)[:20]
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
135
component/trie/domain.go
Normal file
135
component/trie/domain.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wildcard = "*"
|
||||||
|
dotWildcard = ""
|
||||||
|
complexWildcard = "+"
|
||||||
|
domainStep = "."
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidDomain means insert domain is invalid
|
||||||
|
ErrInvalidDomain = errors.New("invalid domain")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
|
||||||
|
// support wildcard domain (e.g *.google.com)
|
||||||
|
type DomainTrie struct {
|
||||||
|
root *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func validAndSplitDomain(domain string) ([]string, bool) {
|
||||||
|
if domain != "" && domain[len(domain)-1] == '.' {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(domain, domainStep)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
if parts[0] == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
if part == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert adds a node to the trie.
|
||||||
|
// Support
|
||||||
|
// 1. www.example.com
|
||||||
|
// 2. *.example.com
|
||||||
|
// 3. subdomain.*.example.com
|
||||||
|
// 4. .example.com
|
||||||
|
// 5. +.example.com
|
||||||
|
func (t *DomainTrie) Insert(domain string, data interface{}) error {
|
||||||
|
parts, valid := validAndSplitDomain(domain)
|
||||||
|
if !valid {
|
||||||
|
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
|
||||||
|
// reverse storage domain part to save space
|
||||||
|
for i := len(parts) - 1; i >= 0; i-- {
|
||||||
|
part := parts[i]
|
||||||
|
if !node.hasChild(part) {
|
||||||
|
node.addChild(part, newNode(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.getChild(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search is the most important part of the Trie.
|
||||||
|
// Priority as:
|
||||||
|
// 1. static part
|
||||||
|
// 2. wildcard domain
|
||||||
|
// 2. dot wildcard domain
|
||||||
|
func (t *DomainTrie) Search(domain string) *Node {
|
||||||
|
parts, valid := validAndSplitDomain(domain)
|
||||||
|
if !valid || parts[0] == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n := t.search(t.root, parts)
|
||||||
|
|
||||||
|
if n == nil || n.Data == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *DomainTrie) search(node *Node, parts []string) *Node {
|
||||||
|
if len(parts) == 0 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := node.getChild(dotWildcard); c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new, empty Trie.
|
||||||
|
func New() *DomainTrie {
|
||||||
|
return &DomainTrie{root: newNode(nil)}
|
||||||
|
}
|
99
component/trie/domain_test.go
Normal file
99
component/trie/domain_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var localIP = net.IP{127, 0, 0, 1}
|
||||||
|
|
||||||
|
func TestTrie_Basic(t *testing.T) {
|
||||||
|
tree := New()
|
||||||
|
domains := []string{
|
||||||
|
"example.com",
|
||||||
|
"google.com",
|
||||||
|
"localhost",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
tree.Insert(domain, localIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
node := tree.Search("example.com")
|
||||||
|
assert.NotNil(t, node)
|
||||||
|
assert.True(t, node.Data.(net.IP).Equal(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) {
|
||||||
|
tree := New()
|
||||||
|
domains := []string{
|
||||||
|
"*.example.com",
|
||||||
|
"sub.*.example.com",
|
||||||
|
"*.dev",
|
||||||
|
".org",
|
||||||
|
".example.net",
|
||||||
|
".apple.*",
|
||||||
|
"+.foo.com",
|
||||||
|
"+.stun.*.*",
|
||||||
|
"+.stun.*.*.*",
|
||||||
|
"+.stun.*.*.*.*",
|
||||||
|
"stun.l.google.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
tree.Insert(domain, localIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotNil(t, tree.Search("sub.example.com"))
|
||||||
|
assert.NotNil(t, tree.Search("sub.foo.example.com"))
|
||||||
|
assert.NotNil(t, tree.Search("test.org"))
|
||||||
|
assert.NotNil(t, tree.Search("test.example.net"))
|
||||||
|
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.example.dev"))
|
||||||
|
assert.Nil(t, tree.Search("example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrie_Priority(t *testing.T) {
|
||||||
|
tree := New()
|
||||||
|
domains := []string{
|
||||||
|
".dev",
|
||||||
|
"example.dev",
|
||||||
|
"*.example.dev",
|
||||||
|
"test.example.dev",
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFn := func(domain string, data int) {
|
||||||
|
node := tree.Search(domain)
|
||||||
|
assert.NotNil(t, node)
|
||||||
|
assert.Equal(t, data, node.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, domain := range domains {
|
||||||
|
tree.Insert(domain, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFn("test.dev", 0)
|
||||||
|
assertFn("foo.bar.dev", 0)
|
||||||
|
assertFn("example.dev", 1)
|
||||||
|
assertFn("foo.example.dev", 2)
|
||||||
|
assertFn("test.example.dev", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrie_Boundary(t *testing.T) {
|
||||||
|
tree := New()
|
||||||
|
tree.Insert("*.dev", localIP)
|
||||||
|
|
||||||
|
assert.NotNil(t, tree.Insert(".", localIP))
|
||||||
|
assert.NotNil(t, tree.Insert("..dev", localIP))
|
||||||
|
assert.Nil(t, tree.Search("dev"))
|
||||||
|
}
|
223
component/trojan/trojan.go
Normal file
223
component/trojan/trojan.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
package trojan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// max packet length
|
||||||
|
maxLength = 8192
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultALPN = []string{"h2", "http/1.1"}
|
||||||
|
crlf = []byte{'\r', '\n'}
|
||||||
|
|
||||||
|
bufPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Command = byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
CommandTCP byte = 1
|
||||||
|
CommandUDP byte = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Password string
|
||||||
|
ALPN []string
|
||||||
|
ServerName string
|
||||||
|
SkipCertVerify bool
|
||||||
|
ClientSessionCache tls.ClientSessionCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trojan struct {
|
||||||
|
option *Option
|
||||||
|
hexPassword []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
|
||||||
|
alpn := defaultALPN
|
||||||
|
if len(t.option.ALPN) != 0 {
|
||||||
|
alpn = t.option.ALPN
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
NextProtos: alpn,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
InsecureSkipVerify: t.option.SkipCertVerify,
|
||||||
|
ServerName: t.option.ServerName,
|
||||||
|
ClientSessionCache: t.option.ClientSessionCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(conn, tlsConfig)
|
||||||
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
|
||||||
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
|
defer bufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
|
buf.Write(t.hexPassword)
|
||||||
|
buf.Write(crlf)
|
||||||
|
|
||||||
|
buf.WriteByte(command)
|
||||||
|
buf.Write(socks5Addr)
|
||||||
|
buf.Write(crlf)
|
||||||
|
|
||||||
|
_, err := w.Write(buf.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) PacketConn(conn net.Conn) net.PacketConn {
|
||||||
|
return &PacketConn{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) {
|
||||||
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
|
defer bufPool.Put(buf)
|
||||||
|
defer buf.Reset()
|
||||||
|
|
||||||
|
buf.Write(socks5Addr)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(len(payload)))
|
||||||
|
buf.Write(crlf)
|
||||||
|
buf.Write(payload)
|
||||||
|
|
||||||
|
return w.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func WritePacket(w io.Writer, socks5Addr, payload []byte) (int, error) {
|
||||||
|
if len(payload) <= maxLength {
|
||||||
|
return writePacket(w, socks5Addr, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
total := len(payload)
|
||||||
|
for {
|
||||||
|
cursor := offset + maxLength
|
||||||
|
if cursor > total {
|
||||||
|
cursor = total
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := writePacket(w, socks5Addr, payload[offset:cursor])
|
||||||
|
if err != nil {
|
||||||
|
return offset + n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = cursor
|
||||||
|
if offset == total {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadPacket(r io.Reader, payload []byte) (net.Addr, int, int, error) {
|
||||||
|
addr, err := socks5.ReadAddr(r, payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, errors.New("read addr error")
|
||||||
|
}
|
||||||
|
uAddr := addr.UDPAddr()
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(r, payload[:2]); err != nil {
|
||||||
|
return nil, 0, 0, errors.New("read length error")
|
||||||
|
}
|
||||||
|
|
||||||
|
total := int(binary.BigEndian.Uint16(payload[:2]))
|
||||||
|
if total > maxLength {
|
||||||
|
return nil, 0, 0, errors.New("packet invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read crlf
|
||||||
|
if _, err = io.ReadFull(r, payload[:2]); err != nil {
|
||||||
|
return nil, 0, 0, errors.New("read crlf error")
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(payload)
|
||||||
|
if total < length {
|
||||||
|
length = total
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.ReadFull(r, payload[:length]); err != nil {
|
||||||
|
return nil, 0, 0, errors.New("read packet error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return uAddr, length, total - length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(option *Option) *Trojan {
|
||||||
|
return &Trojan{option, hexSha224([]byte(option.Password))}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketConn struct {
|
||||||
|
net.Conn
|
||||||
|
remain int
|
||||||
|
rAddr net.Addr
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
return WritePacket(pc, socks5.ParseAddr(addr.String()), b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
|
pc.mux.Lock()
|
||||||
|
defer pc.mux.Unlock()
|
||||||
|
if pc.remain != 0 {
|
||||||
|
length := len(b)
|
||||||
|
if pc.remain < length {
|
||||||
|
length = pc.remain
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := pc.Conn.Read(b[:length])
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.remain -= n
|
||||||
|
addr := pc.rAddr
|
||||||
|
if pc.remain == 0 {
|
||||||
|
pc.rAddr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, n, remain, err := ReadPacket(pc.Conn, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if remain != 0 {
|
||||||
|
pc.remain = remain
|
||||||
|
pc.rAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexSha224(data []byte) []byte {
|
||||||
|
buf := make([]byte, 56)
|
||||||
|
hash := sha256.New224()
|
||||||
|
hash.Write(data)
|
||||||
|
hex.Encode(buf, hash.Sum(nil))
|
||||||
|
return buf
|
||||||
|
}
|
@ -10,11 +10,14 @@ import (
|
|||||||
|
|
||||||
// Option is options of websocket obfs
|
// Option is options of websocket obfs
|
||||||
type Option struct {
|
type Option struct {
|
||||||
Host string
|
Host string
|
||||||
Path string
|
Port string
|
||||||
Headers map[string]string
|
Path string
|
||||||
TLSConfig *tls.Config
|
Headers map[string]string
|
||||||
Mux bool
|
TLS bool
|
||||||
|
SkipCertVerify bool
|
||||||
|
SessionCache tls.ClientSessionCache
|
||||||
|
Mux bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewV2rayObfs return a HTTPObfs
|
// NewV2rayObfs return a HTTPObfs
|
||||||
@ -25,15 +28,17 @@ func NewV2rayObfs(conn net.Conn, option *Option) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := &vmess.WebsocketConfig{
|
config := &vmess.WebsocketConfig{
|
||||||
Host: option.Host,
|
Host: option.Host,
|
||||||
Path: option.Path,
|
Port: option.Port,
|
||||||
TLS: option.TLSConfig != nil,
|
Path: option.Path,
|
||||||
Headers: header,
|
TLS: option.TLS,
|
||||||
TLSConfig: option.TLSConfig,
|
Headers: header,
|
||||||
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
|
SessionCache: option.SessionCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
conn, err = vmess.NewWebsocketConn(conn, config)
|
conn, err = vmess.StreamWebsocketConn(conn, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ 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) {
|
||||||
buf := pool.BufPool.Get().([]byte)
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
defer pool.BufPool.Put(buf[:cap(buf)])
|
defer pool.Put(buf)
|
||||||
length := len(b)
|
length := len(b)
|
||||||
for {
|
for {
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
@ -73,7 +73,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
|
|||||||
n := copy(b, r.buf[r.offset:])
|
n := copy(b, r.buf[r.offset:])
|
||||||
r.offset += n
|
r.offset += n
|
||||||
if r.offset == len(r.buf) {
|
if r.offset == len(r.buf) {
|
||||||
pool.BufPool.Put(r.buf[:cap(r.buf)])
|
pool.Put(r.buf)
|
||||||
r.buf = nil
|
r.buf = nil
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
@ -86,13 +86,13 @@ 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.BufPool.Get().([]byte)
|
buf := pool.Get(size)
|
||||||
_, err = io.ReadFull(r.Reader, buf[:size])
|
_, err = io.ReadFull(r.Reader, buf[:size])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
|
|||||||
realLen := size - r.Overhead()
|
realLen := size - r.Overhead()
|
||||||
n := copy(b, buf[:realLen])
|
n := copy(b, buf[:realLen])
|
||||||
if len(b) >= realLen {
|
if len(b) >= realLen {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
|
|||||||
n := copy(b, cr.buf[cr.offset:])
|
n := copy(b, cr.buf[cr.offset:])
|
||||||
cr.offset += n
|
cr.offset += n
|
||||||
if cr.offset == len(cr.buf) {
|
if cr.offset == len(cr.buf) {
|
||||||
pool.BufPool.Put(cr.buf[:cap(cr.buf)])
|
pool.Put(cr.buf)
|
||||||
cr.buf = nil
|
cr.buf = nil
|
||||||
}
|
}
|
||||||
return n, nil
|
return n, nil
|
||||||
@ -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 {
|
||||||
@ -59,15 +59,15 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := pool.BufPool.Get().([]byte)
|
buf := pool.Get(size)
|
||||||
_, err = io.ReadFull(cr.Reader, buf[:size])
|
_, err = io.ReadFull(cr.Reader, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pool.BufPool.Put(buf[:cap(buf)])
|
pool.Put(buf)
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
n := copy(b, cr.buf[:])
|
n := copy(b, buf)
|
||||||
cr.offset = n
|
cr.offset = n
|
||||||
cr.buf = buf[:size]
|
cr.buf = buf
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,8 +76,8 @@ type chunkWriter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
|
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
|
||||||
buf := pool.BufPool.Get().([]byte)
|
buf := pool.Get(pool.RelayBufferSize)
|
||||||
defer pool.BufPool.Put(buf[:cap(buf)])
|
defer pool.Put(buf)
|
||||||
length := len(b)
|
length := len(b)
|
||||||
for {
|
for {
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
|
111
component/vmess/h2.go
Normal file
111
component/vmess/h2.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package vmess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type h2Conn struct {
|
||||||
|
net.Conn
|
||||||
|
*http2.ClientConn
|
||||||
|
pwriter *io.PipeWriter
|
||||||
|
res *http.Response
|
||||||
|
cfg *H2Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type H2Config struct {
|
||||||
|
Hosts []string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *h2Conn) establishConn() error {
|
||||||
|
preader, pwriter := io.Pipe()
|
||||||
|
|
||||||
|
host := hc.cfg.Hosts[rand.Intn(len(hc.cfg.Hosts))]
|
||||||
|
path := hc.cfg.Path
|
||||||
|
// TODO: connect use VMess Host instead of H2 Host
|
||||||
|
req := http.Request{
|
||||||
|
Method: "PUT",
|
||||||
|
Host: host,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: host,
|
||||||
|
Path: path,
|
||||||
|
},
|
||||||
|
Proto: "HTTP/2",
|
||||||
|
ProtoMajor: 2,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Body: preader,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Accept-Encoding": {"identity"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := hc.ClientConn.RoundTrip(&req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.pwriter = pwriter
|
||||||
|
hc.res = res
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements net.Conn.Read()
|
||||||
|
func (hc *h2Conn) Read(b []byte) (int, error) {
|
||||||
|
if hc.res != nil && !hc.res.Close {
|
||||||
|
n, err := hc.res.Body.Read(b)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hc.establishConn(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return hc.res.Body.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (hc *h2Conn) Write(b []byte) (int, error) {
|
||||||
|
if hc.pwriter != nil {
|
||||||
|
return hc.pwriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hc.establishConn(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return hc.pwriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *h2Conn) Close() error {
|
||||||
|
if err := hc.pwriter.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := hc.ClientConn.Shutdown(hc.res.Request.Context()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := hc.Conn.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamH2Conn(conn net.Conn, cfg *H2Config) (net.Conn, error) {
|
||||||
|
transport := &http2.Transport{}
|
||||||
|
|
||||||
|
cconn, err := transport.NewClientConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &h2Conn{
|
||||||
|
Conn: conn,
|
||||||
|
ClientConn: cconn,
|
||||||
|
cfg: cfg,
|
||||||
|
}, nil
|
||||||
|
}
|
76
component/vmess/http.go
Normal file
76
component/vmess/http.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package vmess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpConn struct {
|
||||||
|
net.Conn
|
||||||
|
cfg *HTTPConfig
|
||||||
|
rhandshake bool
|
||||||
|
whandshake bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPConfig struct {
|
||||||
|
Method string
|
||||||
|
Host string
|
||||||
|
Path []string
|
||||||
|
Headers map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements net.Conn.Read()
|
||||||
|
func (hc *httpConn) Read(b []byte) (int, error) {
|
||||||
|
if hc.rhandshake {
|
||||||
|
n, err := hc.Conn.Read(b)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := textproto.NewConn(hc.Conn)
|
||||||
|
// First line: GET /index.html HTTP/1.0
|
||||||
|
if _, err := reader.ReadLine(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := reader.ReadMIMEHeader(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.rhandshake = true
|
||||||
|
return hc.Conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (hc *httpConn) Write(b []byte) (int, error) {
|
||||||
|
if hc.whandshake {
|
||||||
|
return hc.Conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := hc.cfg.Path[rand.Intn(len(hc.cfg.Path))]
|
||||||
|
u := fmt.Sprintf("http://%s%s", hc.cfg.Host, path)
|
||||||
|
req, _ := http.NewRequest("GET", u, bytes.NewBuffer(b))
|
||||||
|
for key, list := range hc.cfg.Headers {
|
||||||
|
req.Header.Set(key, list[rand.Intn(len(list))])
|
||||||
|
}
|
||||||
|
req.ContentLength = int64(len(b))
|
||||||
|
if err := req.Write(hc.Conn); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
hc.whandshake = true
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *httpConn) Close() error {
|
||||||
|
return hc.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamHTTPConn(conn net.Conn, cfg *HTTPConfig) net.Conn {
|
||||||
|
return &httpConn{
|
||||||
|
Conn: conn,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
26
component/vmess/tls.go
Normal file
26
component/vmess/tls.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package vmess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLSConfig struct {
|
||||||
|
Host string
|
||||||
|
SkipCertVerify bool
|
||||||
|
SessionCache tls.ClientSessionCache
|
||||||
|
NextProtos []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: cfg.Host,
|
||||||
|
InsecureSkipVerify: cfg.SkipCertVerify,
|
||||||
|
ClientSessionCache: cfg.SessionCache,
|
||||||
|
NextProtos: cfg.NextProtos,
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(conn, tlsConfig)
|
||||||
|
err := tlsConn.Handshake()
|
||||||
|
return tlsConn, err
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user