Compare commits

..

144 Commits

Author SHA1 Message Date
b3c1b4a840 Chore: update dependencies 2021-02-19 20:35:10 +08:00
14bbf6eedc Feature: support store group selected node to cache (enable by default) 2021-02-18 23:41:50 +08:00
aa81193d5b Feature: add darwin arm64 to Makefile (Apple Silicon) (#1234) 2021-02-18 18:15:09 +08:00
9eb98e399d Improve: refactor ssr and fix #995 (#1189)
Co-authored-by: goomada <madao@DESKTOP-IOEBS0C.localdomain>
2021-02-15 14:32:03 +08:00
d48cfecf60 Chore: API support patch ipv6 config (#1217) 2021-02-05 16:43:42 +08:00
6036fb63ba Chore: avoid provider unnecessary write file operations (#1210) 2021-02-02 17:52:46 +08:00
cd48f69b1f Fix: wrap net.Conn to avoid using *net.TCPConn.(ReadFrom) (#1209) 2021-02-01 20:06:45 +08:00
fcc594ae26 Chore: use jsdelivr CDN for Country.mmdb (#1057) 2021-01-30 00:40:35 +08:00
f4de055aa1 Refactor: make inbound request contextual 2021-01-23 14:58:09 +08:00
35925cb3da Chore: standardized Dockerfile label (#1191)
Signed-off-by: Junjie Yuan <yuan@junjie.pro>
2021-01-20 16:08:24 +08:00
ff430df845 Fix: connectivity of ssr auth_chain_(ab) protocol (#1180) 2021-01-13 23:35:41 +08:00
e4cdea2111 chore: use singleDo to get interface info 2021-01-13 17:30:54 +08:00
b6ee47a541 Fix: get general should return correct result (#1172) 2021-01-07 13:59:39 +08:00
b25009cde7 Fix: unnecessary write operation on provider (#1170) 2021-01-06 14:20:15 +08:00
6fedd7ec84 Fix: dns client should not bind local address 2021-01-04 00:51:53 +08:00
9619c3fb20 Fix: support unspecified UDP bind address (#1159) 2020-12-31 18:58:03 +08:00
02d029dd2d Fix: close http Response body on provider (#1154) 2020-12-29 11:28:22 +08:00
09c28e0355 Fix: fallback bind fn should not bind global unicast 2020-12-28 22:24:58 +08:00
3600077f3b Chore: update dependencies 2020-12-27 18:59:59 +08:00
de7656a787 Chore: update premium README 2020-12-27 00:14:24 +08:00
5dfe7f8561 Fix: handle keep alive on http connect proxy 2020-12-24 14:55:11 +08:00
ed27898a33 Fix: snell should support the config without obfs 2020-12-24 13:47:56 +08:00
532396d25c Fix: PROCESS-NAME rule for UDP sessions on Windows (#1140) 2020-12-22 15:13:44 +08:00
4b1b494164 Chore: move find process name to a single part 2020-12-17 22:17:27 +08:00
0d33dc3eb9 Chore: health checks return immediately if completed (#1097) 2020-11-24 22:52:23 +08:00
994cbff215 Fix: should not log rule when rule = nil 2020-11-22 23:38:12 +08:00
bea2ee8bf2 Chore: log rule msg on dial error 2020-11-22 19:12:36 +08:00
1e5593f1a9 Chore: update dependencies 2020-11-20 20:36:20 +08:00
34febc4579 Chore: more detailed error when dial failed 2020-11-20 00:27:37 +08:00
97581148b5 Fix: static check 2020-11-19 00:56:36 +08:00
0402878daa Feature: add lazy for proxy group and provider 2020-11-19 00:53:22 +08:00
4735f61fd1 Feature: add disable-udp option for all proxy group 2020-11-13 21:48:52 +08:00
16ae107e70 Chore: push image to github docker registry 2020-11-10 15:19:12 +08:00
83efe2ae57 Feature: add TCP TPROXY support (#1049) 2020-11-09 10:46:10 +08:00
87e4d94290 Fix: tunnel manager & tracker race condition (#1048) 2020-10-29 17:51:14 +08:00
b98e9ea202 Improve: #1038 and #1041 2020-10-29 00:32:31 +08:00
9a62b1081d Feature: support round-robin strategy for load-balance group (#1044) 2020-10-28 22:35:02 +08:00
2cd1b890ce Fix: tunnel UDP race condition (#1043) 2020-10-28 21:26:50 +08:00
ba060bd0ee Fix: should not bind interface on local address 2020-10-25 20:31:01 +08:00
b1795b1e3d Fix: stale typo 2020-10-25 11:53:03 +08:00
76c9820065 Fix: undefined variable 2020-10-23 17:49:34 +08:00
2db4ce57ef Chore: make stale time into 60 days 2020-10-23 00:30:17 +08:00
50b3d497f6 Feature: use native syscall to bind interface on Linux and macOS 2020-10-22 22:32:03 +08:00
2321e9139d Chore: deprecated eapache/channels 2020-10-20 17:44:39 +08:00
baabf21340 Chore: update github workflow 2020-10-17 13:46:05 +08:00
d3bb4c65a8 Fix: missing fake-ip record should return error 2020-10-17 12:52:43 +08:00
8c3e2a7559 Chore: fix typo (#1017) 2020-10-14 19:56:02 +08:00
bc52f8e4fd Chore: return empty record in SVCB/HTTPSSVC on fake-ip mode 2020-10-13 00:15:49 +08:00
d3b14c325f Fix: the priority of fake-ip-filter 2020-10-09 00:04:24 +08:00
4859b158b4 Chore: make builds reproducible (#1006) 2020-10-08 17:54:38 +08:00
d65b51c62b Feature: http support custom sni 2020-10-02 11:34:40 +08:00
a6444bb449 Feature: support domain in fallback filter (#964) 2020-09-28 22:17:10 +08:00
e09931dcf7 Chore: remove broken test temporarily 2020-09-26 20:36:52 +08:00
5bd189f2d0 Feature: support VMess HTTP/2 transport (#903) 2020-09-26 20:33:57 +08:00
8766287e72 Chore: sync necessary changes from premium 2020-09-21 22:22:07 +08:00
10f9571c9e Fix: pool gc test 2020-09-21 00:44:47 +08:00
96a8259c42 Feature: support snell v2 (#952)
Co-authored-by: Dreamacro <8615343+Dreamacro@users.noreply.github.com>
2020-09-21 00:33:13 +08:00
68dd0622b8 Chore: code style 2020-09-20 15:53:27 +08:00
558ac6b965 Chore: split enhanced mode instance (#936)
Co-authored-by: Dreamacro <305009791@qq.com>
2020-09-17 10:48:42 +08:00
e773f95f21 Fix: PROCESS-NAME on FreeBSD 11.x (#947) 2020-09-07 17:43:34 +08:00
314ce1c249 Feature: vmess network http support TLS (https) 2020-09-04 21:27:19 +08:00
13275b1aa6 Chore: use only one goroutine to handle statistic (#940) 2020-09-03 10:30:18 +08:00
02d9169b5d Fix: potential PCB buffer overflow on bsd systems (#941) 2020-09-03 10:27:20 +08:00
7631bcc99e Improve: use atomic for connection statistic (#938) 2020-09-02 16:34:12 +08:00
a32ee13fc9 Feature: reuse dns resolver cache when hot reload 2020-08-31 00:32:18 +08:00
b8ed738238 Chore: update actions version 2020-08-30 23:06:21 +08:00
687c2a21cf Fix: vmess UDP option should be effect 2020-08-30 22:49:55 +08:00
ad18064e6b Chore: code style (#933) 2020-08-30 19:53:00 +08:00
c9735ef75b Fix: static check 2020-08-25 22:36:38 +08:00
b70882f01a Chore: add static check 2020-08-25 22:32:23 +08:00
5805334ccd Chore: pass staticcheck 2020-08-25 22:19:59 +08:00
c1b4382fe8 Feature: add Windows ARM32 build (#902)
Co-authored-by: MarksonHon <50002150+MarksonHon@users.noreply.github.com>
2020-08-16 13:50:56 +08:00
008743f20b Chore: update dependencies 2020-08-16 11:32:51 +08:00
50d778da3c Chore: cache process name when resolve failed (#900) 2020-08-15 16:55:55 +08:00
8b7c731fd6 Fix: ssr broken (#895) 2020-08-12 20:50:56 +08:00
0b7918de9c Migration: go 1.15 2020-08-12 13:47:50 +08:00
4f61c04519 Fix: ssr typo (#887) 2020-08-11 10:35:30 +08:00
89cf06036d Feature: dns server could lookup hosts (#872) 2020-08-11 10:28:17 +08:00
4ba6f248bc Fix: ssr bounds out of range panic (#882) 2020-08-11 10:17:40 +08:00
83a684c551 Change: adjust tolerance logic (#864) 2020-08-06 20:12:03 +08:00
92a23f1eab Feature: PROCESS-NAME for windows (#840) 2020-08-06 19:59:20 +08:00
622ac45258 Feature: PROCESS-NAME for freebsd (#855) 2020-07-31 20:01:19 +08:00
791d203b5f Fix: update cache if a process was found (#850) 2020-07-30 17:15:06 +08:00
77d6f9ae6f Fix: handle snell server reported error message properly (#848) 2020-07-30 15:54:26 +08:00
b1d9dfd6bf Improve: simplify macOS process searching 2020-07-29 11:27:18 +08:00
6532947e71 Fix: invert should resolve ip (#836) 2020-07-27 13:47:00 +08:00
6c5f23f552 Merge branch 'dev' of github.com:Dreamacro/clash into dev 2020-07-27 11:58:02 +08:00
78c3034158 Chore: rename NoResolveIP to ShouldResolveIP 2020-07-27 11:57:55 +08:00
8f0098092d Fix: protect alive with atomic value (#834) 2020-07-25 17:47:11 +08:00
33a6579a3a Feature: add ssr support (#805)
* Refactor ssr stream cipher to expose iv and key

References:
https://github.com/Dreamacro/go-shadowsocks2
https://github.com/sh4d0wfiend/go-shadowsocksr2

* Implement ssr obfs

Reference:
https://github.com/mzz2017/shadowsocksR

* Implement ssr protocol

References:
https://github.com/mzz2017/shadowsocksR
https://github.com/shadowsocksRb/shadowsocksr-libev
https://github.com/shadowsocksr-backup/shadowsocksr
2020-07-22 23:02:15 +08:00
b4221d4b74 Chore: README.md style fixed (#825)
make every item in TODO list has the same style
2020-07-22 21:34:37 +08:00
0e4b9daaad Improve: add cache for macOS PROCESS-NAME 2020-07-22 20:35:27 +08:00
ee72865f48 Fix: recycle buf on http obfs 2020-07-22 20:29:39 +08:00
6521acf8f1 Improve: check uid on process search & fix typo (#824) 2020-07-22 20:22:34 +08:00
4f73410618 Feature: add PROCESS-NAME rule for linux (#822) 2020-07-22 19:05:10 +08:00
20eff200b1 Fix: dns should put msg to cache while exchangeWithoutCache (#820) 2020-07-20 21:16:36 +08:00
ae1e1dc9f6 Feature: support PROCESS-NAME on macOS 2020-07-19 13:18:23 +08:00
cf9e1545a4 Improve: fix go test race detect 2020-07-18 20:56:13 +08:00
6c7a8fffe0 Chore: should not write file on file provider 2020-07-18 19:32:40 +08:00
3a3e2c05af Chore: add rule payload in log 2020-07-18 19:22:09 +08:00
02c7fd8d70 Fix: write msg cache multiple times (#812)
Co-authored-by: john.xu <john.xu@bytedance.com>
2020-07-17 17:34:40 +08:00
e6aa452b51 Fix: ticker leak 2020-07-13 00:25:54 +08:00
35449bfa17 Feature: add github stale action 2020-07-09 10:27:05 +08:00
acd51bbc90 Fix: obfs host should not have 80 port 2020-07-01 00:01:36 +08:00
f44cd9180c Chore: update GitHub issue template 2020-06-30 13:55:26 +08:00
93c987a6cb Fix: typo in dialer.go (#767) 2020-06-28 10:59:04 +08:00
3f0584ac09 Chore: move documentations to wiki (#766) 2020-06-28 10:39:30 +08:00
59968fff1c Fix: github actions tag build 2020-06-27 21:09:04 +08:00
7c62fe41b4 Chore: remove forward compatibility code 2020-06-27 14:28:10 +08:00
2781090405 Chore: move experimental features to stable 2020-06-27 14:19:31 +08:00
14c9cf1b97 Fix: domain trie crash if not match in #758 (#762) 2020-06-24 19:46:37 +08:00
3dfff84cc3 Fix: domain trie should backtrack to parent if match fail (#758) 2020-06-24 18:41:23 +08:00
5f3db72422 Fix: docker multiplatform build 2020-06-21 12:38:14 +08:00
18bb285a90 Fix: external-ui should relative with clash HomeDir 2020-06-18 21:33:57 +08:00
60bad66bc3 Change: ipv6 logic 2020-06-18 18:11:02 +08:00
99b34e8d8b Fix: cannot listen socks5 port on wsl (#748) 2020-06-15 10:34:15 +08:00
9f1d85ab6e Fix: fake-ip-filter on fakeip mode should lookup ip-host mapping (#743) 2020-06-14 00:41:53 +08:00
4323dd24d0 Fix: don't auto health check on provider health check disabled 2020-06-14 00:32:04 +08:00
59bda1d547 Change: local resolve DNS in UDP request due to TURN failed 2020-06-12 23:39:03 +08:00
1c760935f4 Chore: add error msg when dial vmess 2020-06-11 22:19:47 +08:00
4f674755ce Fix: trim . for socks5 host 2020-06-11 12:11:44 +08:00
f1b792bd26 Fix: trim FQDN on http proxy request 2020-06-11 11:10:08 +08:00
58c077b45e Fix: actions tag replace 2020-06-08 13:53:04 +08:00
1854199c47 Chore: update dependencies 2020-06-07 18:14:04 +08:00
ecac8eb8e5 Fix: add lock for inbound proxy recreate 2020-06-07 17:57:41 +08:00
48cff50a4c Feature: connections add rule payload 2020-06-07 17:28:56 +08:00
fb628e9c62 Feature: add default hosts localhost 2020-06-07 17:25:51 +08:00
2dece02df6 Chore: code adjustments 2020-06-07 16:54:41 +08:00
8f32e6a60f Improve: safe write provider file 2020-06-07 00:36:54 +08:00
98614a1f3f Chore: move rule parser to rules 2020-06-05 17:43:50 +08:00
c1b4c94b9c Chore: remove unused hooks directory 2020-06-05 12:49:24 +08:00
7ddbc12cdb Chore: rm unused Dockerfile 2020-06-04 10:57:43 +08:00
1a217e21e9 Chore: use actions build docker image 2020-06-04 10:38:30 +08:00
147a7ce779 Fix: panic of socks5 client missing authentication 2020-06-03 18:49:57 +08:00
fb0289bb4c Chore: open ForceAttemptHTTP2 on DoH 2020-06-01 13:43:26 +08:00
3e7970612a Chore: provider error adjust 2020-06-01 00:39:41 +08:00
46244a6496 Chore: mode use lower case (backward compatible) 2020-06-01 00:32:37 +08:00
71d30e6654 Feature: support vmess tls custom servername 2020-06-01 00:27:04 +08:00
008731c249 Fix: make os.Stat return correct err on provider 2020-05-29 21:56:29 +08:00
5628f97da1 Feature: add tolerance for url-test 2020-05-29 17:47:50 +08:00
8d0c6c6e66 Feature: domain trie support wildcard alias 2020-05-28 12:13:05 +08:00
5073c3cde8 Chore: add trimpath for go build 2020-05-20 15:13:33 +08:00
3a27cfc4a1 Feature: add Mixed(http+socks5) proxy listening (#685) 2020-05-12 11:29:53 +08:00
3638b077cd Chore: update premium link 2020-05-08 21:52:17 +08:00
160 changed files with 6519 additions and 1715 deletions

View File

@ -7,48 +7,54 @@ assignees: ''
---
<!-- The English version is available. -->
<!--
感谢你向 Clash Core 提交 issue
在提交之前,请确认:
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 Openclash、Koolclash 等)的特定问题
- [ ] 我已经使用 Clash core 的 dev 分支版本测试过,问题依旧存在
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
<!--
Thanks for submitting an issue towards the Clash core!
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.
- [ ] Your issue may already be reported! Please search on the [issue tracker](……/) before creating one.
- [ ] I have searched on the [issue tracker](……/) for a related issue.
- [ ] I have tested using the dev branch, and the issue still exists.
- [ ] This is an issue related to the Clash core, not to the derivatives of Clash, like Openclash or Koolclash
- [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
Please understand that we close issues that fail to follow the issue template.
Please understand that we close issues that fail to follow this issue template.
-->
我都确认过了,我要继续提交。
<!-- None of the above, create a bug report -->
------------------------------------------------------------------
<!--
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
-->
### clash core config
### Clash config
<!--
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
-->
```
<details>
<summary>config.yaml</summary>
```yaml
……
```
</details>
### Clash log
<!--
在下方附上 Clash Core 的日志log level 最好使用 DEBUG
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
@ -57,9 +63,7 @@ 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)
* 操作系统 (the OS that the Clash core is running on)
……
* 网路环境或拓扑 (network conditions/topology)
……
@ -67,7 +71,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他
* 其他 (any other information that would be useful)
……
### 说明 Description
@ -85,7 +89,7 @@ Paste the Clash core log below with the log level set to `DEBUG`.
**我预期会发生……?**
<!-- **Expected behavior:** [What you expected to happen] -->
**实际上发生了什**
**实际上发生了什**
<!-- **Actual behavior:** [What actually happened] -->
### 可能的解决方案 Possible Solution

76
.github/workflows/docker.yml vendored Normal file
View 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}}

View File

@ -7,24 +7,27 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: 1.14.x
go-version: 1.16
- name: Check out code into the Go module directory
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Cache go module
uses: actions/cache@v1
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Get dependencies and run test
- name: Get dependencies, run test and static check
run: |
go test ./...
go vet ./...
go get -u honnef.co/go/tools/cmd/staticcheck
staticcheck -- $(go list ./...)
- name: Build
if: startsWith(github.ref, 'refs/tags/')

19
.github/workflows/stale.yml vendored Normal file
View 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

View File

@ -3,12 +3,14 @@ FROM golang:alpine as builder
RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
WORKDIR /clash-src
COPY --from=tonistiigi/xx:golang / /
COPY . /clash-src
RUN go mod download && \
make linux-amd64 && \
mv ./bin/clash-linux-amd64 /clash
make docker && \
mv ./bin/clash-docker /clash
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
RUN apk add --no-cache ca-certificates
COPY --from=builder /Country.mmdb /root/.config/clash/

View File

@ -1,20 +0,0 @@
FROM golang:alpine as builder
RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
wget -O /qemu-arm-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-arm-static && \
chmod +x /qemu-arm-static
WORKDIR /clash-src
COPY . /clash-src
RUN go mod download && \
make linux-armv7 && \
mv ./bin/clash-linux-armv7 /clash
FROM arm32v7/alpine:latest
COPY --from=builder /qemu-arm-static /usr/bin/
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
RUN apk add --no-cache ca-certificates
ENTRYPOINT ["/clash"]

View File

@ -1,20 +0,0 @@
FROM golang:alpine as builder
RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
wget -O /qemu-aarch64-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-aarch64-static && \
chmod +x /qemu-aarch64-static
WORKDIR /clash-src
COPY . /clash-src
RUN go mod download && \
make linux-armv8 && \
mv ./bin/clash-linux-armv8 /clash
FROM arm64v8/alpine:latest
COPY --from=builder /qemu-aarch64-static /usr/bin/
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
RUN apk add --no-cache ca-certificates
ENTRYPOINT ["/clash"]

View File

@ -2,12 +2,13 @@ NAME=clash
BINDIR=bin
VERSION=$(shell git describe --tags || echo "unknown version")
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)" \
-w -s'
-w -s -buildid='
PLATFORM_LIST = \
darwin-amd64 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-armv5 \
@ -25,13 +26,20 @@ PLATFORM_LIST = \
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64
windows-amd64 \
windows-arm32v7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -79,6 +87,9 @@ windows-386:
windows-amd64:
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))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))

369
README.md
View File

@ -19,369 +19,36 @@
## Features
- Local HTTP/HTTPS/SOCKS server with/without authentication
- VMess, Shadowsocks, Trojan (experimental), Snell protocol support for remote connections. UDP is supported.
- Built-in DNS server that aims to minimize DNS pollution attacks, supports DoH/DoT upstream. Fake IP is also supported.
- Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
- 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. You can deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP API controller
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller
## Install
## Premium Features
Clash requires Go >= 1.13. You can build it from source:
- TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
```sh
$ go get -u -v github.com/Dreamacro/clash
```
## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
Pre-built TUN mode binaries are available here: [TUN release](https://github.com/Dreamacro/clash/releases/tag/TUN). Source is not currently available.
Check Clash version with:
```sh
$ clash -v
```
## Daemonize Clash
Unfortunately, there is no native or elegant way to implement daemons on Golang. We recommend using third-party daemon management tools like PM2, Supervisor or the like to keep Clash running as a service.
In the case of [pm2](https://github.com/Unitech/pm2), start the daemon this way:
```sh
$ pm2 start clash
```
If you have Docker installed, it's recommended to deploy Clash directly using `docker-compose`: [run Clash in Docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
## Config
The default configuration directory is `$HOME/.config/clash`.
The name of the configuration file is `config.yaml`.
If you want to use another directory, use `-d` to control the configuration directory.
For example, you can use the current directory as the configuration directory:
```sh
$ clash -d .
```
<details>
<summary>This is an example configuration file (click to expand)</summary>
```yml
# port of HTTP
port: 7890
# port of SOCKS5
socks-port: 7891
# redir port for Linux and macOS
# redir-port: 7892
allow-lan: false
# Only applicable when setting allow-lan to true
# "*": bind all IP addresses
# 192.168.122.11: bind a single IPv4 address
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
# bind-address: "*"
# Rule / Global / Direct (default is Rule)
mode: Rule
# set log level to stdout (default is info)
# info / warning / error / debug / silent
log-level: info
# RESTful API for clash
external-controller: 127.0.0.1:9090
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
# input is a relative path to the configuration directory or an absolute path
# external-ui: folder
# Secret for RESTful API (Optional)
# secret: ""
# experimental feature
experimental:
ignore-resolve-fail: true # ignore dns resolve fail, default value is true
# interface-name: en0 # outbound interface name
# authentication of local SOCKS5/HTTP(S) server
# authentication:
# - "user1:pass1"
# - "user2:pass2"
# # experimental hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com > .example.com)
# hosts:
# '*.clash.dev': 127.0.0.1
# '.dev': 127.0.0.1
# 'alpha.clash.dev': '::1'
# dns:
# enable: true # set true to enable dns (default is false)
# ipv6: false # default is false
# listen: 0.0.0.0:53
# # default-nameserver: # resolve dns nameserver host, should fill pure IP
# # - 114.114.114.114
# # - 8.8.8.8
# enhanced-mode: redir-host # or fake-ip
# # fake-ip-range: 198.18.0.1/16 # if you don't know what it is, don't change it
# fake-ip-filter: # fake ip white domain list
# - '*.lan'
# - localhost.ptlogin2.qq.com
# nameserver:
# - 114.114.114.114
# - tls://dns.rubyfish.cn:853 # dns over tls
# - https://1.1.1.1/dns-query # dns over https
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
# - tcp://1.1.1.1
# fallback-filter:
# geoip: true # default
# ipcidr: # ips in these subnets will be considered polluted
# - 240.0.0.0/4
proxies:
# shadowsocks
# The supported ciphers(encrypt methods):
# aes-128-gcm aes-192-gcm aes-256-gcm
# aes-128-cfb aes-192-cfb aes-256-cfb
# aes-128-ctr aes-192-ctr aes-256-ctr
# rc4-md5 chacha20-ietf xchacha20
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
- name: "ss1"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
# udp: true
# old obfs configuration format remove after prerelease
- name: "ss2"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: obfs
plugin-opts:
mode: tls # or http
# host: bing.com
- name: "ss3"
type: ss
server: server
port: 443
cipher: chacha20-ietf-poly1305
password: "password"
plugin: v2ray-plugin
plugin-opts:
mode: websocket # no QUIC now
# tls: true # wss
# skip-cert-verify: true
# host: bing.com
# path: "/"
# mux: true
# headers:
# custom: value
# vmess
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
- name: "vmess"
type: vmess
server: server
port: 443
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# tls: true
# skip-cert-verify: true
# network: ws
# ws-path: /path
# ws-headers:
# Host: v2ray.com
- name: "vmess-http"
type: vmess
server: server
port: 443
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# network: http
# http-opts:
# # method: "GET"
# # path:
# # - '/'
# # - '/video'
# # headers:
# # Connection:
# # - keep-alive
# socks5
- name: "socks"
type: socks5
server: server
port: 443
# username: username
# password: password
# tls: true
# skip-cert-verify: true
# udp: true
# http
- name: "http"
type: http
server: server
port: 443
# username: username
# password: password
# tls: true # https
# skip-cert-verify: true
# snell
- name: "snell"
type: snell
server: server
port: 44046
psk: yourpsk
# obfs-opts:
# mode: http # or tls
# host: bing.com
# trojan
- name: "trojan"
type: trojan
server: server
port: 443
password: yourpsk
# udp: true
# sni: example.com # aka server name
# alpn:
# - h2
# - http/1.1
# skip-cert-verify: true
proxy-groups:
# relay chains the proxies. proxies shall not contain a relay. No UDP support.
# Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet
- name: "relay"
type: relay
proxies:
- http
- vmess
- ss1
- ss2
# url-test select which proxy will be used by benchmarking speed to a URL.
- name: "auto"
type: url-test
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
- name: "fallback-auto"
type: fallback
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# load-balance: The request of the same eTLD will be dial on the same proxy.
- name: "load-balance"
type: load-balance
proxies:
- ss1
- ss2
- vmess1
url: 'http://www.gstatic.com/generate_204'
interval: 300
# select is used for selecting proxy or proxy group
# you can use RESTful API to switch proxy, is recommended for use in GUI.
- name: Proxy
type: select
proxies:
- ss1
- ss2
- vmess1
- auto
- name: UseProvider
type: select
use:
- provider1
proxies:
- Proxy
- DIRECT
proxy-providers:
provider1:
type: http
url: "url"
interval: 3600
path: ./hk.yaml
health-check:
enable: true
interval: 600
url: http://www.gstatic.com/generate_204
test:
type: file
path: /test.yaml
health-check:
enable: true
interval: 36000
url: http://www.gstatic.com/generate_204
rules:
- DOMAIN-SUFFIX,google.com,auto
- DOMAIN-KEYWORD,google,auto
- DOMAIN,google.com,auto
- DOMAIN-SUFFIX,ad.com,REJECT
# rename SOURCE-IP-CIDR and would remove after prerelease
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
# optional param "no-resolve" for IP rules (GEOIP IP-CIDR)
- IP-CIDR,127.0.0.0/8,DIRECT
- GEOIP,CN,DIRECT
- DST-PORT,80,DIRECT
- SRC-PORT,7777,DIRECT
# FINAL would remove after prerelease
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
- MATCH,auto
```
</details>
## Advanced
[Provider](https://github.com/Dreamacro/clash/wiki/Provider)
## Documentations
https://clash.gitbook.io/
## Premium Release
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
## Credits
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
## License
This software is released under the GPL-3.0 license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
@ -390,4 +57,4 @@ https://clash.gitbook.io/
- [x] Redir proxy
- [x] UDP support
- [x] Connection manager
- [ ] Event API
- [ ] ~~Event API~~

View File

@ -6,33 +6,18 @@ import (
"strings"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
)
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
net.Conn
metadata *C.Metadata
R *http.Request
}
// Metadata return destination metadata
func (h *HTTPAdapter) Metadata() *C.Metadata {
return h.metadata
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
// NewHTTP recieve normal http request and return HTTPContext
func NewHTTP(request *http.Request, conn net.Conn) *context.HTTPContext {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTP
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &HTTPAdapter{
metadata: metadata,
R: request,
Conn: conn,
}
return context.NewHTTPContext(conn, request, metadata)
}
// RemoveHopByHopHeaders remove hop-by-hop header

View File

@ -5,18 +5,16 @@ import (
"net/http"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
)
// NewHTTPS is HTTPAdapter generator
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
// NewHTTPS recieve CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPCONNECT
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return &SocketAdapter{
metadata: metadata,
Conn: conn,
}
return context.NewConnContext(conn, metadata)
}

View File

@ -5,21 +5,11 @@ import (
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
)
// SocketAdapter is a adapter for socks and redir connection
type SocketAdapter struct {
net.Conn
metadata *C.Metadata
}
// Metadata return destination metadata
func (s *SocketAdapter) Metadata() *C.Metadata {
return s.metadata
}
// NewSocket is SocketAdapter generator
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter {
// NewSocket recieve TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = source
@ -28,8 +18,5 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter
metadata.SrcPort = port
}
return &SocketAdapter{
Conn: conn,
metadata: metadata,
}
return context.NewConnContext(conn, metadata)
}

View File

@ -4,6 +4,7 @@ import (
"net"
"net/http"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
@ -16,7 +17,8 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
switch target[0] {
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]))
case socks5.AtypIPv4:
ip := net.IP(target[1 : 1+net.IPv4len])
@ -38,6 +40,9 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
port = "80"
}
// trim FQDN (#737)
host = strings.TrimRight(host, ".")
metadata := &C.Metadata{
NetWork: C.TCP,
AddrType: C.AtypDomainName,

View File

@ -10,10 +10,8 @@ import (
"github.com/Dreamacro/clash/common/queue"
C "github.com/Dreamacro/clash/constant"
)
var (
defaultURLTestTimeout = time.Second * 5
"go.uber.org/atomic"
)
type Base struct {
@ -78,13 +76,8 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}}
}
type PacketConn interface {
net.PacketConn
WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error)
}
type packetConn struct {
PacketConn
net.PacketConn
chain C.Chain
}
@ -96,18 +89,18 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc PacketConn, a C.ProxyAdapter) C.PacketConn {
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}
type Proxy struct {
C.ProxyAdapter
history *queue.Queue
alive bool
alive *atomic.Bool
}
func (p *Proxy) Alive() bool {
return p.alive
return p.alive.Load()
}
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
@ -119,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) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
if err != nil {
p.alive = false
p.alive.Store(false)
}
return conn, err
}
@ -136,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.
func (p *Proxy) LastDelay() (delay uint16) {
var max uint16 = 0xffff
if !p.alive {
if !p.alive.Load() {
return max
}
@ -167,7 +160,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
// URLTest get the delay for the specified URL
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
defer func() {
p.alive = err == nil
p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = t
@ -223,5 +216,5 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), true}
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
}

View File

@ -5,7 +5,6 @@ import (
"net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
)
@ -36,17 +35,6 @@ type directPacketConn struct {
net.PacketConn
}
func (dp *directPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return 0, err
}
metadata.DstIP = ip
}
return dp.WriteTo(p, metadata.UDPAddr())
}
func NewDirect() *Direct {
return &Direct{
Base: &Base{

View File

@ -31,6 +31,7 @@ type HttpOption struct {
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
}
@ -114,10 +115,14 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
func NewHttp(option HttpOption) *Http {
var tlsConfig *tls.Config
if option.TLS {
sni := option.Server
if option.SNI != "" {
sni = option.SNI
}
tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server,
ServerName: sni,
}
}

View File

@ -11,11 +11,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string)
if !existType {
return nil, fmt.Errorf("Missing type")
return nil, fmt.Errorf("missing type")
}
var proxy C.ProxyAdapter
err := fmt.Errorf("Cannot parse")
var (
proxy C.ProxyAdapter
err error
)
switch proxyType {
case "ss":
ssOption := &ShadowSocksOption{}
@ -24,6 +26,13 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
break
}
proxy, err = NewShadowSocks(*ssOption)
case "ssr":
ssrOption := &ShadowSocksROption{}
err = decoder.Decode(mapping, ssrOption)
if err != nil {
break
}
proxy, err = NewShadowSocksR(*ssrOption)
case "socks5":
socksOption := &Socks5Option{}
err = decoder.Decode(mapping, socksOption)
@ -65,7 +74,7 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
}
proxy, err = NewTrojan(*trojanOption)
default:
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}
if err != nil {

View File

@ -37,14 +37,10 @@ type ShadowSocksOption struct {
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,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 {
Mode string `obfs:"mode"`
Mode string `obfs:"mode,omitempty"`
Host string `obfs:"host,omitempty"`
}
@ -122,17 +118,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
var obfsOption *simpleObfsOption
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})
if option.Plugin == "obfs" {
opts := simpleObfsOption{Host: "bing.com"}
@ -197,14 +182,6 @@ func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
if err != nil {
return
}
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.PacketConn.ReadFrom(b)
if e != nil {

View File

@ -0,0 +1,148 @@
package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/ssr/obfs"
"github.com/Dreamacro/clash/component/ssr/protocol"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
)
type ShadowSocksR struct {
*Base
cipher core.Cipher
obfs obfs.Obfs
protocol protocol.Protocol
}
type ShadowSocksROption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
Obfs string `proxy:"obfs"`
ObfsParam string `proxy:"obfs-param,omitempty"`
Protocol string `proxy:"protocol"`
ProtocolParam string `proxy:"protocol-param,omitempty"`
UDP bool `proxy:"udp,omitempty"`
}
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c)
var (
iv []byte
err error
)
switch conn := c.(type) {
case *shadowstream.Conn:
iv, err = conn.ObtainWriteIV()
if err != nil {
return nil, err
}
case *shadowaead.Conn:
return nil, fmt.Errorf("invalid connection type")
}
c = ssr.protocol.StreamConn(c, iv)
_, err = c.Write(serializesSocksAddr(metadata))
return c, err
}
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 = ssr.protocol.PacketConn(pc)
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 error: %w", addr, err)
}
var (
ivSize int
key []byte
)
if option.Cipher == "dummy" {
ivSize = 0
key = core.Kdf(option.Password, 16)
} else {
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
}
ivSize = ciph.IVSize()
key = ciph.Key
}
obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
Host: option.Server,
Port: option.Port,
Key: key,
IVSize: ivSize,
Param: option.ObfsParam,
})
if err != nil {
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
}
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
Key: key,
Overhead: obfsOverhead,
Param: option.ProtocolParam,
})
if err != nil {
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
}
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
},
cipher: coreCiph,
obfs: obfs,
protocol: protocol,
}, nil
}

View File

@ -16,7 +16,9 @@ import (
type Snell struct {
*Base
psk []byte
pool *snell.Pool
obfsOption *simpleObfsOption
version int
}
type SnellOption struct {
@ -24,24 +26,47 @@ type SnellOption struct {
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
}
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch s.obfsOption.Mode {
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, s.obfsOption.Host)
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
case "http":
_, port, _ := net.SplitHostPort(s.addr)
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
_, port, _ := net.SplitHostPort(option.addr)
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
}
c = snell.StreamConn(c, s.psk)
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))
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) {
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 {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
@ -62,11 +87,22 @@ func NewSnell(option SnellOption) (*Snell, error) {
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
}
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
switch obfsOption.Mode {
case "tls", "http", "":
break
default:
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
}
return &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{
name: option.Name,
addr: addr,
@ -74,5 +110,19 @@ func NewSnell(option SnellOption) (*Snell, error) {
},
psk: psk,
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
}

View File

@ -122,7 +122,21 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
pc.Close()
}()
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, ss), nil
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
}
func NewSocks5(option Socks5Option) *Socks5 {
@ -164,14 +178,6 @@ func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
return uc.PacketConn.WriteTo(packet, uc.rAddr)
}
func (uc *socksPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
if err != nil {
return
}
return uc.PacketConn.WriteTo(packet, uc.rAddr)
}
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := uc.PacketConn.ReadFrom(b)
if e != nil {

View File

@ -71,7 +71,7 @@ func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
}
pc := t.instance.PacketConn(c)
return newPacketConn(&trojanPacketConn{pc, c}, t), err
return newPacketConn(pc, t), err
}
func (t *Trojan) MarshalJSON() ([]byte, error) {
@ -105,12 +105,3 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
instance: trojan.New(tOption),
}, nil
}
type trojanPacketConn struct {
net.PacketConn
conn net.Conn
}
func (tpc *trojanPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
return trojan.WritePacket(tpc.conn, serializesSocksAddr(metadata), p)
}

View File

@ -32,9 +32,11 @@ type VmessOption struct {
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
}
type HTTPOptions struct {
@ -43,6 +45,11 @@ type HTTPOptions struct {
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 {
@ -66,9 +73,29 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
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,
@ -78,6 +105,30 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
}
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 {
@ -87,6 +138,11 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = vmess.StreamTLSConn(c, tlsOpts)
}
}
@ -101,7 +157,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error", v.addr)
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
@ -123,7 +179,7 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error", v.addr)
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
c, err = v.StreamConn(c, metadata)
@ -145,13 +201,16 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if err != nil {
return nil, err
}
if option.Network == "h2" && !option.TLS {
return nil, fmt.Errorf("TLS must be true with h2 network")
}
return &Vmess{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: true,
udp: option.UDP,
},
client: client,
option: &option,
@ -195,10 +254,6 @@ func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return uc.Conn.Write(b)
}
func (uc *vmessPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
return uc.Conn.Write(p)
}
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, err := uc.Conn.Read(b)
return n, uc.rAddr, err

View File

@ -11,10 +11,14 @@ const (
defaultGetProxiesDuration = time.Second * 5
)
func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy {
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
proxies = append(proxies, provider.Proxies()...)
if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
}
return proxies
}

View File

@ -12,17 +12,18 @@ import (
type Fallback struct {
*outbound.Base
single *singledo.Single
providers []provider.ProxyProvider
disableUDP bool
single *singledo.Single
providers []provider.ProxyProvider
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(false)
return proxy.Name()
}
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)
if err == nil {
c.AppendToChains(f)
@ -31,7 +32,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
}
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
@ -40,13 +41,17 @@ func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
}
func (f *Fallback) SupportUDP() bool {
proxy := f.findAliveProxy()
if f.disableUDP {
return false
}
proxy := f.findAliveProxy(false)
return proxy.SupportUDP()
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies() {
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
@ -57,33 +62,34 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
}
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(true)
return proxy
}
func (f *Fallback) proxies() []C.Proxy {
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers), nil
return getProvidersProxies(f.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy() C.Proxy {
proxies := f.proxies()
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch)
for _, proxy := range proxies {
if proxy.Alive() {
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{
Base: outbound.NewBase(name, "", C.Fallback, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: options.DisableUDP,
}
}

View File

@ -3,6 +3,8 @@ package outboundgroup
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
@ -14,11 +16,25 @@ import (
"golang.org/x/net/publicsuffix"
)
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct {
*outbound.Base
single *singledo.Single
maxRetry int
providers []provider.ProxyProvider
disableUDP bool
single *singledo.Single
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 {
@ -78,27 +94,50 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
}
func (lb *LoadBalance) SupportUDP() bool {
return true
return !lb.disableUDP
}
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 {
key := uint64(murmur3.Sum32([]byte(getKey(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
}
}
return proxies[0]
proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata)
}
func (lb *LoadBalance) proxies() []C.Proxy {
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers), nil
return getProvidersProxies(lb.providers, touch), nil
})
return elm.([]C.Proxy)
@ -106,7 +145,7 @@ func (lb *LoadBalance) proxies() []C.Proxy {
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.proxies() {
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
@ -115,11 +154,21 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
})
}
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
return &LoadBalance{
Base: outbound.NewBase(name, "", C.LoadBalance, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
maxRetry: 3,
providers: providers,
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
strategyFn = strategyConsistentHashing()
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
}

View File

@ -12,25 +12,28 @@ import (
var (
errFormat = errors.New("format error")
errType = errors.New("unsupport type")
errMissUse = errors.New("`use` field should not be empty")
errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("`duplicate provider name")
)
type GroupCommonOption struct {
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,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) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{}
groupOption := &GroupCommonOption{
Lazy: true,
}
if err := decoder.Decode(config, groupOption); err != nil {
return nil, errFormat
}
@ -55,7 +58,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
// if Use not empty, drop health check options
if len(groupOption.Use) != 0 {
hc := provider.NewHealthCheck(ps, "", 0)
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
@ -63,9 +66,13 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
providers = append(providers, pd)
} else {
if _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider
}
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0)
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
@ -78,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
return nil, errMissHealthCheck
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval))
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
@ -101,15 +108,17 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
var group C.ProxyAdapter
switch groupOption.Type {
case "url-test":
group = NewURLTest(groupName, providers)
opts := parseURLTestOption(config)
group = NewURLTest(groupOption, providers, opts...)
case "select":
group = NewSelector(groupName, providers)
group = NewSelector(groupOption, providers)
case "fallback":
group = NewFallback(groupName, providers)
group = NewFallback(groupOption, providers)
case "load-balance":
group = NewLoadBalance(groupName, providers)
strategy := parseStrategy(config)
return NewLoadBalance(groupOption, providers, strategy)
case "relay":
group = NewRelay(groupName, providers)
group = NewRelay(groupOption, providers)
default:
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
}

View File

@ -20,9 +20,9 @@ type Relay struct {
}
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxies := r.proxies(metadata)
proxies := r.proxies(metadata, true)
if len(proxies) == 0 {
return nil, errors.New("Proxy does not exist")
return nil, errors.New("proxy does not exist")
}
first := proxies[0]
last := proxies[len(proxies)-1]
@ -58,7 +58,7 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range r.rawProxies() {
for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
@ -67,16 +67,16 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
})
}
func (r *Relay) rawProxies() []C.Proxy {
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (interface{}, error) {
return getProvidersProxies(r.providers), nil
return getProvidersProxies(r.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
proxies := r.rawProxies()
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies(touch)
for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata)
@ -89,9 +89,9 @@ func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
return proxies
}
func NewRelay(name string, providers []provider.ProxyProvider) *Relay {
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
Base: outbound.NewBase(name, "", C.Relay, false),
Base: outbound.NewBase(options.Name, "", C.Relay, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
}

View File

@ -13,13 +13,14 @@ import (
type Selector struct {
*outbound.Base
single *singledo.Single
selected string
providers []provider.ProxyProvider
disableUDP bool
single *singledo.Single
selected string
providers []provider.ProxyProvider
}
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy().DialContext(ctx, metadata)
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(s)
}
@ -27,7 +28,7 @@ func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
}
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy().DialUDP(metadata)
pc, err := s.selectedProxy(true).DialUDP(metadata)
if err == nil {
pc.AppendToChains(s)
}
@ -35,12 +36,16 @@ func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
}
func (s *Selector) SupportUDP() bool {
return s.selectedProxy().SupportUDP()
if s.disableUDP {
return false
}
return s.selectedProxy(false).SupportUDP()
}
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range getProvidersProxies(s.providers) {
for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name())
}
@ -52,11 +57,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
}
func (s *Selector) Now() string {
return s.selectedProxy().Name()
return s.selectedProxy(false).Name()
}
func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers) {
for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
@ -64,16 +69,16 @@ func (s *Selector) Set(name string) error {
}
}
return errors.New("Proxy does not exist")
return errors.New("proxy not exist")
}
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy()
return s.selectedProxy(true)
}
func (s *Selector) selectedProxy() C.Proxy {
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers)
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil
@ -86,12 +91,13 @@ func (s *Selector) selectedProxy() C.Proxy {
return elm.(C.Proxy)
}
func NewSelector(name string, providers []provider.ProxyProvider) *Selector {
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
Base: outbound.NewBase(name, "", C.Selector, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
Base: outbound.NewBase(options.Name, "", C.Selector, false),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: options.DisableUDP,
}
}

View File

@ -11,19 +11,30 @@ import (
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 {
*outbound.Base
tolerance uint16
disableUDP bool
fastNode C.Proxy
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
}
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) {
c, err = u.fast().DialContext(ctx, metadata)
c, err = u.fast(true).DialContext(ctx, metadata)
if err == nil {
c.AppendToChains(u)
}
@ -31,7 +42,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Co
}
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast().DialUDP(metadata)
pc, err := u.fast(true).DialUDP(metadata)
if err == nil {
pc.AppendToChains(u)
}
@ -39,20 +50,20 @@ func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
}
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast()
return u.fast(true)
}
func (u *URLTest) proxies() []C.Proxy {
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers), nil
return getProvidersProxies(u.providers, touch), nil
})
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) {
proxies := u.proxies()
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
for _, proxy := range proxies[1:] {
@ -66,19 +77,29 @@ func (u *URLTest) fast() C.Proxy {
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)
}
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) {
var all []string
for _, proxy := range u.proxies() {
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
@ -88,11 +109,31 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
})
}
func NewURLTest(name string, providers []provider.ProxyProvider) *URLTest {
return &URLTest{
Base: outbound.NewBase(name, "", C.URLTest, false),
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
opts := []urlTestOption{}
// 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),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: commonOptions.DisableUDP,
}
for _, option := range options {
option(urlTest)
}
return urlTest
}

View File

@ -5,6 +5,7 @@ import (
"crypto/md5"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/Dreamacro/clash/log"
@ -12,6 +13,7 @@ import (
var (
fileMode os.FileMode = 0666
dirMode os.FileMode = 0755
)
type parser = func([]byte) (interface{}, error)
@ -21,6 +23,7 @@ type fetcher struct {
vehicle Vehicle
updatedAt *time.Time
ticker *time.Ticker
done chan struct{}
hash [16]byte
parser parser
onUpdate func(interface{})
@ -35,10 +38,12 @@ func (f *fetcher) VehicleType() VehicleType {
}
func (f *fetcher) Initial() (interface{}, error) {
var buf []byte
var err error
var isLocal bool
if stat, err := os.Stat(f.vehicle.Path()); err == nil {
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
@ -69,8 +74,10 @@ func (f *fetcher) Initial() (interface{}, error) {
}
}
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
return nil, err
if f.vehicle.Type() != File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err
}
}
f.hash = md5.Sum(buf)
@ -101,8 +108,10 @@ func (f *fetcher) Update() (interface{}, bool, error) {
return nil, false, err
}
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
return nil, false, err
if f.vehicle.Type() != File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
}
}
f.updatedAt = &now
@ -113,31 +122,49 @@ func (f *fetcher) Update() (interface{}, bool, error) {
func (f *fetcher) Destroy() error {
if f.ticker != nil {
f.ticker.Stop()
f.done <- struct{}{}
}
return nil
}
func (f *fetcher) pullLoop() {
for range f.ticker.C {
elm, same, err := f.Update()
if err != nil {
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
continue
}
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
}
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)
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 {
@ -149,6 +176,7 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
ticker: ticker,
vehicle: vehicle,
parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate,
}
}

View File

@ -2,9 +2,12 @@ package provider
import (
"context"
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
)
const (
@ -17,10 +20,12 @@ type HealthCheckOption struct {
}
type HealthCheck struct {
url string
proxies []C.Proxy
interval uint
done chan struct{}
url string
proxies []C.Proxy
interval uint
lazy bool
lastTouch *atomic.Int64
done chan struct{}
}
func (hc *HealthCheck) process() {
@ -30,7 +35,10 @@ func (hc *HealthCheck) process() {
for {
select {
case <-ticker.C:
hc.check()
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done:
ticker.Stop()
return
@ -46,13 +54,24 @@ func (hc *HealthCheck) auto() bool {
return hc.interval != 0
}
func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
wg := &sync.WaitGroup{}
for _, proxy := range hc.proxies {
go proxy.URLTest(ctx, hc.url)
wg.Add(1)
go func(p C.Proxy) {
p.URLTest(ctx, hc.url)
wg.Done()
}(proxy)
}
<-ctx.Done()
wg.Wait()
cancel()
}
@ -60,11 +79,13 @@ func (hc *HealthCheck) close() {
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{
proxies: proxies,
url: url,
interval: interval,
done: make(chan struct{}, 1),
proxies: proxies,
url: url,
interval: interval,
lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
}
}

View File

@ -17,6 +17,7 @@ type healthCheckSchema struct {
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
}
type proxyProviderSchema struct {
@ -30,7 +31,11 @@ type proxyProviderSchema struct {
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
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 {
return nil, err
}
@ -39,7 +44,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
if schema.HealthCheck.Enable {
hcInterval = uint(schema.HealthCheck.Interval)
}
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval)
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
path := C.Path.Resolve(schema.Path)

View File

@ -50,6 +50,9 @@ type Provider interface {
type ProxyProvider interface {
Provider
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()
}
@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(buf []byte) (interface{}, error) {
schema := &ProxySchema{}
@ -120,20 +128,20 @@ func proxiesParse(buf []byte) (interface{}, error) {
}
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{}
for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping)
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)
}
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
@ -142,7 +150,9 @@ func proxiesParse(buf []byte) (interface{}, error) {
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
go pp.healthCheck.check()
if pp.healthCheck.auto() {
go pp.healthCheck.check()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
@ -221,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}

View File

@ -107,6 +107,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {

View File

@ -121,7 +121,7 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
c.SetWithExpire(key, value, time.Unix(expires, 0))
}
// SetWithExpire stores the interface{} representation of a response for a given key and given exires.
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
// The expires time will round to second.
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
c.mu.Lock()
@ -146,6 +146,23 @@ func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires tim
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()
@ -171,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key string) {
func (c *LruCache) Delete(key interface{}) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {

View File

@ -164,3 +164,21 @@ func TestStale(t *testing.T) {
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"))
}

11
common/net/io.go Normal file
View File

@ -0,0 +1,11 @@
package net
import "io"
type ReadOnlyReader struct {
io.Reader
}
type WriteOnlyWriter struct {
io.Writer
}

View File

@ -1,12 +1,12 @@
package observable
import (
"runtime"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/atomic"
)
func iterator(item []interface{}) chan interface{} {
@ -33,25 +33,25 @@ func TestObservable(t *testing.T) {
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})
src := NewObservable(iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
count := 0
var count = atomic.NewInt32(0)
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
for range ch {
count++
count.Inc()
}
wg.Done()
}
go waitCh(ch1)
go waitCh(ch2)
wg.Wait()
assert.Equal(t, count, 10)
assert.Equal(t, int32(10), count.Load())
}
func TestObservable_UnSubscribe(t *testing.T) {
@ -82,9 +82,6 @@ func TestObservable_UnSubscribeWithNotExistSubscription(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})
src := NewObservable(iter)
max := 100
@ -107,6 +104,43 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
go waitCh(ch)
}
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()
}

View File

@ -2,34 +2,32 @@ package observable
import (
"sync"
"gopkg.in/eapache/channels.v1"
)
type Subscription <-chan interface{}
type Subscriber struct {
buffer *channels.InfiniteChannel
buffer chan interface{}
once sync.Once
}
func (s *Subscriber) Emit(item interface{}) {
s.buffer.In() <- item
s.buffer <- item
}
func (s *Subscriber) Out() Subscription {
return s.buffer.Out()
return s.buffer
}
func (s *Subscriber) Close() {
s.once.Do(func() {
s.buffer.Close()
close(s.buffer)
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: channels.NewInfiniteChannel(),
buffer: make(chan interface{}, 200),
}
return sub
}

View File

@ -55,11 +55,13 @@ func (alloc *Allocator) Put(buf []byte) error {
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 significiant bit
// msb return the pos of most significant bit
func msb(size int) uint16 {
return uint16(bits.Len32(uint32(size)) - 1)
}

View File

@ -25,11 +25,11 @@ func TestAllocGet(t *testing.T) {
func TestAllocPut(t *testing.T) {
alloc := NewAllocator()
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 3, 3)), "put elem:3 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 4, 4)), "put elem:4 []bytes 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, 65536)), "put elem:65536 []bytes misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 65537, 65537)), "put elem:65537 []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) {

View File

@ -24,6 +24,8 @@ type Result struct {
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) {
s.mux.Lock()
now := time.Now()
@ -44,9 +46,12 @@ func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, s
s.mux.Unlock()
call.val, call.err = fn()
call.wg.Done()
s.mux.Lock()
s.call = nil
s.result = &Result{call.val, call.err}
s.last = now
s.mux.Unlock()
return call.val, call.err, false
}

View File

@ -6,12 +6,13 @@ import (
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/atomic"
)
func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
shardCount := 0
var shardCount = atomic.NewInt32(0)
call := func() (interface{}, error) {
foo++
time.Sleep(time.Millisecond * 5)
@ -25,7 +26,7 @@ func TestBasic(t *testing.T) {
go func() {
_, _, shard := single.Do(call)
if shard {
shardCount++
shardCount.Inc()
}
wg.Done()
}()
@ -33,7 +34,7 @@ func TestBasic(t *testing.T) {
wg.Wait()
assert.Equal(t, 1, foo)
assert.Equal(t, 4, shardCount)
assert.Equal(t, int32(4), shardCount.Load())
}
func TestTimer(t *testing.T) {

118
component/dialer/bind.go Normal file
View File

@ -0,0 +1,118 @@
package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
// In some OS, such as Windows, it takes a little longer to get interface information
var ifaceSingle = singledo.NewSingle(time.Second * 20)
var (
errPlatformNotSupport = errors.New("unsupport platform")
)
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
if !ip.IsGlobalUnicast() {
return nil
}
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return err
}
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
case "udp", "udp4", "udp6":
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
}
return nil
}
func fallbackBindToListenConfig(name string) (string, error) {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return "", err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return "", err
}
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return net.JoinHostPort(addr.IP.String(), "0"), nil
}
return "", ErrAddrNotFound
}

View File

@ -0,0 +1,53 @@
package dialer
import (
"net"
"syscall"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int) controlFn {
return func(network, address string, c syscall.RawConn) error {
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return nil
}
}
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx)
}
})
}
}
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
if err != nil {
return err
}
dialer.Control = bindControl(iface.(*net.Interface).Index)
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
if err != nil {
return err
}
lc.Control = bindControl(iface.(*net.Interface).Index)
return nil
}

View 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
}

View 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
}

View File

@ -19,17 +19,6 @@ func Dialer() (*net.Dialer, error) {
return dialer, nil
}
func ListenConfig() (*net.ListenConfig, error) {
cfg := &net.ListenConfig{}
if ListenConfigHook != nil {
if err := ListenConfigHook(cfg); err != nil {
return nil, err
}
}
return cfg, nil
}
func Dial(network, address string) (net.Conn, error) {
return DialContext(context.Background(), network, address)
}
@ -66,29 +55,26 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
case "tcp", "udp":
return dualStackDailContext(ctx, network, address)
return dualStackDialContext(ctx, network, address)
default:
return nil, errors.New("network invalid")
}
}
func ListenPacket(network, address string) (net.PacketConn, error) {
lc, err := ListenConfig()
if err != nil {
return nil, err
}
if ListenPacketHook != nil && address == "" {
ip, err := ListenPacketHook()
cfg := &net.ListenConfig{}
if ListenPacketHook != nil {
var err error
address, err = ListenPacketHook(cfg, address)
if err != nil {
return nil, err
}
address = net.JoinHostPort(ip.String(), "0")
}
return lc.ListenPacket(context.Background(), network, address)
return cfg.ListenPacket(context.Background(), network, address)
}
func dualStackDailContext(ctx context.Context, network, address string) (net.Conn, error) {
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
@ -147,28 +133,27 @@ func dualStackDailContext(ctx context.Context, network, address string) (net.Con
go startRacer(ctx, network+"4", host, false)
go startRacer(ctx, network+"6", host, true)
for {
select {
case res := <-results:
if res.error == nil {
return res.Conn, nil
}
for res := range results {
if res.error == nil {
return res.Conn, nil
}
if !res.ipv6 {
primary = res
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 {
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, primary.error
}
}
}
return nil, errors.New("never touched")
}

View File

@ -3,20 +3,15 @@ package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
type DialerHookFunc = func(dialer *net.Dialer) error
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
type ListenConfigHookFunc = func(*net.ListenConfig) error
type ListenPacketHookFunc = func() (net.IP, error)
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
var (
DialerHook DialerHookFunc
DialHook DialHookFunc
ListenConfigHook ListenConfigHookFunc
ListenPacketHook ListenPacketHookFunc
)
@ -25,124 +20,24 @@ var (
ErrNetworkNotSupport = errors.New("network not support")
)
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
single := singledo.NewSingle(5 * time.Second)
return func() (net.IP, error) {
elm, err, _ := single.Do(func() (interface{}, error) {
iface, err := net.InterfaceByName(name)
if err != nil {
return nil, err
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
return addrs, nil
})
if err != nil {
return nil, err
return func(lc *net.ListenConfig, address string) (string, error) {
err := bindIfaceToListenConfig(lc, name)
if err == errPlatformNotSupport {
address, err = fallbackBindToListenConfig(name)
}
addrs := elm.([]net.Addr)
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return addr.IP, nil
}
return nil, ErrAddrNotFound
return address, err
}
}
func DialerWithInterface(name string) DialHookFunc {
single := singledo.NewSingle(5 * time.Second)
return func(dialer *net.Dialer, network string, ip net.IP) error {
elm, err, _ := single.Do(func() (interface{}, error) {
iface, err := net.InterfaceByName(name)
if err != nil {
return nil, err
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
return addrs, nil
})
if err != nil {
return err
err := bindIfaceToDialer(dialer, name)
if err == errPlatformNotSupport {
err = fallbackBindToDialer(dialer, network, ip, name)
}
addrs := elm.([]net.Addr)
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
return err
}
}

View File

@ -6,7 +6,7 @@ import (
"sync"
"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
@ -16,7 +16,8 @@ type Pool struct {
gateway uint32
offset uint32
mux sync.Mutex
host *trie.Trie
host *trie.DomainTrie
ipnet *net.IPNet
cache *cache.LruCache
}
@ -89,6 +90,16 @@ func (p *Pool) Gateway() net.IP {
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 {
current := p.offset
for {
@ -116,11 +127,11 @@ func ipToUint(ip net.IP) uint32 {
}
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
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
ones, bits := ipnet.Mask.Size()
@ -136,6 +147,7 @@ func New(ipnet *net.IPNet, size int, host *trie.Trie) (*Pool, error) {
max: max,
gateway: min - 1,
host: host,
ipnet: ipnet,
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
}, nil
}

View File

@ -22,9 +22,9 @@ func (t *Table) Get(key string) C.PacketConn {
return item.(C.PacketConn)
}
func (t *Table) GetOrCreateLock(key string) (*sync.WaitGroup, bool) {
item, loaded := t.mapping.LoadOrStore(key, &sync.WaitGroup{})
return item.(*sync.WaitGroup), loaded
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item.(*sync.Cond), loaded
}
func (t *Table) Delete(key string) {

114
component/pool/pool.go Normal file
View 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
}

View 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))
}

View File

@ -0,0 +1,21 @@
package process
import (
"errors"
"net"
)
var (
ErrInvalidNetwork = errors.New("invalid network")
ErrPlatformNotSupport = errors.New("not support on this platform")
ErrNotFound = errors.New("process not found")
)
const (
TCP = "tcp"
UDP = "udp"
)
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}

View File

@ -0,0 +1,107 @@
package process
import (
"bytes"
"encoding/binary"
"net"
"path/filepath"
"syscall"
"unsafe"
)
const (
procpidpathinfo = 0xb
procpidpathinfosize = 1024
proccallnumpidinfo = 0x2
)
func findProcessName(network string, ip net.IP, port int) (string, error) {
var spath string
switch network {
case TCP:
spath = "net.inet.tcp.pcblist_n"
case UDP:
spath = "net.inet.udp.pcblist_n"
default:
return "", ErrInvalidNetwork
}
isIPv4 := ip.To4() != nil
value, err := syscall.Sysctl(spath)
if err != nil {
return "", err
}
buf := []byte(value)
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := 384
if network == TCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208
}
// skip the first xinpgen(24 bytes) block
for i := 24; i+itemSize <= len(buf); i += itemSize {
// offset of xinpcb_n and xsocket_n
inp, so := i, i+104
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
if uint16(port) != srcPort {
continue
}
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP net.IP
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+64 : inp+80])
default:
continue
}
if !ip.Equal(srcIP) {
continue
}
// xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid)
}
return "", ErrNotFound
}
func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]byte, procpidpathinfosize)
_, _, errno := syscall.Syscall6(
syscall.SYS_PROC_INFO,
proccallnumpidinfo,
uintptr(pid),
procpidpathinfo,
0,
uintptr(unsafe.Pointer(&buf[0])),
procpidpathinfosize)
if errno != 0 {
return "", errno
}
firstZero := bytes.IndexByte(buf, 0)
if firstZero <= 0 {
return "", nil
}
return filepath.Base(string(buf[:firstZero])), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@ -0,0 +1,228 @@
package process
import (
"encoding/binary"
"fmt"
"net"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/log"
)
// store process name for when dealing with multiple PROCESS-NAME rules
var (
defaultSearcher *searcher
once sync.Once
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
once.Do(func() {
if err := initSearcher(); err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
log.Warnln("All PROCESS-NAME rules will be skipped")
return
}
})
var spath string
isTCP := network == TCP
switch network {
case TCP:
spath = "net.inet.tcp.pcblist"
case UDP:
spath = "net.inet.udp.pcblist"
default:
return "", ErrInvalidNetwork
}
value, err := syscall.Sysctl(spath)
if err != nil {
return "", err
}
buf := []byte(value)
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
}
func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]byte, 2048)
size := uint64(len(buf))
// CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid
mib := [4]uint32{1, 14, 12, pid}
_, _, errno := syscall.Syscall6(
syscall.SYS___SYSCTL,
uintptr(unsafe.Pointer(&mib[0])),
uintptr(len(mib)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0,
0)
if errno != 0 || size == 0 {
return "", errno
}
return filepath.Base(string(buf[:size-1])), nil
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
type searcher struct {
// sizeof(struct xinpgen)
headSize int
// sizeof(struct xtcpcb)
tcpItemSize int
// sizeof(struct xinpcb)
udpItemSize int
udpInpOffset int
port int
ip int
vflag int
socket int
// sizeof(struct xfile)
fileItemSize int
data int
pid int
}
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
var itemSize int
var inpOffset int
if isTCP {
// struct xtcpcb
itemSize = s.tcpItemSize
inpOffset = 8
} else {
// struct xinpcb
itemSize = s.udpItemSize
inpOffset = s.udpInpOffset
}
isIPv4 := ip.To4() != nil
// skip the first xinpgen block
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
inp := i + inpOffset
srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2])
if port != srcPort {
continue
}
// xinpcb.inp_vflag
flag := buf[inp+s.vflag]
var srcIP net.IP
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
default:
continue
}
if !ip.Equal(srcIP) {
continue
}
// xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison
socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8])
return s.searchSocketPid(socket)
}
return 0, ErrNotFound
}
func (s *searcher) searchSocketPid(socket uint64) (uint32, error) {
value, err := syscall.Sysctl("kern.file")
if err != nil {
return 0, err
}
buf := []byte(value)
// struct xfile
itemSize := s.fileItemSize
for i := 0; i+itemSize <= len(buf); i += itemSize {
// xfile.xf_data
data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8])
if data == socket {
// xfile.xf_pid
pid := readNativeUint32(buf[i+s.pid : i+s.pid+4])
return pid, nil
}
}
return 0, ErrNotFound
}
func newSearcher(major int) *searcher {
var s *searcher = nil
switch major {
case 11:
s = &searcher{
headSize: 32,
tcpItemSize: 1304,
udpItemSize: 632,
port: 198,
ip: 228,
vflag: 116,
socket: 88,
fileItemSize: 80,
data: 56,
pid: 8,
udpInpOffset: 8,
}
case 12:
s = &searcher{
headSize: 64,
tcpItemSize: 744,
udpItemSize: 400,
port: 254,
ip: 284,
vflag: 392,
socket: 16,
fileItemSize: 128,
data: 56,
pid: 8,
}
}
return s
}
func initSearcher() error {
osRelease, err := syscall.Sysctl("kern.osrelease")
if err != nil {
return err
}
dot := strings.Index(osRelease, ".")
if dot != -1 {
osRelease = osRelease[:dot]
}
major, err := strconv.Atoi(osRelease)
if err != nil {
return err
}
defaultSearcher = newSearcher(major)
if defaultSearcher == nil {
return fmt.Errorf("unsupported freebsd version %d", major)
}
return nil
}

View File

@ -0,0 +1,238 @@
package process
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net"
"path"
"path/filepath"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/pool"
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}
type SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
type ProcessNameResolver func(inode, uid int) (name string, err error)
// export for android
var (
DefaultSocketResolver SocketResolver = resolveSocketByNetlink
DefaultProcessNameResolver ProcessNameResolver = resolveProcessNameByProcSearch
)
const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20
pathProc = "/proc"
)
var nativeEndian binary.ByteOrder = binary.LittleEndian
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := DefaultSocketResolver(network, ip, srcPort)
if err != nil {
return "", err
}
return DefaultProcessNameResolver(inode, uid)
}
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, error) {
var family byte
var protocol byte
switch network {
case TCP:
protocol = syscall.IPPROTO_TCP
case UDP:
protocol = syscall.IPPROTO_UDP
default:
return 0, 0, ErrInvalidNetwork
}
if ip.To4() != nil {
family = syscall.AF_INET
} else {
family = syscall.AF_INET6
}
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil {
return 0, 0, err
}
defer syscall.Close(socket)
syscall.SetNonblock(socket, true)
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 50})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 50})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0,
Groups: 0,
}); err != nil {
return 0, 0, err
}
if _, err := syscall.Write(socket, req); err != nil {
return 0, 0, err
}
rb := pool.Get(pool.RelayBufferSize)
defer pool.Put(rb)
n, err := syscall.Read(socket, rb)
if err != nil {
return 0, 0, err
}
messages, err := syscall.ParseNetlinkMessage(rb[:n])
if err != nil {
return 0, 0, err
} else if len(messages) == 0 {
return 0, 0, io.ErrUnexpectedEOF
}
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, syscall.ESRCH
}
uid, inode := unpackSocketDiagResponse(&messages[0])
return int(uid), int(inode), nil
}
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
s := make([]byte, 16)
if v4 := source.To4(); v4 != nil {
copy(s, v4)
} else {
copy(s, source)
}
buf := make([]byte, sizeOfSocketDiagRequest)
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
nativeEndian.PutUint32(buf[8:12], 0)
nativeEndian.PutUint32(buf[12:16], 0)
buf[16] = family
buf[17] = protocol
buf[18] = 0
buf[19] = 0
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
if len(msg.Data) < 72 {
return 0, 0
}
data := msg.Data
uid = nativeEndian.Uint32(data[64:68])
inode = nativeEndian.Uint32(data[68:72])
return
}
func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
files, err := ioutil.ReadDir(pathProc)
if err != nil {
return "", err
}
buffer := make([]byte, syscall.PathMax)
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
if f.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
continue
}
processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd")
fds, err := ioutil.ReadDir(fdPath)
if err != nil {
continue
}
for _, fd := range fds {
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
if err != nil {
continue
}
if bytes.Equal(buffer[:n], socket) {
cmdline, err := ioutil.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
}
}
}
return "", syscall.ESRCH
}
func splitCmdline(cmdline []byte) string {
indexOfEndOfString := len(cmdline)
for i, c := range cmdline {
if c == 0 {
indexOfEndOfString = i
break
}
}
return filepath.Base(string(cmdline[:indexOfEndOfString]))
}
func isPid(s string) bool {
for _, s := range s {
if s < '0' || s > '9' {
return false
}
}
return true
}

