Compare commits

..
68 Commits
Author SHA1 Message Date
Dreamacro 3e68faecb2 Improve: add rc4-md5 and chacha20 supported with the fork 2018-09-21 15:27:51 +08:00
Dreamacro eb778ad6e2 Improve: cleanup code 2018-09-21 11:33:29 +08:00
Dreamacro 0caa8e05a3 Improve: better relay copies 2018-09-17 00:15:58 +08:00
Dreamacro 1d0d5667fd Fix: Rule with ipv6 address 2018-09-16 23:35:37 +08:00
Dreamacro 0724d4ea52 Update: README.md 2018-09-16 23:08:04 +08:00
Dreamacro 5d34cba681 Feature: add shadowsocks simple-obfs support 2018-09-16 23:02:32 +08:00
Dreamacro 4082a2c700 Improve: add DOMAIN rule 2018-09-09 15:01:46 +08:00
Dreamacro fcb46e7706 Feature: support vmess tls mode 2018-09-08 19:53:24 +08:00
Dreamacro 834baa9e27 Feature: ✨ add vmess support 2018-09-06 10:53:29 +08:00
Dreamacro af13acc171 Clean: unused code 2018-09-03 23:52:16 +08:00
Dreamacro f2dbabeaa0 Fix: close connection when response closed 2018-08-31 21:24:10 +08:00
Dreamacro f4c51cdb0e Improve: Better handling of TCP connections 2018-08-29 15:00:12 +08:00
Dreamacro a5f2bd3152 Fix: log format type 2018-08-27 08:50:27 +08:00
Dreamacro 613a0c36dd Update: travis ci with golang 1.11 2018-08-27 00:07:57 +08:00
Dreamacro 8ec025b56a Improve: HTTP proxy server handler 2018-08-27 00:06:40 +08:00
Dreamacro 2a2e61652f Fix: updateConfig api crash 2018-08-26 22:43:38 +08:00
Dreamacro ad25a89dd2 Update: README.md 2018-08-14 18:06:56 +08:00
Dreamacro 26618901d4 Fix: log api query 2018-08-12 19:35:48 +08:00
Dreamacro ebe1cee6dc Improve: clean code 2018-08-12 16:18:58 +08:00
Dreamacro fc4f119049 Fix: Makefile 2018-08-12 14:06:50 +08:00
Dreamacro 2b87b907ae New: custom socks5 proxy support 2018-08-12 13:50:54 +08:00
Dreamacro 35e572406b Fix: firefox one socket to process multiple domains 2018-08-12 12:14:59 +08:00
Dreamacro 228d674d93 Fix: import package 2018-08-12 11:06:17 +08:00
Dreamacro 752944d381 Update: README.md 2018-08-12 04:22:14 +08:00
Dreamacro 63308472ad New: redir proxy 2018-08-12 04:00:34 +08:00
Dreamacro 0208e32933 Fix: update proxy config api 2018-08-12 02:23:46 +08:00
Dreamacro 410b272b50 Fix: issue #3 2018-08-11 22:51:30 +08:00
Dreamacro ea424a7694 Add: delay test api for proxies 2018-08-08 11:51:06 +08:00
Dreamacro 36f4ceafa1 Fix: returns the sorted proxy name 2018-08-07 14:45:24 +08:00
Dreamacro 674f4be24d Fix: add JSON Content-Type for stream api to avoid buffer in swift 2018-08-07 14:45:16 +08:00
Dreamacro 63c967b2b3 Fixed: global mode and update log level 2018-08-04 23:04:16 +08:00
Dreamacro a1a58c31ae Improve: lazy load mmdb 2018-08-01 00:18:29 +08:00
Dreamacro 295d649624 Update: Initialize the config file outside of the init function 2018-07-31 17:54:16 +08:00
Dreamacro 7347c28f75 Fix: io copy REJECT error 2018-07-31 17:53:39 +08:00
Dreamacro 8389150318 Improve: config convergent and add log-level 2018-07-26 00:04:59 +08:00
Dreamacro 7357d2d0c2 Add: cors for external controller 2018-07-19 09:31:53 +08:00
Dreamacro d540a0d29f Fix: typo 2018-07-18 21:50:16 +08:00
Dreamacro d55e1b664b Lint: simplify code 2018-07-16 00:47:03 +08:00
fossabot 88c7ccf749 Add: license scan report and status
* Add license scan report and status

Signed-off-by: fossabot <[email protected]>

* Improve: adjust indentation and position
2018-07-15 22:32:20 +08:00
Dreamacro 3cacfb8a7f Update: add config route 2018-07-15 22:23:20 +08:00
Dreamacro 0eef9bbf5d Add: selector and proxys & rules router 2018-07-12 23:28:38 +08:00
Dreamacro 39b45513af Improve: delete useless code and code coverage is now 100% 2018-07-12 18:03:02 +08:00
Dreamacro 283e4e1f4f Update: README.md logo and badges 2018-06-23 00:44:28 +08:00
Dreamacro bd0a1ae1a7 Improve: gofmt -s for shadowsocks.go 2018-06-22 23:56:41 +08:00
Dreamacro 018a6ba041 Improve: add log level 2018-06-22 13:15:20 +08:00
Dreamacro 9c2ace1f91 Fixed: handle chunked data 2018-06-22 10:26:56 +08:00
Dreamacro 17224a32cb Add: add windows release 2018-06-20 23:00:16 +08:00
Dreamacro 05ab653103 Add: config hub route 2018-06-20 22:41:02 +08:00
Dreamacro c2c8f82f96 Fixed: the configuration can now be updated correctly 2018-06-19 20:31:36 +08:00
Dreamacro 330a3391e3 Improve: ci config and Dockerfile & update dependencies 2018-06-18 18:24:11 +08:00
Dreamacro 79e5338113 Add: external controller 2018-06-18 11:31:49 +08:00
Dreamacro bc4ca2818b Add: Record network traffic 2018-06-17 22:41:32 +08:00
Dreamacro 46b8e2eec1 Update: Dockerfile 2018-06-17 10:00:52 +08:00
Dreamacro cd9f496fc9 Add: Dockerfile and Makefile 2018-06-17 08:18:39 +08:00
Dreamacro ef301af04f Update: add url-test intro & add banner 2018-06-16 21:48:23 +08:00
Dreamacro 6ffbb7c89e Add: url-test proxy group support 2018-06-16 21:34:13 +08:00
Dreamacro 961250f998 Fixed: throw error when shadowsocks cipher type wrong 2018-06-16 15:41:26 +08:00
Dreamacro c35b2fd958 Improve: using travis ci 2018-06-15 17:49:14 +08:00
Dreamacro a74856dbf3 Fixed: race condition in update config 2018-06-15 00:49:52 +08:00
Dreamacro 2897ce916a Fixed: crash when net.ResolveIPAddr error 2018-06-15 00:44:31 +08:00
Dreamacro 883ce4fea8 Update: README.md 2018-06-15 00:35:33 +08:00
Dreamacro 47e038cf18 Improve: using native http request 2018-06-14 01:00:58 +08:00
Dreamacro 6e2b9edb45 Fixed: remove proxys and rules then update config 2018-06-13 13:38:46 +08:00
Dreamacro 20a3c231de Fixed: https proxy doesn't works in Safari 2018-06-12 22:43:34 +08:00
Dreamacro 795557a2df Fixed: close connection when read target error 2018-06-12 09:18:15 +08:00
Dreamacro 4f769debe7 Improve: simple dns prefetch 2018-06-11 18:36:39 +08:00
Dreamacro 4f192ef575 Init: first commit 🎉 2018-06-10 22:50:03 +08:00
Dreamacro 8532718345 Initial commit 2018-06-10 22:28:14 +08:00
76 changed files with 4825 additions and 382 deletions

No files matched your search