View File

@ -0,0 +1,10 @@
// +build !darwin,!linux,!windows
// +build !freebsd !amd64
package process
import "net"
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
return "", ErrPlatformNotSupport
}

View File

@ -0,0 +1,224 @@
package process
import (
"fmt"
"net"
"path/filepath"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows"
)
const (
tcpTableFunc = "GetExtendedTcpTable"
tcpTablePidConn = 4
udpTableFunc = "GetExtendedUdpTable"
udpTablePid = 1
queryProcNameFunc = "QueryFullProcessImageNameW"
)
var (
getExTcpTable uintptr
getExUdpTable uintptr
queryProcName uintptr
once sync.Once
)
func initWin32API() error {
h, err := windows.LoadLibrary("iphlpapi.dll")
if err != nil {
return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error())
}
getExTcpTable, err = windows.GetProcAddress(h, tcpTableFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error())
}
getExUdpTable, err = windows.GetProcAddress(h, udpTableFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error())
}
h, err = windows.LoadLibrary("kernel32.dll")
if err != nil {
return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error())
}
queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc)
if err != nil {
return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error())
}
return nil
}
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
once.Do(func() {
err := initWin32API()
if err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
log.Warnln("All PROCESS-NAMES rules will be skiped")
return
}
})
family := windows.AF_INET
if ip.To4() == nil {
family = windows.AF_INET6
}
var class int
var fn uintptr
switch network {
case TCP:
fn = getExTcpTable
class = tcpTablePidConn
case UDP:
fn = getExUdpTable
class = udpTablePid
default:
return "", ErrInvalidNetwork
}
buf, err := getTransportTable(fn, family, class)
if err != nil {
return "", err
}
s := newSearcher(family == windows.AF_INET, network == TCP)
pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil {
return "", err
}
return getExecPathFromPID(pid)
}
type searcher struct {
itemSize int
port int
ip int
ipSize int
pid int
tcpState int
}
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
row := b[4+itemSize*i : 4+itemSize*(i+1)]
if s.tcpState >= 0 {
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
if tcpState != 5 {
continue
}
}
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
// this field can be illustrated as follows depends on different machine endianess:
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
if srcPort != port {
continue
}
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
pid := readNativeUint32(row[s.pid : s.pid+4])
return pid, nil
}
return 0, ErrNotFound
}
func newSearcher(isV4, isTCP bool) *searcher {
var itemSize, port, ip, ipSize, pid int
tcpState := -1
switch {
case isV4 && isTCP:
// struct MIB_TCPROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
case isV4 && !isTCP:
// struct MIB_UDPROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
case !isV4 && isTCP:
// struct MIB_TCP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
case !isV4 && !isTCP:
// struct MIB_UDP6ROW_OWNER_PID
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
}
return &searcher{
itemSize: itemSize,
port: port,
ip: ip,
ipSize: ipSize,
pid: pid,
tcpState: tcpState,
}
}
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err {
case 0:
return buf, nil
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
buf = make([]byte, size)
default:
return nil, fmt.Errorf("syscall error: %d", err)
}
}
}
func readNativeUint32(b []byte) uint32 {
return *(*uint32)(unsafe.Pointer(&b[0]))
}
func getExecPathFromPID(pid uint32) (string, error) {
// kernel process starts with a colon in order to distinguish with normal processes
switch pid {
case 0:
// reserved pid for system idle process
return ":System Idle Process", nil
case 4:
// reserved pid for windows kernel image
return ":System", nil
}
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return "", err
}
defer windows.CloseHandle(h)
buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf))
r1, _, err := syscall.Syscall6(
queryProcName, 4,
uintptr(h),
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0, 0)
if r1 == 0 {
return "", err
}
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil
}

View File

@ -0,0 +1,115 @@
package cachefile
import (
"bytes"
"encoding/gob"
"io/ioutil"
"os"
"sync"
"github.com/Dreamacro/clash/component/profile"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var (
initOnce sync.Once
fileMode os.FileMode = 0666
defaultCache *CacheFile
)
type cache struct {
Selected map[string]string
}
// CacheFile store and update the cache file
type CacheFile struct {
path string
model *cache
enc *gob.Encoder
buf *bytes.Buffer
mux sync.Mutex
}
func (c *CacheFile) SetSelected(group, selected string) {
if !profile.StoreSelected.Load() {
return
}
c.mux.Lock()
defer c.mux.Unlock()
model, err := c.element()
if err != nil {
log.Warnln("[CacheFile] read cache %s failed: %s", c.path, err.Error())
return
}
model.Selected[group] = selected
c.buf.Reset()
if err := c.enc.Encode(model); err != nil {
log.Warnln("[CacheFile] encode gob failed: %s", err.Error())
return
}
if err := ioutil.WriteFile(c.path, c.buf.Bytes(), fileMode); err != nil {
log.Warnln("[CacheFile] write cache to %s failed: %s", c.path, err.Error())
return
}
}
func (c *CacheFile) SelectedMap() map[string]string {
if !profile.StoreSelected.Load() {
return nil
}
c.mux.Lock()
defer c.mux.Unlock()
model, err := c.element()
if err != nil {
log.Warnln("[CacheFile] read cache %s failed: %s", c.path, err.Error())
return nil
}
mapping := map[string]string{}
for k, v := range model.Selected {
mapping[k] = v
}
return mapping
}
func (c *CacheFile) element() (*cache, error) {
if c.model != nil {
return c.model, nil
}
model := &cache{
Selected: map[string]string{},
}
if buf, err := ioutil.ReadFile(c.path); err == nil {
bufReader := bytes.NewBuffer(buf)
dec := gob.NewDecoder(bufReader)
if err := dec.Decode(model); err != nil {
return nil, err
}
}
c.model = model
return c.model, nil
}
// Cache return singleton of CacheFile
func Cache() *CacheFile {
initOnce.Do(func() {
buf := &bytes.Buffer{}
defaultCache = &CacheFile{
path: C.Path.Cache(),
buf: buf,
enc: gob.NewEncoder(buf),
}
})
return defaultCache
}

View File

@ -0,0 +1,10 @@
package profile
import (
"go.uber.org/atomic"
)
var (
// StoreSelected is a global switch for storing selected proxy to cache
StoreSelected = atomic.NewBool(true)
)

View 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
}