@@ -1,57 +0,0 @@
---
name: "[English] Bug report"
about: Create a report to help us improve
title: "[BUG] "
labels: ''
assignees: ''
---
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
<!-- Be sure to put a clear title after [BUG] in the text box above -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Device Info (please complete the following information):**
- Device: [e.g. Pixel 4]
- ROM: [e.g: AOSP]
- ROM Version:
- Android Version [e.g. 10]
**Application Info (please complete the following information):**
- Version: [e.g. 1.1.10]
- Apk File Name: [e.g. app-release-arm64-v8a.apk]
- Distribution Channel: [e.g. Google Play]
**Additional context**
Add any other context about the problem here.
**Configure**
If applicable, paste **removed server info** configure
```yaml
# paste here
```
**Logs**
If applicable, paste logs to help detect problem
```
<paste here>
```
@@ -1,24 +0,0 @@
---
name: "[English] Feature request"
about: Suggest an idea for this app
title: "[Feature Request] "
labels: ''
assignees: ''
---
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
<!-- Be sure to put a clear title after [Feature Request] in the text box above -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,55 +0,0 @@
---
name: "[简体中文] 创建错误报告"
about: 创建错误报告以帮助我们改进应用
title: "[BUG] "
labels: ''
assignees: ''
---
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [BUG] 后填入清晰明了的标题 -->
**描述出现的错误**
请简洁的描述你遇到的错误
**如何复现该错误**
复现步骤:
1. ...
2. ...
3. ...
4. ...
**预期行为**
清晰简单的描述你预期的应用应该表现的行为
**屏幕截图**
如果适用, 上传屏幕截图以帮助描述错误
**设备信息 (请完成以下信息):**
- 机型: [例如: Pixel 4]
- 系统/ROM: [例如: MIUI 11]
- Android 版本 [例如: 10]
- ROM版本 [例如: 20.3.19]
**应用信息**
- 版本: [例如: 1.1.10]
- 安装包文件名: [例如: app-release-arm64-v8a.apk]
- 应用来源: [例如: Google Play]
**附加信息**
其他的可能与改错误相关的信息
**配置文件**
如果适用, 在此粘贴 **去除服务器信息的** 的 **配置文件**
```yaml
# 在此粘贴
```
**日志**
如果适用, 粘贴日志以帮助侦测错误
```
<在此粘贴>
```
@@ -1,21 +0,0 @@
---
name: "[简体中文] 功能请求"
about: 你希望的能够在应用中增加的功能
title: "[Feature Request] "
labels: ''
assignees: ''
---
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
**功能描述**
请清晰的描述你想要的功能
**描述你希望的实现方式**
清晰的描述应用应该如何实现该功能
**附加信息**
其他的与改功能相关的附加信息
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
+15
View File
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# dep
vendor
+27
View File
@@ -0,0 +1,27 @@
language: go
sudo: false
go:
- "1.11"
before_install:
- go get -u github.com/golang/dep/cmd/dep
install:
- "$GOPATH/bin/dep ensure"
env:
global:
- NAME=clash
- BINDIR=bin
script:
- go test ./...
before_deploy: make -j releases
deploy:
provider: releases
prerelease: true
skip_cleanup: true
api_key:
secure: dp1tc1h0er7aaAZ1hY0Xk/cUKwB0ifsAjg6e0M/Ad5NC87oucP6ESNFkDu0e9rUS1yB826/VnVGzNE/Z5zdjXVzPft+g5v5oRxzI4BKLhf07t9s+x8Z+3sApTxdsC5BvcN9x+5yRbpDLQ3biDPxSFu86j7m2pkEWw6XYNZO3/5y+RZXX7zu+d4MzTLUaA2kWl7KQAP0tEJNuw9ACDhpkw7LYbU/8q3E76prOTeme5/AT6Gxj7XhKUNP27lazhhqBSWM14ybPANqojNLEfMFHN/Eu2phYO07MuLTd4zuOIuw9y65kgvTFcHRlORjwUhnviXyA69obQejjgDI1WDOtU4PqpFaSLrxWtKI6k5VNWHARYggDm/wKl0WG7F0Kgio1KiGGhDg2yrbseXr/zBNaDhBtTFh6XJffqqwmgby1PXB6PWwfvWXooJMaQiFZczLWeMBl8v6XbSN6jtMTh/PQlKai6BcDd4LM8GQ7VHpSeff4qXEU4Vpnadjgs8VDPOHng6/HV+wDs8q2LrlMbnxLWxbCjOMUB6w7YnSrwH9owzKSoUs/531I4tTCRQIgipJtTK2b881/8osVjdMGS1mDXhBWO+OM0LCAdORJz+kN4PIkXXvKLt6jX74k6z4M3swFaqqtlTduN2Yy/ErsjguQO1VZfHmcpNssmJXI5QB9sxA=
file: bin/*
file_glob: true
on:
repo: Dreamacro/clash
branch: master
tags: true
+19
View File
@@ -0,0 +1,19 @@
FROM golang:latest as builder
RUN wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz -O /tmp/GeoLite2-Country.tar.gz && \
tar zxvf /tmp/GeoLite2-Country.tar.gz -C /tmp && \
cp /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh && \
mkdir -p /go/src/github.com/Dreamacro/clash
WORKDIR /go/src/github.com/Dreamacro/clash
COPY . /go/src/github.com/Dreamacro/clash
RUN dep ensure && \
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash && \
chmod +x /clash
FROM alpine:latest
RUN apk --no-cache add ca-certificates && \
mkdir -p /root/.config/clash
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash .
EXPOSE 7890 7891
ENTRYPOINT ["/clash"]
Generated
+149
View File
@@ -0,0 +1,149 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:d21e998147c1c8cd727bd34148f373d3faa46fd8669c389b53c4eaabbe4eb2b3"
name = "github.com/Dreamacro/go-shadowsocks2"
packages = [
"core",
"shadowaead",
"shadowstream",
"socks",
]
pruneopts = "UT"
revision = "1c1fd6c192eb76261ea3ccd80e3b141b25f20db4"
version = "v0.1.1"
[[projects]]
branch = "master"
digest = "1:8fa55a6e302771a90a86ceae1ca3c0df4ef15d21092198e8313f61dde9eea963"
name = "github.com/Yawning/chacha20"
packages = ["."]
pruneopts = "UT"
revision = "e3b1f968fc6397b51d963fee8ec8711a47bc0ce8"
[[projects]]
digest = "1:444b82bfe35c83bbcaf84e310fb81a1f9ece03edfed586483c869e2c046aef69"
name = "github.com/eapache/queue"
packages = ["."]
pruneopts = "UT"
revision = "44cc805cf13205b55f69e14bcb69867d1ae92f98"
version = "v1.1.0"
[[projects]]
digest = "1:b9914f85d95a0968bafd1be1908ba29e2eafafd88d6fd13696be42bf5368c380"
name = "github.com/go-chi/chi"
packages = ["."]
pruneopts = "UT"
revision = "b5294d10673813fac8558e7f47242bc9e61b4c25"
version = "v3.3.3"
[[projects]]
digest = "1:dfa416a1bb8139f30832543340f972f65c0db9932034cb6a1b42c5ac615a3fb8"
name = "github.com/go-chi/cors"
packages = ["."]
pruneopts = "UT"
revision = "dba6525398619dead495962a916728e7ee2ca322"
version = "v1.0.0"
[[projects]]
digest = "1:54d7b4b9ab2bb2bae35b55eea900bc8fe15cba05a95fc78bf7fd7b82a9a07afa"
name = "github.com/go-chi/render"
packages = ["."]
pruneopts = "UT"
revision = "3215478343fbc559bd3fc08f7031bb134d6bdad5"
version = "v1.0.1"
[[projects]]
digest = "1:ce579162ae1341f3e5ab30c0dce767f28b1eb6a81359aad01723f1ba6b4becdf"
name = "github.com/gofrs/uuid"
packages = ["."]
pruneopts = "UT"
revision = "370558f003bfe29580cd0f698d8640daccdcc45c"
version = "v3.1.1"
[[projects]]
digest = "1:2e8bdbc8a11d716dab1bf66d326285d7e5c92fa4c996c1574ba1153e57534b85"
name = "github.com/oschwald/geoip2-golang"
packages = ["."]
pruneopts = "UT"
revision = "7118115686e16b77967cdbf55d1b944fe14ad312"
version = "v1.2.1"
[[projects]]
digest = "1:07e8589503b7ec22430dae6eed6f2c17e4249ab245574f4fd0ba8fc9c597d138"
name = "github.com/oschwald/maxminddb-golang"
packages = ["."]
pruneopts = "UT"
revision = "c5bec84d1963260297932a1b7a1753c8420717a7"
version = "v1.3.0"
[[projects]]
digest = "1:d867dfa6751c8d7a435821ad3b736310c2ed68945d05b50fb9d23aee0540c8cc"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]]
branch = "master"
digest = "1:d33888518d56c3f0cc9009594f56be4faf33ffff358fe10ff8e7e8cccf0e6617"
name = "golang.org/x/crypto"
packages = [
"chacha20poly1305",
"hkdf",
"internal/chacha20",
"internal/subtle",
"poly1305",
"ssh/terminal",
]
pruneopts = "UT"
revision = "0e37d006457bf46f9e6692014ba72ef82c33022c"
[[projects]]
branch = "master"
digest = "1:ddbafa32d1899456edbf7a64aec7afe5aa287b840e6a12b996f8a8425c1d9a6a"
name = "golang.org/x/sys"
packages = [
"cpu",
"unix",
"windows",
]
pruneopts = "UT"
revision = "d641721ec2dead6fe5ca284096fe4b1fcd49e427"
[[projects]]
digest = "1:975a4480c40f2d0b95e1f83d3ec1aa29a2774e80179e08a9a4ba2aab86721b23"
name = "gopkg.in/eapache/channels.v1"
packages = ["."]
pruneopts = "UT"
revision = "47238d5aae8c0fefd518ef2bee46290909cf8263"
version = "v1.1.0"
[[projects]]
digest = "1:5abd6a22805b1919f6a6bca0ae58b13cef1f3412812f38569978f43ef02743d4"
name = "gopkg.in/ini.v1"
packages = ["."]
pruneopts = "UT"
revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e"
version = "v1.38.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Dreamacro/go-shadowsocks2/core",
"github.com/Dreamacro/go-shadowsocks2/socks",
"github.com/go-chi/chi",
"github.com/go-chi/cors",
"github.com/go-chi/render",
"github.com/gofrs/uuid",
"github.com/oschwald/geoip2-golang",
"github.com/sirupsen/logrus",
"golang.org/x/crypto/chacha20poly1305",
"gopkg.in/eapache/channels.v1",
"gopkg.in/ini.v1",
]
solver-name = "gps-cdcl"
solver-version = 1
+70
View File
@@ -0,0 +1,70 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/Dreamacro/go-shadowsocks2"
version = "0.1.1"
[[constraint]]
name = "github.com/go-chi/chi"
version = "3.3.3"
[[constraint]]
name = "github.com/go-chi/cors"
version = "1.0.0"
[[constraint]]
name = "github.com/go-chi/render"
version = "1.0.1"
[[constraint]]
name = "github.com/gofrs/uuid"
version = "3.1.1"
[[constraint]]
name = "github.com/oschwald/geoip2-golang"
version = "1.2.1"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.6"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
name = "gopkg.in/eapache/channels.v1"
version = "1.1.0"
[[constraint]]
name = "gopkg.in/ini.v1"
version = "1.38.2"
[prune]
go-tests = true
unused-packages = true
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Dreamacro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
NAME=clash
BINDIR=bin
GOBUILD=CGO_ENABLED=0 go build -ldflags '-w -s'
all: linux macos win64
linux:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
macos:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
win64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
releases: linux macos win64
chmod +x $(BINDIR)/$(NAME)-*
gzip $(BINDIR)/$(NAME)-linux
gzip $(BINDIR)/$(NAME)-macos
zip -m -j $(BINDIR)/$(NAME)-win64.zip $(BINDIR)/$(NAME)-win64.exe
clean:
rm $(BINDIR)/*
-160
View File
@@ -1,160 +0,0 @@
3th-party software licenses
* Clash
==========================================================================
closed-source assurance
* Android Open Source Project
* Android X Support Library
==========================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
-50
View File
@@ -1,50 +0,0 @@
## Privacy Policy
The Clash for Android is built as an Open Source software. This app is provided by personal at no cost and is intended for use as is.
This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our app.
If you choose to use our app, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the app. We will not use or share your information with anyone except as described in this Privacy Policy.
The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Clash for Android unless otherwise defined in this Privacy Policy.
**Information Collection and Use**
For a better experience, while using our app, we may require you to provide us with certain personally identifiable information. The information that we request will be retained by us and used as described in this privacy policy.
The app does use third party services that may collect information used to identify you.
Link to privacy policy of third party service providers used by the app
* [Google Play Services](https://www.google.com/policies/privacy/)
* [AppCenter](https://docs.microsoft.com/en-us/appcenter/gdpr/)
**Log Data**
We want to inform you that whenever you use our app, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our App, the time and date of your use of the app, and other statistics.
**Cookies**
Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
This app does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this app.
**Security**
We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
**Links to Other Sites**
This app may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
**Children’s Privacy**
These Services do not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from children under 13\. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions.
**Changes to This Privacy Policy**
We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
**Contact Us**
If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.
+108 -14
View File
@@ -1,33 +1,127 @@
## Clash for Android
<h1 align="center">
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
<br>
Clash
<br>
</h1>
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
<h4 align="center">A rule based proxy in Go.</h4>
<a href="https://play.google.com/store/apps/details?id=com.github.kr328.clash"><img width="200px" alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or [Releases](https://github.com/Kr328/ClashForAndroid/releases)
<p align="center">
<a href="https://travis-ci.org/Dreamacro/clash">
<img src="https://img.shields.io/travis/Dreamacro/clash.svg?style=flat-square"
alt="Travis-CI">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
</p>
### Feature
## Features
Fully feature of [Clash Premium](https://github.com/Dreamacro/clash/releases)
- HTTP/HTTPS and SOCKS proxy
- Surge like configuration
- GeoIP rule support
- Support Vmess/Shadowsocks/Socks5
- Support for Netfilter TCP redirect
## Discussion
### Requirement
[Telegram Group](https://t.me/clash_discuss)
* Android 7.0+
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
## Install
You can build from source:
### 3rd Part License
```sh
go get -u -v github.com/Dreamacro/clash
```
See also [NOTICE](./NOTICE)
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
Requires Go >= 1.10.
## Daemon
### Privacy Policy
Unfortunately, there is no native elegant way to implement golang's daemon.
See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
So we can use third-party daemon tools like pm2, supervisor, and so on.
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
```sh
pm2 start clash
```
### NOTICE
If you have Docker installed, you can run clash directly using `docker-compose`.
**Clash for Android** now uses **closed-source** upstream branches. According to the upstream license agreement, Clash for Android also needs to **close source**.
[Run clash in docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
## Config
Configuration file at `$HOME/.config/clash/config.ini`
Below is a simple demo configuration file:
```ini
[General]
port = 7890
socks-port = 7891
# redir proxy for Linux and macOS
redir-port = 7892
# A RESTful API for clash
external-controller = 127.0.0.1:8080
[Proxy]
# name = ss, server, port, cipher, password(, obfs=tls/http, obfs-host=bing.com)
# The types of cipher are consistent with go-shadowsocks2
# support AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AES-128-CTR AES-192-CTR AES-256-CTR AES-128-CFB AES-192-CFB AES-256-CFB CHACHA20-IETF XCHACHA20
ss1 = ss, server1, port, AEAD_CHACHA20_POLY1305, password
ss2 = ss, server2, port, AEAD_CHACHA20_POLY1305, password
# name = vmess, server, port, uuid, alterId, cipher(, tls=true)
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
vmess1 = vmess, server, port, uuid, 32, auto, tls=true
# name = socks5, server, port
socks = socks5, server, port
[Proxy Group]
# url-test select which proxy will be used by benchmarking speed to a URL.
# name = url-test, [proxies], url, interval(second)
auto = url-test, ss1, ss2, http://www.google.com/generate_204, 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 = select, [proxies]
Proxy = select, ss1, ss2, auto
[Rule]
DOMAIN-SUFFIX,google.com,Proxy
DOMAIN-KEYWORD,google,Proxy
DOMAIN-SUFFIX,ad.com,REJECT
GEOIP,CN,DIRECT
FINAL,,Proxy # note: there is two ","
```
## Thanks
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
## 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
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [ ] UDP support
- [ ] Connection manager
+64
View File
@@ -0,0 +1,64 @@
package adapters
import (
"net"
"net/http"
"strings"
C "github.com/Dreamacro/clash/constant"
)
// HTTPAdapter is a adapter for HTTP connection
type HTTPAdapter struct {
addr *C.Addr
conn net.Conn
R *http.Request
}
// Close HTTP connection
func (h *HTTPAdapter) Close() {
h.conn.Close()
}
// Addr return destination address
func (h *HTTPAdapter) Addr() *C.Addr {
return h.addr
}
// Conn return raw net.Conn of HTTP
func (h *HTTPAdapter) Conn() net.Conn {
return h.conn
}
// NewHTTP is HTTPAdapter generator
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
return &HTTPAdapter{
addr: parseHTTPAddr(request),
R: request,
conn: conn,
}
}
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}
+14
View File
@@ -0,0 +1,14 @@
package adapters
import (
"net"
"net/http"
)
// NewHTTPS is HTTPAdapter generator
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
return &SocketAdapter{
addr: parseHTTPAddr(request),
conn: conn,
}
}
+37
View File
@@ -0,0 +1,37 @@
package adapters
import (
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// SocketAdapter is a adapter for socks and redir connection
type SocketAdapter struct {
conn net.Conn
addr *C.Addr
}
// Close socks and redir connection
func (s *SocketAdapter) Close() {
s.conn.Close()
}
// Addr return destination address
func (s *SocketAdapter) Addr() *C.Addr {
return s.addr
}
// Conn return raw net.Conn
func (s *SocketAdapter) Conn() net.Conn {
return s.conn
}
// NewSocket is SocketAdapter generator
func NewSocket(target socks.Addr, conn net.Conn) *SocketAdapter {
return &SocketAdapter{
conn: conn,
addr: parseSocksAddr(target),
}
}
+71
View File
@@ -0,0 +1,71 @@
package adapters
import (
"net"
"net/http"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parseSocksAddr(target socks.Addr) *C.Addr {
var host, port string
var ip net.IP
switch target[0] {
case socks.AtypDomainName:
host = string(target[2 : 2+target[1]])
port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
ipAddr, err := net.ResolveIPAddr("ip", host)
if err == nil {
ip = ipAddr.IP
}
case socks.AtypIPv4:
ip = net.IP(target[1 : 1+net.IPv4len])
port = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks.AtypIPv6:
ip = net.IP(target[1 : 1+net.IPv6len])
port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return &C.Addr{
NetWork: C.TCP,
AddrType: int(target[0]),
Host: host,
IP: &ip,
Port: port,
}
}
func parseHTTPAddr(request *http.Request) *C.Addr {
host := request.URL.Hostname()
port := request.URL.Port()
if port == "" {
port = "80"
}
ipAddr, err := net.ResolveIPAddr("ip", host)
var resolveIP *net.IP
if err == nil {
resolveIP = &ipAddr.IP
}
var addType int
ip := net.ParseIP(host)
switch {
case ip == nil:
addType = socks.AtypDomainName
case ip.To4() == nil:
addType = socks.AtypIPv6
default:
addType = socks.AtypIPv4
}
return &C.Addr{
NetWork: C.TCP,
AddrType: addType,
Host: host,
IP: resolveIP,
Port: port,
}
}
+45
View File
@@ -0,0 +1,45 @@
package adapters
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
// DirectAdapter is a directly connected adapter
type DirectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (d *DirectAdapter) Close() {
d.conn.Close()
}
// Conn is used to http request
func (d *DirectAdapter) Conn() net.Conn {
return d.conn
}
type Direct struct{}
func (d *Direct) Name() string {
return "Direct"
}
func (d *Direct) Type() C.AdapterType {
return C.Direct
}
func (d *Direct) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", net.JoinHostPort(addr.String(), addr.Port))
if err != nil {
return
}
tcpKeepAlive(c)
return &DirectAdapter{conn: c}, nil
}
func NewDirect() *Direct {
return &Direct{}
}
+69
View File
@@ -0,0 +1,69 @@
package adapters
import (
"io"
"net"
"time"
C "github.com/Dreamacro/clash/constant"
)
// RejectAdapter is a reject connected adapter
type RejectAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (r *RejectAdapter) Close() {}
// Conn is used to http request
func (r *RejectAdapter) Conn() net.Conn {
return r.conn
}
type Reject struct {
}
func (r *Reject) Name() string {
return "Reject"
}
func (r *Reject) Type() C.AdapterType {
return C.Reject
}
func (r *Reject) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return &RejectAdapter{conn: &NopConn{}}, nil
}
func NewReject() *Reject {
return &Reject{}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return len(b), nil
}
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }
+67
View File
@@ -0,0 +1,67 @@
package adapters
import (
"errors"
"sort"
C "github.com/Dreamacro/clash/constant"
)
type Selector struct {
name string
selected C.Proxy
proxies map[string]C.Proxy
}
func (s *Selector) Name() string {
return s.name
}
func (s *Selector) Type() C.AdapterType {
return C.Selector
}
func (s *Selector) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return s.selected.Generator(addr)
}
func (s *Selector) Now() string {
return s.selected.Name()
}
func (s *Selector) All() []string {
var all []string
for k := range s.proxies {
all = append(all, k)
}
sort.Strings(all)
return all
}
func (s *Selector) Set(name string) error {
proxy, exist := s.proxies[name]
if !exist {
return errors.New("Proxy does not exist")
}
s.selected = proxy
return nil
}
func NewSelector(name string, proxies map[string]C.Proxy) (*Selector, error) {
if len(proxies) == 0 {
return nil, errors.New("Provide at least one proxy")
}
mapping := make(map[string]C.Proxy)
var init string
for k, v := range proxies {
mapping[k] = v
init = k
}
s := &Selector{
name: name,
proxies: mapping,
selected: proxies[init],
}
return s, nil
}
+123
View File
@@ -0,0 +1,123 @@
package adapters
import (
"bytes"
"fmt"
"net"
"net/url"
"strconv"
"github.com/Dreamacro/clash/common/simple-obfs"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// ShadowsocksAdapter is a shadowsocks adapter
type ShadowsocksAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *ShadowsocksAdapter) Close() {
ss.conn.Close()
}
func (ss *ShadowsocksAdapter) Conn() net.Conn {
return ss.conn
}
type ShadowSocks struct {
server string
name string
obfs string
obfsHost string
cipher core.Cipher
}
func (ss *ShadowSocks) Name() string {
return ss.name
}
func (ss *ShadowSocks) Type() C.AdapterType {
return C.Shadowsocks
}
func (ss *ShadowSocks) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
}
tcpKeepAlive(c)
switch ss.obfs {
case "tls":
c = obfs.NewTLSObfs(c, ss.obfsHost)
case "http":
_, port, _ := net.SplitHostPort(ss.server)
c = obfs.NewHTTPObfs(c, ss.obfsHost, port)
}
c = ss.cipher.StreamConn(c)
_, err = c.Write(serializesSocksAddr(addr))
return &ShadowsocksAdapter{conn: c}, err
}
func NewShadowSocks(name string, ssURL string, option map[string]string) (*ShadowSocks, error) {
server, cipher, password, _ := parseURL(ssURL)
ciph, err := core.PickCipher(cipher, nil, password)
if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
}
obfs := ""
obfsHost := "bing.com"
if value, ok := option["obfs"]; ok {
obfs = value
}
if value, ok := option["obfs-host"]; ok {
obfsHost = value
}
return &ShadowSocks{
server: server,
name: name,
cipher: ciph,
obfs: obfs,
obfsHost: obfsHost,
}, nil
}
func parseURL(s string) (addr, cipher, password string, err error) {
u, err := url.Parse(s)
if err != nil {
return
}
addr = u.Host
if u.User != nil {
cipher = u.User.Username()
password, _ = u.User.Password()
}
return
}
func serializesSocksAddr(addr *C.Addr) []byte {
var buf [][]byte
aType := uint8(addr.AddrType)
p, _ := strconv.Atoi(addr.Port)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addr.AddrType {
case socks.AtypDomainName:
len := uint8(len(addr.Host))
host := []byte(addr.Host)
buf = [][]byte{{aType, len}, host, port}
case socks.AtypIPv4:
host := addr.IP.To4()
buf = [][]byte{{aType}, host, port}
case socks.AtypIPv6:
host := addr.IP.To16()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
}
+90
View File
@@ -0,0 +1,90 @@
package adapters
import (
"bytes"
"errors"
"fmt"
"io"
"net"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
// Socks5Adapter is a shadowsocks adapter
type Socks5Adapter struct {
conn net.Conn
}
// Close is used to close connection
func (ss *Socks5Adapter) Close() {
ss.conn.Close()
}
func (ss *Socks5Adapter) Conn() net.Conn {
return ss.conn
}
type Socks5 struct {
addr string
name string
}
func (ss *Socks5) Name() string {
return ss.name
}
func (ss *Socks5) Type() C.AdapterType {
return C.Socks5
}
func (ss *Socks5) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.addr)
}
tcpKeepAlive(c)
if err := ss.shakeHand(addr, c); err != nil {
return nil, err
}
return &Socks5Adapter{conn: c}, nil
}
func (ss *Socks5) shakeHand(addr *C.Addr, rw io.ReadWriter) error {
buf := make([]byte, socks.MaxAddrLen)
// VER, CMD, RSV
_, err := rw.Write([]byte{5, 1, 0})
if err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
return err
}
if buf[0] != 5 {
return errors.New("SOCKS version error")
} else if buf[1] != 0 {
return errors.New("SOCKS need auth")
}
// VER, CMD, RSV, ADDR
if _, err := rw.Write(bytes.Join([][]byte{{5, 1, 0}, serializesSocksAddr(addr)}, []byte(""))); err != nil {
return err
}
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
return err
}
return nil
}
func NewSocks5(name, addr string) *Socks5 {
return &Socks5{
addr: addr,
name: name,
}
}
+102
View File
@@ -0,0 +1,102 @@
package adapters
import (
"sync"
"time"
C "github.com/Dreamacro/clash/constant"
)
type URLTest struct {
name string
proxies []C.Proxy
rawURL string
fast C.Proxy
delay time.Duration
done chan struct{}
}
func (u *URLTest) Name() string {
return u.name
}
func (u *URLTest) Type() C.AdapterType {
return C.URLTest
}
func (u *URLTest) Now() string {
return u.fast.Name()
}
func (u *URLTest) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
return u.fast.Generator(addr)
}
func (u *URLTest) Close() {
u.done <- struct{}{}
}
func (u *URLTest) loop() {
tick := time.NewTicker(u.delay)
go u.speedTest()
Loop:
for {
select {
case <-tick.C:
go u.speedTest()
case <-u.done:
break Loop
}
}
}
func (u *URLTest) speedTest() {
wg := sync.WaitGroup{}
wg.Add(len(u.proxies))
c := make(chan interface{})
fast := selectFast(c)
timer := time.NewTimer(u.delay)
for _, p := range u.proxies {
go func(p C.Proxy) {
_, err := DelayTest(p, u.rawURL)
if err == nil {
c <- p
}
wg.Done()
}(p)
}
go func() {
wg.Wait()
close(c)
}()
select {
case <-timer.C:
// Wait for fast to return or close.
<-fast
case p, open := <-fast:
if open {
u.fast = p.(C.Proxy)
}
}
}
func NewURLTest(name string, proxies []C.Proxy, rawURL string, delay time.Duration) (*URLTest, error) {
_, err := urlToAddr(rawURL)
if err != nil {
return nil, err
}
urlTest := &URLTest{
name: name,
proxies: proxies[:],
rawURL: rawURL,
fast: proxies[0],
delay: delay,
done: make(chan struct{}),
}
go urlTest.loop()
return urlTest, nil
}
+93
View File
@@ -0,0 +1,93 @@
package adapters
import (
"fmt"
"net"
"net/http"
"net/url"
"time"
C "github.com/Dreamacro/clash/constant"
)
// DelayTest get the delay for the specified URL
func DelayTest(proxy C.Proxy, url string) (t int16, err error) {
addr, err := urlToAddr(url)
if err != nil {
return
}
start := time.Now()
instance, err := proxy.Generator(&addr)
if err != nil {
return
}
defer instance.Close()
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
return instance.Conn(), nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := http.Client{Transport: transport}
req, err := client.Get(url)
if err != nil {
return
}
req.Body.Close()
t = int16(time.Since(start) / time.Millisecond)
return
}
func urlToAddr(rawURL string) (addr C.Addr, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
if u.Scheme == "https" {
port = "443"
} else if u.Scheme == "http" {
port = "80"
} else {
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Addr{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
IP: nil,
Port: port,
}
return
}
func selectFast(in chan interface{}) chan interface{} {
out := make(chan interface{})
go func() {
p, open := <-in
if open {
out <- p
}
close(out)
for range in {
}
}()
return out
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(3 * time.Minute)
}
}
+95
View File
@@ -0,0 +1,95 @@
package adapters
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/vmess"
C "github.com/Dreamacro/clash/constant"
)
// VmessAdapter is a vmess adapter
type VmessAdapter struct {
conn net.Conn
}
// Close is used to close connection
func (v *VmessAdapter) Close() {
v.conn.Close()
}
func (v *VmessAdapter) Conn() net.Conn {
return v.conn
}
type Vmess struct {
name string
server string
client *vmess.Client
}
func (ss *Vmess) Name() string {
return ss.name
}
func (ss *Vmess) Type() C.AdapterType {
return C.Vmess
}
func (ss *Vmess) Generator(addr *C.Addr) (adapter C.ProxyAdapter, err error) {
c, err := net.Dial("tcp", ss.server)
if err != nil {
return nil, fmt.Errorf("%s connect error", ss.server)
}
tcpKeepAlive(c)
c = ss.client.New(c, parseVmessAddr(addr))
return &VmessAdapter{conn: c}, err
}
func NewVmess(name string, server string, uuid string, alterID uint16, security string, option map[string]string) (*Vmess, error) {
security = strings.ToLower(security)
client, err := vmess.NewClient(vmess.Config{
UUID: uuid,
AlterID: alterID,
Security: security,
TLS: option["tls"] == "true",
})
if err != nil {
return nil, err
}
return &Vmess{
name: name,
server: server,
client: client,
}, nil
}
func parseVmessAddr(info *C.Addr) *vmess.DstAddr {
var addrType byte
var addr []byte
switch info.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], info.IP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], info.IP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(info.Host)+1)
addr[0] = byte(len(info.Host))
copy(addr[1:], []byte(info.Host))
}
port, _ := strconv.Atoi(info.Port)
return &vmess.DstAddr{
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}
+88
View File
@@ -0,0 +1,88 @@
package obfs
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
type HTTPObfs struct {
net.Conn
host string
port string
buf []byte
offset int
firstRequest bool
firstResponse bool
}
func (ho *HTTPObfs) Read(b []byte) (int, error) {
if ho.buf != nil {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
ho.buf = nil
}
return n, nil
}
if ho.firstResponse {
buf := bufPool.Get().([]byte)
n, err := ho.Conn.Read(buf)
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
bufPool.Put(buf[:cap(buf)])
return 0, io.EOF
}
ho.firstResponse = false
length := n - (idx + 4)
n = copy(b, buf[idx+4:n])
if length > n {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
bufPool.Put(buf[:cap(buf)])
}
return n, nil
}
return ho.Conn.Read(b)
}
func (ho *HTTPObfs) Write(b []byte) (int, error) {
if ho.firstRequest {
randBytes := make([]byte, 16)
rand.Read(randBytes)
req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s/", ho.host), bytes.NewBuffer(b[:]))
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.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
req.ContentLength = int64(len(b))
err := req.Write(ho.Conn)
ho.firstRequest = false
return len(b), err
}
return ho.Conn.Write(b)
}
// NewHTTPObfs return a HTTPObfs
func NewHTTPObfs(conn net.Conn, host string, port string) net.Conn {
return &HTTPObfs{
Conn: conn,
firstRequest: true,
firstResponse: true,
host: host,
port: port,
}
}
+190
View File
@@ -0,0 +1,190 @@
package obfs
import (
"bytes"
"encoding/binary"
"io"
"math/rand"
"net"
"sync"
"time"
)
func init() {
rand.Seed(time.Now().Unix())
}
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 2048) }}
// TLSObfs is shadowsocks tls simple-obfs implementation
type TLSObfs struct {
net.Conn
server string
remain int
firstRequest bool
firstResponse bool
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := bufPool.Get().([]byte)
_, err := io.ReadFull(to.Conn, buf[:discardN])
if err != nil {
return 0, err
}
bufPool.Put(buf[:cap(buf)])
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
if err != nil {
return 0, nil
}
length := int(binary.BigEndian.Uint16(sizeBuf))
if length > len(b) {
n, err := to.Conn.Read(b)
if err != nil {
return n, err
}
to.remain = length - n
return n, nil
}
return io.ReadFull(to.Conn, b[:length])
}
func (to *TLSObfs) Read(b []byte) (int, error) {
if to.remain > 0 {
length := to.remain
if length > len(b) {
length = len(b)
}
n, err := io.ReadFull(to.Conn, b[:length])
to.remain -= n
return n, err
}
if to.firstResponse {
// type + ver + lensize + 91 = 96
// type + ver + lensize + 1 = 6
// type + ver = 3
to.firstResponse = false
return to.read(b, 105)
}
// type + ver = 3
return to.read(b, 3)
}
func (to *TLSObfs) Write(b []byte) (int, error) {
if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg)
to.firstRequest = false
return len(b), err
}
size := bufPool.Get().([]byte)
binary.BigEndian.PutUint16(size[:2], uint16(len(b)))
buf := &bytes.Buffer{}
buf.Write([]byte{0x17, 0x03, 0x03})
buf.Write(size[:2])
buf.Write(b)
_, err := to.Conn.Write(buf.Bytes())
bufPool.Put(size[:cap(size)])
return len(b), err
}
// NewTLSObfs return a SimpleObfs
func NewTLSObfs(conn net.Conn, server string) net.Conn {
return &TLSObfs{
Conn: conn,
server: server,
firstRequest: true,
firstResponse: true,
}
}
func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 32)
sessionID := make([]byte, 32)
size := make([]byte, 2)
rand.Read(random)
rand.Read(sessionID)
buf := &bytes.Buffer{}
// handshake, TLS 1.0 version, length
buf.WriteByte(22)
buf.Write([]byte{0x03, 0x01})
length := uint16(212 + len(data) + len(server))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length & 0xff))
// clientHello, length, TLS 1.2 version
buf.WriteByte(1)
binary.BigEndian.PutUint16(size, uint16(208+len(data)+len(server)))
buf.WriteByte(0)
buf.Write(size)
buf.Write([]byte{0x03, 0x03})
// random, sid len, sid
buf.Write(random)
buf.WriteByte(32)
buf.Write(sessionID)
// cipher suites
buf.Write([]byte{0x00, 0x38})
buf.Write([]byte{
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
})
// compression
buf.Write([]byte{0x01, 0x00})
// extension length
binary.BigEndian.PutUint16(size, uint16(79+len(data)+len(server)))
buf.Write(size)
// session ticket
buf.Write([]byte{0x00, 0x23})
binary.BigEndian.PutUint16(size, uint16(len(data)))
buf.Write(size)
buf.Write(data)
// server name
buf.Write([]byte{0x00, 0x00})
binary.BigEndian.PutUint16(size, uint16(len(server)+5))
buf.Write(size)
binary.BigEndian.PutUint16(size, uint16(len(server)+3))
buf.Write(size)
buf.WriteByte(0)
binary.BigEndian.PutUint16(size, uint16(len(server)))
buf.Write(size)
buf.Write([]byte(server))
// ec_point
buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02})
// groups
buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
// signature
buf.Write([]byte{
0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05,
0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01,
0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
})
// encrypt then mac
buf.Write([]byte{0x00, 0x16, 0x00, 0x00})
// extended master secret
buf.Write([]byte{0x00, 0x17, 0x00, 0x00})
return buf.Bytes()
}
+115
View File
@@ -0,0 +1,115 @@
package vmess
import (
"crypto/cipher"
"encoding/binary"
"errors"
"io"
)
type aeadWriter struct {
io.Writer
cipher.AEAD
nonce [32]byte
count uint16
iv []byte
}
func newAEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) *aeadWriter {
return &aeadWriter{Writer: w, AEAD: aead, iv: iv}
}
func (w *aeadWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize - w.Overhead()
if length < readLen {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize-w.Overhead()]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen+w.Overhead()))
binary.BigEndian.PutUint16(w.nonce[:2], w.count)
copy(w.nonce[2:], w.iv[2:12])
w.Seal(payloadBuf[:0], w.nonce[:w.NonceSize()], payloadBuf[:readLen], nil)
w.count++
_, err = w.Writer.Write(buf[:lenSize+readLen+w.Overhead()])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}
type aeadReader struct {
io.Reader
cipher.AEAD
nonce [32]byte
buf []byte
offset int
iv []byte
sizeBuf []byte
count uint16
}
func newAEADReader(r io.Reader, aead cipher.AEAD, iv []byte) *aeadReader {
return &aeadReader{Reader: r, AEAD: aead, iv: iv, sizeBuf: make([]byte, lenSize)}
}
func (r *aeadReader) Read(b []byte) (int, error) {
if r.buf != nil {
n := copy(b, r.buf[r.offset:])
r.offset += n
if r.offset == len(r.buf) {
bufPool.Put(r.buf[:cap(r.buf)])
r.buf = nil
}
return n, nil
}
_, err := io.ReadFull(r.Reader, r.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(r.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(r.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
binary.BigEndian.PutUint16(r.nonce[:2], r.count)
copy(r.nonce[2:], r.iv[2:12])
_, err = r.Open(buf[:0], r.nonce[:r.NonceSize()], buf[:size], nil)
r.count++
if err != nil {
return 0, err
}
realLen := size - r.Overhead()
n := copy(b, buf[:realLen])
if len(b) >= realLen {
bufPool.Put(buf[:cap(buf)])
return n, nil
}
r.offset = n
r.buf = buf[:realLen]
return n, nil
}
+103
View File
@@ -0,0 +1,103 @@
package vmess
import (
"encoding/binary"
"errors"
"io"
"sync"
)
const (
lenSize = 2
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
maxSize = 17 * 1024 // 2 + chunkSize + aead.Overhead()
)
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, maxSize) }}
type chunkReader struct {
io.Reader
buf []byte
sizeBuf []byte
offset int
}
func newChunkReader(reader io.Reader) *chunkReader {
return &chunkReader{Reader: reader, sizeBuf: make([]byte, lenSize)}
}
func newChunkWriter(writer io.WriteCloser) *chunkWriter {
return &chunkWriter{Writer: writer}
}
func (cr *chunkReader) Read(b []byte) (int, error) {
if cr.buf != nil {
n := copy(b, cr.buf[cr.offset:])
cr.offset += n
if cr.offset == len(cr.buf) {
bufPool.Put(cr.buf[:cap(cr.buf)])
cr.buf = nil
}
return n, nil
}
_, err := io.ReadFull(cr.Reader, cr.sizeBuf)
if err != nil {
return 0, err
}
size := int(binary.BigEndian.Uint16(cr.sizeBuf))
if size > maxSize {
return 0, errors.New("Buffer is larger than standard")
}
if len(b) >= size {
_, err := io.ReadFull(cr.Reader, b[:size])
if err != nil {
return 0, err
}
return size, nil
}
buf := bufPool.Get().([]byte)
_, err = io.ReadFull(cr.Reader, buf[:size])
if err != nil {
bufPool.Put(buf[:cap(buf)])
return 0, err
}
n := copy(b, cr.buf[:])
cr.offset = n
cr.buf = buf[:size]
return n, nil
}
type chunkWriter struct {
io.Writer
}
func (cw *chunkWriter) Write(b []byte) (n int, err error) {
buf := bufPool.Get().([]byte)
defer bufPool.Put(buf[:cap(buf)])
length := len(b)
for {
if length == 0 {
break
}
readLen := chunkSize
if length < chunkSize {
readLen = length
}
payloadBuf := buf[lenSize : lenSize+chunkSize]
copy(payloadBuf, b[n:n+readLen])
binary.BigEndian.PutUint16(buf[:lenSize], uint16(readLen))
_, err = cw.Writer.Write(buf[:lenSize+readLen])
if err != nil {
break
}
n += readLen
length -= readLen
}
return
}
+213
View File
@@ -0,0 +1,213 @@
package vmess
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"errors"
"hash/fnv"
"io"
"math/rand"
"net"
"time"
"golang.org/x/crypto/chacha20poly1305"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Conn wrapper a net.Conn with vmess protocol
type Conn struct {
net.Conn
reader io.Reader
writer io.Writer
dst *DstAddr
id *ID
reqBodyIV []byte
reqBodyKey []byte
respBodyIV []byte
respBodyKey []byte
respV byte
security byte
sent bool
received bool
}
func (vc *Conn) Write(b []byte) (int, error) {
if vc.sent {
return vc.writer.Write(b)
}
if err := vc.sendRequest(); err != nil {
return 0, err
}
vc.sent = true
return vc.writer.Write(b)
}
func (vc *Conn) Read(b []byte) (int, error) {
if vc.received {
return vc.reader.Read(b)
}
if err := vc.recvResponse(); err != nil {
return 0, err
}
vc.received = true
return vc.reader.Read(b)
}
func (vc *Conn) sendRequest() error {
timestamp := make([]byte, 8)
binary.BigEndian.PutUint64(timestamp, uint64(time.Now().UTC().Unix()))
h := hmac.New(md5.New, vc.id.UUID.Bytes())
h.Write(timestamp)
_, err := vc.Conn.Write(h.Sum(nil))
if err != nil {
return err
}
buf := &bytes.Buffer{}
// Ver IV Key V Opt
buf.WriteByte(Version)
buf.Write(vc.reqBodyIV[:])
buf.Write(vc.reqBodyKey[:])
buf.WriteByte(vc.respV)
buf.WriteByte(OptionChunkStream)
p := rand.Intn(16)
// P Sec Reserve Cmd
buf.WriteByte(byte(p<<4) | byte(vc.security))
buf.WriteByte(0)
buf.WriteByte(CommandTCP)
// Port AddrType Addr
binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port))
buf.WriteByte(vc.dst.AddrType)
buf.Write(vc.dst.Addr)
// padding
if p > 0 {
padding := make([]byte, p)
rand.Read(padding)
buf.Write(padding)
}
fnv1a := fnv.New32a()
fnv1a.Write(buf.Bytes())
buf.Write(fnv1a.Sum(nil))
block, err := aes.NewCipher(vc.id.CmdKey)
if err != nil {
return err
}
stream := cipher.NewCFBEncrypter(block, hashTimestamp(time.Now().UTC()))
stream.XORKeyStream(buf.Bytes(), buf.Bytes())
_, err = vc.Conn.Write(buf.Bytes())
return err
}
func (vc *Conn) recvResponse() error {
block, err := aes.NewCipher(vc.respBodyKey[:])
if err != nil {
return err
}
stream := cipher.NewCFBDecrypter(block, vc.respBodyIV[:])
buf := make([]byte, 4)
_, err = io.ReadFull(vc.Conn, buf)
if err != nil {
return err
}
stream.XORKeyStream(buf, buf)
if buf[0] != vc.respV {
return errors.New("unexpected response header")
}
if buf[2] != 0 {
return errors.New("dynamic port is not supported now")
}
return nil
}
func hashTimestamp(t time.Time) []byte {
md5hash := md5.New()
ts := make([]byte, 8)
binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix()))
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
md5hash.Write(ts)
return md5hash.Sum(nil)
}
// newConn return a Conn instance
func newConn(conn net.Conn, id *ID, dst *DstAddr, security Security) *Conn {
randBytes := make([]byte, 33)
rand.Read(randBytes)
reqBodyIV := make([]byte, 16)
reqBodyKey := make([]byte, 16)
copy(reqBodyIV[:], randBytes[:16])
copy(reqBodyKey[:], randBytes[16:32])
respV := randBytes[32]
respBodyKey := md5.Sum(reqBodyKey[:])
respBodyIV := md5.Sum(reqBodyIV[:])
var writer io.Writer
var reader io.Reader
switch security {
case SecurityNone:
reader = newChunkReader(conn)
writer = newChunkWriter(conn)
case SecurityAES128GCM:
block, _ := aes.NewCipher(reqBodyKey[:])
aead, _ := cipher.NewGCM(block)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
block, _ = aes.NewCipher(respBodyKey[:])
aead, _ = cipher.NewGCM(block)
reader = newAEADReader(conn, aead, respBodyIV[:])
case SecurityCHACHA20POLY1305:
key := make([]byte, 32)
t := md5.Sum(reqBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ := chacha20poly1305.New(key)
writer = newAEADWriter(conn, aead, reqBodyIV[:])
t = md5.Sum(respBodyKey[:])
copy(key, t[:])
t = md5.Sum(key[:16])
copy(key[16:], t[:])
aead, _ = chacha20poly1305.New(key)
reader = newAEADReader(conn, aead, respBodyIV[:])
}
return &Conn{
Conn: conn,
id: id,
dst: dst,
reqBodyIV: reqBodyIV,
reqBodyKey: reqBodyKey,
respV: respV,
respBodyIV: respBodyIV[:],
respBodyKey: respBodyKey[:],
reader: reader,
writer: writer,
security: security,
}
}
+54
View File
@@ -0,0 +1,54 @@
package vmess
import (
"bytes"
"crypto/md5"
"github.com/gofrs/uuid"
)
// ID cmdKey length
const (
IDBytesLen = 16
)
// The ID of en entity, in the form of a UUID.
type ID struct {
UUID *uuid.UUID
CmdKey []byte
}
// newID returns an ID with given UUID.
func newID(uuid *uuid.UUID) *ID {
id := &ID{UUID: uuid, CmdKey: make([]byte, IDBytesLen)}
md5hash := md5.New()
md5hash.Write(uuid.Bytes())
md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21"))
md5hash.Sum(id.CmdKey[:0])
return id
}
func nextID(u *uuid.UUID) *uuid.UUID {
md5hash := md5.New()
md5hash.Write(u.Bytes())
md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81"))
var newid uuid.UUID
for {
md5hash.Sum(newid[:0])
if !bytes.Equal(newid.Bytes(), u.Bytes()) {
return &newid
}
md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2"))
}
}
func newAlterIDs(primary *ID, alterIDCount uint16) []*ID {
alterIDs := make([]*ID, alterIDCount)
prevID := primary.UUID
for idx := range alterIDs {
newid := nextID(prevID)
alterIDs[idx] = &ID{UUID: newid, CmdKey: primary.CmdKey[:]}
prevID = newid
}
return alterIDs
}
+117
View File
@@ -0,0 +1,117 @@
package vmess
import (
"crypto/tls"
"fmt"
"math/rand"
"net"
"runtime"
"github.com/gofrs/uuid"
)
// Version of vmess
const Version byte = 1
// Request Options
const (
OptionChunkStream byte = 1
OptionChunkMasking byte = 4
)
// Security type vmess
type Security = byte
// Cipher types
const (
SecurityAES128GCM Security = 3
SecurityCHACHA20POLY1305 Security = 4
SecurityNone Security = 5
)
// CipherMapping return
var CipherMapping = map[string]byte{
"none": SecurityNone,
"aes-128-gcm": SecurityAES128GCM,
"chacha20-poly1305": SecurityCHACHA20POLY1305,
}
var tlsConfig = &tls.Config{
InsecureSkipVerify: true,
}
// Command types
const (
CommandTCP byte = 1
CommandUDP byte = 2
)
// Addr types
const (
AtypIPv4 byte = 1
AtypDomainName byte = 2
AtypIPv6 byte = 3
)
// DstAddr store destination address
type DstAddr struct {
AddrType byte
Addr []byte
Port uint
}
// Client is vmess connection generator
type Client struct {
user []*ID
uuid *uuid.UUID
security Security
tls bool
}
// Config of vmess
type Config struct {
UUID string
AlterID uint16
Security string
TLS bool
}
// New return a Conn with net.Conn and DstAddr
func (c *Client) New(conn net.Conn, dst *DstAddr) net.Conn {
r := rand.Intn(len(c.user))
if c.tls {
conn = tls.Client(conn, tlsConfig)
}
return newConn(conn, c.user[r], dst, c.security)
}
// NewClient return Client instance
func NewClient(config Config) (*Client, error) {
uid, err := uuid.FromString(config.UUID)
if err != nil {
return nil, err
}
var security Security
switch config.Security {
case "aes-128-gcm":
security = SecurityAES128GCM
case "chacha20-poly1305":
security = SecurityCHACHA20POLY1305
case "none":
security = SecurityNone
case "auto":
security = SecurityCHACHA20POLY1305
if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" {
security = SecurityAES128GCM
}
default:
return nil, fmt.Errorf("Unknown security type: %s", config.Security)
}
return &Client{
user: newAlterIDs(newID(&uid), config.AlterID),
uuid: &uid,
security: security,
tls: config.TLS,
}, nil
}
+407
View File
@@ -0,0 +1,407 @@
package config
import (
"bufio"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapters/remote"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/observable"
R "github.com/Dreamacro/clash/rules"
log "github.com/sirupsen/logrus"
"gopkg.in/ini.v1"
)
var (
config *Config
once sync.Once
)
// General config
type General struct {
Port int
SocksPort int
RedirPort int
AllowLan bool
Mode Mode
LogLevel C.LogLevel
}
// ProxyConfig is update proxy schema
type ProxyConfig struct {
Port *int
SocksPort *int
RedirPort *int
AllowLan *bool
}
// Config is clash config manager
type Config struct {
general *General
rules []C.Rule
proxies map[string]C.Proxy
lastUpdate time.Time
event chan<- interface{}
reportCh chan interface{}
observable *observable.Observable
}
// Event is event of clash config
type Event struct {
Type string
Payload interface{}
}
// Subscribe config stream
func (c *Config) Subscribe() observable.Subscription {
sub, _ := c.observable.Subscribe()
return sub
}
// Report return a channel for collecting report message
func (c *Config) Report() chan<- interface{} {
return c.reportCh
}
func (c *Config) readConfig() (*ini.File, error) {
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
return nil, err
}
return ini.LoadSources(
ini.LoadOptions{
AllowBooleanKeys: true,
UnparseableSections: []string{"Rule"},
},
C.ConfigPath,
)
}
// Parse config
func (c *Config) Parse() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
if err := c.parseGeneral(cfg); err != nil {
return err
}
if err := c.parseProxies(cfg); err != nil {
return err
}
return c.parseRules(cfg)
}
// Proxies return proxies of clash
func (c *Config) Proxies() map[string]C.Proxy {
return c.proxies
}
// Rules return rules of clash
func (c *Config) Rules() []C.Rule {
return c.rules
}
// SetMode change mode of clash
func (c *Config) SetMode(mode Mode) {
c.general.Mode = mode
c.event <- &Event{Type: "mode", Payload: mode}
}
// SetLogLevel change log level of clash
func (c *Config) SetLogLevel(level C.LogLevel) {
c.general.LogLevel = level
c.event <- &Event{Type: "log-level", Payload: level}
}
// General return clash general config
func (c *Config) General() General {
return *c.general
}
// UpdateRules is a function for hot reload rules
func (c *Config) UpdateRules() error {
cfg, err := c.readConfig()
if err != nil {
return err
}
return c.parseRules(cfg)
}
func (c *Config) parseGeneral(cfg *ini.File) error {
general := cfg.Section("General")
port := general.Key("port").RangeInt(0, 1, 65535)
socksPort := general.Key("socks-port").RangeInt(0, 1, 65535)
redirPort := general.Key("redir-port").RangeInt(0, 1, 65535)
allowLan := general.Key("allow-lan").MustBool()
logLevelString := general.Key("log-level").MustString(C.INFO.String())
modeString := general.Key("mode").MustString(Rule.String())
mode, exist := ModeMapping[modeString]
if !exist {
return fmt.Errorf("General.mode value invalid")
}
logLevel, exist := C.LogLevelMapping[logLevelString]
if !exist {
return fmt.Errorf("General.log-level value invalid")
}
c.general = &General{
Port: port,
SocksPort: socksPort,
RedirPort: redirPort,
AllowLan: allowLan,
Mode: mode,
LogLevel: logLevel,
}
if restAddr := general.Key("external-controller").String(); restAddr != "" {
c.event <- &Event{Type: "external-controller", Payload: restAddr}
}
c.UpdateGeneral(*c.general)
return nil
}
// UpdateGeneral dispatch update event
func (c *Config) UpdateGeneral(general General) {
c.UpdateProxy(ProxyConfig{
Port: &general.Port,
SocksPort: &general.SocksPort,
RedirPort: &general.RedirPort,
AllowLan: &general.AllowLan,
})
c.event <- &Event{Type: "mode", Payload: general.Mode}
c.event <- &Event{Type: "log-level", Payload: general.LogLevel}
}
// UpdateProxy dispatch update proxy event
func (c *Config) UpdateProxy(pc ProxyConfig) {
if pc.AllowLan != nil {
c.general.AllowLan = *pc.AllowLan
}
c.general.Port = *or(pc.Port, &c.general.Port)
if c.general.Port != 0 && (pc.AllowLan != nil || pc.Port != nil) {
c.event <- &Event{Type: "http-addr", Payload: genAddr(c.general.Port, c.general.AllowLan)}
}
c.general.SocksPort = *or(pc.SocksPort, &c.general.SocksPort)
if c.general.SocksPort != 0 && (pc.AllowLan != nil || pc.SocksPort != nil) {
c.event <- &Event{Type: "socks-addr", Payload: genAddr(c.general.SocksPort, c.general.AllowLan)}
}
c.general.RedirPort = *or(pc.RedirPort, &c.general.RedirPort)
if c.general.RedirPort != 0 && (pc.AllowLan != nil || pc.RedirPort != nil) {
c.event <- &Event{Type: "redir-addr", Payload: genAddr(c.general.RedirPort, c.general.AllowLan)}
}
}
func (c *Config) parseProxies(cfg *ini.File) error {
proxies := make(map[string]C.Proxy)
proxiesConfig := cfg.Section("Proxy")
groupsConfig := cfg.Section("Proxy Group")
// parse proxy
for _, key := range proxiesConfig.Keys() {
proxy := key.Strings(",")
if len(proxy) == 0 {
continue
}
switch proxy[0] {
// ss, server, port, cipter, password
case "ss":
if len(proxy) < 5 {
continue
}
ssURL := url.URL{
Scheme: "ss",
User: url.UserPassword(proxy[3], proxy[4]),
Host: fmt.Sprintf("%s:%s", proxy[1], proxy[2]),
}
option := parseOptions(5, proxy...)
ss, err := adapters.NewShadowSocks(key.Name(), ssURL.String(), option)
if err != nil {
return err
}
proxies[key.Name()] = ss
// socks5, server, port
case "socks5":
if len(proxy) < 3 {
continue
}
addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2])
socks5 := adapters.NewSocks5(key.Name(), addr)
proxies[key.Name()] = socks5
// vmess, server, port, uuid, alterId, security
case "vmess":
if len(proxy) < 6 {
continue
}
addr := fmt.Sprintf("%s:%s", proxy[1], proxy[2])
alterID, err := strconv.Atoi(proxy[4])
if err != nil {
return err
}
option := parseOptions(6, proxy...)
vmess, err := adapters.NewVmess(key.Name(), addr, proxy[3], uint16(alterID), proxy[5], option)
if err != nil {
return err
}
proxies[key.Name()] = vmess
}
}
// parse proxy group
for _, key := range groupsConfig.Keys() {
rule := strings.Split(key.Value(), ",")
rule = trimArr(rule)
switch rule[0] {
case "url-test":
if len(rule) < 4 {
return fmt.Errorf("URLTest need more than 4 param")
}
proxyNames := rule[1 : len(rule)-2]
delay, _ := strconv.Atoi(rule[len(rule)-1])
url := rule[len(rule)-2]
var ps []C.Proxy
for _, name := range proxyNames {
if p, ok := proxies[name]; ok {
ps = append(ps, p)
}
}
adapter, err := adapters.NewURLTest(key.Name(), ps, url, time.Duration(delay)*time.Second)
if err != nil {
return fmt.Errorf("Config error: %s", err.Error())
}
proxies[key.Name()] = adapter
case "select":
if len(rule) < 2 {
return fmt.Errorf("Selector need more than 2 param")
}
proxyNames := rule[1:]
selectProxy := make(map[string]C.Proxy)
for _, name := range proxyNames {
proxy, exist := proxies[name]
if !exist {
return fmt.Errorf("Proxy %s not exist", name)
}
selectProxy[name] = proxy
}
selector, err := adapters.NewSelector(key.Name(), selectProxy)
if err != nil {
return fmt.Errorf("Selector create error: %s", err.Error())
}
proxies[key.Name()] = selector
}
}
// init proxy
proxies["GLOBAL"], _ = adapters.NewSelector("GLOBAL", proxies)
proxies["DIRECT"] = adapters.NewDirect()
proxies["REJECT"] = adapters.NewReject()
c.proxies = proxies
c.event <- &Event{Type: "proxies", Payload: proxies}
return nil
}
func (c *Config) parseRules(cfg *ini.File) error {
rules := []C.Rule{}
rulesConfig := cfg.Section("Rule")
// parse rules
reader := bufio.NewReader(strings.NewReader(rulesConfig.Body()))
for {
line, _, err := reader.ReadLine()
if err != nil {
break
}
rule := strings.Split(string(line), ",")
if len(rule) < 3 {
continue
}
rule = trimArr(rule)
switch rule[0] {
case "DOMAIN":
rules = append(rules, R.NewDomain(rule[1], rule[2]))
case "DOMAIN-SUFFIX":
rules = append(rules, R.NewDomainSuffix(rule[1], rule[2]))
case "DOMAIN-KEYWORD":
rules = append(rules, R.NewDomainKeyword(rule[1], rule[2]))
case "GEOIP":
rules = append(rules, R.NewGEOIP(rule[1], rule[2]))
case "IP-CIDR", "IP-CIDR6":
rules = append(rules, R.NewIPCIDR(rule[1], rule[2]))
case "FINAL":
rules = append(rules, R.NewFinal(rule[2]))
}
}
c.rules = rules
c.event <- &Event{Type: "rules", Payload: rules}
return nil
}
func (c *Config) handleResponseMessage() {
for elm := range c.reportCh {
event := elm.(*Event)
switch event.Type {
case "http-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening HTTP proxy at %d error", c.general.Port)
c.general.Port = 0
}
case "socks-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening SOCKS proxy at %d error", c.general.SocksPort)
c.general.SocksPort = 0
}
case "redir-addr":
if event.Payload.(bool) == false {
log.Errorf("Listening Redir proxy at %d error", c.general.RedirPort)
c.general.RedirPort = 0
}
}
}
}
func newConfig() *Config {
event := make(chan interface{})
reportCh := make(chan interface{})
config := &Config{
general: &General{},
proxies: make(map[string]C.Proxy),
rules: []C.Rule{},
lastUpdate: time.Now(),
event: event,
reportCh: reportCh,
observable: observable.NewObservable(event),
}
go config.handleResponseMessage()
return config
}
// Instance return singleton instance of Config
func Instance() *Config {
once.Do(func() {
config = newConfig()
})
return config
}
+72
View File
@@ -0,0 +1,72 @@
package config
import (
"archive/tar"
"compress/gzip"
"io"
"net/http"
"os"
"strings"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
func downloadMMDB(path string) (err error) {
resp, err := http.Get("http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz")
if err != nil {
return
}
defer resp.Body.Close()
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return
}
defer gr.Close()
tr := tar.NewReader(gr)
for {
h, err := tr.Next()
if err == io.EOF {
break
} else if err != nil {
return err
}
if !strings.HasSuffix(h.Name, "GeoLite2-Country.mmdb") {
continue
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
}
return nil
}
// Init prepare necessary files
func Init() {
// initial config.ini
if _, err := os.Stat(C.ConfigPath); os.IsNotExist(err) {
log.Info("Can't find config, create a empty file")
os.OpenFile(C.ConfigPath, os.O_CREATE|os.O_WRONLY, 0644)
}
// initial mmdb
if _, err := os.Stat(C.MMDBPath); os.IsNotExist(err) {
log.Info("Can't find MMDB, start download")
err := downloadMMDB(C.MMDBPath)
if err != nil {
log.Fatalf("Can't download MMDB: %s", err.Error())
}
}
}
+31
View File
@@ -0,0 +1,31 @@
package config
type Mode int
var (
// ModeMapping is a mapping for Mode enum
ModeMapping = map[string]Mode{
"Global": Global,
"Rule": Rule,
"Direct": Direct,
}
)
const (
Global Mode = iota
Rule
Direct
)
func (m Mode) String() string {
switch m {
case Global:
return "Global"
case Rule:
return "Rule"
case Direct:
return "Direct"
default:
return "Unknow"
}
}
+46
View File
@@ -0,0 +1,46 @@
package config
import (
"fmt"
"strings"
)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
func genAddr(port int, allowLan bool) string {
if allowLan {
return fmt.Sprintf(":%d", port)
}
return fmt.Sprintf("127.0.0.1:%d", port)
}
func or(pointers ...*int) *int {
for _, p := range pointers {
if p != nil {
return p
}
}
return pointers[len(pointers)-1]
}
func parseOptions(startIdx int, params ...string) map[string]string {
mapping := make(map[string]string)
if len(params) <= startIdx {
return mapping
}
for _, option := range params[startIdx:] {
pair := strings.SplitN(option, "=", 2)
if len(pair) != 2 {
continue
}
mapping[strings.Trim(pair[0], " ")] = strings.Trim(pair[1], " ")
}
return mapping
}
+56
View File
@@ -0,0 +1,56 @@
package constant
import (
"net"
)
// Adapter Type
const (
Direct AdapterType = iota
Reject
Selector
Shadowsocks
Socks5
URLTest
Vmess
)
type ProxyAdapter interface {
Conn() net.Conn
Close()
}
type ServerAdapter interface {
Addr() *Addr
Close()
}
type Proxy interface {
Name() string
Type() AdapterType
Generator(addr *Addr) (ProxyAdapter, error)
}
// AdapterType is enum of adapter type
type AdapterType int
func (at AdapterType) String() string {
switch at {
case Direct:
return "Direct"
case Reject:
return "Reject"
case Selector:
return "Selector"
case Shadowsocks:
return "Shadowsocks"
case Socks5:
return "Socks5"
case URLTest:
return "URLTest"
case Vmess:
return "Vmess"
default:
return "Unknow"
}
}
+46
View File
@@ -0,0 +1,46 @@
package constant
import (
"net"
)
// Socks addr type
const (
AtypIPv4 = 1
AtypDomainName = 3
AtypIPv6 = 4
TCP NetWork = iota
UDP
HTTP SourceType = iota
SOCKS
)
type NetWork int
func (n *NetWork) String() string {
if *n == TCP {
return "tcp"
}
return "udp"
}
type SourceType int
// Addr is used to store connection address
type Addr struct {
NetWork NetWork
Source SourceType
AddrType int
Host string
IP *net.IP
Port string
}
func (addr *Addr) String() string {
if addr.Host == "" {
return addr.IP.String()
}
return addr.Host
}
+51
View File
@@ -0,0 +1,51 @@
package constant
import (
"os"
"os/user"
"path"
log "github.com/sirupsen/logrus"
)
const (
Name = "clash"
)
var (
HomeDir string
ConfigPath string
MMDBPath string
)
type General struct {
Mode *string `json:"mode,omitempty"`
AllowLan *bool `json:"allow-lan,omitempty"`
Port *int `json:"port,omitempty"`
SocksPort *int `json:"socks-port,omitempty"`
RedirPort *int `json:"redir-port,omitempty"`
LogLevel *string `json:"log-level,omitempty"`
}
func init() {
currentUser, err := user.Current()
if err != nil {
dir := os.Getenv("HOME")
if dir == "" {
log.Fatalf("Can't get current user: %s", err.Error())
}
HomeDir = dir
} else {
HomeDir = currentUser.HomeDir
}
dirPath := path.Join(HomeDir, ".config", Name)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
if err := os.MkdirAll(dirPath, 0777); err != nil {
log.Fatalf("Can't create config directory %s: %s", dirPath, err.Error())
}
}
ConfigPath = path.Join(dirPath, "config.ini")
MMDBPath = path.Join(dirPath, "Country.mmdb")
}
+35
View File
@@ -0,0 +1,35 @@
package constant
var (
// LogLevelMapping is a mapping for LogLevel enum
LogLevelMapping = map[string]LogLevel{
"error": ERROR,
"warning": WARNING,
"info": INFO,
"debug": DEBUG,
}
)
const (
ERROR LogLevel = iota
WARNING
INFO
DEBUG
)
type LogLevel int
func (l LogLevel) String() string {
switch l {
case INFO:
return "info"
case WARNING:
return "warning"
case ERROR:
return "error"
case DEBUG:
return "debug"
default:
return "unknow"
}
}
+7
View File
@@ -0,0 +1,7 @@
package constant
// ProxySignal is used to handle graceful shutdown of proxy
type ProxySignal struct {
Done chan<- struct{}
Closed <-chan struct{}
}
+39
View File
@@ -0,0 +1,39 @@
package constant
// Rule Type
const (
Domain RuleType = iota
DomainSuffix
DomainKeyword
GEOIP
IPCIDR
FINAL
)
type RuleType int
func (rt RuleType) String() string {
switch rt {
case Domain:
return "Domain"
case DomainSuffix:
return "DomainSuffix"
case DomainKeyword:
return "DomainKeyword"
case GEOIP:
return "GEOIP"
case IPCIDR:
return "IPCIDR"
case FINAL:
return "FINAL"
default:
return "Unknow"
}
}
type Rule interface {
RuleType() RuleType
IsMatch(addr *Addr) bool
Adapter() string
Payload() string
}
+55
View File
@@ -0,0 +1,55 @@
package constant
import (
"time"
)
type Traffic struct {
up chan int64
down chan int64
upCount int64
downCount int64
upTotal int64
downTotal int64
interval time.Duration
}
func (t *Traffic) Up() chan<- int64 {
return t.up
}
func (t *Traffic) Down() chan<- int64 {
return t.down
}
func (t *Traffic) Now() (up int64, down int64) {
return t.upTotal, t.downTotal
}
func (t *Traffic) handle() {
go t.handleCh(t.up, &t.upCount, &t.upTotal)
go t.handleCh(t.down, &t.downCount, &t.downTotal)
}
func (t *Traffic) handleCh(ch <-chan int64, count *int64, total *int64) {
ticker := time.NewTicker(t.interval)
for {
select {
case n := <-ch:
*count += n
case <-ticker.C:
*total = *count
*count = 0
}
}
}
func NewTraffic(interval time.Duration) *Traffic {
t := &Traffic{
up: make(chan int64),
down: make(chan int64),
interval: interval,
}
go t.handle()
return t
}
Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+33
View File
@@ -0,0 +1,33 @@
package hub
import (
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/proxy"
T "github.com/Dreamacro/clash/tunnel"
)
var (
tunnel = T.Instance()
cfg = config.Instance()
listener = proxy.Instance()
)
type Error struct {
Error string `json:"error"`
}
type Errors struct {
Errors map[string]string `json:"errors"`
}
func formatErrors(errorsMap map[string]error) (bool, Errors) {
errors := make(map[string]string)
hasError := false
for key, err := range errorsMap {
if err != nil {
errors[key] = err.Error()
hasError = true
}
}
return hasError, Errors{Errors: errors}
}
+96
View File
@@ -0,0 +1,96 @@
package hub
import (
"fmt"
"net/http"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func configRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs)
r.Put("/", updateConfigs)
return r
}
type configSchema struct {
Port int `json:"port"`
SocksPort int `json:"socket-port"`
RedirPort int `json:"redir-port"`
AllowLan bool `json:"allow-lan"`
Mode string `json:"mode"`
LogLevel string `json:"log-level"`
}
func getConfigs(w http.ResponseWriter, r *http.Request) {
general := cfg.General()
render.JSON(w, r, configSchema{
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
AllowLan: general.AllowLan,
Mode: general.Mode.String(),
LogLevel: general.LogLevel.String(),
})
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
general := &C.General{}
err := render.DecodeJSON(r.Body, general)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
// update errors
var modeErr, logLevelErr error
// update mode
if general.Mode != nil {
mode, ok := config.ModeMapping[*general.Mode]
if !ok {
modeErr = fmt.Errorf("Mode error")
} else {
cfg.SetMode(mode)
}
}
// update log-level
if general.LogLevel != nil {
level, ok := C.LogLevelMapping[*general.LogLevel]
if !ok {
logLevelErr = fmt.Errorf("Log Level error")
} else {
cfg.SetLogLevel(level)
}
}
hasError, errors := formatErrors(map[string]error{
"mode": modeErr,
"log-level": logLevelErr,
})
if hasError {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, errors)
return
}
// update proxy
cfg.UpdateProxy(config.ProxyConfig{
AllowLan: general.AllowLan,
Port: general.Port,
SocksPort: general.SocksPort,
RedirPort: general.RedirPort,
})
w.WriteHeader(http.StatusNoContent)
}
+193
View File
@@ -0,0 +1,193 @@
package hub
import (
"fmt"
"net/http"
"strconv"
"time"
A "github.com/Dreamacro/clash/adapters/remote"
C "github.com/Dreamacro/clash/constant"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.Get("/{name}", getProxy)
r.Get("/{name}/delay", getProxyDelay)
r.Put("/{name}", updateProxy)
return r
}
type SampleProxy struct {
Type string `json:"type"`
}
type Selector struct {
Type string `json:"type"`
Now string `json:"now"`
All []string `json:"all"`
}
type URLTest struct {
Type string `json:"type"`
Now string `json:"now"`
}
func transformProxy(proxy C.Proxy) interface{} {
t := proxy.Type()
switch t {
case C.Selector:
selector := proxy.(*A.Selector)
return Selector{
Type: t.String(),
Now: selector.Now(),
All: selector.All(),
}
case C.URLTest:
return URLTest{
Type: t.String(),
Now: proxy.(*A.URLTest).Now(),
}
default:
return SampleProxy{
Type: proxy.Type().String(),
}
}
}
type GetProxiesResponse struct {
Proxies map[string]interface{} `json:"proxies"`
}
func getProxies(w http.ResponseWriter, r *http.Request) {
rawProxies := cfg.Proxies()
proxies := make(map[string]interface{})
for name, proxy := range rawProxies {
proxies[name] = transformProxy(proxy)
}
render.JSON(w, r, GetProxiesResponse{Proxies: proxies})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
render.JSON(w, r, transformProxy(proxy))
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
selector, ok := proxy.(*A.Selector)
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Proxy can't update",
})
return
}
if err := selector.Set(req.Name); err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: fmt.Sprintf("Selector update error: %s", err.Error()),
})
return
}
w.WriteHeader(http.StatusNoContent)
}
type GetProxyDelayRequest struct {
URL string `json:"url"`
Timeout int16 `json:"timeout"`
}
type GetProxyDelayResponse struct {
Delay int16 `json:"delay"`
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Format error",
})
return
}
name := chi.URLParam(r, "name")
proxies := cfg.Proxies()
proxy, exist := proxies[name]
if !exist {
w.WriteHeader(http.StatusNotFound)
render.JSON(w, r, Error{
Error: "Proxy not found",
})
return
}
sigCh := make(chan int16)
go func() {
t, err := A.DelayTest(proxy, url)
if err != nil {
sigCh <- 0
}
sigCh <- t
}()
select {
case <-time.After(time.Millisecond * time.Duration(timeout)):
w.WriteHeader(http.StatusRequestTimeout)
render.JSON(w, r, Error{
Error: "Proxy delay test timeout",
})
case t := <-sigCh:
if t == 0 {
w.WriteHeader(http.StatusServiceUnavailable)
render.JSON(w, r, Error{
Error: "An error occurred in the delay test",
})
} else {
render.JSON(w, r, GetProxyDelayResponse{
Delay: t,
})
}
}
}
+53
View File
@@ -0,0 +1,53 @@
package hub
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
func ruleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRules)
r.Put("/", updateRules)
return r
}
type Rule struct {
Name string `json:"name"`
Payload string `json:"type"`
}
type GetRulesResponse struct {
Rules []Rule `json:"rules"`
}
func getRules(w http.ResponseWriter, r *http.Request) {
rawRules := cfg.Rules()
var rules []Rule
for _, rule := range rawRules {
rules = append(rules, Rule{
Name: rule.RuleType().String(),
Payload: rule.Payload(),
})
}
w.WriteHeader(http.StatusOK)
render.JSON(w, r, GetRulesResponse{
Rules: rules,
})
}
func updateRules(w http.ResponseWriter, r *http.Request) {
err := cfg.UpdateRules()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
w.WriteHeader(http.StatusNoContent)
}
+137
View File
@@ -0,0 +1,137 @@
package hub
import (
"encoding/json"
"net/http"
"time"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
T "github.com/Dreamacro/clash/tunnel"
"github.com/go-chi/chi"
"github.com/go-chi/cors"
"github.com/go-chi/render"
log "github.com/sirupsen/logrus"
)
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func newHub(signal chan struct{}) {
var addr string
ch := config.Instance().Subscribe()
signal <- struct{}{}
for {
elm := <-ch
event := elm.(*config.Event)
if event.Type == "external-controller" {
addr = event.Payload.(string)
break
}
}
r := chi.NewRouter()
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type"},
MaxAge: 300,
})
r.Use(cors.Handler)
r.With(jsonContentType).Get("/traffic", traffic)
r.With(jsonContentType).Get("/logs", getLogs)
r.Mount("/configs", configRouter())
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter())
log.Infof("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
if err != nil {
log.Errorf("External controller error: %s", err.Error())
}
}
func jsonContentType(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func traffic(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
tick := time.NewTicker(time.Second)
t := tunnel.Traffic()
for range tick.C {
up, down := t.Now()
if err := json.NewEncoder(w).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
levelText = "info"
}
level, ok := C.LogLevelMapping[levelText]
if !ok {
w.WriteHeader(http.StatusBadRequest)
render.JSON(w, r, Error{
Error: "Level error",
})
return
}
src := tunnel.Log()
sub, err := src.Subscribe()
defer src.UnSubscribe(sub)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
render.JSON(w, r, Error{
Error: err.Error(),
})
return
}
render.Status(r, http.StatusOK)
for elm := range sub {
log := elm.(T.Log)
if log.LogLevel > level {
continue
}
if err := json.NewEncoder(w).Encode(Log{
Type: log.Type(),
Payload: log.Payload,
}); err != nil {
break
}
w.(http.Flusher).Flush()
}
}
// Run initial hub
func Run() {
signal := make(chan struct{})
go newHub(signal)
<-signal
}
+30
View File
@@ -0,0 +1,30 @@
package main
import (
"os"
"os/signal"
"syscall"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub"
"github.com/Dreamacro/clash/proxy"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
func main() {
tunnel.Instance().Run()
proxy.Instance().Run()
hub.Run()
config.Init()
err := config.Instance().Parse()
if err != nil {
log.Fatalf("Parse config error: %s", err.Error())
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
}
+3
View File
@@ -0,0 +1,3 @@
package observable
type Iterable <-chan interface{}
+67
View File
@@ -0,0 +1,67 @@
package observable
import (
"errors"
"sync"
)
type Observable struct {
iterable Iterable
listener *sync.Map
done bool
doneLock sync.RWMutex
}
func (o *Observable) process() {
for item := range o.iterable {
o.listener.Range(func(key, value interface{}) bool {
elm := value.(*Subscriber)
elm.Emit(item)
return true
})
}
o.close()
}
func (o *Observable) close() {
o.doneLock.Lock()
o.done = true
o.doneLock.Unlock()
o.listener.Range(func(key, value interface{}) bool {
elm := value.(*Subscriber)
elm.Close()
return true
})
}
func (o *Observable) Subscribe() (Subscription, error) {
o.doneLock.RLock()
done := o.done
o.doneLock.RUnlock()
if done == true {
return nil, errors.New("Observable is closed")
}
subscriber := newSubscriber()
o.listener.Store(subscriber.Out(), subscriber)
return subscriber.Out(), nil
}
func (o *Observable) UnSubscribe(sub Subscription) {
elm, exist := o.listener.Load(sub)
if !exist {
return
}
subscriber := elm.(*Subscriber)
o.listener.Delete(subscriber.Out())
subscriber.Close()
}
func NewObservable(any Iterable) *Observable {
observable := &Observable{
iterable: any,
listener: &sync.Map{},
}
go observable.process()
return observable
}
+124
View File
@@ -0,0 +1,124 @@
package observable
import (
"runtime"
"sync"
"testing"
"time"
)
func iterator(item []interface{}) chan interface{} {
ch := make(chan interface{})
go func() {
time.Sleep(100 * time.Millisecond)
for _, elm := range item {
ch <- elm
}
close(ch)
}()
return ch
}
func TestObservable(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
if err != nil {
t.Error(err)
}
count := 0
for range data {
count = count + 1
}
if count != 5 {
t.Error("Revc number error")
}
}
func TestObservable_MutilSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
count := 0
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
for range ch {
count = count + 1
}
wg.Done()
}
go waitCh(ch1)
go waitCh(ch2)
wg.Wait()
if count != 10 {
t.Error("Revc number error")
}
}
func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
if err != nil {
t.Error(err)
}
src.UnSubscribe(data)
_, open := <-data
if open {
t.Error("Revc number error")
}
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
_, closed := src.Subscribe()
if closed == nil {
t.Error("Observable should be closed")
}
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{}))
iter := iterator([]interface{}{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
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
var list []Subscription
for i := 0; i < max; i++ {
ch, _ := src.Subscribe()
list = append(list, ch)
}
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan interface{}) {
for range ch {
}
wg.Done()
}
for _, ch := range list {
go waitCh(ch)
}
wg.Wait()
now := runtime.NumGoroutine()
if init != now {
t.Errorf("Goroutine Leak: init %d now %d", init, now)
}
}
+35
View File
@@ -0,0 +1,35 @@
package observable
import (
"sync"
"gopkg.in/eapache/channels.v1"
)
type Subscription <-chan interface{}
type Subscriber struct {
buffer *channels.InfiniteChannel
once sync.Once
}
func (s *Subscriber) Emit(item interface{}) {
s.buffer.In() <- item
}
func (s *Subscriber) Out() Subscription {
return s.buffer.Out()
}
func (s *Subscriber) Close() {
s.once.Do(func() {
s.buffer.Close()
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: channels.NewInfiniteChannel(),
}
return sub
}
+74
View File
@@ -0,0 +1,74 @@
package http
import (
"bufio"
"net"
"net/http"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewHttpProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("HTTP proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleConn(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleConn(conn net.Conn) {
br := bufio.NewReader(conn)
request, err := http.ReadRequest(br)
if err != nil {
conn.Close()
return
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return
}
tun.Add(adapters.NewHTTPS(request, conn))
return
}
tun.Add(adapters.NewHTTP(request, conn))
}
+116
View File
@@ -0,0 +1,116 @@
package proxy
import (
"sync"
"github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/proxy/http"
"github.com/Dreamacro/clash/proxy/redir"
"github.com/Dreamacro/clash/proxy/socks"
)
var (
listener *Listener
once sync.Once
)
type Listener struct {
// signal for update
httpSignal *C.ProxySignal
socksSignal *C.ProxySignal
redirSignal *C.ProxySignal
}
func (l *Listener) updateHTTP(addr string) error {
if l.httpSignal != nil {
signal := l.httpSignal
signal.Done <- struct{}{}
<-signal.Closed
l.httpSignal = nil
}
signal, err := http.NewHttpProxy(addr)
if err != nil {
return err
}
l.httpSignal = signal
return nil
}
func (l *Listener) updateSocks(addr string) error {
if l.socksSignal != nil {
signal := l.socksSignal
signal.Done <- struct{}{}
<-signal.Closed
l.socksSignal = nil
}
signal, err := socks.NewSocksProxy(addr)
if err != nil {
return err
}
l.socksSignal = signal
return nil
}
func (l *Listener) updateRedir(addr string) error {
if l.redirSignal != nil {
signal := l.redirSignal
signal.Done <- struct{}{}
<-signal.Closed
l.redirSignal = nil
}
signal, err := redir.NewRedirProxy(addr)
if err != nil {
return err
}
l.redirSignal = signal
return nil
}
func (l *Listener) process(signal chan<- struct{}) {
sub := config.Instance().Subscribe()
signal <- struct{}{}
reportCH := config.Instance().Report()
for elm := range sub {
event := elm.(*config.Event)
switch event.Type {
case "http-addr":
addr := event.Payload.(string)
err := l.updateHTTP(addr)
reportCH <- &config.Event{Type: "http-addr", Payload: err == nil}
case "socks-addr":
addr := event.Payload.(string)
err := l.updateSocks(addr)
reportCH <- &config.Event{Type: "socks-addr", Payload: err == nil}
case "redir-addr":
addr := event.Payload.(string)
err := l.updateRedir(addr)
reportCH <- &config.Event{Type: "redir-addr", Payload: err == nil}
}
}
}
// Run ensure config monitoring
func (l *Listener) Run() {
signal := make(chan struct{})
go l.process(signal)
<-signal
}
func newListener() *Listener {
return &Listener{}
}
// Instance return singleton instance of Listener
func Instance() *Listener {
once.Do(func() {
listener = newListener()
})
return listener
}
+62
View File
@@ -0,0 +1,62 @@
package redir
import (
"net"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewRedirProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("Redir proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleRedir(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleRedir(conn net.Conn) {
target, err := parserPacket(conn)
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
}
+58
View File
@@ -0,0 +1,58 @@
package redir
import (
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parserPacket(c net.Conn) (socks.Addr, error) {
const (
PfInout = 0
PfIn = 1
PfOut = 2
IOCOut = 0x40000000
IOCIn = 0x80000000
IOCInOut = IOCIn | IOCOut
IOCPARMMask = 0x1FFF
LEN = 4*16 + 4*4 + 4*1
// #define _IOC(inout,group,num,len) (inout | ((len & IOCPARMMask) << 16) | ((group) << 8) | (num))
// #define _IOWR(g,n,t) _IOC(IOCInOut, (g), (n), sizeof(t))
// #define DIOCNATLOOK _IOWR('D', 23, struct pfioc_natlook)
DIOCNATLOOK = IOCInOut | ((LEN & IOCPARMMask) << 16) | ('D' << 8) | 23
)
fd, err := syscall.Open("/dev/pf", 0, syscall.O_RDONLY)
if err != nil {
return nil, err
}
defer syscall.Close(fd)
nl := struct { // struct pfioc_natlook
saddr, daddr, rsaddr, rdaddr [16]byte
sxport, dxport, rsxport, rdxport [4]byte
af, proto, protoVariant, direction uint8
}{
af: syscall.AF_INET,
proto: syscall.IPPROTO_TCP,
direction: PfOut,
}
saddr := c.RemoteAddr().(*net.TCPAddr)
daddr := c.LocalAddr().(*net.TCPAddr)
copy(nl.saddr[:], saddr.IP)
copy(nl.daddr[:], daddr.IP)
nl.sxport[0], nl.sxport[1] = byte(saddr.Port>>8), byte(saddr.Port)
nl.dxport[0], nl.dxport[1] = byte(daddr.Port>>8), byte(daddr.Port)
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), DIOCNATLOOK, uintptr(unsafe.Pointer(&nl))); errno != 0 {
return nil, errno
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], nl.rdaddr[:4])
copy(addr[1+net.IPv4len:], nl.rdxport[:2])
return addr, nil
}
+51
View File
@@ -0,0 +1,51 @@
package redir
import (
"errors"
"net"
"syscall"
"unsafe"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
const (
SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv4.h
IP6T_SO_ORIGINAL_DST = 80 // from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
c, ok := conn.(*net.TCPConn)
if !ok {
return nil, errors.New("only work with TCP connection")
}
rc, err := c.SyscallConn()
if err != nil {
return nil, err
}
var addr socks.Addr
rc.Control(func(fd uintptr) {
addr, err = getorigdst(fd)
})
return addr, err
}
// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c
func getorigdst(fd uintptr) (socks.Addr, error) {
raw := syscall.RawSockaddrInet4{}
siz := unsafe.Sizeof(raw)
if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil {
return nil, err
}
addr := make([]byte, 1+net.IPv4len+2)
addr[0] = socks.AtypIPv4
copy(addr[1:1+net.IPv4len], raw.Addr[:])
port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian
addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1]
return addr, nil
}
+17
View File
@@ -0,0 +1,17 @@
package redir
import (
"syscall"
"unsafe"
)
const GETSOCKOPT = 15 // https://golang.org/src/syscall/syscall_linux_386.go#L183
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
var a [6]uintptr
a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5
if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 {
return errno
}
return nil
}
+14
View File
@@ -0,0 +1,14 @@
// +build linux,!386
package redir
import "syscall"
const GETSOCKOPT = syscall.SYS_GETSOCKOPT
func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error {
if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 {
return errno
}
return nil
}
+12
View File
@@ -0,0 +1,12 @@
package redir
import (
"errors"
"net"
"github.com/Dreamacro/go-shadowsocks2/socks"
)
func parserPacket(conn net.Conn) (socks.Addr, error) {
return nil, errors.New("Windows not support yet")
}
+63
View File
@@ -0,0 +1,63 @@
package socks
import (
"net"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
"github.com/Dreamacro/go-shadowsocks2/socks"
log "github.com/sirupsen/logrus"
)
var (
tun = tunnel.Instance()
)
func NewSocksProxy(addr string) (*C.ProxySignal, error) {
l, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
done := make(chan struct{})
closed := make(chan struct{})
signal := &C.ProxySignal{
Done: done,
Closed: closed,
}
go func() {
log.Infof("SOCKS proxy listening at: %s", addr)
for {
c, err := l.Accept()
if err != nil {
if _, open := <-done; !open {
break
}
continue
}
go handleSocks(c)
}
}()
go func() {
<-done
close(done)
l.Close()
closed <- struct{}{}
}()
return signal, nil
}
func handleSocks(conn net.Conn) {
target, err := socks.Handshake(conn)
if err != nil {
conn.Close()
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
tun.Add(adapters.NewSocket(target, conn))
}
+1
View File
@@ -0,0 +1 @@
package socks
+39
View File
@@ -0,0 +1,39 @@
package rules
import (
"strings"
C "github.com/Dreamacro/clash/constant"
)
type DomainKeyword struct {
keyword string
adapter string
}
func (dk *DomainKeyword) RuleType() C.RuleType {
return C.DomainKeyword
}
func (dk *DomainKeyword) IsMatch(addr *C.Addr) bool {
if addr.AddrType != C.AtypDomainName {
return false
}
domain := addr.Host
return strings.Contains(domain, dk.keyword)
}
func (dk *DomainKeyword) Adapter() string {
return dk.adapter
}
func (dk *DomainKeyword) Payload() string {
return dk.keyword
}
func NewDomainKeyword(keyword string, adapter string) *DomainKeyword {
return &DomainKeyword{
keyword: keyword,
adapter: adapter,
}
}
+39
View File
@@ -0,0 +1,39 @@
package rules
import (
"strings"
C "github.com/Dreamacro/clash/constant"
)
type DomainSuffix struct {
suffix string
adapter string
}
func (ds *DomainSuffix) RuleType() C.RuleType {
return C.DomainSuffix
}
func (ds *DomainSuffix) IsMatch(addr *C.Addr) bool {
if addr.AddrType != C.AtypDomainName {
return false
}
domain := addr.Host
return strings.HasSuffix(domain, "."+ds.suffix) || domain == ds.suffix
}
func (ds *DomainSuffix) Adapter() string {
return ds.adapter
}
func (ds *DomainSuffix) Payload() string {
return ds.suffix
}
func NewDomainSuffix(suffix string, adapter string) *DomainSuffix {
return &DomainSuffix{
suffix: suffix,
adapter: adapter,
}
}
+36
View File
@@ -0,0 +1,36 @@
package rules
import (
C "github.com/Dreamacro/clash/constant"
)
type Domain struct {
domain string
adapter string
}
func (d *Domain) RuleType() C.RuleType {
return C.Domain
}
func (d *Domain) IsMatch(addr *C.Addr) bool {
if addr.AddrType != C.AtypDomainName {
return false
}
return addr.Host == d.domain
}
func (d *Domain) Adapter() string {
return d.adapter
}
func (d *Domain) Payload() string {
return d.domain
}
func NewDomain(domain string, adapter string) *Domain {
return &Domain{
domain: domain,
adapter: adapter,
}
}
+31
View File
@@ -0,0 +1,31 @@
package rules
import (
C "github.com/Dreamacro/clash/constant"
)
type Final struct {
adapter string
}
func (f *Final) RuleType() C.RuleType {
return C.FINAL
}
func (f *Final) IsMatch(addr *C.Addr) bool {
return true
}
func (f *Final) Adapter() string {
return f.adapter
}
func (f *Final) Payload() string {
return ""
}
func NewFinal(adapter string) *Final {
return &Final{
adapter: adapter,
}
}
+54
View File
@@ -0,0 +1,54 @@
package rules
import (
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
log "github.com/sirupsen/logrus"
)
var (
mmdb *geoip2.Reader
once sync.Once
)
type GEOIP struct {
country string
adapter string
}
func (g *GEOIP) RuleType() C.RuleType {
return C.GEOIP
}
func (g *GEOIP) IsMatch(addr *C.Addr) bool {
if addr.IP == nil {
return false
}
record, _ := mmdb.Country(*addr.IP)
return record.Country.IsoCode == g.country
}
func (g *GEOIP) Adapter() string {
return g.adapter
}
func (g *GEOIP) Payload() string {
return g.country
}
func NewGEOIP(country string, adapter string) *GEOIP {
once.Do(func() {
var err error
mmdb, err = geoip2.Open(C.MMDBPath)
if err != nil {
log.Fatalf("Can't load mmdb: %s", err.Error())
}
})
return &GEOIP{
country: country,
adapter: adapter,
}
}
+42
View File
@@ -0,0 +1,42 @@
package rules
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
type IPCIDR struct {
ipnet *net.IPNet
adapter string
}
func (i *IPCIDR) RuleType() C.RuleType {
return C.IPCIDR
}
func (i *IPCIDR) IsMatch(addr *C.Addr) bool {
if addr.IP == nil {
return false
}
return i.ipnet.Contains(*addr.IP)
}
func (i *IPCIDR) Adapter() string {
return i.adapter
}
func (i *IPCIDR) Payload() string {
return i.ipnet.String()
}
func NewIPCIDR(s string, adapter string) *IPCIDR {
_, ipnet, err := net.ParseCIDR(s)
if err != nil {
}
return &IPCIDR{
ipnet: ipnet,
adapter: adapter,
}
}
+77
View File
@@ -0,0 +1,77 @@
package tunnel
import (
"bufio"
"io"
"net"
"net/http"
"time"
"github.com/Dreamacro/clash/adapters/local"
C "github.com/Dreamacro/clash/constant"
)
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
req := request.R
host := req.Host
for {
req.Header.Set("Connection", "close")
req.RequestURI = ""
adapters.RemoveHopByHopHeaders(req.Header)
err := req.Write(conn)
if err != nil {
break
}
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, req)
if err != nil {
break
}
adapters.RemoveHopByHopHeaders(resp.Header)
if resp.ContentLength >= 0 {
resp.Header.Set("Proxy-Connection", "keep-alive")
resp.Header.Set("Connection", "keep-alive")
resp.Header.Set("Keep-Alive", "timeout=4")
resp.Close = false
} else {
resp.Close = true
}
err = resp.Write(request.Conn())
if err != nil || resp.Close {
break
}
req, err = http.ReadRequest(bufio.NewReader(request.Conn()))
if err != nil {
break
}
// Sometimes firefox just open a socket to process multiple domains in HTTP
// The temporary solution is close connection when encountering different HOST
if req.Host != host {
break
}
}
}
func (t *Tunnel) handleSOCKS(request *adapters.SocketAdapter, proxy C.ProxyAdapter) {
conn := newTrafficTrack(proxy.Conn(), t.traffic)
relay(request.Conn(), conn)
}
// relay copies between left and right bidirectionally.
func relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
go func() {
_, err := io.Copy(leftConn, rightConn)
leftConn.SetReadDeadline(time.Now())
ch <- err
}()
io.Copy(rightConn, leftConn)
rightConn.SetReadDeadline(time.Now())
<-ch
}
+51
View File
@@ -0,0 +1,51 @@
package tunnel
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
log "github.com/sirupsen/logrus"
)
type Log struct {
LogLevel C.LogLevel
Payload string
}
func (l *Log) Type() string {
return l.LogLevel.String()
}
func print(data Log) {
switch data.LogLevel {
case C.INFO:
log.Infoln(data.Payload)
case C.WARNING:
log.Warnln(data.Payload)
case C.ERROR:
log.Errorln(data.Payload)
case C.DEBUG:
log.Debugln(data.Payload)
}
}
func (t *Tunnel) subscribeLogs() {
sub, err := t.observable.Subscribe()
if err != nil {
log.Fatalf("Can't subscribe tunnel log: %s", err.Error())
}
for elm := range sub {
data := elm.(Log)
if data.LogLevel <= t.logLevel {
print(data)
}
}
}
func newLog(logLevel C.LogLevel, format string, v ...interface{}) Log {
return Log{
LogLevel: logLevel,
Payload: fmt.Sprintf(format, v...),
}
}
+161
View File
@@ -0,0 +1,161 @@
package tunnel
import (
"sync"
"time"
LocalAdapter "github.com/Dreamacro/clash/adapters/local"
cfg "github.com/Dreamacro/clash/config"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/observable"
"gopkg.in/eapache/channels.v1"
)
var (
tunnel *Tunnel
once sync.Once
)
// Tunnel handle proxy socket and HTTP/SOCKS socket
type Tunnel struct {
queue *channels.InfiniteChannel
rules []C.Rule
proxies map[string]C.Proxy
configLock *sync.RWMutex
traffic *C.Traffic
// Outbound Rule
mode cfg.Mode
// Log
logCh chan interface{}
observable *observable.Observable
logLevel C.LogLevel
}
// Add request to queue
func (t *Tunnel) Add(req C.ServerAdapter) {
t.queue.In() <- req
}
// Traffic return traffic of all connections
func (t *Tunnel) Traffic() *C.Traffic {
return t.traffic
}
// Log return clash log stream
func (t *Tunnel) Log() *observable.Observable {
return t.observable
}
func (t *Tunnel) configMonitor(signal chan<- struct{}) {
sub := cfg.Instance().Subscribe()
signal <- struct{}{}
for elm := range sub {
event := elm.(*cfg.Event)
switch event.Type {
case "proxies":
proxies := event.Payload.(map[string]C.Proxy)
t.configLock.Lock()
t.proxies = proxies
t.configLock.Unlock()
case "rules":
rules := event.Payload.([]C.Rule)
t.configLock.Lock()
t.rules = rules
t.configLock.Unlock()
case "mode":
t.mode = event.Payload.(cfg.Mode)
case "log-level":
t.logLevel = event.Payload.(C.LogLevel)
}
}
}
func (t *Tunnel) process() {
queue := t.queue.Out()
for {
elm := <-queue
conn := elm.(C.ServerAdapter)
go t.handleConn(conn)
}
}
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
defer localConn.Close()
addr := localConn.Addr()
var proxy C.Proxy
switch t.mode {
case cfg.Direct:
proxy = t.proxies["DIRECT"]
case cfg.Global:
proxy = t.proxies["GLOBAL"]
// Rule
default:
proxy = t.match(addr)
}
remoConn, err := proxy.Generator(addr)
if err != nil {
t.logCh <- newLog(C.WARNING, "Proxy connect error: %s", err.Error())
return
}
defer remoConn.Close()
switch adapter := localConn.(type) {
case *LocalAdapter.HTTPAdapter:
t.handleHTTP(adapter, remoConn)
case *LocalAdapter.SocketAdapter:
t.handleSOCKS(adapter, remoConn)
}
}
func (t *Tunnel) match(addr *C.Addr) C.Proxy {
t.configLock.RLock()
defer t.configLock.RUnlock()
for _, rule := range t.rules {
if rule.IsMatch(addr) {
a, ok := t.proxies[rule.Adapter()]
if !ok {
continue
}
t.logCh <- newLog(C.INFO, "%v match %s using %s", addr.String(), rule.RuleType().String(), rule.Adapter())
return a
}
}
t.logCh <- newLog(C.INFO, "%v doesn't match any rule using DIRECT", addr.String())
return t.proxies["DIRECT"]
}
// Run initial task
func (t *Tunnel) Run() {
go t.process()
go t.subscribeLogs()
signal := make(chan struct{})
go t.configMonitor(signal)
<-signal
}
func newTunnel() *Tunnel {
logCh := make(chan interface{})
return &Tunnel{
queue: channels.NewInfiniteChannel(),
proxies: make(map[string]C.Proxy),
observable: observable.NewObservable(logCh),
logCh: logCh,
configLock: &sync.RWMutex{},
traffic: C.NewTraffic(time.Second),
mode: cfg.Rule,
logLevel: C.INFO,
}
}
// Instance return singleton instance of Tunnel
func Instance() *Tunnel {
once.Do(func() {
tunnel = newTunnel()
})
return tunnel
}
+29
View File
@@ -0,0 +1,29 @@
package tunnel
import (
"net"
C "github.com/Dreamacro/clash/constant"
)
// TrafficTrack record traffic of net.Conn
type TrafficTrack struct {
net.Conn
traffic *C.Traffic
}
func (tt *TrafficTrack) Read(b []byte) (int, error) {
n, err := tt.Conn.Read(b)
tt.traffic.Down() <- int64(n)
return n, err
}
func (tt *TrafficTrack) Write(b []byte) (int, error) {
n, err := tt.Conn.Write(b)
tt.traffic.Up() <- int64(n)
return n, err
}
func newTrafficTrack(conn net.Conn, traffic *C.Traffic) *TrafficTrack {
return &TrafficTrack{traffic: traffic, Conn: conn}
}