View File

@ -5,20 +5,25 @@ import (
"net"
"strings"
trie "github.com/Dreamacro/clash/component/domain-trie"
"github.com/Dreamacro/clash/component/trie"
)
var (
// DefaultResolver aim to resolve ip
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")
ErrIPNotFound = errors.New("couldn't find ip")
ErrIPVersion = errors.New("ip version error")
ErrIPv6Disabled = errors.New("ipv6 disabled")
)
type Resolver interface {
@ -63,6 +68,10 @@ func ResolveIPv4(host string) (net.IP, error) {
// ResolveIPv6 with a host, return ipv6
func ResolveIPv6(host string) (net.IP, error) {
if DisableIPv6 {
return nil, ErrIPv6Disabled
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To16(); ip != nil {
return ip, nil
@ -102,7 +111,12 @@ func ResolveIP(host string) (net.IP, error) {
}
if DefaultResolver != nil {
if DisableIPv6 {
return DefaultResolver.ResolveIPv4(host)
}
return DefaultResolver.ResolveIP(host)
} else if DisableIPv6 {
return ResolveIPv4(host)
}
ip := net.ParseIP(host)

View File

@ -28,6 +28,7 @@ func (ho *HTTPObfs) Read(b []byte) (int, error) {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
pool.Put(ho.buf)
ho.buf = 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("Upgrade", "websocket")
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.ContentLength = int64(len(b))
err := req.Write(ho.Conn)

View File

@ -1,21 +1,54 @@
package snell
import (
"crypto/aes"
"crypto/cipher"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
)
type snellCipher struct {
psk []byte
keySize int
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) 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) {
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
View 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}
}

View File

@ -4,19 +4,27 @@ import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"golang.org/x/crypto/chacha20poly1305"
)
const (
CommandPing byte = 0
CommandConnect byte = 1
Version1 = 1
Version2 = 2
DefaultSnellVersion = Version1
)
const (
CommandPing byte = 0
CommandConnect byte = 1
CommandConnectV2 byte = 5
CommandTunnel byte = 0
CommandPong byte = 1
CommandError byte = 2
Version byte = 1
@ -24,6 +32,7 @@ const (
var (
bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
endSignal = []byte{}
)
type Snell struct {
@ -45,14 +54,20 @@ func (s *Snell) Read(b []byte) (int, error) {
if s.buffer[0] == CommandTunnel {
return s.Conn.Read(b)
} else if s.buffer[0] != CommandError {
return 0, errors.New("Command not support")
return 0, errors.New("command not support")
}
// CommandError
// 1 byte error code
if _, err := io.ReadFull(s.Conn, s.buffer[:]); err != nil {
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])
msg := make([]byte, length)
@ -60,15 +75,19 @@ func (s *Snell) Read(b []byte) (int, error) {
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.Reset()
defer bufferPool.Put(buf)
buf.WriteByte(Version)
buf.WriteByte(CommandConnect)
if version == Version2 {
buf.WriteByte(CommandConnectV2)
} else {
buf.WriteByte(CommandConnect)
}
// clientID length & id
buf.WriteByte(0)
@ -85,7 +104,24 @@ func WriteHeader(conn net.Conn, host string, port uint) error {
return nil
}
func StreamConn(conn net.Conn, psk []byte) net.Conn {
cipher := &snellCipher{psk, chacha20poly1305.New}
// HalfClose works only on version2
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)}
}

View File

@ -21,6 +21,8 @@ func (err Error) Error() string {
// Command is request commands as defined in RFC 1928 section 4.
type Command = uint8
const Version = 5
// SOCKS request commands as defined in RFC 1928 section 4.
const (
CmdConnect Command = 1
@ -227,6 +229,10 @@ func ClientHandshake(rw io.ReadWriter, addr Addr, command Command, user *User) (
}
if buf[1] == 2 {
if user == nil {
return nil, ErrAuth
}
// password protocol version
authMsg := &bytes.Buffer{}
authMsg.WriteByte(1)

View File

@ -0,0 +1,9 @@
package obfs
type Base struct {
Host string
Port int
Key []byte
IVSize int
Param string
}

View File

@ -0,0 +1,9 @@
package obfs
func init() {
register("http_post", newHTTPPost, 0)
}
func newHTTPPost(b *Base) Obfs {
return &httpObfs{Base: b, post: true}
}

View File

@ -0,0 +1,407 @@
package obfs
import (
"bytes"
"encoding/hex"
"io"
"math/rand"
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
)
func init() {
register("http_simple", newHTTPSimple, 0)
}
type httpObfs struct {
*Base
post bool
}
func newHTTPSimple(b *Base) Obfs {
return &httpObfs{Base: b}
}
type httpConn struct {
net.Conn
*httpObfs
hasSentHeader bool
hasRecvHeader bool
buf []byte
}
func (h *httpObfs) StreamConn(c net.Conn) net.Conn {
return &httpConn{Conn: c, httpObfs: h}
}
func (c *httpConn) Read(b []byte) (int, error) {
if c.buf != nil {
n := copy(b, c.buf)
if n == len(c.buf) {
c.buf = nil
} else {
c.buf = c.buf[n:]
}
return n, nil
}
if c.hasRecvHeader {
return c.Conn.Read(b)
}
buf := pool.Get(pool.RelayBufferSize)
defer pool.Put(buf)
n, err := c.Conn.Read(buf)
if err != nil {
return 0, err
}
pos := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if pos == -1 {
return 0, io.EOF
}
c.hasRecvHeader = true
dataLength := n - pos - 4
n = copy(b, buf[4+pos:n])
if dataLength > n {
c.buf = append(c.buf, buf[4+pos+n:4+pos+dataLength]...)
}
return n, nil
}
func (c *httpConn) Write(b []byte) (int, error) {
if c.hasSentHeader {
return c.Conn.Write(b)
}
// 30: head length
headLength := c.IVSize + 30
bLength := len(b)
headDataLength := bLength
if bLength-headLength > 64 {
headDataLength = headLength + rand.Intn(65)
}
headData := b[:headDataLength]
b = b[headDataLength:]
var body string
host := c.Host
if len(c.Param) > 0 {
pos := strings.Index(c.Param, "#")
if pos != -1 {
body = strings.ReplaceAll(c.Param[pos+1:], "\n", "\r\n")
body = strings.ReplaceAll(body, "\\n", "\r\n")
host = c.Param[:pos]
} else {
host = c.Param
}
}
hosts := strings.Split(host, ",")
host = hosts[rand.Intn(len(hosts))]
buf := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(buf)
defer buf.Reset()
if c.post {
buf.WriteString("POST /")
} else {
buf.WriteString("GET /")
}
packURLEncodedHeadData(buf, headData)
buf.WriteString(" HTTP/1.1\r\nHost: " + host)
if c.Port != 80 {
buf.WriteString(":" + strconv.Itoa(c.Port))
}
buf.WriteString("\r\n")
if len(body) > 0 {
buf.WriteString(body + "\r\n\r\n")
} else {
buf.WriteString("User-Agent: ")
buf.WriteString(userAgent[rand.Intn(len(userAgent))])
buf.WriteString("\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n")
if c.post {
packBoundary(buf)
}
buf.WriteString("DNT: 1\r\nConnection: keep-alive\r\n\r\n")
}
buf.Write(b)
_, err := c.Conn.Write(buf.Bytes())
if err != nil {
return 0, nil
}
c.hasSentHeader = true
return bLength, nil
}
func packURLEncodedHeadData(buf *bytes.Buffer, data []byte) {
dataLength := len(data)
for i := 0; i < dataLength; i++ {
buf.WriteRune('%')
buf.WriteString(hex.EncodeToString(data[i : i+1]))
}
}
func packBoundary(buf *bytes.Buffer) {
buf.WriteString("Content-Type: multipart/form-data; boundary=")
set := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for i := 0; i < 32; i++ {
buf.WriteByte(set[rand.Intn(62)])
}
buf.WriteString("\r\n")
}
var userAgent = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
}

View File

@ -0,0 +1,42 @@
package obfs
import (
"errors"
"fmt"
"net"
)
var (
errTLS12TicketAuthIncorrectMagicNumber = errors.New("tls1.2_ticket_auth incorrect magic number")
errTLS12TicketAuthTooShortData = errors.New("tls1.2_ticket_auth too short data")
errTLS12TicketAuthHMACError = errors.New("tls1.2_ticket_auth hmac verifying failed")
)
type authData struct {
clientID [32]byte
}
type Obfs interface {
StreamConn(net.Conn) net.Conn
}
type obfsCreator func(b *Base) Obfs
var obfsList = make(map[string]struct {
overhead int
new obfsCreator
})
func register(name string, c obfsCreator, o int) {
obfsList[name] = struct {
overhead int
new obfsCreator
}{overhead: o, new: c}
}
func PickObfs(name string, b *Base) (Obfs, int, error) {
if choice, ok := obfsList[name]; ok {
return choice.new(b), choice.overhead, nil
}
return nil, 0, fmt.Errorf("Obfs %s not supported", name)
}

View File

@ -0,0 +1,15 @@
package obfs
import "net"
type plain struct{}
func init() {
register("plain", newPlain, 0)
}
func newPlain(b *Base) Obfs {
return &plain{}
}
func (p *plain) StreamConn(c net.Conn) net.Conn { return c }

View File

@ -0,0 +1,71 @@
package obfs
import (
"encoding/binary"
"hash/crc32"
"math/rand"
"net"
"github.com/Dreamacro/clash/common/pool"
)
func init() {
register("random_head", newRandomHead, 0)
}
type randomHead struct {
*Base
}
func newRandomHead(b *Base) Obfs {
return &randomHead{Base: b}
}
type randomHeadConn struct {
net.Conn
*randomHead
hasSentHeader bool
rawTransSent bool
rawTransRecv bool
buf []byte
}
func (r *randomHead) StreamConn(c net.Conn) net.Conn {
return &randomHeadConn{Conn: c, randomHead: r}
}
func (c *randomHeadConn) Read(b []byte) (int, error) {
if c.rawTransRecv {
return c.Conn.Read(b)
}
buf := pool.Get(pool.RelayBufferSize)
defer pool.Put(buf)
c.Conn.Read(buf)
c.rawTransRecv = true
c.Write(nil)
return 0, nil
}
func (c *randomHeadConn) Write(b []byte) (int, error) {
if c.rawTransSent {
return c.Conn.Write(b)
}
c.buf = append(c.buf, b...)
if !c.hasSentHeader {
c.hasSentHeader = true
dataLength := rand.Intn(96) + 4
buf := pool.Get(dataLength + 4)
defer pool.Put(buf)
rand.Read(buf[:dataLength])
binary.LittleEndian.PutUint32(buf[dataLength:], 0xffffffff-crc32.ChecksumIEEE(buf[:dataLength]))
_, err := c.Conn.Write(buf)
return len(b), err
}
if c.rawTransRecv {
_, err := c.Conn.Write(c.buf)
c.buf = nil
c.rawTransSent = true
return len(b), err
}
return len(b), nil
}

View File

@ -0,0 +1,231 @@
package obfs
import (
"bytes"
"crypto/hmac"
"encoding/binary"
"math/rand"
"net"
"strings"
"time"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
)
func init() {
register("tls1.2_ticket_auth", newTLS12Ticket, 5)
register("tls1.2_ticket_fastauth", newTLS12Ticket, 5)
}
type tls12Ticket struct {
*Base
*authData
}
func newTLS12Ticket(b *Base) Obfs {
r := &tls12Ticket{Base: b, authData: &authData{}}
rand.Read(r.clientID[:])
return r
}
type tls12TicketConn struct {
net.Conn
*tls12Ticket
handshakeStatus int
decoded bytes.Buffer
underDecoded bytes.Buffer
sendBuf bytes.Buffer
}
func (t *tls12Ticket) StreamConn(c net.Conn) net.Conn {
return &tls12TicketConn{Conn: c, tls12Ticket: t}
}
func (c *tls12TicketConn) Read(b []byte) (int, error) {
if c.decoded.Len() > 0 {
return c.decoded.Read(b)
}
buf := pool.Get(pool.RelayBufferSize)
defer pool.Put(buf)
n, err := c.Conn.Read(buf)
if err != nil {
return 0, err
}
if c.handshakeStatus == 8 {
c.underDecoded.Write(buf[:n])
for c.underDecoded.Len() > 5 {
if !bytes.Equal(c.underDecoded.Bytes()[:3], []byte{0x17, 3, 3}) {
c.underDecoded.Reset()
return 0, errTLS12TicketAuthIncorrectMagicNumber
}
size := int(binary.BigEndian.Uint16(c.underDecoded.Bytes()[3:5]))
if c.underDecoded.Len() < 5+size {
break
}
c.underDecoded.Next(5)
c.decoded.Write(c.underDecoded.Next(size))
}
n, _ = c.decoded.Read(b)
return n, nil
}
if n < 11+32+1+32 {
return 0, errTLS12TicketAuthTooShortData
}
if !hmac.Equal(buf[33:43], c.hmacSHA1(buf[11:33])[:10]) || !hmac.Equal(buf[n-10:n], c.hmacSHA1(buf[:n-10])[:10]) {
return 0, errTLS12TicketAuthHMACError
}
c.Write(nil)
return 0, nil
}
func (c *tls12TicketConn) Write(b []byte) (int, error) {
length := len(b)
if c.handshakeStatus == 8 {
buf := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(buf)
defer buf.Reset()
for len(b) > 2048 {
size := rand.Intn(4096) + 100
if len(b) < size {
size = len(b)
}
packData(buf, b[:size])
b = b[size:]
}
if len(b) > 0 {
packData(buf, b)
}
_, err := c.Conn.Write(buf.Bytes())
if err != nil {
return 0, err
}
return length, nil
}
if len(b) > 0 {
packData(&c.sendBuf, b)
}
if c.handshakeStatus == 0 {
c.handshakeStatus = 1
data := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(data)
defer data.Reset()
data.Write([]byte{3, 3})
c.packAuthData(data)
data.WriteByte(0x20)
data.Write(c.clientID[:])
data.Write([]byte{0x00, 0x1c, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13, 0xc0, 0x0a, 0xc0, 0x14, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0x0a})
data.Write([]byte{0x1, 0x0})
ext := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(ext)
defer ext.Reset()
host := c.getHost()
ext.Write([]byte{0xff, 0x01, 0x00, 0x01, 0x00})
packSNIData(ext, host)
ext.Write([]byte{0, 0x17, 0, 0})
c.packTicketBuf(ext, host)
ext.Write([]byte{0x00, 0x0d, 0x00, 0x16, 0x00, 0x14, 0x06, 0x01, 0x06, 0x03, 0x05, 0x01, 0x05, 0x03, 0x04, 0x01, 0x04, 0x03, 0x03, 0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03})
ext.Write([]byte{0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00})
ext.Write([]byte{0x00, 0x12, 0x00, 0x00})
ext.Write([]byte{0x75, 0x50, 0x00, 0x00})
ext.Write([]byte{0x00, 0x0b, 0x00, 0x02, 0x01, 0x00})
ext.Write([]byte{0x00, 0x0a, 0x00, 0x06, 0x00, 0x04, 0x00, 0x17, 0x00, 0x18})
binary.Write(data, binary.BigEndian, uint16(ext.Len()))
data.ReadFrom(ext)
ret := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(ret)
defer ret.Reset()
ret.Write([]byte{0x16, 3, 1})
binary.Write(ret, binary.BigEndian, uint16(data.Len()+4))
ret.Write([]byte{1, 0})
binary.Write(ret, binary.BigEndian, uint16(data.Len()))
ret.ReadFrom(data)
_, err := c.Conn.Write(ret.Bytes())
if err != nil {
return 0, err
}
return length, nil
} else if c.handshakeStatus == 1 && len(b) == 0 {
buf := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(buf)
defer buf.Reset()
buf.Write([]byte{0x14, 3, 3, 0, 1, 1, 0x16, 3, 3, 0, 0x20})
tools.AppendRandBytes(buf, 22)
buf.Write(c.hmacSHA1(buf.Bytes())[:10])
buf.ReadFrom(&c.sendBuf)
c.handshakeStatus = 8
_, err := c.Conn.Write(buf.Bytes())
return 0, err
}
return length, nil
}
func packData(buf *bytes.Buffer, data []byte) {
buf.Write([]byte{0x17, 3, 3})
binary.Write(buf, binary.BigEndian, uint16(len(data)))
buf.Write(data)
}
func (t *tls12Ticket) packAuthData(buf *bytes.Buffer) {
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
tools.AppendRandBytes(buf, 18)
buf.Write(t.hmacSHA1(buf.Bytes()[buf.Len()-22:])[:10])
}
func packSNIData(buf *bytes.Buffer, u string) {
len := uint16(len(u))
buf.Write([]byte{0, 0})
binary.Write(buf, binary.BigEndian, len+5)
binary.Write(buf, binary.BigEndian, len+3)
buf.WriteByte(0)
binary.Write(buf, binary.BigEndian, len)
buf.WriteString(u)
}
func (c *tls12TicketConn) packTicketBuf(buf *bytes.Buffer, u string) {
length := 16 * (rand.Intn(17) + 8)
buf.Write([]byte{0, 0x23})
binary.Write(buf, binary.BigEndian, uint16(length))
tools.AppendRandBytes(buf, length)
}
func (t *tls12Ticket) hmacSHA1(data []byte) []byte {
key := pool.Get(len(t.Key) + 32)
defer pool.Put(key)
copy(key, t.Key)
copy(key[len(t.Key):], t.clientID[:])
sha1Data := tools.HmacSHA1(key, data)
return sha1Data[:10]
}
func (t *tls12Ticket) getHost() string {
host := t.Param
if len(host) == 0 {
host = t.Host
}
if len(host) > 0 && host[len(host)-1] >= '0' && host[len(host)-1] <= '9' {
host = ""
}
hosts := strings.Split(host, ",")
host = hosts[rand.Intn(len(hosts))]
return host
}

View File

@ -0,0 +1,18 @@
package protocol
import "github.com/Dreamacro/clash/component/ssr/tools"
func init() {
register("auth_aes128_md5", newAuthAES128MD5, 9)
}
func newAuthAES128MD5(b *Base) Protocol {
a := &authAES128{
Base: b,
authData: &authData{},
authAES128Function: &authAES128Function{salt: "auth_aes128_md5", hmac: tools.HmacMD5, hashDigest: tools.MD5Sum},
userData: &userData{},
}
a.initUserData()
return a
}

View File

@ -0,0 +1,275 @@
package protocol
import (
"bytes"
"encoding/binary"
"math"
"math/rand"
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
"github.com/Dreamacro/clash/log"
)
type hmacMethod func(key, data []byte) []byte
type hashDigestMethod func([]byte) []byte
func init() {
register("auth_aes128_sha1", newAuthAES128SHA1, 9)
}
type authAES128Function struct {
salt string
hmac hmacMethod
hashDigest hashDigestMethod
}
type authAES128 struct {
*Base
*authData
*authAES128Function
*userData
iv []byte
hasSentHeader bool
rawTrans bool
packID uint32
recvID uint32
}
func newAuthAES128SHA1(b *Base) Protocol {
a := &authAES128{
Base: b,
authData: &authData{},
authAES128Function: &authAES128Function{salt: "auth_aes128_sha1", hmac: tools.HmacSHA1, hashDigest: tools.SHA1Sum},
userData: &userData{},
}
a.initUserData()
return a
}
func (a *authAES128) initUserData() {
params := strings.Split(a.Param, ":")
if len(params) > 1 {
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
binary.LittleEndian.PutUint32(a.userID[:], uint32(userID))
a.userKey = a.hashDigest([]byte(params[1]))
} else {
log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt)
}
}
if len(a.userKey) == 0 {
a.userKey = a.Key
rand.Read(a.userID[:])
}
}
func (a *authAES128) StreamConn(c net.Conn, iv []byte) net.Conn {
p := &authAES128{
Base: a.Base,
authData: a.next(),
authAES128Function: a.authAES128Function,
userData: a.userData,
packID: 1,
recvID: 1,
}
p.iv = iv
return &Conn{Conn: c, Protocol: p}
}
func (a *authAES128) PacketConn(c net.PacketConn) net.PacketConn {
p := &authAES128{
Base: a.Base,
authAES128Function: a.authAES128Function,
userData: a.userData,
}
return &PacketConn{PacketConn: c, Protocol: p}
}
func (a *authAES128) Decode(dst, src *bytes.Buffer) error {
if a.rawTrans {
dst.ReadFrom(src)
return nil
}
for src.Len() > 4 {
macKey := pool.Get(len(a.userKey) + 4)
defer pool.Put(macKey)
copy(macKey, a.userKey)
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID)
if !bytes.Equal(a.hmac(macKey, src.Bytes()[:2])[:2], src.Bytes()[2:4]) {
src.Reset()
return errAuthAES128MACError
}
length := int(binary.LittleEndian.Uint16(src.Bytes()[:2]))
if length >= 8192 || length < 7 {
a.rawTrans = true
src.Reset()
return errAuthAES128LengthError
}
if length > src.Len() {
break
}
if !bytes.Equal(a.hmac(macKey, src.Bytes()[:length-4])[:4], src.Bytes()[length-4:length]) {
a.rawTrans = true
src.Reset()
return errAuthAES128ChksumError
}
a.recvID++
pos := int(src.Bytes()[4])
if pos < 255 {
pos += 4
} else {
pos = int(binary.LittleEndian.Uint16(src.Bytes()[5:7])) + 4
}
dst.Write(src.Bytes()[pos : length-4])
src.Next(length)
}
return nil
}
func (a *authAES128) Encode(buf *bytes.Buffer, b []byte) error {
fullDataLength := len(b)
if !a.hasSentHeader {
dataLength := getDataLength(b)
a.packAuthData(buf, b[:dataLength])
b = b[dataLength:]
a.hasSentHeader = true
}
for len(b) > 8100 {
a.packData(buf, b[:8100], fullDataLength)
b = b[8100:]
}
if len(b) > 0 {
a.packData(buf, b, fullDataLength)
}
return nil
}
func (a *authAES128) DecodePacket(b []byte) ([]byte, error) {
if !bytes.Equal(a.hmac(a.Key, b[:len(b)-4])[:4], b[len(b)-4:]) {
return nil, errAuthAES128ChksumError
}
return b[:len(b)-4], nil
}
func (a *authAES128) EncodePacket(buf *bytes.Buffer, b []byte) error {
buf.Write(b)
buf.Write(a.userID[:])
buf.Write(a.hmac(a.userKey, buf.Bytes())[:4])
return nil
}
func (a *authAES128) packData(poolBuf *bytes.Buffer, data []byte, fullDataLength int) {
dataLength := len(data)
randDataLength := a.getRandDataLengthForPackData(dataLength, fullDataLength)
/*
2: uint16 LittleEndian packedDataLength
2: hmac of packedDataLength
3: maxRandDataLengthPrefix (min:1)
4: hmac of packedData except the last 4 bytes
*/
packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4
if randDataLength < 128 {
packedDataLength -= 2
}
macKey := pool.Get(len(a.userKey) + 4)
defer pool.Put(macKey)
copy(macKey, a.userKey)
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID)
a.packID++
binary.Write(poolBuf, binary.LittleEndian, uint16(packedDataLength))
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-2:])[:2])
a.packRandData(poolBuf, randDataLength)
poolBuf.Write(data)
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:])[:4])
}
func trapezoidRandom(max int, d float64) int {
base := rand.Float64()
if d-0 > 1e-6 {
a := 1 - d
base = (math.Sqrt(a*a+4*d*base) - a) / (2 * d)
}
return int(base * float64(max))
}
func (a *authAES128) getRandDataLengthForPackData(dataLength, fullDataLength int) int {
if fullDataLength >= 32*1024-a.Overhead {
return 0
}
// 1460: tcp_mss
revLength := 1460 - dataLength - 9
if revLength == 0 {
return 0
}
if revLength < 0 {
if revLength > -1460 {
return trapezoidRandom(revLength+1460, -0.3)
}
return rand.Intn(32)
}
if dataLength > 900 {
return rand.Intn(revLength)
}
return trapezoidRandom(revLength, -0.3)
}
func (a *authAES128) packAuthData(poolBuf *bytes.Buffer, data []byte) {
if len(data) == 0 {
return
}
dataLength := len(data)
randDataLength := a.getRandDataLengthForPackAuthData(dataLength)
/*
7: checkHead(1) and hmac of checkHead(6)
4: userID
16: encrypted data of authdata(12), uint16 BigEndian packedDataLength(2) and uint16 BigEndian randDataLength(2)
4: hmac of userID and encrypted data
4: hmac of packedAuthData except the last 4 bytes
*/
packedAuthDataLength := 7 + 4 + 16 + 4 + randDataLength + dataLength + 4
macKey := pool.Get(len(a.iv) + len(a.Key))
defer pool.Put(macKey)
copy(macKey, a.iv)
copy(macKey[len(a.iv):], a.Key)
poolBuf.WriteByte(byte(rand.Intn(256)))
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes())[:6])
poolBuf.Write(a.userID[:])
err := a.authData.putEncryptedData(poolBuf, a.userKey, [2]int{packedAuthDataLength, randDataLength}, a.salt)
if err != nil {
poolBuf.Reset()
return
}
poolBuf.Write(a.hmac(macKey, poolBuf.Bytes()[7:])[:4])
tools.AppendRandBytes(poolBuf, randDataLength)
poolBuf.Write(data)
poolBuf.Write(a.hmac(a.userKey, poolBuf.Bytes())[:4])
}
func (a *authAES128) getRandDataLengthForPackAuthData(size int) int {
if size > 400 {
return rand.Intn(512)
}
return rand.Intn(1024)
}
func (a *authAES128) packRandData(poolBuf *bytes.Buffer, size int) {
if size < 128 {
poolBuf.WriteByte(byte(size + 1))
tools.AppendRandBytes(poolBuf, size)
return
}
poolBuf.WriteByte(255)
binary.Write(poolBuf, binary.LittleEndian, uint16(size+3))
tools.AppendRandBytes(poolBuf, size)
}

View File

@ -0,0 +1,309 @@
package protocol
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"crypto/rc4"
"encoding/base64"
"encoding/binary"
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/go-shadowsocks2/core"
)
func init() {
register("auth_chain_a", newAuthChainA, 4)
}
type randDataLengthMethod func(int, []byte, *tools.XorShift128Plus) int
type authChainA struct {
*Base
*authData
*userData
iv []byte
salt string
hasSentHeader bool
rawTrans bool
lastClientHash []byte
lastServerHash []byte
encrypter cipher.Stream
decrypter cipher.Stream
randomClient tools.XorShift128Plus
randomServer tools.XorShift128Plus
randDataLength randDataLengthMethod
packID uint32
recvID uint32
}
func newAuthChainA(b *Base) Protocol {
a := &authChainA{
Base: b,
authData: &authData{},
userData: &userData{},
salt: "auth_chain_a",
}
a.initUserData()
return a
}
func (a *authChainA) initUserData() {
params := strings.Split(a.Param, ":")
if len(params) > 1 {
if userID, err := strconv.ParseUint(params[0], 10, 32); err == nil {
binary.LittleEndian.PutUint32(a.userID[:], uint32(userID))
a.userKey = []byte(params[1])
} else {
log.Warnln("Wrong protocol-param for %s, only digits are expected before ':'", a.salt)
}
}
if len(a.userKey) == 0 {
a.userKey = a.Key
rand.Read(a.userID[:])
}
}
func (a *authChainA) StreamConn(c net.Conn, iv []byte) net.Conn {
p := &authChainA{
Base: a.Base,
authData: a.next(),
userData: a.userData,
salt: a.salt,
packID: 1,
recvID: 1,
}
p.iv = iv
p.randDataLength = p.getRandLength
return &Conn{Conn: c, Protocol: p}
}
func (a *authChainA) PacketConn(c net.PacketConn) net.PacketConn {
p := &authChainA{
Base: a.Base,
salt: a.salt,
userData: a.userData,
}
return &PacketConn{PacketConn: c, Protocol: p}
}
func (a *authChainA) Decode(dst, src *bytes.Buffer) error {
if a.rawTrans {
dst.ReadFrom(src)
return nil
}
for src.Len() > 4 {
macKey := pool.Get(len(a.userKey) + 4)
defer pool.Put(macKey)
copy(macKey, a.userKey)
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.recvID)
dataLength := int(binary.LittleEndian.Uint16(src.Bytes()[:2]) ^ binary.LittleEndian.Uint16(a.lastServerHash[14:16]))
randDataLength := a.randDataLength(dataLength, a.lastServerHash, &a.randomServer)
length := dataLength + randDataLength
if length >= 4096 {
a.rawTrans = true
src.Reset()
return errAuthChainLengthError
}
if 4+length > src.Len() {
break
}
serverHash := tools.HmacMD5(macKey, src.Bytes()[:length+2])
if !bytes.Equal(serverHash[:2], src.Bytes()[length+2:length+4]) {
a.rawTrans = true
src.Reset()
return errAuthChainChksumError
}
a.lastServerHash = serverHash
pos := 2
if dataLength > 0 && randDataLength > 0 {
pos += getRandStartPos(randDataLength, &a.randomServer)
}
wantedData := src.Bytes()[pos : pos+dataLength]
a.decrypter.XORKeyStream(wantedData, wantedData)
if a.recvID == 1 {
dst.Write(wantedData[2:])
} else {
dst.Write(wantedData)
}
a.recvID++
src.Next(length + 4)
}
return nil
}
func (a *authChainA) Encode(buf *bytes.Buffer, b []byte) error {
if !a.hasSentHeader {
dataLength := getDataLength(b)
a.packAuthData(buf, b[:dataLength])
b = b[dataLength:]
a.hasSentHeader = true
}
for len(b) > 2800 {
a.packData(buf, b[:2800])
b = b[2800:]
}
if len(b) > 0 {
a.packData(buf, b)
}
return nil
}
func (a *authChainA) DecodePacket(b []byte) ([]byte, error) {
if len(b) < 9 {
return nil, errAuthChainLengthError
}
if !bytes.Equal(tools.HmacMD5(a.userKey, b[:len(b)-1])[:1], b[len(b)-1:]) {
return nil, errAuthChainChksumError
}
md5Data := tools.HmacMD5(a.Key, b[len(b)-8:len(b)-1])
randDataLength := udpGetRandLength(md5Data, &a.randomServer)
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16)
rc4Cipher, err := rc4.NewCipher(key)
if err != nil {
return nil, err
}
wantedData := b[:len(b)-8-randDataLength]
rc4Cipher.XORKeyStream(wantedData, wantedData)
return wantedData, nil
}
func (a *authChainA) EncodePacket(buf *bytes.Buffer, b []byte) error {
authData := pool.Get(3)
defer pool.Put(authData)
rand.Read(authData)
md5Data := tools.HmacMD5(a.Key, authData)
randDataLength := udpGetRandLength(md5Data, &a.randomClient)
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(md5Data), 16)
rc4Cipher, err := rc4.NewCipher(key)
if err != nil {
return err
}
rc4Cipher.XORKeyStream(b, b)
buf.Write(b)
tools.AppendRandBytes(buf, randDataLength)
buf.Write(authData)
binary.Write(buf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(md5Data[:4]))
buf.Write(tools.HmacMD5(a.userKey, buf.Bytes())[:1])
return nil
}
func (a *authChainA) packAuthData(poolBuf *bytes.Buffer, data []byte) {
/*
dataLength := len(data)
12: checkHead(4) and hmac of checkHead(8)
4: uint32 LittleEndian uid (uid = userID ^ last client hash)
16: encrypted data of authdata(12), uint16 LittleEndian overhead(2) and uint16 LittleEndian number zero(2)
4: last server hash(4)
packedAuthDataLength := 12 + 4 + 16 + 4 + dataLength
*/
macKey := pool.Get(len(a.iv) + len(a.Key))
defer pool.Put(macKey)
copy(macKey, a.iv)
copy(macKey[len(a.iv):], a.Key)
// check head
tools.AppendRandBytes(poolBuf, 4)
a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes())
a.initRC4Cipher()
poolBuf.Write(a.lastClientHash[:8])
// uid
binary.Write(poolBuf, binary.LittleEndian, binary.LittleEndian.Uint32(a.userID[:])^binary.LittleEndian.Uint32(a.lastClientHash[8:12]))
// encrypted data
err := a.putEncryptedData(poolBuf, a.userKey, [2]int{a.Overhead, 0}, a.salt)
if err != nil {
poolBuf.Reset()
return
}
// last server hash
a.lastServerHash = tools.HmacMD5(a.userKey, poolBuf.Bytes()[12:])
poolBuf.Write(a.lastServerHash[:4])
// packed data
a.packData(poolBuf, data)
}
func (a *authChainA) packData(poolBuf *bytes.Buffer, data []byte) {
a.encrypter.XORKeyStream(data, data)
macKey := pool.Get(len(a.userKey) + 4)
defer pool.Put(macKey)
copy(macKey, a.userKey)
binary.LittleEndian.PutUint32(macKey[len(a.userKey):], a.packID)
a.packID++
length := uint16(len(data)) ^ binary.LittleEndian.Uint16(a.lastClientHash[14:16])
originalLength := poolBuf.Len()
binary.Write(poolBuf, binary.LittleEndian, length)
a.putMixedRandDataAndData(poolBuf, data)
a.lastClientHash = tools.HmacMD5(macKey, poolBuf.Bytes()[originalLength:])
poolBuf.Write(a.lastClientHash[:2])
}
func (a *authChainA) putMixedRandDataAndData(poolBuf *bytes.Buffer, data []byte) {
randDataLength := a.randDataLength(len(data), a.lastClientHash, &a.randomClient)
if len(data) == 0 {
tools.AppendRandBytes(poolBuf, randDataLength)
return
}
if randDataLength > 0 {
startPos := getRandStartPos(randDataLength, &a.randomClient)
tools.AppendRandBytes(poolBuf, startPos)
poolBuf.Write(data)
tools.AppendRandBytes(poolBuf, randDataLength-startPos)
return
}
poolBuf.Write(data)
}
func getRandStartPos(length int, random *tools.XorShift128Plus) int {
if length == 0 {
return 0
}
return int(random.Next()%8589934609) % length
}
func (a *authChainA) getRandLength(length int, lastHash []byte, random *tools.XorShift128Plus) int {
if length > 1440 {
return 0
}
random.InitFromBinAndLength(lastHash, length)
if length > 1300 {
return int(random.Next() % 31)
}
if length > 900 {
return int(random.Next() % 127)
}
if length > 400 {
return int(random.Next() % 521)
}
return int(random.Next() % 1021)
}
func (a *authChainA) initRC4Cipher() {
key := core.Kdf(base64.StdEncoding.EncodeToString(a.userKey)+base64.StdEncoding.EncodeToString(a.lastClientHash), 16)
a.encrypter, _ = rc4.NewCipher(key)
a.decrypter, _ = rc4.NewCipher(key)
}
func udpGetRandLength(lastHash []byte, random *tools.XorShift128Plus) int {
random.InitFromBin(lastHash)
return int(random.Next() % 127)
}

View File

@ -0,0 +1,97 @@
package protocol
import (
"net"
"sort"
"github.com/Dreamacro/clash/component/ssr/tools"
)
func init() {
register("auth_chain_b", newAuthChainB, 4)
}
type authChainB struct {
*authChainA
dataSizeList []int
dataSizeList2 []int
}
func newAuthChainB(b *Base) Protocol {
a := &authChainB{
authChainA: &authChainA{
Base: b,
authData: &authData{},
userData: &userData{},
salt: "auth_chain_b",
},
}
a.initUserData()
return a
}
func (a *authChainB) StreamConn(c net.Conn, iv []byte) net.Conn {
p := &authChainB{
authChainA: &authChainA{
Base: a.Base,
authData: a.next(),
userData: a.userData,
salt: a.salt,
packID: 1,
recvID: 1,
},
}
p.iv = iv
p.randDataLength = p.getRandLength
p.initDataSize()
return &Conn{Conn: c, Protocol: p}
}
func (a *authChainB) initDataSize() {
a.dataSizeList = a.dataSizeList[:0]
a.dataSizeList2 = a.dataSizeList2[:0]
a.randomServer.InitFromBin(a.Key)
length := a.randomServer.Next()%8 + 4
for ; length > 0; length-- {
a.dataSizeList = append(a.dataSizeList, int(a.randomServer.Next()%2340%2040%1440))
}
sort.Ints(a.dataSizeList)
length = a.randomServer.Next()%16 + 8
for ; length > 0; length-- {
a.dataSizeList2 = append(a.dataSizeList2, int(a.randomServer.Next()%2340%2040%1440))
}
sort.Ints(a.dataSizeList2)
}
func (a *authChainB) getRandLength(length int, lashHash []byte, random *tools.XorShift128Plus) int {
if length >= 1440 {
return 0
}
random.InitFromBinAndLength(lashHash, length)
pos := sort.Search(len(a.dataSizeList), func(i int) bool { return a.dataSizeList[i] >= length+a.Overhead })
finalPos := pos + int(random.Next()%uint64(len(a.dataSizeList)))
if finalPos < len(a.dataSizeList) {
return a.dataSizeList[finalPos] - length - a.Overhead
}
pos = sort.Search(len(a.dataSizeList2), func(i int) bool { return a.dataSizeList2[i] >= length+a.Overhead })
finalPos = pos + int(random.Next()%uint64(len(a.dataSizeList2)))
if finalPos < len(a.dataSizeList2) {
return a.dataSizeList2[finalPos] - length - a.Overhead
}
if finalPos < pos+len(a.dataSizeList2)-1 {
return 0
}
if length > 1300 {
return int(random.Next() % 31)
}
if length > 900 {
return int(random.Next() % 127)
}
if length > 400 {
return int(random.Next() % 521)
}
return int(random.Next() % 1021)
}

View File

@ -0,0 +1,182 @@
package protocol
import (
"bytes"
"encoding/binary"
"hash/adler32"
"hash/crc32"
"math/rand"
"net"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
)
func init() {
register("auth_sha1_v4", newAuthSHA1V4, 7)
}
type authSHA1V4 struct {
*Base
*authData
iv []byte
hasSentHeader bool
rawTrans bool
}
func newAuthSHA1V4(b *Base) Protocol {
return &authSHA1V4{Base: b, authData: &authData{}}
}
func (a *authSHA1V4) StreamConn(c net.Conn, iv []byte) net.Conn {
p := &authSHA1V4{Base: a.Base, authData: a.next()}
p.iv = iv
return &Conn{Conn: c, Protocol: p}
}
func (a *authSHA1V4) PacketConn(c net.PacketConn) net.PacketConn {
return c
}
func (a *authSHA1V4) Decode(dst, src *bytes.Buffer) error {
if a.rawTrans {
dst.ReadFrom(src)
return nil
}
for src.Len() > 4 {
if uint16(crc32.ChecksumIEEE(src.Bytes()[:2])&0xffff) != binary.LittleEndian.Uint16(src.Bytes()[2:4]) {
src.Reset()
return errAuthSHA1V4CRC32Error
}
length := int(binary.BigEndian.Uint16(src.Bytes()[:2]))
if length >= 8192 || length < 7 {
a.rawTrans = true
src.Reset()
return errAuthSHA1V4LengthError
}
if length > src.Len() {
break
}
if adler32.Checksum(src.Bytes()[:length-4]) != binary.LittleEndian.Uint32(src.Bytes()[length-4:length]) {
a.rawTrans = true
src.Reset()
return errAuthSHA1V4Adler32Error
}
pos := int(src.Bytes()[4])
if pos < 255 {
pos += 4
} else {
pos = int(binary.BigEndian.Uint16(src.Bytes()[5:7])) + 4
}
dst.Write(src.Bytes()[pos : length-4])
src.Next(length)
}
return nil
}
func (a *authSHA1V4) Encode(buf *bytes.Buffer, b []byte) error {
if !a.hasSentHeader {
dataLength := getDataLength(b)
a.packAuthData(buf, b[:dataLength])
b = b[dataLength:]
a.hasSentHeader = true
}
for len(b) > 8100 {
a.packData(buf, b[:8100])
b = b[8100:]
}
if len(b) > 0 {
a.packData(buf, b)
}
return nil
}
func (a *authSHA1V4) DecodePacket(b []byte) ([]byte, error) { return b, nil }
func (a *authSHA1V4) EncodePacket(buf *bytes.Buffer, b []byte) error {
buf.Write(b)
return nil
}
func (a *authSHA1V4) packData(poolBuf *bytes.Buffer, data []byte) {
dataLength := len(data)
randDataLength := a.getRandDataLength(dataLength)
/*
2: uint16 BigEndian packedDataLength
2: uint16 LittleEndian crc32Data & 0xffff
3: maxRandDataLengthPrefix (min:1)
4: adler32Data
*/
packedDataLength := 2 + 2 + 3 + randDataLength + dataLength + 4
if randDataLength < 128 {
packedDataLength -= 2
}
binary.Write(poolBuf, binary.BigEndian, uint16(packedDataLength))
binary.Write(poolBuf, binary.LittleEndian, uint16(crc32.ChecksumIEEE(poolBuf.Bytes()[poolBuf.Len()-2:])&0xffff))
a.packRandData(poolBuf, randDataLength)
poolBuf.Write(data)
binary.Write(poolBuf, binary.LittleEndian, adler32.Checksum(poolBuf.Bytes()[poolBuf.Len()-packedDataLength+4:]))
}
func (a *authSHA1V4) packAuthData(poolBuf *bytes.Buffer, data []byte) {
dataLength := len(data)
randDataLength := a.getRandDataLength(12 + dataLength)
/*
2: uint16 BigEndian packedAuthDataLength
4: uint32 LittleEndian crc32Data
3: maxRandDataLengthPrefix (min: 1)
12: authDataLength
10: hmacSHA1DataLength
*/
packedAuthDataLength := 2 + 4 + 3 + randDataLength + 12 + dataLength + 10
if randDataLength < 128 {
packedAuthDataLength -= 2
}
salt := []byte("auth_sha1_v4")
crcData := pool.Get(len(salt) + len(a.Key) + 2)
defer pool.Put(crcData)
binary.BigEndian.PutUint16(crcData, uint16(packedAuthDataLength))
copy(crcData[2:], salt)
copy(crcData[2+len(salt):], a.Key)
key := pool.Get(len(a.iv) + len(a.Key))
defer pool.Put(key)
copy(key, a.iv)
copy(key[len(a.iv):], a.Key)
poolBuf.Write(crcData[:2])
binary.Write(poolBuf, binary.LittleEndian, crc32.ChecksumIEEE(crcData))
a.packRandData(poolBuf, randDataLength)
a.putAuthData(poolBuf)
poolBuf.Write(data)
poolBuf.Write(tools.HmacSHA1(key, poolBuf.Bytes()[poolBuf.Len()-packedAuthDataLength+10:])[:10])
}
func (a *authSHA1V4) packRandData(poolBuf *bytes.Buffer, size int) {
if size < 128 {
poolBuf.WriteByte(byte(size + 1))
tools.AppendRandBytes(poolBuf, size)
return
}
poolBuf.WriteByte(255)
binary.Write(poolBuf, binary.BigEndian, uint16(size+3))
tools.AppendRandBytes(poolBuf, size)
}
func (a *authSHA1V4) getRandDataLength(size int) int {
if size > 1200 {
return 0
}
if size > 400 {
return rand.Intn(256)
}
return rand.Intn(512)
}

View File

@ -0,0 +1,77 @@
package protocol
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/binary"
"math/rand"
"sync"
"time"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/go-shadowsocks2/core"
)
type Base struct {
Key []byte
Overhead int
Param string
}
type userData struct {
userKey []byte
userID [4]byte
}
type authData struct {
clientID [4]byte
connectionID uint32
mutex sync.Mutex
}
func (a *authData) next() *authData {
r := &authData{}
a.mutex.Lock()
defer a.mutex.Unlock()
if a.connectionID > 0xff000000 || a.connectionID == 0 {
rand.Read(a.clientID[:])
a.connectionID = rand.Uint32() & 0xffffff
}
a.connectionID++
copy(r.clientID[:], a.clientID[:])
r.connectionID = a.connectionID
return r
}
func (a *authData) putAuthData(buf *bytes.Buffer) {
binary.Write(buf, binary.LittleEndian, uint32(time.Now().Unix()))
buf.Write(a.clientID[:])
binary.Write(buf, binary.LittleEndian, a.connectionID)
}
func (a *authData) putEncryptedData(b *bytes.Buffer, userKey []byte, paddings [2]int, salt string) error {
encrypt := pool.Get(16)
defer pool.Put(encrypt)
binary.LittleEndian.PutUint32(encrypt, uint32(time.Now().Unix()))
copy(encrypt[4:], a.clientID[:])
binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID)
binary.LittleEndian.PutUint16(encrypt[12:], uint16(paddings[0]))
binary.LittleEndian.PutUint16(encrypt[14:], uint16(paddings[1]))
cipherKey := core.Kdf(base64.StdEncoding.EncodeToString(userKey)+salt, 16)
block, err := aes.NewCipher(cipherKey)
if err != nil {
log.Warnln("New cipher error: %s", err.Error())
return err
}
iv := bytes.Repeat([]byte{0}, 16)
cbcCipher := cipher.NewCBCEncrypter(block, iv)
cbcCipher.CryptBlocks(encrypt, encrypt)
b.Write(encrypt)
return nil
}

View File

@ -0,0 +1,33 @@
package protocol
import (
"bytes"
"net"
)
type origin struct{}
func init() { register("origin", newOrigin, 0) }
func newOrigin(b *Base) Protocol { return &origin{} }
func (o *origin) StreamConn(c net.Conn, iv []byte) net.Conn { return c }
func (o *origin) PacketConn(c net.PacketConn) net.PacketConn { return c }
func (o *origin) Decode(dst, src *bytes.Buffer) error {
dst.ReadFrom(src)
return nil
}
func (o *origin) Encode(buf *bytes.Buffer, b []byte) error {
buf.Write(b)
return nil
}
func (o *origin) DecodePacket(b []byte) ([]byte, error) { return b, nil }
func (o *origin) EncodePacket(buf *bytes.Buffer, b []byte) error {
buf.Write(b)
return nil
}

View File

@ -0,0 +1,38 @@
package protocol
import (
"bytes"
"net"
"github.com/Dreamacro/clash/component/ssr/tools"
)
type PacketConn struct {
net.PacketConn
Protocol
}
func (c *PacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
buf := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(buf)
defer buf.Reset()
err := c.EncodePacket(buf, b)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(buf.Bytes(), addr)
return len(b), err
}
func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, addr, err := c.PacketConn.ReadFrom(b)
if err != nil {
return n, addr, err
}
decoded, err := c.DecodePacket(b[:n])
if err != nil {
return n, addr, err
}
copy(b, decoded)
return len(decoded), addr, nil
}

View File

@ -0,0 +1,76 @@
package protocol
import (
"bytes"
"errors"
"fmt"
"math/rand"
"net"
)
var (
errAuthSHA1V4CRC32Error = errors.New("auth_sha1_v4 decode data wrong crc32")
errAuthSHA1V4LengthError = errors.New("auth_sha1_v4 decode data wrong length")
errAuthSHA1V4Adler32Error = errors.New("auth_sha1_v4 decode data wrong adler32")
errAuthAES128MACError = errors.New("auth_aes128 decode data wrong mac")
errAuthAES128LengthError = errors.New("auth_aes128 decode data wrong length")
errAuthAES128ChksumError = errors.New("auth_aes128 decode data wrong checksum")
errAuthChainLengthError = errors.New("auth_chain decode data wrong length")
errAuthChainChksumError = errors.New("auth_chain decode data wrong checksum")
)
type Protocol interface {
StreamConn(net.Conn, []byte) net.Conn
PacketConn(net.PacketConn) net.PacketConn
Decode(dst, src *bytes.Buffer) error
Encode(buf *bytes.Buffer, b []byte) error
DecodePacket([]byte) ([]byte, error)
EncodePacket(buf *bytes.Buffer, b []byte) error
}
type protocolCreator func(b *Base) Protocol
var protocolList = make(map[string]struct {
overhead int
new protocolCreator
})
func register(name string, c protocolCreator, o int) {
protocolList[name] = struct {
overhead int
new protocolCreator
}{overhead: o, new: c}
}
func PickProtocol(name string, b *Base) (Protocol, error) {
if choice, ok := protocolList[name]; ok {
b.Overhead += choice.overhead
return choice.new(b), nil
}
return nil, fmt.Errorf("protocol %s not supported", name)
}
func getHeadSize(b []byte, defaultValue int) int {
if len(b) < 2 {
return defaultValue
}
headType := b[0] & 7
switch headType {
case 1:
return 7
case 4:
return 19
case 3:
return 4 + int(b[1])
}
return defaultValue
}
func getDataLength(b []byte) int {
bLength := len(b)
dataLength := getHeadSize(b, 30) + rand.Intn(32)
if bLength < dataLength {
return bLength
}
return dataLength
}

View File

@ -0,0 +1,52 @@
package protocol
import (
"bytes"
"net"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/component/ssr/tools"
)
type Conn struct {
net.Conn
Protocol
decoded bytes.Buffer
underDecoded bytes.Buffer
}
func (c *Conn) Read(b []byte) (int, error) {
if c.decoded.Len() > 0 {
return c.decoded.Read(b)
}
buf := pool.Get(pool.RelayBufferSize)
defer pool.Put(buf)
n, err := c.Conn.Read(buf)
if err != nil {
return 0, err
}
c.underDecoded.Write(buf[:n])
err = c.Decode(&c.decoded, &c.underDecoded)
if err != nil {
return 0, err
}
n, _ = c.decoded.Read(b)
return n, nil
}
func (c *Conn) Write(b []byte) (int, error) {
bLength := len(b)
buf := tools.BufPool.Get().(*bytes.Buffer)
defer tools.BufPool.Put(buf)
defer buf.Reset()
err := c.Encode(buf, b)
if err != nil {
return 0, err
}
_, err = c.Conn.Write(buf.Bytes())
if err != nil {
return 0, err
}
return bLength, nil
}

View File

@ -0,0 +1,18 @@
package tools
import (
"bytes"
"math/rand"
"sync"
"github.com/Dreamacro/clash/common/pool"
)
var BufPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}
func AppendRandBytes(b *bytes.Buffer, length int) {
randBytes := pool.Get(length)
defer pool.Put(randBytes)
rand.Read(randBytes)
b.Write(randBytes)
}

View File

@ -0,0 +1,33 @@
package tools
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
)
const HmacSHA1Len = 10
func HmacMD5(key, data []byte) []byte {
hmacMD5 := hmac.New(md5.New, key)
hmacMD5.Write(data)
return hmacMD5.Sum(nil)
}
func HmacSHA1(key, data []byte) []byte {
hmacSHA1 := hmac.New(sha1.New, key)
hmacSHA1.Write(data)
return hmacSHA1.Sum(nil)
}
func MD5Sum(b []byte) []byte {
h := md5.New()
h.Write(b)
return h.Sum(nil)
}
func SHA1Sum(b []byte) []byte {
h := sha1.New()
h.Write(b)
return h.Sum(nil)
}

View File

@ -0,0 +1,57 @@
package tools
import (
"encoding/binary"
"github.com/Dreamacro/clash/common/pool"
)
// XorShift128Plus - a pseudorandom number generator
type XorShift128Plus struct {
s [2]uint64
}
func (r *XorShift128Plus) Next() uint64 {
x := r.s[0]
y := r.s[1]
r.s[0] = y
x ^= x << 23
x ^= y ^ (x >> 17) ^ (y >> 26)
r.s[1] = x
return x + y
}
func (r *XorShift128Plus) InitFromBin(bin []byte) {
var full []byte
if len(bin) < 16 {
full := pool.Get(16)[:0]
defer pool.Put(full)
full = append(full, bin...)
for len(full) < 16 {
full = append(full, 0)
}
} else {
full = bin
}
r.s[0] = binary.LittleEndian.Uint64(full[:8])
r.s[1] = binary.LittleEndian.Uint64(full[8:16])
}
func (r *XorShift128Plus) InitFromBinAndLength(bin []byte, length int) {
var full []byte
if len(bin) < 16 {
full := pool.Get(16)[:0]
defer pool.Put(full)
full = append(full, bin...)
for len(full) < 16 {
full = append(full, 0)
}
}
full = bin
binary.LittleEndian.PutUint16(full, uint16(length))
r.s[0] = binary.LittleEndian.Uint64(full[:8])
r.s[1] = binary.LittleEndian.Uint64(full[8:16])
for i := 0; i < 4; i++ {
r.Next()
}
}

View File

@ -6,9 +6,10 @@ import (
)
const (
wildcard = "*"
dotWildcard = ""
domainStep = "."
wildcard = "*"
dotWildcard = ""
complexWildcard = "+"
domainStep = "."
)
var (
@ -16,9 +17,9 @@ var (
ErrInvalidDomain = errors.New("invalid domain")
)
// Trie contains the main logic for adding and searching nodes for domain segments.
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com)
type Trie struct {
type DomainTrie struct {
root *Node
}
@ -29,7 +30,11 @@ func validAndSplitDomain(domain string) ([]string, bool) {
parts := strings.Split(domain, domainStep)
if len(parts) == 1 {
return nil, false
if parts[0] == "" {
return nil, false
}
return parts, true
}
for _, part := range parts[1:] {
@ -47,12 +52,25 @@ func validAndSplitDomain(domain string) ([]string, bool) {
// 2. *.example.com
// 3. subdomain.*.example.com
// 4. .example.com
func (t *Trie) Insert(domain string, data interface{}) error {
// 5. +.example.com
func (t *DomainTrie) Insert(domain string, data interface{}) error {
parts, valid := validAndSplitDomain(domain)
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-- {
@ -65,7 +83,6 @@ func (t *Trie) Insert(domain string, data interface{}) error {
}
node.Data = data
return nil
}
// Search is the most important part of the Trie.
@ -73,54 +90,46 @@ func (t *Trie) Insert(domain string, data interface{}) error {
// 1. static part
// 2. wildcard domain
// 2. dot wildcard domain
func (t *Trie) Search(domain string) *Node {
func (t *DomainTrie) Search(domain string) *Node {
parts, valid := validAndSplitDomain(domain)
if !valid || parts[0] == "" {
return nil
}
n := t.root
var dotWildcardNode *Node
var wildcardNode *Node
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
n := t.search(t.root, parts)
if node := n.getChild(dotWildcard); node != nil {
dotWildcardNode = node
}
child := n.getChild(part)
if child == nil && wildcardNode != nil {
child = wildcardNode.getChild(part)
}
wildcardNode = n.getChild(wildcard)
n = child
if n == nil {
n = wildcardNode
wildcardNode = nil
}
if n == nil {
break
}
}
if n == nil {
if dotWildcardNode != nil {
return dotWildcardNode
}
return nil
}
if n.Data == nil {
if n == nil || n.Data == nil {
return nil
}
return n
}
// New returns a new, empty Trie.
func New() *Trie {
return &Trie{root: newNode(nil)}
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)}
}

View File

@ -14,6 +14,7 @@ func TestTrie_Basic(t *testing.T) {
domains := []string{
"example.com",
"google.com",
"localhost",
}
for _, domain := range domains {
@ -24,6 +25,9 @@ func TestTrie_Basic(t *testing.T) {
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) {
@ -35,6 +39,11 @@ func TestTrie_Wildcard(t *testing.T) {
".org",
".example.net",
".apple.*",
"+.foo.com",
"+.stun.*.*",
"+.stun.*.*.*",
"+.stun.*.*.*.*",
"stun.l.google.com",
}
for _, domain := range domains {
@ -46,6 +55,9 @@ func TestTrie_Wildcard(t *testing.T) {
assert.NotNil(t, tree.Search("test.org"))
assert.NotNil(t, tree.Search("test.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"))

View File

@ -70,8 +70,8 @@ func (t *Trojan) StreamConn(conn net.Conn) (net.Conn, error) {
func (t *Trojan) WriteHeader(w io.Writer, command Command, socks5Addr []byte) error {
buf := bufPool.Get().(*bytes.Buffer)
defer buf.Reset()
defer bufPool.Put(buf)
defer buf.Reset()
buf.Write(t.hexPassword)
buf.Write(crlf)
@ -92,8 +92,8 @@ func (t *Trojan) PacketConn(conn net.Conn) net.PacketConn {
func writePacket(w io.Writer, socks5Addr, payload []byte) (int, error) {
buf := bufPool.Get().(*bytes.Buffer)
defer buf.Reset()
defer bufPool.Put(buf)
defer buf.Reset()
buf.Write(socks5Addr)
binary.Write(buf, binary.BigEndian, uint16(len(payload)))

View File

@ -86,7 +86,7 @@ func (r *aeadReader) Read(b []byte) (int, error) {
size := int(binary.BigEndian.Uint16(r.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
return 0, errors.New("buffer is larger than standard")
}
buf := pool.Get(size)

View File

@ -47,7 +47,7 @@ func (cr *chunkReader) Read(b []byte) (int, error) {
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
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 {

112
component/vmess/h2.go Normal file
View File

@ -0,0 +1,112 @@
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"},
},
}
// it will be close at : `func (hc *h2Conn) Close() error`
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
}

View File

@ -9,6 +9,7 @@ type TLSConfig struct {
Host string
SkipCertVerify bool
SessionCache tls.ClientSessionCache
NextProtos []string
}
func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
@ -16,6 +17,7 @@ func StreamTLSConn(conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
ServerName: cfg.Host,
InsecureSkipVerify: cfg.SkipCertVerify,
ClientSessionCache: cfg.SessionCache,
NextProtos: cfg.NextProtos,
}
tlsConn := tls.Client(conn, tlsConfig)

View File

@ -1,12 +1,10 @@
package vmess
import (
"crypto/tls"
"fmt"
"math/rand"
"net"
"runtime"
"sync"
"github.com/gofrs/uuid"
)
@ -37,11 +35,6 @@ var CipherMapping = map[string]byte{
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
var (
clientSessionCache tls.ClientSessionCache
once sync.Once
)
// Command types
const (
CommandTCP byte = 1
@ -106,7 +99,7 @@ func NewClient(config Config) (*Client, error) {
security = SecurityAES128GCM
}
default:
return nil, fmt.Errorf("Unknown security type: %s", config.Security)
return nil, fmt.Errorf("unknown security type: %s", config.Security)
}
return &Client{

View File

@ -31,6 +31,7 @@ type WebsocketConfig struct {
Headers http.Header
TLS bool
SkipCertVerify bool
ServerName string
SessionCache tls.ClientSessionCache
}
@ -72,7 +73,7 @@ func (wsc *websocketConn) Close() error {
errors = append(errors, err.Error())
}
if len(errors) > 0 {
return fmt.Errorf("Failed to close connection: %s", strings.Join(errors, ","))
return fmt.Errorf("failed to close connection: %s", strings.Join(errors, ","))
}
return nil
}
@ -132,7 +133,9 @@ func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
ClientSessionCache: c.SessionCache,
}
if host := c.Headers.Get("Host"); host != "" {
if c.ServerName != "" {
dialer.TLSClientConfig.ServerName = c.ServerName
} else if host := c.Headers.Get("Host"); host != "" {
dialer.TLSClientConfig.ServerName = host
}
}
@ -156,7 +159,7 @@ func StreamWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
if resp != nil {
reason = resp.Status
}
return nil, fmt.Errorf("Dial %s error: %s", uri.Host, reason)
return nil, fmt.Errorf("dial %s error: %s", uri.Host, reason)
}
return &websocketConn{

View File

@ -12,8 +12,8 @@ import (
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/component/auth"
trie "github.com/Dreamacro/clash/component/domain-trie"
"github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
@ -25,17 +25,31 @@ import (
// General config
type General struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
Mode T.TunnelMode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"`
ExternalController string `json:"-"`
ExternalUI string `json:"-"`
Secret string `json:"-"`
Inbound
Controller
Mode T.TunnelMode `json:"mode"`
LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"`
Interface string `json:"-"`
}
// Inbound
type Inbound struct {
Port int `json:"port"`
SocksPort int `json:"socks-port"`
RedirPort int `json:"redir-port"`
TProxyPort int `json:"tproxy-port"`
MixedPort int `json:"mixed-port"`
Authentication []string `json:"authentication"`
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
}
// Controller
type Controller struct {
ExternalController string `json:"-"`
ExternalUI string `json:"-"`
Secret string `json:"-"`
}
// DNS config
@ -49,26 +63,31 @@ type DNS struct {
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie
}
// FallbackFilter config
type FallbackFilter struct {
GeoIP bool `yaml:"geoip"`
IPCIDR []*net.IPNet `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
}
// Profile config
type Profile struct {
StoreSelected bool `yaml:"store-selected"`
}
// Experimental config
type Experimental struct {
IgnoreResolveFail bool `yaml:"ignore-resolve-fail"`
Interface string `yaml:"interface-name"`
}
type Experimental struct{}
// Config is clash config manager
type Config struct {
General *General
DNS *DNS
Experimental *Experimental
Hosts *trie.Trie
Hosts *trie.DomainTrie
Profile *Profile
Rules []C.Rule
Users []auth.AuthUser
Proxies map[string]C.Proxy
@ -78,6 +97,7 @@ type Config struct {
type RawDNS struct {
Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"`
UseHosts bool `yaml:"use-hosts"`
NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter"`
@ -91,34 +111,34 @@ type RawDNS struct {
type RawFallbackFilter struct {
GeoIP bool `yaml:"geoip"`
IPCIDR []string `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
}
type RawConfig struct {
Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"`
RedirPort int `yaml:"redir-port"`
TProxyPort int `yaml:"tproxy-port"`
MixedPort int `yaml:"mixed-port"`
Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"`
Mode T.TunnelMode `yaml:"mode"`
LogLevel log.LogLevel `yaml:"log-level"`
IPv6 bool `yaml:"ipv6"`
ExternalController string `yaml:"external-controller"`
ExternalUI string `yaml:"external-ui"`
Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"`
ProxyProvider map[string]map[string]interface{} `yaml:"proxy-providers"`
Hosts map[string]string `yaml:"hosts"`
DNS RawDNS `yaml:"dns"`
Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"`
Proxy []map[string]interface{} `yaml:"proxies"`
ProxyGroup []map[string]interface{} `yaml:"proxy-groups"`
Rule []string `yaml:"rules"`
// remove after 1.0
ProxyProviderOld map[string]map[string]interface{} `yaml:"proxy-provider"`
ProxyOld []map[string]interface{} `yaml:"Proxy"`
ProxyGroupOld []map[string]interface{} `yaml:"Proxy Group"`
RuleOld []string `yaml:"Rule"`
}
// Parse config
@ -132,7 +152,7 @@ func Parse(buf []byte) (*Config, error) {
}
func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
// config with some default value
// config with default value
rawCfg := &RawConfig{
AllowLan: false,
BindAddress: "*",
@ -143,11 +163,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Rule: []string{},
Proxy: []map[string]interface{}{},
ProxyGroup: []map[string]interface{}{},
Experimental: Experimental{
IgnoreResolveFail: true,
},
DNS: RawDNS{
Enable: false,
UseHosts: true,
FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{
GeoIP: true,
@ -158,11 +176,9 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"8.8.8.8",
},
},
// remove after 1.0
RuleOld: []string{},
ProxyOld: []map[string]interface{}{},
ProxyGroupOld: []map[string]interface{}{},
Profile: Profile{
StoreSelected: true,
},
}
if err := yaml.Unmarshal(buf, &rawCfg); err != nil {
@ -176,6 +192,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config := &Config{}
config.Experimental = &rawCfg.Experimental
config.Profile = &rawCfg.Profile
general, err := parseGeneral(rawCfg)
if err != nil {
@ -196,35 +213,27 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.Rules = rules
dnsCfg, err := parseDNS(rawCfg.DNS)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
hosts, err := parseHosts(rawCfg)
if err != nil {
return nil, err
}
config.Hosts = hosts
dnsCfg, err := parseDNS(rawCfg.DNS, hosts)
if err != nil {
return nil, err
}
config.DNS = dnsCfg
config.Users = parseAuthentication(rawCfg.Authentication)
return config, nil
}
func parseGeneral(cfg *RawConfig) (*General, error) {
port := cfg.Port
socksPort := cfg.SocksPort
redirPort := cfg.RedirPort
allowLan := cfg.AllowLan
bindAddress := cfg.BindAddress
externalController := cfg.ExternalController
externalUI := cfg.ExternalUI
secret := cfg.Secret
mode := cfg.Mode
logLevel := cfg.LogLevel
// checkout externalUI exist
if externalUI != "" {
externalUI = C.Path.Resolve(externalUI)
@ -233,19 +242,26 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
}
}
general := &General{
Port: port,
SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
BindAddress: bindAddress,
Mode: mode,
LogLevel: logLevel,
ExternalController: externalController,
ExternalUI: externalUI,
Secret: secret,
}
return general, nil
return &General{
Inbound: Inbound{
Port: cfg.Port,
SocksPort: cfg.SocksPort,
RedirPort: cfg.RedirPort,
TProxyPort: cfg.TProxyPort,
MixedPort: cfg.MixedPort,
AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress,
},
Controller: Controller{
ExternalController: cfg.ExternalController,
ExternalUI: cfg.ExternalUI,
Secret: cfg.Secret,
},
Mode: cfg.Mode,
LogLevel: cfg.LogLevel,
IPv6: cfg.IPv6,
Interface: cfg.Interface,
}, nil
}
func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]provider.ProxyProvider, err error) {
@ -256,18 +272,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
groupsConfig := cfg.ProxyGroup
providersConfig := cfg.ProxyProvider
if len(proxiesConfig) == 0 {
proxiesConfig = cfg.ProxyOld
}
if len(groupsConfig) == 0 {
groupsConfig = cfg.ProxyGroupOld
}
if len(providersConfig) == 0 {
providersConfig = cfg.ProxyProviderOld
}
proxies["DIRECT"] = outbound.NewProxy(outbound.NewDirect())
proxies["REJECT"] = outbound.NewProxy(outbound.NewReject())
proxyList = append(proxyList, "DIRECT", "REJECT")
@ -276,11 +280,11 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for idx, mapping := range proxiesConfig {
proxy, err := outbound.ParseProxy(mapping)
if err != nil {
return nil, nil, fmt.Errorf("Proxy %d: %w", idx, err)
return nil, nil, fmt.Errorf("proxy %d: %w", idx, err)
}
if _, exist := proxies[proxy.Name()]; exist {
return nil, nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
return nil, nil, fmt.Errorf("proxy %s is the duplicate name", proxy.Name())
}
proxies[proxy.Name()] = proxy
proxyList = append(proxyList, proxy.Name())
@ -290,7 +294,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for idx, mapping := range groupsConfig {
groupName, existName := mapping["name"].(string)
if !existName {
return nil, nil, fmt.Errorf("ProxyGroup %d: missing name", idx)
return nil, nil, fmt.Errorf("proxy group %d: missing name", idx)
}
proxyList = append(proxyList, groupName)
}
@ -308,7 +312,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
pd, err := provider.ParseProxyProvider(name, mapping)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("parse proxy provider %s error: %w", name, err)
}
providersMap[name] = pd
@ -317,7 +321,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for _, provider := range providersMap {
log.Infoln("Start initial provider %s", provider.Name())
if err := provider.Initial(); err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("initial proxy provider %s error: %w", provider.Name(), err)
}
}
@ -325,12 +329,12 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for idx, mapping := range groupsConfig {
group, err := outboundgroup.ParseProxyGroup(mapping, proxies, providersMap)
if err != nil {
return nil, nil, fmt.Errorf("ProxyGroup[%d]: %w", idx, err)
return nil, nil, fmt.Errorf("proxy group[%d]: %w", idx, err)
}
groupName := group.Name()
if _, exist := proxies[groupName]; exist {
return nil, nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName)
return nil, nil, fmt.Errorf("proxy group %s: the duplicate name", groupName)
}
proxies[groupName] = outbound.NewProxy(group)
@ -352,25 +356,24 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for _, v := range proxyList {
ps = append(ps, proxies[v])
}
hc := provider.NewHealthCheck(ps, "", 0)
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)
providersMap[provider.ReservedName] = pd
global := outboundgroup.NewSelector("GLOBAL", []provider.ProxyProvider{pd})
global := outboundgroup.NewSelector(
&outboundgroup.GroupCommonOption{
Name: "GLOBAL",
},
[]provider.ProxyProvider{pd},
)
proxies["GLOBAL"] = outbound.NewProxy(global)
return proxies, providersMap, nil
}
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
rules := []C.Rule{}
rulesConfig := cfg.Rule
// remove after 1.0
if len(rulesConfig) == 0 {
rulesConfig = cfg.RuleOld
}
// parse rules
for idx, line := range rulesConfig {
rule := trimArr(strings.Split(line, ","))
@ -391,53 +394,19 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
target = rule[2]
params = rule[3:]
default:
return nil, fmt.Errorf("Rules[%d] [%s] error: format invalid", idx, line)
return nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line)
}
if _, ok := proxies[target]; !ok {
return nil, fmt.Errorf("Rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
return nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
}
rule = trimArr(rule)
params = trimArr(params)
var (
parseErr error
parsed C.Rule
)
switch rule[0] {
case "DOMAIN":
parsed = R.NewDomain(payload, target)
case "DOMAIN-SUFFIX":
parsed = R.NewDomainSuffix(payload, target)
case "DOMAIN-KEYWORD":
parsed = R.NewDomainKeyword(payload, target)
case "GEOIP":
noResolve := R.HasNoResolve(params)
parsed = R.NewGEOIP(payload, target, noResolve)
case "IP-CIDR", "IP-CIDR6":
noResolve := R.HasNoResolve(params)
parsed, parseErr = R.NewIPCIDR(payload, target, R.WithIPCIDRNoResolve(noResolve))
// deprecated when bump to 1.0
case "SOURCE-IP-CIDR":
fallthrough
case "SRC-IP-CIDR":
parsed, parseErr = R.NewIPCIDR(payload, target, R.WithIPCIDRSourceIP(true), R.WithIPCIDRNoResolve(true))
case "SRC-PORT":
parsed, parseErr = R.NewPort(payload, target, true)
case "DST-PORT":
parsed, parseErr = R.NewPort(payload, target, false)
case "MATCH":
fallthrough
// deprecated when bump to 1.0
case "FINAL":
parsed = R.NewMatch(target)
default:
parseErr = fmt.Errorf("unsupported rule type %s", rule[0])
}
parsed, parseErr := R.ParseRule(rule[0], payload, target, params)
if parseErr != nil {
return nil, fmt.Errorf("Rules[%d] [%s] error: %s", idx, line, parseErr.Error())
return nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error())
}
rules = append(rules, parsed)
@ -446,8 +415,14 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, error) {
return rules, nil
}
func parseHosts(cfg *RawConfig) (*trie.Trie, error) {
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) {
tree := trie.New()
// add default hosts
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil {
log.Errorln("insert localhost to host error: %s", err.Error())
}
if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr)
@ -539,9 +514,9 @@ func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
return ipNets, nil
}
func parseDNS(cfg RawDNS) (*DNS, error) {
func parseDNS(cfg RawDNS, hosts *trie.DomainTrie) (*DNS, error) {
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
}
dnsCfg := &DNS{
@ -582,7 +557,7 @@ func parseDNS(cfg RawDNS) (*DNS, error) {
return nil, err
}
var host *trie.Trie
var host *trie.DomainTrie
// fake ip skip host filter
if len(cfg.FakeIPFilter) != 0 {
host = trie.New()
@ -603,6 +578,11 @@ func parseDNS(cfg RawDNS) (*DNS, error) {
if fallbackip, err := parseFallbackIPCIDR(cfg.FallbackFilter.IPCIDR); err == nil {
dnsCfg.FallbackFilter.IPCIDR = fallbackip
}
dnsCfg.FallbackFilter.Domain = cfg.FallbackFilter.Domain
if cfg.UseHosts {
dnsCfg.Hosts = hosts
}
return dnsCfg, nil
}

Some files were not shown because too many files have changed in this diff Show More