Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8d6c0945 | ||
|
|
f0ec5d353c | ||
|
|
7ac60771bc | ||
|
|
88999fc572 | ||
|
|
3a2ebce4c0 | ||
|
|
acb718edad | ||
|
|
389ecb3e8f | ||
|
|
160b96d829 | ||
|
|
5b80873810 | ||
|
|
9cae7f9ad7 | ||
|
|
1333c554a9 | ||
|
|
4d6000af22 | ||
|
|
3601d1166b | ||
|
|
7691a96996 | ||
|
|
db215a5510 | ||
|
|
17c17452b3 | ||
|
|
d7b0fa5e29 | ||
|
|
a0b5e595c8 | ||
|
|
728cc0d10b | ||
|
|
514c84ec7e | ||
|
|
f4af98f54a | ||
|
|
e1f4fef157 | ||
|
|
2004f2392a | ||
|
|
d7f0cf91c7 | ||
|
|
f34d71eb88 | ||
|
|
b0ed302d5e | ||
|
|
a4b74a7523 | ||
|
|
8581f53b20 | ||
|
|
432fa115e2 | ||
|
|
2759f59029 | ||
|
|
d24496a01e | ||
|
|
123bf65c55 | ||
|
|
f6263940f1 | ||
|
|
cba3d2d848 | ||
|
|
cecb1522fe | ||
|
|
fd6ecda520 | ||
|
|
147df6c180 | ||
|
|
8d6310d0ea | ||
|
|
c455dfc7df | ||
|
|
e381319ae8 | ||
|
|
297b39625f | ||
|
|
b42d762fa0 | ||
|
|
f23e6e82b0 | ||
|
|
2af11c29a5 | ||
|
|
b0bf57e715 | ||
|
|
cdd0e3dd30 | ||
|
|
5823955c71 | ||
|
|
714c9a554f | ||
|
|
e621324b48 | ||
|
|
dfe1e7ecd9 | ||
|
|
4c3380d822 | ||
|
|
1d68516cdb | ||
|
|
821bb49914 | ||
|
|
c95993a629 | ||
|
|
e6495f5e1f | ||
|
|
c104942b5f | ||
|
|
59835667aa | ||
|
|
8a98eea8fa | ||
|
|
3dc20b9e70 | ||
|
|
d8ab6fd755 | ||
|
|
cc499a7897 | ||
|
|
76f5261b20 | ||
|
|
6a506b761f | ||
|
|
fe966f9c5d | ||
|
|
bcf7f793ec | ||
|
|
75de6ce041 | ||
|
|
4417a85ef7 | ||
|
|
9e0b67c04a | ||
|
|
8cd203e7b0 | ||
|
|
211160f7f8 | ||
|
|
2afc7262eb | ||
|
|
94a0282c70 | ||
|
|
96b78829ab | ||
|
|
35d5a90b2a | ||
|
|
50d73ad498 | ||
|
|
d1f8bc3062 | ||
|
|
d3353b1aa5 | ||
|
|
cd48fb4e08 | ||
|
|
f70738ecf5 | ||
|
|
2d63466597 | ||
|
|
ecf67443fb | ||
|
|
43987ad111 | ||
|
|
5fbd6c6196 | ||
|
|
1ec7f1f7e9 | ||
|
|
c5104c241a | ||
|
|
021ba3519d | ||
|
|
119f74cd05 | ||
|
|
f0b61c9e1f | ||
|
|
a573631068 | ||
|
|
a12444b258 | ||
|
|
17257228a9 | ||
|
|
53b6a9dfc1 | ||
|
|
c45f8ddfc3 | ||
|
|
e04c9c15ff | ||
|
|
d5cb363579 | ||
|
|
0748021ffa | ||
|
|
1d21368385 | ||
|
|
2a5865397a | ||
|
|
f5f378f3b0 | ||
|
|
861d5fa871 | ||
|
|
4518825aca | ||
|
|
260ebc83ff | ||
|
|
640cf3e75c | ||
|
|
7466990e0c | ||
|
|
b41715686c | ||
|
|
bca2a7a520 | ||
|
|
545c43fe2c | ||
|
|
eaf16318de | ||
|
|
a809f94a02 | ||
|
|
541862cda8 | ||
|
|
7ab2df9afe | ||
|
|
c45c4a1ad8 | ||
|
|
2b71f9a9f7 | ||
|
|
3db2e1c618 | ||
|
|
d24617d842 | ||
|
|
29dc8dc492 | ||
|
|
57b8ce3d7f | ||
|
|
1536c9f056 | ||
|
|
c2280555d3 | ||
|
|
532760e71a | ||
|
|
6da7eff62c | ||
|
|
a75192b6bf | ||
|
|
f4ceebb12c | ||
|
|
151327e9ba | ||
|
|
a343678bad | ||
|
|
1b2c97d381 | ||
|
|
d034eb6f4b | ||
|
|
08dc165450 | ||
|
|
50eef54ab6 | ||
|
|
a75bdd457f | ||
|
|
d5d219789f | ||
|
|
04eed1a768 | ||
|
|
87d17bcb9a | ||
|
|
2d3bd5c5cf | ||
|
|
12d76a2ad1 | ||
|
|
a0026b1bb3 | ||
|
|
761f11fa5f | ||
|
|
a7b28692ea | ||
|
|
daac6e1e5c | ||
|
|
8c9b7e4a01 | ||
|
|
1b0778141c | ||
|
|
96ed63b704 | ||
|
|
bb07bc639d | ||
|
|
c3eb4b2f51 | ||
|
|
9deeb37f21 | ||
|
|
0befdc0d34 | ||
|
|
606a21dff0 | ||
|
|
ef6a37fb7c | ||
|
|
2f7b8c4b59 | ||
|
|
968e82b072 | ||
|
|
0240eea776 | ||
|
|
dc8d94fa31 | ||
|
|
f6ec8cc882 | ||
|
|
b648d21068 | ||
|
|
2b63997b22 | ||
|
|
198daaf720 | ||
|
|
a1847dc6f2 | ||
|
|
8ebb3a5f31 | ||
|
|
7cf3dc2bf2 | ||
|
|
2346095323 | ||
|
|
40ae2d456c | ||
|
|
3152bcaaa6 | ||
|
|
c69c41c57f | ||
|
|
ea1f424b33 | ||
|
|
1ef828497c | ||
|
|
88b28faa48 | ||
|
|
7bfaacf936 | ||
|
|
05d8dae4e0 | ||
|
|
27cd92b4b1 | ||
|
|
4b6b5b5d95 | ||
|
|
6468575a42 | ||
|
|
5ea22cecad | ||
|
|
c8205d3f95 | ||
|
|
639377760c | ||
|
|
a9323f0528 | ||
|
|
2e31e90225 | ||
|
|
9d5b8188eb | ||
|
|
1f9e330f6a | ||
|
|
a69776fe33 | ||
|
|
d42dc46b8c | ||
|
|
c37dc2f874 | ||
|
|
9c20f13f95 | ||
|
|
e958e7f675 | ||
|
|
68113e694e | ||
|
|
88aec66ef8 | ||
|
|
f500596621 | ||
|
|
df5bafd0bb | ||
|
|
01fe9deb20 | ||
|
|
4e44298e98 | ||
|
|
a7c3a05c23 | ||
|
|
05fa36497b | ||
|
|
e6f8ae265e | ||
|
|
d107847747 | ||
|
|
ea8e270df6 | ||
|
|
4bc40e0663 | ||
|
|
5e6ccd990a | ||
|
|
1a8d673742 | ||
|
|
a4069aa6f6 | ||
|
|
3cfb90c078 | ||
|
|
594949d3f0 | ||
|
|
8fd34b5258 | ||
|
|
1161482a5b | ||
|
|
8a123e7a0d | ||
|
|
4ef3a20929 | ||
|
|
16c2e9b694 | ||
|
|
c9a9d310ef | ||
|
|
84996a5652 | ||
|
|
115afc5735 | ||
|
|
2ae75e876d | ||
|
|
126291d544 | ||
|
|
37db81f6b1 | ||
|
|
a3cfd10d39 | ||
|
|
0e61f02046 | ||
|
|
d8b1c0b7b8 | ||
|
|
8bf908b731 | ||
|
|
568f1f12e7 | ||
|
|
ca739a5e2f | ||
|
|
7833f747d5 | ||
|
|
977519b383 | ||
|
|
b39b3812d0 | ||
|
|
f7dddb41fa | ||
|
|
053300919c | ||
|
|
93dff86383 | ||
|
|
1b7e9c6b12 | ||
|
|
f072cd0839 | ||
|
|
f626a0fc5c | ||
|
|
63da12f046 | ||
|
|
ce1beafb08 | ||
|
|
f6ecc5c953 | ||
|
|
de87e3fd74 | ||
|
|
e34a7a3081 | ||
|
|
ecb1678e48 | ||
|
|
ca395033a5 | ||
|
|
8095451e14 | ||
|
|
c0435ead01 | ||
|
|
d346344e17 | ||
|
|
68d6af4255 | ||
|
|
253d465d32 | ||
|
|
7ab7c8abee | ||
|
|
ea3aee0883 | ||
|
|
b4a5d7b1be | ||
|
|
028346713e | ||
|
|
d519bc1a23 | ||
|
|
a6d5fbc2ca | ||
|
|
e9a6bc2fbc | ||
|
|
ef42612c4b | ||
|
|
f4b421c953 | ||
|
|
14aa981ca4 | ||
|
|
515942274d | ||
|
|
03785f427a | ||
|
|
5cc1f43a03 | ||
|
|
a557255099 | ||
|
|
932778b153 | ||
|
|
1e8cadb878 | ||
|
|
eba2869c1b | ||
|
|
121108c416 | ||
|
|
780dfda610 | ||
|
|
3dd3431401 | ||
|
|
ba8a6247f5 | ||
|
|
5f1ee513f2 | ||
|
|
17fd5954a0 | ||
|
|
87e4061879 | ||
|
|
5c1ce96239 | ||
|
|
ef058f223d | ||
|
|
ab03e551da | ||
|
|
168a097111 | ||
|
|
232572114c | ||
|
|
54c67f83ab | ||
|
|
e63f210cc8 | ||
|
|
48169224c0 | ||
|
|
f2d99a50a7 | ||
|
|
bfa3c8375b | ||
|
|
34697da10c | ||
|
|
3e7d7f5d60 | ||
|
|
8a4149c800 | ||
|
|
0d7650cbc0 | ||
|
|
72932a255d | ||
|
|
4848a0f6d9 | ||
|
|
a5064fdd4f | ||
|
|
5d4b6b4fb5 | ||
|
|
d5a0299d91 | ||
|
|
718388db51 | ||
|
|
01d9d231b5 | ||
|
|
88e0425736 | ||
|
|
05f2a12baf | ||
|
|
c9d3b8e836 | ||
|
|
33fbd378c0 | ||
|
|
f76bed2f8b | ||
|
|
23027b8813 | ||
|
|
896c57aa03 | ||
|
|
ee6458a60e | ||
|
|
114456a1f2 | ||
|
|
cca7ec81e1 | ||
|
|
e9e59f6026 | ||
|
|
a29c95ceef | ||
|
|
c60d8cfb53 | ||
|
|
4ee8679d29 | ||
|
|
419c6301dd | ||
|
|
e979bec321 | ||
|
|
fa6ac580d0 | ||
|
|
a44bb05345 | ||
|
|
19ddb86430 | ||
|
|
1eb6f59167 | ||
|
|
2ba82ebd97 | ||
|
|
00163fbfd4 | ||
|
|
0ef8734856 | ||
|
|
a4151fb198 | ||
|
|
292ba5af95 | ||
|
|
90456263ae | ||
|
|
bc3f7d0549 | ||
|
|
382f083fd1 | ||
|
|
a287e287b9 | ||
|
|
166558433e | ||
|
|
1915b005a9 | ||
|
|
ed63036329 | ||
|
|
a49899add3 | ||
|
|
85072e4520 | ||
|
|
a31f864ab8 | ||
|
|
5bcbc4563e | ||
|
|
62910f5e7e | ||
|
|
b3ff9abc0d | ||
|
|
eccc9bd886 | ||
|
|
0bd1fcf51b | ||
|
|
51acbc75f5 | ||
|
|
c3cd2351a5 | ||
|
|
dde7f77c29 | ||
|
|
dafba83b8d | ||
|
|
ba472b5768 | ||
|
|
c4f4c157bd | ||
|
|
5ddeaf2783 | ||
|
|
1b87bc0417 | ||
|
|
a154230bca | ||
|
|
4b498d7dc7 | ||
|
|
41b263a7ee | ||
|
|
ddeff030e3 | ||
|
|
d48beadcbc | ||
|
|
7238e919c5 | ||
|
|
7cd52a84db | ||
|
|
e97a2d58c8 | ||
|
|
340fa28c6a | ||
|
|
6c58f552b8 | ||
|
|
7cc53bbf32 | ||
|
|
88ef4d2f10 | ||
|
|
968ef77896 | ||
|
|
64803bef1e | ||
|
|
d7df27b91d | ||
|
|
32b6608f11 | ||
|
|
a665acaf63 | ||
|
|
e2073b5f7c | ||
|
|
475dd96ac2 | ||
|
|
9fe2ec39ae | ||
|
|
9cca3c958e | ||
|
|
903c201f2d | ||
|
|
2b47776554 | ||
|
|
41a2e7effc | ||
|
|
6947118d2e | ||
|
|
3b7a0b0101 | ||
|
|
19dcc4a1fa | ||
|
|
8ec689a893 | ||
|
|
a1d623c42b | ||
|
|
9a893fc0a0 | ||
|
|
f13abcbe49 | ||
|
|
24e1732091 | ||
|
|
8a2826c242 | ||
|
|
b36f19f4c4 | ||
|
|
9967601e3e | ||
|
|
c330bf8238 | ||
|
|
b38d9ec95e | ||
|
|
1069ba1991 | ||
|
|
ea29017687 | ||
|
|
b41b9267bd | ||
|
|
5ffa1aeb1d | ||
|
|
d2b91341f0 | ||
|
|
5e69959cf5 | ||
|
|
845f1b37ca | ||
|
|
21fb12ef71 | ||
|
|
53fa597556 | ||
|
|
adffebadbc | ||
|
|
ea93c0b3ab | ||
|
|
ea3b63fd80 | ||
|
|
bda542d2e5 | ||
|
|
a72c2d8abb | ||
|
|
8ad202cc07 | ||
|
|
bc8cea7bd8 | ||
|
|
8878070efc | ||
|
|
4e2dceef48 | ||
|
|
c9973393ff | ||
|
|
f0aba3758d | ||
|
|
d8e5e74444 | ||
|
|
0a05d76850 | ||
|
|
996d82dfbf | ||
|
|
961f9c7633 | ||
|
|
e7d59ec633 | ||
|
|
32828a6ee9 | ||
|
|
e8a48c0f0d | ||
|
|
afbebb4c8b | ||
|
|
a911a0c4dd | ||
|
|
fe9047ee40 | ||
|
|
bd159ecb6e | ||
|
|
1e8a57e949 | ||
|
|
f418a3649e | ||
|
|
fa99b3f981 | ||
|
|
2661f01c19 | ||
|
|
8633ab2de6 | ||
|
|
70c8bb19a7 | ||
|
|
cdf7107d5a | ||
|
|
c11a1bb2a3 | ||
|
|
8466662226 | ||
|
|
7765d14588 | ||
|
|
8968469310 | ||
|
|
d7d36f32a3 | ||
|
|
aa4b6c23bb | ||
|
|
ceae371826 | ||
|
|
7fba8b09cc | ||
|
|
527f99ea00 | ||
|
|
db1efcb85d | ||
|
|
456c853923 | ||
|
|
e5abd0cba8 | ||
|
|
f61013d99d | ||
|
|
4768bd92dc | ||
|
|
9023809c89 | ||
|
|
e14e91a2a4 | ||
|
|
0e6cb05796 | ||
|
|
da532df009 | ||
|
|
9aa20c6715 | ||
|
|
c7107e9d9b | ||
|
|
f882375cfa | ||
|
|
a6abb51046 | ||
|
|
438392ddf6 | ||
|
|
6646593c5b | ||
|
|
ab4b8b5057 | ||
|
|
3b03624f32 | ||
|
|
7270f94930 | ||
|
|
838d492334 | ||
|
|
600537af9c | ||
|
|
774d83ed9f | ||
|
|
e3c5fb1a8c | ||
|
|
f237879921 | ||
|
|
765892e768 | ||
|
|
5caa778a71 | ||
|
|
f2a0e4877a | ||
|
|
7d04980e30 | ||
|
|
565415d8d1 | ||
|
|
3f91c10c10 | ||
|
|
96b8fe110b | ||
|
|
d0aa037b4c | ||
|
|
792190b5f4 | ||
|
|
d332b6941a | ||
|
|
cd1a8bf5e4 | ||
|
|
5c7e00ce59 | ||
|
|
2b1c32faf7 | ||
|
|
b98d5a5ce0 | ||
|
|
4c84616797 | ||
|
|
b1d22f3c3c | ||
|
|
e8a3335e21 | ||
|
|
6cecb23dd8 | ||
|
|
9987a83334 | ||
|
|
fe194eac76 | ||
|
|
21b9d0fa27 | ||
|
|
cf4e1c5b95 | ||
|
|
e624618be6 | ||
|
|
fa6e5841bf | ||
|
|
c3ebd9d6e3 |
No files matched your search
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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>
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
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
|
||||
# 在此粘贴
|
||||
```
|
||||
|
||||
**日志**
|
||||
如果适用, 粘贴日志以帮助侦测错误
|
||||
```
|
||||
<在此粘贴>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: "[简体中文] 功能请求"
|
||||
about: 你希望的能够在应用中增加的功能
|
||||
title: "[Feature Request] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
<!-- 请务必在上方文本框处 [Feature Request] 后填入清晰明了的标题 -->
|
||||
|
||||
**功能描述**
|
||||
请清晰的描述你想要的功能
|
||||
|
||||
**描述你希望的实现方式**
|
||||
清晰的描述应用应该如何实现该功能
|
||||
|
||||
**附加信息**
|
||||
其他的与改功能相关的附加信息
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
bin/*
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# dep
|
||||
vendor
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- '1.12'
|
||||
install:
|
||||
- "go mod download"
|
||||
env:
|
||||
global:
|
||||
- NAME=clash
|
||||
- BINDIR=bin
|
||||
- GO111MODULE=on
|
||||
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
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
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 && \
|
||||
mv /tmp/GeoLite2-Country_*/GeoLite2-Country.mmdb /Country.mmdb
|
||||
|
||||
WORKDIR /clash-src
|
||||
|
||||
COPY . /clash-src
|
||||
|
||||
RUN go mod download && \
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o /clash
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||
COPY --from=builder /clash /
|
||||
|
||||
EXPOSE 7890 7891
|
||||
|
||||
ENTRYPOINT ["/clash"]
|
||||
@@ -1,21 +0,0 @@
|
||||
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.
|
||||
@@ -1,93 +0,0 @@
|
||||
NAME=clash
|
||||
BINDIR=bin
|
||||
VERSION=$(shell git describe --tags --long --dirty || echo "unkown version")
|
||||
BUILDTIME=$(shell date -u)
|
||||
GOBUILD=CGO_ENABLED=0 go build -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
|
||||
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
||||
-w -s'
|
||||
|
||||
PLATFORM_LIST = \
|
||||
darwin-amd64 \
|
||||
linux-386 \
|
||||
linux-amd64 \
|
||||
linux-armv5 \
|
||||
linux-armv6 \
|
||||
linux-armv7 \
|
||||
linux-armv8 \
|
||||
linux-mips-softfloat \
|
||||
linux-mips-hardfloat \
|
||||
linux-mipsle \
|
||||
linux-mips64 \
|
||||
linux-mips64le \
|
||||
freebsd-386 \
|
||||
freebsd-amd64
|
||||
|
||||
WINDOWS_ARCH_LIST = \
|
||||
windows-386 \
|
||||
windows-amd64
|
||||
|
||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||
|
||||
darwin-amd64:
|
||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-386:
|
||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64:
|
||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv5:
|
||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv6:
|
||||
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv7:
|
||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv8:
|
||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips-softfloat:
|
||||
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips-hardfloat:
|
||||
GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mipsle:
|
||||
GOARCH=mipsle GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips64:
|
||||
GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips64le:
|
||||
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
freebsd-386:
|
||||
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
freebsd-amd64:
|
||||
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
windows-386:
|
||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64:
|
||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
||||
|
||||
$(gz_releases): %.gz : %
|
||||
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
|
||||
tar -czf $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).tar.gz -C $(BINDIR) $(NAME)-$(basename $@)
|
||||
|
||||
$(zip_releases): %.zip : %
|
||||
zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
|
||||
|
||||
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
||||
|
||||
releases: $(gz_releases) $(zip_releases)
|
||||
clean:
|
||||
rm $(BINDIR)/*
|
||||
@@ -0,0 +1,160 @@
|
||||
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
|
||||
@@ -0,0 +1,50 @@
|
||||
## 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.
|
||||
@@ -1,227 +1,33 @@
|
||||
<h1 align="center">
|
||||
<img src="https://github.com/Dreamacro/clash/raw/master/docs/logo.png" alt="Clash" width="200">
|
||||
<br>Clash<br>
|
||||
</h1>
|
||||
## Clash for Android
|
||||
|
||||
<h4 align="center">A rule-based tunnel in Go.</h4>
|
||||
A Graphical user interface of [clash](https://github.com/Dreamacro/clash) for Android
|
||||
|
||||
<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>
|
||||
<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)
|
||||
|
||||
## Features
|
||||
### Feature
|
||||
|
||||
- HTTP/HTTPS and SOCKS protocol
|
||||
- Surge like configuration
|
||||
- GeoIP rule support
|
||||
- Support Vmess/Shadowsocks/Socks5
|
||||
- Support for Netfilter TCP redirect
|
||||
Fully feature of [Clash Premium](https://github.com/Dreamacro/clash/releases)
|
||||
|
||||
## Install
|
||||
|
||||
You can build from source:
|
||||
### Requirement
|
||||
|
||||
```sh
|
||||
go get -u -v github.com/Dreamacro/clash
|
||||
```
|
||||
* Android 7.0+
|
||||
* `armeabi-v7a` , `arm64-v8a`, `x86` or `x86_64` Architecture
|
||||
|
||||
Pre-built binaries are available: [release](https://github.com/Dreamacro/clash/releases)
|
||||
|
||||
Requires Go >= 1.12.
|
||||
### 3rd Part License
|
||||
|
||||
Checkout Clash version:
|
||||
See also [NOTICE](./NOTICE)
|
||||
|
||||
```sh
|
||||
clash -v
|
||||
```
|
||||
|
||||
## Daemon
|
||||
|
||||
Unfortunately, there is no native elegant way to implement golang's daemon.
|
||||
### Privacy Policy
|
||||
|
||||
So we can use third-party daemon tools like pm2, supervisor, and so on.
|
||||
See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md)
|
||||
|
||||
In the case of [pm2](https://github.com/Unitech/pm2), we can start the daemon this way:
|
||||
|
||||
```sh
|
||||
pm2 start clash
|
||||
```
|
||||
|
||||
If you have Docker installed, you can run clash directly using `docker-compose`.
|
||||
### NOTICE
|
||||
|
||||
[Run clash in docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
|
||||
**Clash for Android** now uses **closed-source** upstream branches. According to the upstream license agreement, Clash for Android also needs to **close source**.
|
||||
|
||||
## Config
|
||||
|
||||
The default configuration directory is `$HOME/.config/clash`
|
||||
|
||||
The name of the configuration file is `config.yml`
|
||||
|
||||
If you want to use another directory, you can use `-d` to control the configuration directory
|
||||
|
||||
For example, you can use the current directory as the configuration directory
|
||||
|
||||
```sh
|
||||
clash -d .
|
||||
```
|
||||
|
||||
Below is a simple demo configuration file:
|
||||
|
||||
```yml
|
||||
# port of HTTP
|
||||
port: 7890
|
||||
|
||||
# port of SOCKS5
|
||||
socks-port: 7891
|
||||
|
||||
# redir port for Linux and macOS
|
||||
# redir-port: 7892
|
||||
|
||||
allow-lan: false
|
||||
|
||||
# Rule / Global/ Direct (default is Rule)
|
||||
mode: Rule
|
||||
|
||||
# set log level to stdout (default is info)
|
||||
# info / warning / error / debug / silent
|
||||
log-level: info
|
||||
|
||||
# A RESTful API for clash
|
||||
external-controller: 127.0.0.1:9090
|
||||
|
||||
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
|
||||
# input is a relative path to the configuration directory or an absolute path
|
||||
# external-ui: folder
|
||||
|
||||
# Secret for RESTful API (Optional)
|
||||
# secret: ""
|
||||
|
||||
# dns:
|
||||
# enable: true # set true to enable dns (default is false)
|
||||
# ipv6: false # default is false
|
||||
# listen: 0.0.0.0:53
|
||||
# enhanced-mode: redir-host
|
||||
# nameserver:
|
||||
# - 114.114.114.114
|
||||
# - tls://dns.rubyfish.cn:853 # dns over tls
|
||||
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
|
||||
# - tcp://1.1.1.1
|
||||
|
||||
Proxy:
|
||||
|
||||
# shadowsocks
|
||||
# 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
|
||||
# In addition to what go-shadowsocks2 supports, it also supports chacha20 rc4-md5 xchacha20-ietf-poly1305
|
||||
- { name: "ss1", type: ss, server: server, port: 443, cipher: AEAD_CHACHA20_POLY1305, password: "password" }
|
||||
|
||||
# old obfs configuration remove after prerelease
|
||||
- name: "ss2"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: AEAD_CHACHA20_POLY1305
|
||||
password: "password"
|
||||
plugin: obfs
|
||||
plugin-opts:
|
||||
mode: tls # or http
|
||||
# host: bing.com
|
||||
|
||||
- name: "ss3"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: AEAD_CHACHA20_POLY1305
|
||||
password: "password"
|
||||
plugin: v2ray-plugin
|
||||
plugin-opts:
|
||||
mode: websocket # no QUIC now
|
||||
# tls: true # wss
|
||||
# skip-cert-verify: true
|
||||
# host: bing.com
|
||||
# path: "/"
|
||||
# headers:
|
||||
# custom: value
|
||||
|
||||
# vmess
|
||||
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto }
|
||||
# with tls
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true }
|
||||
# with tls and skip-cert-verify
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, tls: true, skip-cert-verify: true }
|
||||
# with ws-path and ws-headers
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, ws-headers: { Host: v2ray.com } }
|
||||
# with ws + tls
|
||||
- { name: "vmess", type: vmess, server: server, port: 443, uuid: uuid, alterId: 32, cipher: auto, network: ws, ws-path: /path, tls: true }
|
||||
|
||||
# socks5
|
||||
- { name: "socks", type: socks5, server: server, port: 443 }
|
||||
# socks5 with authentication
|
||||
- { name: "socks", type: socks5, server: server, port: 443, username: "username", password: "password" }
|
||||
# with tls
|
||||
- { name: "socks", type: socks5, server: server, port: 443, tls: true }
|
||||
# with tls and skip-cert-verify
|
||||
- { name: "socks", type: socks5, server: server, port: 443, tls: true, skip-cert-verify: true }
|
||||
|
||||
# http
|
||||
- { name: "http", type: http, server: server, port: 443 }
|
||||
# http with authentication
|
||||
- { name: "http", type: http, server: server, port: 443, username: "username", password: "password" }
|
||||
# with tls (https)
|
||||
- { name: "http", type: http, server: server, port: 443, tls: true }
|
||||
# with tls (https) and skip-cert-verify
|
||||
- { name: "http", type: http, server: server, port: 443, tls: true, skip-cert-verify: true }
|
||||
|
||||
Proxy Group:
|
||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
||||
- { name: "auto", type: url-test, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
|
||||
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
|
||||
- { name: "fallback-auto", type: fallback, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
|
||||
# load-balance: The request of the same eTLD will be dial on the same proxy.
|
||||
- { name: "load-balance", type: load-balance, proxies: ["ss1", "ss2", "vmess1"], url: "http://www.gstatic.com/generate_204", interval: 300 }
|
||||
|
||||
# select is used for selecting proxy or proxy group
|
||||
# you can use RESTful API to switch proxy, is recommended for use in GUI.
|
||||
- { name: "Proxy", type: select, proxies: ["ss1", "ss2", "vmess1", "auto"] }
|
||||
|
||||
Rule:
|
||||
- DOMAIN-SUFFIX,google.com,auto
|
||||
- DOMAIN-KEYWORD,google,auto
|
||||
- DOMAIN,google.com,auto
|
||||
- DOMAIN-SUFFIX,ad.com,REJECT
|
||||
- IP-CIDR,127.0.0.0/8,DIRECT
|
||||
- SOURCE-IP-CIDR,192.168.1.201/32,DIRECT
|
||||
- GEOIP,CN,DIRECT
|
||||
# FINAL would remove after prerelease
|
||||
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
|
||||
- MATCH,auto
|
||||
```
|
||||
|
||||
## Thanks
|
||||
|
||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||
|
||||
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||
|
||||
## License
|
||||
|
||||
[](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
|
||||
@@ -1,66 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
// HTTPAdapter is a adapter for HTTP connection
|
||||
type HTTPAdapter struct {
|
||||
metadata *C.Metadata
|
||||
conn net.Conn
|
||||
R *http.Request
|
||||
}
|
||||
|
||||
// Close HTTP connection
|
||||
func (h *HTTPAdapter) Close() {
|
||||
h.conn.Close()
|
||||
}
|
||||
|
||||
// Metadata return destination metadata
|
||||
func (h *HTTPAdapter) Metadata() *C.Metadata {
|
||||
return h.metadata
|
||||
}
|
||||
|
||||
// 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 {
|
||||
metadata := parseHTTPAddr(request)
|
||||
metadata.SourceIP = parseSourceIP(conn)
|
||||
return &HTTPAdapter{
|
||||
metadata: metadata,
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// NewHTTPS is HTTPAdapter generator
|
||||
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
|
||||
metadata := parseHTTPAddr(request)
|
||||
metadata.SourceIP = parseSourceIP(conn)
|
||||
return &SocketAdapter{
|
||||
metadata: metadata,
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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
|
||||
metadata *C.Metadata
|
||||
}
|
||||
|
||||
// Close socks and redir connection
|
||||
func (s *SocketAdapter) Close() {
|
||||
s.conn.Close()
|
||||
}
|
||||
|
||||
// Metadata return destination metadata
|
||||
func (s *SocketAdapter) Metadata() *C.Metadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
// 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, source C.SourceType) *SocketAdapter {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.Source = source
|
||||
metadata.SourceIP = parseSourceIP(conn)
|
||||
|
||||
return &SocketAdapter{
|
||||
conn: conn,
|
||||
metadata: metadata,
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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.Metadata {
|
||||
metadata := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
AddrType: int(target[0]),
|
||||
}
|
||||
|
||||
switch target[0] {
|
||||
case socks.AtypDomainName:
|
||||
metadata.Host = string(target[2 : 2+target[1]])
|
||||
metadata.Port = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
||||
case socks.AtypIPv4:
|
||||
ip := net.IP(target[1 : 1+net.IPv4len])
|
||||
metadata.IP = &ip
|
||||
metadata.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])
|
||||
metadata.IP = &ip
|
||||
metadata.Port = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func parseHTTPAddr(request *http.Request) *C.Metadata {
|
||||
host := request.URL.Hostname()
|
||||
port := request.URL.Port()
|
||||
if port == "" {
|
||||
port = "80"
|
||||
}
|
||||
|
||||
metadata := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
Source: C.HTTP,
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
IP: nil,
|
||||
Port: port,
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
switch {
|
||||
case ip.To4() == nil:
|
||||
metadata.AddrType = C.AtypIPv6
|
||||
default:
|
||||
metadata.AddrType = C.AtypIPv4
|
||||
}
|
||||
metadata.IP = &ip
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func parseSourceIP(conn net.Conn) *net.IP {
|
||||
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
|
||||
return &addr.IP
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
name string
|
||||
tp C.AdapterType
|
||||
}
|
||||
|
||||
func (b *Base) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
func (b *Base) Type() C.AdapterType {
|
||||
return b.tp
|
||||
}
|
||||
|
||||
func (b *Base) Destroy() {}
|
||||
|
||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"type": b.Type().String(),
|
||||
})
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue
|
||||
alive bool
|
||||
}
|
||||
|
||||
func (p *Proxy) Alive() bool {
|
||||
return p.alive
|
||||
}
|
||||
|
||||
func (p *Proxy) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.Dial(metadata)
|
||||
if err != nil {
|
||||
p.alive = false
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
queue := p.history.Copy()
|
||||
histories := []C.DelayHistory{}
|
||||
for _, item := range queue {
|
||||
histories = append(histories, item.(C.DelayHistory))
|
||||
}
|
||||
return histories
|
||||
}
|
||||
|
||||
// LastDelay return last history record. if proxy is not alive, return the max value of int16.
|
||||
func (p *Proxy) LastDelay() (delay uint16) {
|
||||
var max uint16 = 0xffff
|
||||
if !p.alive {
|
||||
return max
|
||||
}
|
||||
|
||||
head := p.history.First()
|
||||
if head == nil {
|
||||
return max
|
||||
}
|
||||
history := head.(C.DelayHistory)
|
||||
if history.Delay == 0 {
|
||||
return max
|
||||
}
|
||||
return history.Delay
|
||||
}
|
||||
|
||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||
if err != nil {
|
||||
return inner, err
|
||||
}
|
||||
|
||||
mapping := map[string]interface{}{}
|
||||
json.Unmarshal(inner, &mapping)
|
||||
mapping["history"] = p.DelayHistory()
|
||||
return json.Marshal(mapping)
|
||||
}
|
||||
|
||||
// URLTest get the delay for the specified URL
|
||||
func (p *Proxy) URLTest(url string) (t uint16, err error) {
|
||||
defer func() {
|
||||
p.alive = err == nil
|
||||
record := C.DelayHistory{Time: time.Now()}
|
||||
if err == nil {
|
||||
record.Delay = t
|
||||
}
|
||||
p.history.Put(record)
|
||||
if p.history.Len() > 10 {
|
||||
p.history.Pop()
|
||||
}
|
||||
}()
|
||||
|
||||
addr, err := urlToMetadata(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
instance, err := p.Dial(&addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer instance.Close()
|
||||
transport := &http.Transport{
|
||||
Dial: func(string, string) (net.Conn, error) {
|
||||
return instance, nil
|
||||
},
|
||||
// from http.DefaultTransport
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
client := http.Client{Transport: transport}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
t = uint16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
return &Proxy{adapter, queue.New(10), true}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Direct struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
func (d *Direct) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
address := net.JoinHostPort(metadata.Host, metadata.Port)
|
||||
if metadata.IP != nil {
|
||||
address = net.JoinHostPort(metadata.IP.String(), metadata.Port)
|
||||
}
|
||||
|
||||
c, err := net.DialTimeout("tcp", address, tcpTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{
|
||||
Base: &Base{
|
||||
name: "DIRECT",
|
||||
tp: C.Direct,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Fallback struct {
|
||||
*Base
|
||||
proxies []C.Proxy
|
||||
rawURL string
|
||||
interval time.Duration
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type FallbackOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Proxies []string `proxy:"proxies"`
|
||||
URL string `proxy:"url"`
|
||||
Interval int `proxy:"interval"`
|
||||
}
|
||||
|
||||
func (f *Fallback) Now() string {
|
||||
proxy := f.findAliveProxy()
|
||||
return proxy.Name()
|
||||
}
|
||||
|
||||
func (f *Fallback) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
proxy := f.findAliveProxy()
|
||||
return proxy.Dial(metadata)
|
||||
}
|
||||
|
||||
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range f.proxies {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": f.Type().String(),
|
||||
"now": f.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func (f *Fallback) Destroy() {
|
||||
f.done <- struct{}{}
|
||||
}
|
||||
|
||||
func (f *Fallback) loop() {
|
||||
tick := time.NewTicker(f.interval)
|
||||
go f.validTest()
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
go f.validTest()
|
||||
case <-f.done:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fallback) findAliveProxy() C.Proxy {
|
||||
for _, proxy := range f.proxies {
|
||||
if proxy.Alive() {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
return f.proxies[0]
|
||||
}
|
||||
|
||||
func (f *Fallback) validTest() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(f.proxies))
|
||||
|
||||
for _, p := range f.proxies {
|
||||
go func(p C.Proxy) {
|
||||
p.URLTest(f.rawURL)
|
||||
wg.Done()
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func NewFallback(option FallbackOption, proxies []C.Proxy) (*Fallback, error) {
|
||||
_, err := urlToMetadata(option.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(proxies) < 1 {
|
||||
return nil, errors.New("The number of proxies cannot be 0")
|
||||
}
|
||||
|
||||
interval := time.Duration(option.Interval) * time.Second
|
||||
|
||||
Fallback := &Fallback{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Fallback,
|
||||
},
|
||||
proxies: proxies,
|
||||
rawURL: option.URL,
|
||||
interval: interval,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go Fallback.loop()
|
||||
return Fallback, nil
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Http struct {
|
||||
*Base
|
||||
addr string
|
||||
user string
|
||||
pass string
|
||||
tls bool
|
||||
skipCertVerify bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
type HttpOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UserName string `proxy:"username,omitempty"`
|
||||
Password string `proxy:"password,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Http) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := net.DialTimeout("tcp", h.addr, tcpTimeout)
|
||||
if err == nil && h.tls {
|
||||
cc := tls.Client(c, h.tlsConfig)
|
||||
err = cc.Handshake()
|
||||
c = cc
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", h.addr)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
if err := h.shakeHand(metadata, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
var buf bytes.Buffer
|
||||
var err error
|
||||
|
||||
addr := net.JoinHostPort(metadata.String(), metadata.Port)
|
||||
buf.WriteString("CONNECT " + addr + " HTTP/1.1\r\n")
|
||||
buf.WriteString("Host: " + metadata.String() + "\r\n")
|
||||
buf.WriteString("Proxy-Connection: Keep-Alive\r\n")
|
||||
|
||||
if h.user != "" && h.pass != "" {
|
||||
auth := h.user + ":" + h.pass
|
||||
buf.WriteString("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n")
|
||||
}
|
||||
// header ended
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
_, err = rw.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var req http.Request
|
||||
resp, err := http.ReadResponse(bufio.NewReader(rw), &req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == 407 {
|
||||
return errors.New("HTTP need auth")
|
||||
}
|
||||
|
||||
if resp.StatusCode == 405 {
|
||||
return errors.New("CONNECT method not allowed by proxy")
|
||||
}
|
||||
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
func NewHttp(option HttpOption) *Http {
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: option.Server,
|
||||
}
|
||||
}
|
||||
|
||||
return &Http{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Http,
|
||||
},
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
tls: option.TLS,
|
||||
skipCertVerify: option.SkipCertVerify,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/murmur3"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type LoadBalance struct {
|
||||
*Base
|
||||
proxies []C.Proxy
|
||||
maxRetry int
|
||||
rawURL string
|
||||
interval time.Duration
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func getKey(metadata *C.Metadata) string {
|
||||
if metadata.Host != "" {
|
||||
// ip host
|
||||
if ip := net.ParseIP(metadata.Host); ip != nil {
|
||||
return metadata.Host
|
||||
}
|
||||
|
||||
if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil {
|
||||
return etld
|
||||
}
|
||||
}
|
||||
|
||||
if metadata.IP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return metadata.IP.String()
|
||||
}
|
||||
|
||||
func jumpHash(key uint64, buckets int32) int32 {
|
||||
var b, j int64
|
||||
|
||||
for j < int64(buckets) {
|
||||
b = j
|
||||
key = key*2862933555777941757 + 1
|
||||
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
|
||||
}
|
||||
|
||||
return int32(b)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||
buckets := int32(len(lb.proxies))
|
||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
||||
idx := jumpHash(key, buckets)
|
||||
proxy := lb.proxies[idx]
|
||||
if proxy.Alive() {
|
||||
return proxy.Dial(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return lb.proxies[0].Dial(metadata)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) Destroy() {
|
||||
lb.done <- struct{}{}
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) validTest() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(lb.proxies))
|
||||
|
||||
for _, p := range lb.proxies {
|
||||
go func(p C.Proxy) {
|
||||
p.URLTest(lb.rawURL)
|
||||
wg.Done()
|
||||
}(p)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) loop() {
|
||||
tick := time.NewTicker(lb.interval)
|
||||
go lb.validTest()
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
go lb.validTest()
|
||||
case <-lb.done:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range lb.proxies {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": lb.Type().String(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
type LoadBalanceOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Proxies []string `proxy:"proxies"`
|
||||
URL string `proxy:"url"`
|
||||
Interval int `proxy:"interval"`
|
||||
}
|
||||
|
||||
func NewLoadBalance(option LoadBalanceOption, proxies []C.Proxy) (*LoadBalance, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("Provide at least one proxy")
|
||||
}
|
||||
|
||||
interval := time.Duration(option.Interval) * time.Second
|
||||
|
||||
lb := &LoadBalance{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.LoadBalance,
|
||||
},
|
||||
proxies: proxies,
|
||||
maxRetry: 3,
|
||||
rawURL: option.URL,
|
||||
interval: interval,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go lb.loop()
|
||||
return lb, nil
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Reject struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
func (r *Reject) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
return &NopConn{}, nil
|
||||
}
|
||||
|
||||
func NewReject() *Reject {
|
||||
return &Reject{
|
||||
Base: &Base{
|
||||
name: "REJECT",
|
||||
tp: C.Reject,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type NopConn struct{}
|
||||
|
||||
func (rw *NopConn) Read(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -1,72 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Selector struct {
|
||||
*Base
|
||||
selected C.Proxy
|
||||
proxies map[string]C.Proxy
|
||||
}
|
||||
|
||||
type SelectorOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Proxies []string `proxy:"proxies"`
|
||||
}
|
||||
|
||||
func (s *Selector) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
return s.selected.Dial(metadata)
|
||||
}
|
||||
|
||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for k := range s.proxies {
|
||||
all = append(all, k)
|
||||
}
|
||||
sort.Strings(all)
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": s.Type().String(),
|
||||
"now": s.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Selector) Now() string {
|
||||
return s.selected.Name()
|
||||
}
|
||||
|
||||
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 []C.Proxy) (*Selector, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("Provide at least one proxy")
|
||||
}
|
||||
|
||||
mapping := make(map[string]C.Proxy)
|
||||
for _, proxy := range proxies {
|
||||
mapping[proxy.Name()] = proxy
|
||||
}
|
||||
|
||||
s := &Selector{
|
||||
Base: &Base{
|
||||
name: name,
|
||||
tp: C.Selector,
|
||||
},
|
||||
proxies: mapping,
|
||||
selected: proxies[0],
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
||||
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||
"github.com/Dreamacro/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
type ShadowSocks struct {
|
||||
*Base
|
||||
server string
|
||||
cipher core.Cipher
|
||||
|
||||
// obfs
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
wsOption *v2rayObfs.WebsocketOption
|
||||
}
|
||||
|
||||
type ShadowSocksOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
Plugin string `proxy:"plugin,omitempty"`
|
||||
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
||||
|
||||
// deprecated when bump to 1.0
|
||||
Obfs string `proxy:"obfs,omitempty"`
|
||||
ObfsHost string `proxy:"obfs-host,omitempty"`
|
||||
}
|
||||
|
||||
type simpleObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
}
|
||||
|
||||
type v2rayObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := net.DialTimeout("tcp", ss.server, tcpTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
switch ss.obfsMode {
|
||||
case "tls":
|
||||
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
||||
case "http":
|
||||
_, port, _ := net.SplitHostPort(ss.server)
|
||||
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||
case "websocket":
|
||||
var err error
|
||||
c, err = v2rayObfs.NewWebsocketObfs(c, ss.wsOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", ss.server, err.Error())
|
||||
}
|
||||
}
|
||||
c = ss.cipher.StreamConn(c)
|
||||
_, err = c.Write(serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"type": ss.Type().String(),
|
||||
})
|
||||
}
|
||||
|
||||
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
server := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
cipher := option.Cipher
|
||||
password := option.Password
|
||||
ciph, err := core.PickCipher(cipher, nil, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize error: %s", server, err.Error())
|
||||
}
|
||||
|
||||
var wsOption *v2rayObfs.WebsocketOption
|
||||
var obfsOption *simpleObfsOption
|
||||
obfsMode := ""
|
||||
|
||||
// forward compatibility before 1.0
|
||||
if option.Obfs != "" {
|
||||
obfsMode = option.Obfs
|
||||
obfsOption = &simpleObfsOption{
|
||||
Host: "bing.com",
|
||||
}
|
||||
if option.ObfsHost != "" {
|
||||
obfsOption.Host = option.ObfsHost
|
||||
}
|
||||
}
|
||||
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||
if option.Plugin == "obfs" {
|
||||
opts := simpleObfsOption{Host: "bing.com"}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize obfs error: %s", server, err.Error())
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
obfsOption = &opts
|
||||
} else if option.Plugin == "v2ray-plugin" {
|
||||
opts := v2rayObfsOption{Host: "bing.com"}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %s", server, err.Error())
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
var tlsConfig *tls.Config
|
||||
if opts.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: opts.Host,
|
||||
InsecureSkipVerify: opts.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
}
|
||||
}
|
||||
wsOption = &v2rayObfs.WebsocketOption{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
return &ShadowSocks{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Shadowsocks,
|
||||
},
|
||||
server: server,
|
||||
cipher: ciph,
|
||||
|
||||
obfsMode: obfsMode,
|
||||
wsOption: wsOption,
|
||||
obfsOption: obfsOption,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||
var buf [][]byte
|
||||
aType := uint8(metadata.AddrType)
|
||||
p, _ := strconv.Atoi(metadata.Port)
|
||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
||||
switch metadata.AddrType {
|
||||
case socks.AtypDomainName:
|
||||
len := uint8(len(metadata.Host))
|
||||
host := []byte(metadata.Host)
|
||||
buf = [][]byte{{aType, len}, host, port}
|
||||
case socks.AtypIPv4:
|
||||
host := metadata.IP.To4()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
case socks.AtypIPv6:
|
||||
host := metadata.IP.To16()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
}
|
||||
return bytes.Join(buf, nil)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
type Socks5 struct {
|
||||
*Base
|
||||
addr string
|
||||
user string
|
||||
pass string
|
||||
tls bool
|
||||
skipCertVerify bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
type Socks5Option struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UserName string `proxy:"username,omitempty"`
|
||||
Password string `proxy:"password,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (ss *Socks5) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := net.DialTimeout("tcp", ss.addr, tcpTimeout)
|
||||
|
||||
if err == nil && ss.tls {
|
||||
cc := tls.Client(c, ss.tlsConfig)
|
||||
err = cc.Handshake()
|
||||
c = cc
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", ss.addr)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
if err := ss.shakeHand(metadata, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (ss *Socks5) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
buf := make([]byte, socks.MaxAddrLen)
|
||||
var err error
|
||||
|
||||
// VER, NMETHODS, METHODS
|
||||
if len(ss.user) > 0 {
|
||||
_, err = rw.Write([]byte{5, 1, 2})
|
||||
} else {
|
||||
_, err = rw.Write([]byte{5, 1, 0})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// VER, METHOD
|
||||
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buf[0] != 5 {
|
||||
return errors.New("SOCKS version error")
|
||||
}
|
||||
|
||||
if buf[1] == 2 {
|
||||
// password protocol version
|
||||
authMsg := &bytes.Buffer{}
|
||||
authMsg.WriteByte(1)
|
||||
authMsg.WriteByte(uint8(len(ss.user)))
|
||||
authMsg.WriteString(ss.user)
|
||||
authMsg.WriteByte(uint8(len(ss.pass)))
|
||||
authMsg.WriteString(ss.pass)
|
||||
|
||||
if _, err := rw.Write(authMsg.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:2]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return errors.New("rejected username/password")
|
||||
}
|
||||
} 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(metadata)}, []byte(""))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(rw, buf[:10]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSocks5(option Socks5Option) *Socks5 {
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: option.Server,
|
||||
}
|
||||
}
|
||||
|
||||
return &Socks5{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Socks5,
|
||||
},
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
tls: option.TLS,
|
||||
skipCertVerify: option.SkipCertVerify,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/picker"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type URLTest struct {
|
||||
*Base
|
||||
proxies []C.Proxy
|
||||
rawURL string
|
||||
fast C.Proxy
|
||||
interval time.Duration
|
||||
done chan struct{}
|
||||
once int32
|
||||
}
|
||||
|
||||
type URLTestOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Proxies []string `proxy:"proxies"`
|
||||
URL string `proxy:"url"`
|
||||
Interval int `proxy:"interval"`
|
||||
}
|
||||
|
||||
func (u *URLTest) Now() string {
|
||||
return u.fast.Name()
|
||||
}
|
||||
|
||||
func (u *URLTest) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
a, err := u.fast.Dial(metadata)
|
||||
if err != nil {
|
||||
u.fallback()
|
||||
}
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range u.proxies {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
sort.Strings(all)
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": u.Type().String(),
|
||||
"now": u.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *URLTest) Destroy() {
|
||||
u.done <- struct{}{}
|
||||
}
|
||||
|
||||
func (u *URLTest) loop() {
|
||||
tick := time.NewTicker(u.interval)
|
||||
go u.speedTest()
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
go u.speedTest()
|
||||
case <-u.done:
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *URLTest) fallback() {
|
||||
fast := u.proxies[0]
|
||||
min := fast.LastDelay()
|
||||
for _, proxy := range u.proxies[1:] {
|
||||
if !proxy.Alive() {
|
||||
continue
|
||||
}
|
||||
|
||||
delay := proxy.LastDelay()
|
||||
if delay < min {
|
||||
fast = proxy
|
||||
min = delay
|
||||
}
|
||||
}
|
||||
u.fast = fast
|
||||
}
|
||||
|
||||
func (u *URLTest) speedTest() {
|
||||
if !atomic.CompareAndSwapInt32(&u.once, 0, 1) {
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&u.once, 0)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(u.proxies))
|
||||
c := make(chan interface{})
|
||||
fast := picker.SelectFast(context.Background(), c)
|
||||
timer := time.NewTimer(u.interval)
|
||||
|
||||
for _, p := range u.proxies {
|
||||
go func(p C.Proxy) {
|
||||
_, err := p.URLTest(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(option URLTestOption, proxies []C.Proxy) (*URLTest, error) {
|
||||
_, err := urlToMetadata(option.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(proxies) < 1 {
|
||||
return nil, errors.New("The number of proxies cannot be 0")
|
||||
}
|
||||
|
||||
interval := time.Duration(option.Interval) * time.Second
|
||||
urlTest := &URLTest{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.URLTest,
|
||||
},
|
||||
proxies: proxies[:],
|
||||
rawURL: option.URL,
|
||||
fast: proxies[0],
|
||||
interval: interval,
|
||||
done: make(chan struct{}),
|
||||
once: 0,
|
||||
}
|
||||
go urlTest.loop()
|
||||
return urlTest, nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
tcpTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
globalClientSessionCache tls.ClientSessionCache
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func urlToMetadata(rawURL string) (addr C.Metadata, 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.Metadata{
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: u.Hostname(),
|
||||
IP: nil,
|
||||
Port: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
tcp.SetKeepAlive(true)
|
||||
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func getClientSessionCache() tls.ClientSessionCache {
|
||||
once.Do(func() {
|
||||
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
|
||||
})
|
||||
return globalClientSessionCache
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/vmess"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Vmess struct {
|
||||
*Base
|
||||
server string
|
||||
client *vmess.Client
|
||||
}
|
||||
|
||||
type VmessOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UUID string `proxy:"uuid"`
|
||||
AlterID int `proxy:"alterId"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
WSPath string `proxy:"ws-path,omitempty"`
|
||||
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Vmess) Dial(metadata *C.Metadata) (net.Conn, error) {
|
||||
c, err := net.DialTimeout("tcp", v.server, tcpTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error", v.server)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
c, err = v.client.New(c, parseVmessAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
security := strings.ToLower(option.Cipher)
|
||||
client, err := vmess.NewClient(vmess.Config{
|
||||
UUID: option.UUID,
|
||||
AlterID: uint16(option.AlterID),
|
||||
Security: security,
|
||||
TLS: option.TLS,
|
||||
HostName: option.Server,
|
||||
Port: strconv.Itoa(option.Port),
|
||||
NetWork: option.Network,
|
||||
WebSocketPath: option.WSPath,
|
||||
WebSocketHeaders: option.WSHeaders,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
SessionCacahe: getClientSessionCache(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Vmess{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Vmess,
|
||||
},
|
||||
server: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||
var addrType byte
|
||||
var addr []byte
|
||||
switch metadata.AddrType {
|
||||
case C.AtypIPv4:
|
||||
addrType = byte(vmess.AtypIPv4)
|
||||
addr = make([]byte, net.IPv4len)
|
||||
copy(addr[:], metadata.IP.To4())
|
||||
case C.AtypIPv6:
|
||||
addrType = byte(vmess.AtypIPv6)
|
||||
addr = make([]byte, net.IPv6len)
|
||||
copy(addr[:], metadata.IP.To16())
|
||||
case C.AtypDomainName:
|
||||
addrType = byte(vmess.AtypDomainName)
|
||||
addr = make([]byte, len(metadata.Host)+1)
|
||||
addr[0] = byte(len(metadata.Host))
|
||||
copy(addr[1:], []byte(metadata.Host))
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(metadata.Port)
|
||||
return &vmess.DstAddr{
|
||||
AddrType: addrType,
|
||||
Addr: addr,
|
||||
Port: uint(port),
|
||||
}
|
||||
}
|
||||
Vendored
-106
@@ -1,106 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache store element with a expired time
|
||||
type Cache struct {
|
||||
*cache
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
mapping sync.Map
|
||||
janitor *janitor
|
||||
}
|
||||
|
||||
type element struct {
|
||||
Expired time.Time
|
||||
Payload interface{}
|
||||
}
|
||||
|
||||
// Put element in Cache with its ttl
|
||||
func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) {
|
||||
c.mapping.Store(key, &element{
|
||||
Payload: payload,
|
||||
Expired: time.Now().Add(ttl),
|
||||
})
|
||||
}
|
||||
|
||||
// Get element in Cache, and drop when it expired
|
||||
func (c *cache) Get(key interface{}) interface{} {
|
||||
item, exist := c.mapping.Load(key)
|
||||
if !exist {
|
||||
return nil
|
||||
}
|
||||
elm := item.(*element)
|
||||
// expired
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return elm.Payload
|
||||
}
|
||||
|
||||
// GetWithExpire element in Cache with Expire Time
|
||||
func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired time.Time) {
|
||||
item, exist := c.mapping.Load(key)
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
elm := item.(*element)
|
||||
// expired
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
return
|
||||
}
|
||||
return elm.Payload, elm.Expired
|
||||
}
|
||||
|
||||
func (c *cache) cleanup() {
|
||||
c.mapping.Range(func(k, v interface{}) bool {
|
||||
key := k.(string)
|
||||
elm := v.(*element)
|
||||
if time.Since(elm.Expired) > 0 {
|
||||
c.mapping.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
type janitor struct {
|
||||
interval time.Duration
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (j *janitor) process(c *cache) {
|
||||
ticker := time.NewTicker(j.interval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.cleanup()
|
||||
case <-j.stop:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopJanitor(c *Cache) {
|
||||
c.janitor.stop <- struct{}{}
|
||||
}
|
||||
|
||||
// New return *Cache
|
||||
func New(interval time.Duration) *Cache {
|
||||
j := &janitor{
|
||||
interval: interval,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
c := &cache{janitor: j}
|
||||
go j.process(c)
|
||||
C := &Cache{c}
|
||||
runtime.SetFinalizer(C, stopJanitor)
|
||||
return C
|
||||
}
|
||||
Vendored
-70
@@ -1,70 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCache_Basic(t *testing.T) {
|
||||
interval := 200 * time.Millisecond
|
||||
ttl := 20 * time.Millisecond
|
||||
c := New(interval)
|
||||
c.Put("int", 1, ttl)
|
||||
c.Put("string", "a", ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
if i.(int) != 1 {
|
||||
t.Error("should recv 1")
|
||||
}
|
||||
|
||||
s := c.Get("string")
|
||||
if s.(string) != "a" {
|
||||
t.Error("should recv 'a'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_TTL(t *testing.T) {
|
||||
interval := 200 * time.Millisecond
|
||||
ttl := 20 * time.Millisecond
|
||||
c := New(interval)
|
||||
c.Put("int", 1, ttl)
|
||||
|
||||
i := c.Get("int")
|
||||
if i.(int) != 1 {
|
||||
t.Error("should recv 1")
|
||||
}
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i = c.Get("int")
|
||||
if i != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_AutoCleanup(t *testing.T) {
|
||||
interval := 10 * time.Millisecond
|
||||
ttl := 15 * time.Millisecond
|
||||
c := New(interval)
|
||||
c.Put("int", 1, ttl)
|
||||
|
||||
time.Sleep(ttl * 2)
|
||||
i := c.Get("int")
|
||||
if i != nil {
|
||||
t.Error("should recv nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCache_AutoGC(t *testing.T) {
|
||||
sign := make(chan struct{})
|
||||
go func() {
|
||||
interval := 10 * time.Millisecond
|
||||
ttl := 15 * time.Millisecond
|
||||
c := New(interval)
|
||||
c.Put("int", 1, ttl)
|
||||
sign <- struct{}{}
|
||||
}()
|
||||
|
||||
<-sign
|
||||
runtime.GC()
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package murmur3
|
||||
|
||||
type bmixer interface {
|
||||
bmix(p []byte) (tail []byte)
|
||||
Size() (n int)
|
||||
reset()
|
||||
}
|
||||
|
||||
type digest struct {
|
||||
clen int // Digested input cumulative length.
|
||||
tail []byte // 0 to Size()-1 bytes view of `buf'.
|
||||
buf [16]byte // Expected (but not required) to be Size() large.
|
||||
seed uint32 // Seed for initializing the hash.
|
||||
bmixer
|
||||
}
|
||||
|
||||
func (d *digest) BlockSize() int { return 1 }
|
||||
|
||||
func (d *digest) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
d.clen += n
|
||||
|
||||
if len(d.tail) > 0 {
|
||||
// Stick back pending bytes.
|
||||
nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1].
|
||||
if nfree < len(p) {
|
||||
// One full block can be formed.
|
||||
block := append(d.tail, p[:nfree]...)
|
||||
p = p[nfree:]
|
||||
_ = d.bmix(block) // No tail.
|
||||
} else {
|
||||
// Tail's buf is large enough to prevent reallocs.
|
||||
p = append(d.tail, p...)
|
||||
}
|
||||
}
|
||||
|
||||
d.tail = d.bmix(p)
|
||||
|
||||
// Keep own copy of the 0 to Size()-1 pending bytes.
|
||||
nn := copy(d.buf[:], d.tail)
|
||||
d.tail = d.buf[:nn]
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (d *digest) Reset() {
|
||||
d.clen = 0
|
||||
d.tail = nil
|
||||
d.bmixer.reset()
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package murmur3
|
||||
|
||||
// https://github.com/spaolacci/murmur3/blob/master/murmur32.go
|
||||
|
||||
import (
|
||||
"hash"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Make sure interfaces are correctly implemented.
|
||||
var (
|
||||
_ hash.Hash32 = new(digest32)
|
||||
_ bmixer = new(digest32)
|
||||
)
|
||||
|
||||
const (
|
||||
c1_32 uint32 = 0xcc9e2d51
|
||||
c2_32 uint32 = 0x1b873593
|
||||
)
|
||||
|
||||
// digest32 represents a partial evaluation of a 32 bites hash.
|
||||
type digest32 struct {
|
||||
digest
|
||||
h1 uint32 // Unfinalized running hash.
|
||||
}
|
||||
|
||||
// New32 returns new 32-bit hasher
|
||||
func New32() hash.Hash32 { return New32WithSeed(0) }
|
||||
|
||||
// New32WithSeed returns new 32-bit hasher set with explicit seed value
|
||||
func New32WithSeed(seed uint32) hash.Hash32 {
|
||||
d := new(digest32)
|
||||
d.seed = seed
|
||||
d.bmixer = d
|
||||
d.Reset()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *digest32) Size() int { return 4 }
|
||||
|
||||
func (d *digest32) reset() { d.h1 = d.seed }
|
||||
|
||||
func (d *digest32) Sum(b []byte) []byte {
|
||||
h := d.Sum32()
|
||||
return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h))
|
||||
}
|
||||
|
||||
// Digest as many blocks as possible.
|
||||
func (d *digest32) bmix(p []byte) (tail []byte) {
|
||||
h1 := d.h1
|
||||
|
||||
nblocks := len(p) / 4
|
||||
for i := 0; i < nblocks; i++ {
|
||||
k1 := *(*uint32)(unsafe.Pointer(&p[i*4]))
|
||||
|
||||
k1 *= c1_32
|
||||
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
|
||||
k1 *= c2_32
|
||||
|
||||
h1 ^= k1
|
||||
h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13)
|
||||
h1 = h1*4 + h1 + 0xe6546b64
|
||||
}
|
||||
d.h1 = h1
|
||||
return p[nblocks*d.Size():]
|
||||
}
|
||||
|
||||
func (d *digest32) Sum32() (h1 uint32) {
|
||||
|
||||
h1 = d.h1
|
||||
|
||||
var k1 uint32
|
||||
switch len(d.tail) & 3 {
|
||||
case 3:
|
||||
k1 ^= uint32(d.tail[2]) << 16
|
||||
fallthrough
|
||||
case 2:
|
||||
k1 ^= uint32(d.tail[1]) << 8
|
||||
fallthrough
|
||||
case 1:
|
||||
k1 ^= uint32(d.tail[0])
|
||||
k1 *= c1_32
|
||||
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
|
||||
k1 *= c2_32
|
||||
h1 ^= k1
|
||||
}
|
||||
|
||||
h1 ^= uint32(d.clen)
|
||||
|
||||
h1 ^= h1 >> 16
|
||||
h1 *= 0x85ebca6b
|
||||
h1 ^= h1 >> 13
|
||||
h1 *= 0xc2b2ae35
|
||||
h1 ^= h1 >> 16
|
||||
|
||||
return h1
|
||||
}
|
||||
|
||||
func Sum32(data []byte) uint32 { return Sum32WithSeed(data, 0) }
|
||||
|
||||
func Sum32WithSeed(data []byte, seed uint32) uint32 {
|
||||
h1 := seed
|
||||
|
||||
nblocks := len(data) / 4
|
||||
var p uintptr
|
||||
if len(data) > 0 {
|
||||
p = uintptr(unsafe.Pointer(&data[0]))
|
||||
}
|
||||
p1 := p + uintptr(4*nblocks)
|
||||
for ; p < p1; p += 4 {
|
||||
k1 := *(*uint32)(unsafe.Pointer(p))
|
||||
|
||||
k1 *= c1_32
|
||||
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
|
||||
k1 *= c2_32
|
||||
|
||||
h1 ^= k1
|
||||
h1 = (h1 << 13) | (h1 >> 19) // rotl32(h1, 13)
|
||||
h1 = h1*4 + h1 + 0xe6546b64
|
||||
}
|
||||
|
||||
tail := data[nblocks*4:]
|
||||
|
||||
var k1 uint32
|
||||
switch len(tail) & 3 {
|
||||
case 3:
|
||||
k1 ^= uint32(tail[2]) << 16
|
||||
fallthrough
|
||||
case 2:
|
||||
k1 ^= uint32(tail[1]) << 8
|
||||
fallthrough
|
||||
case 1:
|
||||
k1 ^= uint32(tail[0])
|
||||
k1 *= c1_32
|
||||
k1 = (k1 << 15) | (k1 >> 17) // rotl32(k1, 15)
|
||||
k1 *= c2_32
|
||||
h1 ^= k1
|
||||
}
|
||||
|
||||
h1 ^= uint32(len(data))
|
||||
|
||||
h1 ^= h1 >> 16
|
||||
h1 *= 0x85ebca6b
|
||||
h1 ^= h1 >> 13
|
||||
h1 *= 0xc2b2ae35
|
||||
h1 ^= h1 >> 16
|
||||
|
||||
return h1
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
package observable
|
||||
|
||||
type Iterable <-chan interface{}
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package picker
|
||||
|
||||
import "context"
|
||||
|
||||
func SelectFast(ctx context.Context, in <-chan interface{}) <-chan interface{} {
|
||||
out := make(chan interface{})
|
||||
go func() {
|
||||
select {
|
||||
case p, open := <-in:
|
||||
if open {
|
||||
out <- p
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
close(out)
|
||||
for range in {
|
||||
}
|
||||
}()
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package picker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sleepAndSend(delay int, in chan<- interface{}, input interface{}) {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
in <- input
|
||||
}
|
||||
|
||||
func sleepAndClose(delay int, in chan interface{}) {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
close(in)
|
||||
}
|
||||
|
||||
func TestPicker_Basic(t *testing.T) {
|
||||
in := make(chan interface{})
|
||||
fast := SelectFast(context.Background(), in)
|
||||
go sleepAndSend(20, in, 1)
|
||||
go sleepAndSend(30, in, 2)
|
||||
go sleepAndClose(40, in)
|
||||
|
||||
number, exist := <-fast
|
||||
if !exist || number != 1 {
|
||||
t.Error("should recv 1", exist, number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPicker_Timeout(t *testing.T) {
|
||||
in := make(chan interface{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*5)
|
||||
defer cancel()
|
||||
fast := SelectFast(ctx, in)
|
||||
go sleepAndSend(20, in, 1)
|
||||
go sleepAndClose(30, in)
|
||||
|
||||
_, exist := <-fast
|
||||
if exist {
|
||||
t.Error("should recv false")
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Queue is a simple concurrent safe queue
|
||||
type Queue struct {
|
||||
items []interface{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Put add the item to the queue.
|
||||
func (q *Queue) Put(items ...interface{}) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
q.lock.Lock()
|
||||
q.items = append(q.items, items...)
|
||||
q.lock.Unlock()
|
||||
}
|
||||
|
||||
// Pop returns the head of items.
|
||||
func (q *Queue) Pop() interface{} {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.lock.Lock()
|
||||
head := q.items[0]
|
||||
q.items = q.items[1:]
|
||||
q.lock.Unlock()
|
||||
return head
|
||||
}
|
||||
|
||||
// First returns the head of items without deleting.
|
||||
func (q *Queue) First() interface{} {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q.lock.RLock()
|
||||
head := q.items[0]
|
||||
q.lock.RUnlock()
|
||||
return head
|
||||
}
|
||||
|
||||
// Copy get the copy of queue.
|
||||
func (q *Queue) Copy() []interface{} {
|
||||
items := []interface{}{}
|
||||
q.lock.RLock()
|
||||
items = append(items, q.items...)
|
||||
q.lock.RUnlock()
|
||||
return items
|
||||
}
|
||||
|
||||
// Len returns the number of items in this queue.
|
||||
func (q *Queue) Len() int64 {
|
||||
q.lock.Lock()
|
||||
defer q.lock.Unlock()
|
||||
|
||||
return int64(len(q.items))
|
||||
}
|
||||
|
||||
// New is a constructor for a new concurrent safe queue.
|
||||
func New(hint int64) *Queue {
|
||||
return &Queue{
|
||||
items: make([]interface{}, 0, hint),
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
package structure
|
||||
|
||||
// references: https://github.com/mitchellh/mapstructure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Option is the configuration that is used to create a new decoder
|
||||
type Option struct {
|
||||
TagName string
|
||||
WeaklyTypedInput bool
|
||||
}
|
||||
|
||||
// Decoder is the core of structure
|
||||
type Decoder struct {
|
||||
option *Option
|
||||
}
|
||||
|
||||
// NewDecoder return a Decoder by Option
|
||||
func NewDecoder(option Option) *Decoder {
|
||||
if option.TagName == "" {
|
||||
option.TagName = "structure"
|
||||
}
|
||||
return &Decoder{option: &option}
|
||||
}
|
||||
|
||||
// Decode transform a map[string]interface{} to a struct
|
||||
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("Decode must recive a ptr struct")
|
||||
}
|
||||
t := reflect.TypeOf(dst).Elem()
|
||||
v := reflect.ValueOf(dst).Elem()
|
||||
for idx := 0; idx < v.NumField(); idx++ {
|
||||
field := t.Field(idx)
|
||||
|
||||
tag := field.Tag.Get(d.option.TagName)
|
||||
str := strings.SplitN(tag, ",", 2)
|
||||
key := str[0]
|
||||
omitempty := false
|
||||
if len(str) > 1 {
|
||||
omitempty = str[1] == "omitempty"
|
||||
}
|
||||
|
||||
value, ok := src[key]
|
||||
if !ok || value == nil {
|
||||
if omitempty {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("key '%s' missing", key)
|
||||
}
|
||||
|
||||
err := d.decode(key, value, v.Field(idx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
|
||||
switch val.Kind() {
|
||||
case reflect.Int:
|
||||
return d.decodeInt(name, data, val)
|
||||
case reflect.String:
|
||||
return d.decodeString(name, data, val)
|
||||
case reflect.Bool:
|
||||
return d.decodeBool(name, data, val)
|
||||
case reflect.Slice:
|
||||
return d.decodeSlice(name, data, val)
|
||||
case reflect.Map:
|
||||
return d.decodeMap(name, data, val)
|
||||
case reflect.Interface:
|
||||
return d.setInterface(name, data, val)
|
||||
default:
|
||||
return fmt.Errorf("type %s not support", val.Kind().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case kind == reflect.Int:
|
||||
val.SetInt(dataVal.Int())
|
||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||
var i int64
|
||||
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
|
||||
if err == nil {
|
||||
val.SetInt(i)
|
||||
} else {
|
||||
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
name, val.Type(), dataVal.Type(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case kind == reflect.String:
|
||||
val.SetString(dataVal.String())
|
||||
case kind == reflect.Int && d.option.WeaklyTypedInput:
|
||||
val.SetString(strconv.FormatInt(dataVal.Int(), 10))
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
name, val.Type(), dataVal.Type(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case kind == reflect.Bool:
|
||||
val.SetBool(dataVal.Bool())
|
||||
case kind == reflect.Int && d.option.WeaklyTypedInput:
|
||||
val.SetBool(dataVal.Int() != 0)
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
name, val.Type(), dataVal.Type(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
|
||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||
valType := val.Type()
|
||||
valElemType := valType.Elem()
|
||||
|
||||
if dataVal.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("'%s' is not a slice", name)
|
||||
}
|
||||
|
||||
valSlice := val
|
||||
for i := 0; i < dataVal.Len(); i++ {
|
||||
currentData := dataVal.Index(i).Interface()
|
||||
for valSlice.Len() <= i {
|
||||
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
||||
}
|
||||
currentField := valSlice.Index(i)
|
||||
|
||||
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
||||
if err := d.decode(fieldName, currentData, currentField); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
val.Set(valSlice)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
|
||||
valType := val.Type()
|
||||
valKeyType := valType.Key()
|
||||
valElemType := valType.Elem()
|
||||
|
||||
valMap := val
|
||||
|
||||
if valMap.IsNil() {
|
||||
mapType := reflect.MapOf(valKeyType, valElemType)
|
||||
valMap = reflect.MakeMap(mapType)
|
||||
}
|
||||
|
||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||
if dataVal.Kind() != reflect.Map {
|
||||
return fmt.Errorf("'%s' expected a map, got '%s'", name, dataVal.Kind())
|
||||
}
|
||||
|
||||
return d.decodeMapFromMap(name, dataVal, val, valMap)
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val reflect.Value, valMap reflect.Value) error {
|
||||
valType := val.Type()
|
||||
valKeyType := valType.Key()
|
||||
valElemType := valType.Elem()
|
||||
|
||||
errors := make([]string, 0)
|
||||
|
||||
if dataVal.Len() == 0 {
|
||||
if dataVal.IsNil() {
|
||||
if !val.IsNil() {
|
||||
val.Set(dataVal)
|
||||
}
|
||||
} else {
|
||||
val.Set(valMap)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, k := range dataVal.MapKeys() {
|
||||
fieldName := fmt.Sprintf("%s[%s]", name, k)
|
||||
|
||||
currentKey := reflect.Indirect(reflect.New(valKeyType))
|
||||
if err := d.decode(fieldName, k.Interface(), currentKey); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
v := dataVal.MapIndex(k).Interface()
|
||||
currentVal := reflect.Indirect(reflect.New(valElemType))
|
||||
if err := d.decode(fieldName, v, currentVal); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
valMap.SetMapIndex(currentKey, currentVal)
|
||||
}
|
||||
|
||||
val.Set(valMap)
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf(strings.Join(errors, ","))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
val.Set(dataVal)
|
||||
return nil
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package structure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var decoder = NewDecoder(Option{TagName: "test"})
|
||||
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
||||
|
||||
type Baz struct {
|
||||
Foo int `test:"foo"`
|
||||
Bar string `test:"bar"`
|
||||
}
|
||||
|
||||
type BazSlice struct {
|
||||
Foo int `test:"foo"`
|
||||
Bar []string `test:"bar"`
|
||||
}
|
||||
|
||||
type BazOptional struct {
|
||||
Foo int `test:"foo,omitempty"`
|
||||
Bar string `test:"bar,omitempty"`
|
||||
}
|
||||
|
||||
func TestStructure_Basic(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": "test",
|
||||
"extra": false,
|
||||
}
|
||||
|
||||
goal := &Baz{
|
||||
Foo: 1,
|
||||
Bar: "test",
|
||||
}
|
||||
|
||||
s := &Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_Slice(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": []string{"one", "two"},
|
||||
}
|
||||
|
||||
goal := &BazSlice{
|
||||
Foo: 1,
|
||||
Bar: []string{"one", "two"},
|
||||
}
|
||||
|
||||
s := &BazSlice{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_Optional(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": 1,
|
||||
}
|
||||
|
||||
goal := &BazOptional{
|
||||
Foo: 1,
|
||||
}
|
||||
|
||||
s := &BazOptional{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_MissingKey(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": 1,
|
||||
}
|
||||
|
||||
s := &Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_ParamError(t *testing.T) {
|
||||
rawMap := map[string]interface{}{}
|
||||
s := Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_SliceTypeError(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": 1,
|
||||
"bar": []int{1, 2},
|
||||
}
|
||||
|
||||
s := &BazSlice{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructure_WeakType(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
"foo": "1",
|
||||
"bar": []int{1},
|
||||
}
|
||||
|
||||
goal := &BazSlice{
|
||||
Foo: 1,
|
||||
Bar: []string{"1"},
|
||||
}
|
||||
|
||||
s := &BazSlice{}
|
||||
err := weakTypeDecoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
}
|
||||
|
||||
const (
|
||||
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
|
||||
)
|
||||
|
||||
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) {
|
||||
length := len(b)
|
||||
for i := 0; i < length; i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
n, err := to.write(b[i:end])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
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, 28)
|
||||
sessionID := make([]byte, 32)
|
||||
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)
|
||||
buf.WriteByte(0)
|
||||
binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
|
||||
buf.Write([]byte{0x03, 0x03})
|
||||
|
||||
// random with timestamp, sid len, sid
|
||||
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
|
||||
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.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
|
||||
|
||||
// session ticket
|
||||
buf.Write([]byte{0x00, 0x23})
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(data)))
|
||||
buf.Write(data)
|
||||
|
||||
// server name
|
||||
buf.Write([]byte{0x00, 0x00})
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
|
||||
buf.WriteByte(0)
|
||||
binary.Write(buf, binary.BigEndian, uint16(len(server)))
|
||||
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()
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
type SessionStatus = byte
|
||||
|
||||
const (
|
||||
SessionStatusNew SessionStatus = 0x01
|
||||
SessionStatusKeep SessionStatus = 0x02
|
||||
SessionStatusEnd SessionStatus = 0x03
|
||||
SessionStatusKeepAlive SessionStatus = 0x04
|
||||
)
|
||||
|
||||
const (
|
||||
OptionNone = byte(0x00)
|
||||
OptionData = byte(0x01)
|
||||
OptionError = byte(0x02)
|
||||
)
|
||||
|
||||
type MuxOption struct {
|
||||
ID [2]byte
|
||||
Port uint16
|
||||
Host string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Mux is an mux-compatible client for v2ray-plugin, not a complete implementation
|
||||
type Mux struct {
|
||||
net.Conn
|
||||
buf bytes.Buffer
|
||||
id [2]byte
|
||||
length [2]byte
|
||||
status [2]byte
|
||||
otb []byte
|
||||
remain int
|
||||
}
|
||||
|
||||
func (m *Mux) Read(b []byte) (int, error) {
|
||||
if m.remain != 0 {
|
||||
length := m.remain
|
||||
if len(b) < m.remain {
|
||||
length = len(b)
|
||||
}
|
||||
|
||||
n, err := m.Conn.Read(b[:length])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m.remain = m.remain - n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for {
|
||||
_, err := io.ReadFull(m.Conn, m.length[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
length := binary.BigEndian.Uint16(m.length[:])
|
||||
if length > 512 {
|
||||
return 0, errors.New("invalid metalen")
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(m.Conn, m.id[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
_, err = m.Conn.Read(m.status[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
opcode := m.status[0]
|
||||
if opcode == SessionStatusKeepAlive {
|
||||
continue
|
||||
}
|
||||
|
||||
opts := m.status[1]
|
||||
|
||||
if opts != OptionData {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = io.ReadFull(m.Conn, m.length[:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
dataLen := int(binary.BigEndian.Uint16(m.length[:]))
|
||||
m.remain = dataLen
|
||||
if dataLen > len(b) {
|
||||
dataLen = len(b)
|
||||
}
|
||||
|
||||
n, err := m.Conn.Read(b[:dataLen])
|
||||
m.remain -= n
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mux) Write(b []byte) (int, error) {
|
||||
if m.otb != nil {
|
||||
// create a sub connection
|
||||
if _, err := m.Conn.Write(m.otb); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m.otb = nil
|
||||
}
|
||||
m.buf.Reset()
|
||||
binary.Write(&m.buf, binary.BigEndian, uint16(4))
|
||||
m.buf.Write(m.id[:])
|
||||
m.buf.WriteByte(SessionStatusKeep)
|
||||
m.buf.WriteByte(OptionData)
|
||||
binary.Write(&m.buf, binary.BigEndian, uint16(len(b)))
|
||||
m.buf.Write(b)
|
||||
|
||||
return m.Conn.Write(m.buf.Bytes())
|
||||
}
|
||||
|
||||
func (m *Mux) Close() error {
|
||||
_, err := m.Conn.Write([]byte{0x0, 0x4, m.id[0], m.id[1], SessionStatusEnd, OptionNone})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.Conn.Close()
|
||||
}
|
||||
|
||||
func NewMux(conn net.Conn, option MuxOption) *Mux {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// fill empty length
|
||||
buf.Write([]byte{0x0, 0x0})
|
||||
buf.Write(option.ID[:])
|
||||
buf.WriteByte(SessionStatusNew)
|
||||
buf.WriteByte(OptionNone)
|
||||
|
||||
// tcp
|
||||
netType := byte(0x1)
|
||||
if option.Type == "udp" {
|
||||
netType = byte(0x2)
|
||||
}
|
||||
buf.WriteByte(netType)
|
||||
|
||||
// port
|
||||
binary.Write(buf, binary.BigEndian, option.Port)
|
||||
|
||||
// address
|
||||
ip := net.ParseIP(option.Host)
|
||||
if ip == nil {
|
||||
buf.WriteByte(0x2)
|
||||
buf.WriteString(option.Host)
|
||||
} else if ipv4 := ip.To4(); ipv4 != nil {
|
||||
buf.WriteByte(0x1)
|
||||
buf.Write(ipv4)
|
||||
} else {
|
||||
buf.WriteByte(0x3)
|
||||
buf.Write(ip.To16())
|
||||
}
|
||||
|
||||
metadata := buf.Bytes()
|
||||
binary.BigEndian.PutUint16(metadata[:2], uint16(len(metadata)-2))
|
||||
|
||||
return &Mux{
|
||||
Conn: conn,
|
||||
id: option.ID,
|
||||
otb: metadata,
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package obfs
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/component/vmess"
|
||||
)
|
||||
|
||||
// WebsocketOption is options of websocket obfs
|
||||
type WebsocketOption struct {
|
||||
Host string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewWebsocketObfs return a HTTPObfs
|
||||
func NewWebsocketObfs(conn net.Conn, option *WebsocketOption) (net.Conn, error) {
|
||||
config := &vmess.WebsocketConfig{
|
||||
Host: option.Host,
|
||||
Path: option.Path,
|
||||
TLS: option.TLSConfig != nil,
|
||||
Headers: option.Headers,
|
||||
TLSConfig: option.TLSConfig,
|
||||
}
|
||||
|
||||
var err error
|
||||
conn, err = vmess.NewWebsocketConn(conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn = NewMux(conn, MuxOption{
|
||||
ID: [2]byte{0, 0},
|
||||
Host: "127.0.0.1",
|
||||
Port: 0,
|
||||
})
|
||||
return conn, nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
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
|
||||
|
||||
received bool
|
||||
}
|
||||
|
||||
func (vc *Conn) Write(b []byte) (int, error) {
|
||||
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 := time.Now()
|
||||
|
||||
h := hmac.New(md5.New, vc.id.UUID.Bytes())
|
||||
binary.Write(h, binary.BigEndian, uint64(timestamp.Unix()))
|
||||
_, 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(timestamp))
|
||||
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.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, error) {
|
||||
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[:])
|
||||
}
|
||||
|
||||
c := &Conn{
|
||||
Conn: conn,
|
||||
id: id,
|
||||
dst: dst,
|
||||
reqBodyIV: reqBodyIV,
|
||||
reqBodyKey: reqBodyKey,
|
||||
respV: respV,
|
||||
respBodyIV: respBodyIV[:],
|
||||
respBodyKey: respBodyKey[:],
|
||||
reader: reader,
|
||||
writer: writer,
|
||||
security: security,
|
||||
}
|
||||
if err := c.sendRequest(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
}
|
||||
alterIDs = append(alterIDs, primary)
|
||||
return alterIDs
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"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 (
|
||||
clientSessionCache tls.ClientSessionCache
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// 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
|
||||
host string
|
||||
wsConfig *WebsocketConfig
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// Config of vmess
|
||||
type Config struct {
|
||||
UUID string
|
||||
AlterID uint16
|
||||
Security string
|
||||
TLS bool
|
||||
HostName string
|
||||
Port string
|
||||
NetWork string
|
||||
WebSocketPath string
|
||||
WebSocketHeaders map[string]string
|
||||
SkipCertVerify bool
|
||||
SessionCacahe tls.ClientSessionCache
|
||||
}
|
||||
|
||||
// New return a Conn with net.Conn and DstAddr
|
||||
func (c *Client) New(conn net.Conn, dst *DstAddr) (net.Conn, error) {
|
||||
var err error
|
||||
r := rand.Intn(len(c.user))
|
||||
if c.wsConfig != nil {
|
||||
conn, err = NewWebsocketConn(conn, c.wsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if c.tls {
|
||||
conn = tls.Client(conn, c.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)
|
||||
}
|
||||
|
||||
if config.NetWork != "" && config.NetWork != "ws" {
|
||||
return nil, fmt.Errorf("Unknown network type: %s", config.NetWork)
|
||||
}
|
||||
|
||||
host := net.JoinHostPort(config.HostName, config.Port)
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if config.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
ServerName: config.HostName,
|
||||
InsecureSkipVerify: config.SkipCertVerify,
|
||||
ClientSessionCache: config.SessionCacahe,
|
||||
}
|
||||
if tlsConfig.ClientSessionCache == nil {
|
||||
tlsConfig.ClientSessionCache = getClientSessionCache()
|
||||
}
|
||||
}
|
||||
|
||||
var wsConfig *WebsocketConfig
|
||||
if config.NetWork == "ws" {
|
||||
wsConfig = &WebsocketConfig{
|
||||
Host: host,
|
||||
Path: config.WebSocketPath,
|
||||
Headers: config.WebSocketHeaders,
|
||||
TLS: config.TLS,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
user: newAlterIDs(newID(&uid), config.AlterID),
|
||||
uuid: &uid,
|
||||
security: security,
|
||||
tls: config.TLS,
|
||||
host: host,
|
||||
wsConfig: wsConfig,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getClientSessionCache() tls.ClientSessionCache {
|
||||
once.Do(func() {
|
||||
clientSessionCache = tls.NewLRUClientSessionCache(128)
|
||||
})
|
||||
return clientSessionCache
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package vmess
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type websocketConn struct {
|
||||
conn *websocket.Conn
|
||||
reader io.Reader
|
||||
remoteAddr net.Addr
|
||||
}
|
||||
|
||||
type WebsocketConfig struct {
|
||||
Host string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// Read implements net.Conn.Read()
|
||||
func (wsc *websocketConn) Read(b []byte) (int, error) {
|
||||
for {
|
||||
reader, err := wsc.getReader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
nBytes, err := reader.Read(b)
|
||||
if err == io.EOF {
|
||||
wsc.reader = nil
|
||||
continue
|
||||
}
|
||||
return nBytes, err
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer.
|
||||
func (wsc *websocketConn) Write(b []byte) (int, error) {
|
||||
if err := wsc.conn.WriteMessage(websocket.BinaryMessage, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) Close() error {
|
||||
var errors []string
|
||||
if err := wsc.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Second*5)); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
if err := wsc.conn.Close(); err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("Failed to close connection: %s", strings.Join(errors, ","))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) getReader() (io.Reader, error) {
|
||||
if wsc.reader != nil {
|
||||
return wsc.reader, nil
|
||||
}
|
||||
|
||||
_, reader, err := wsc.conn.NextReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wsc.reader = reader
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) LocalAddr() net.Addr {
|
||||
return wsc.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) RemoteAddr() net.Addr {
|
||||
return wsc.remoteAddr
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetDeadline(t time.Time) error {
|
||||
if err := wsc.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return wsc.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetReadDeadline(t time.Time) error {
|
||||
return wsc.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (wsc *websocketConn) SetWriteDeadline(t time.Time) error {
|
||||
return wsc.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func NewWebsocketConn(conn net.Conn, c *WebsocketConfig) (net.Conn, error) {
|
||||
dialer := &websocket.Dialer{
|
||||
NetDial: func(network, addr string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
},
|
||||
ReadBufferSize: 4 * 1024,
|
||||
WriteBufferSize: 4 * 1024,
|
||||
HandshakeTimeout: time.Second * 8,
|
||||
}
|
||||
|
||||
scheme := "ws"
|
||||
if c.TLS {
|
||||
scheme = "wss"
|
||||
dialer.TLSClientConfig = c.TLSConfig
|
||||
}
|
||||
|
||||
host, port, _ := net.SplitHostPort(c.Host)
|
||||
if (scheme == "ws" && port != "80") || (scheme == "wss" && port != "443") {
|
||||
host = c.Host
|
||||
}
|
||||
|
||||
uri := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: c.Path,
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
if c.Headers != nil {
|
||||
for k, v := range c.Headers {
|
||||
headers.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
wsConn, resp, err := dialer.Dial(uri.String(), headers)
|
||||
if err != nil {
|
||||
var reason string
|
||||
if resp != nil {
|
||||
reason = resp.Status
|
||||
}
|
||||
return nil, fmt.Errorf("Dial %s error: %s", host, reason)
|
||||
}
|
||||
|
||||
return &websocketConn{
|
||||
conn: wsConn,
|
||||
remoteAddr: conn.RemoteAddr(),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
R "github.com/Dreamacro/clash/rules"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// General config
|
||||
type General struct {
|
||||
Port int `json:"port"`
|
||||
SocksPort int `json:"socks-port"`
|
||||
RedirPort int `json:"redir-port"`
|
||||
AllowLan bool `json:"allow-lan"`
|
||||
Mode T.Mode `json:"mode"`
|
||||
LogLevel log.LogLevel `json:"log-level"`
|
||||
ExternalController string `json:"-"`
|
||||
ExternalUI string `json:"-"`
|
||||
Secret string `json:"-"`
|
||||
}
|
||||
|
||||
// DNS config
|
||||
type DNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []dns.NameServer `yaml:"nameserver"`
|
||||
Fallback []dns.NameServer `yaml:"fallback"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
}
|
||||
|
||||
// Config is clash config manager
|
||||
type Config struct {
|
||||
General *General
|
||||
DNS *DNS
|
||||
Rules []C.Rule
|
||||
Proxies map[string]C.Proxy
|
||||
}
|
||||
|
||||
type rawDNS struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
IPv6 bool `yaml:"ipv6"`
|
||||
NameServer []string `yaml:"nameserver"`
|
||||
Fallback []string `yaml:"fallback"`
|
||||
Listen string `yaml:"listen"`
|
||||
EnhancedMode dns.EnhancedMode `yaml:"enhanced-mode"`
|
||||
}
|
||||
|
||||
type rawConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
SocksPort int `yaml:"socks-port"`
|
||||
RedirPort int `yaml:"redir-port"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
Mode T.Mode `yaml:"mode"`
|
||||
LogLevel log.LogLevel `yaml:"log-level"`
|
||||
ExternalController string `yaml:"external-controller"`
|
||||
ExternalUI string `yaml:"external-ui"`
|
||||
Secret string `yaml:"secret"`
|
||||
|
||||
DNS rawDNS `yaml:"dns"`
|
||||
Proxy []map[string]interface{} `yaml:"Proxy"`
|
||||
ProxyGroup []map[string]interface{} `yaml:"Proxy Group"`
|
||||
Rule []string `yaml:"Rule"`
|
||||
}
|
||||
|
||||
func readConfig(path string) (*rawConfig, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("Configuration file %s is empty", C.Path.Config())
|
||||
}
|
||||
|
||||
// config with some default value
|
||||
rawConfig := &rawConfig{
|
||||
AllowLan: false,
|
||||
Mode: T.Rule,
|
||||
LogLevel: log.INFO,
|
||||
Rule: []string{},
|
||||
Proxy: []map[string]interface{}{},
|
||||
ProxyGroup: []map[string]interface{}{},
|
||||
DNS: rawDNS{
|
||||
Enable: false,
|
||||
},
|
||||
}
|
||||
err = yaml.Unmarshal([]byte(data), &rawConfig)
|
||||
return rawConfig, err
|
||||
}
|
||||
|
||||
// Parse config
|
||||
func Parse(path string) (*Config, error) {
|
||||
config := &Config{}
|
||||
|
||||
rawCfg, err := readConfig(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
general, err := parseGeneral(rawCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.General = general
|
||||
|
||||
proxies, err := parseProxies(rawCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Proxies = proxies
|
||||
|
||||
rules, err := parseRules(rawCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Rules = rules
|
||||
|
||||
dnsCfg, err := parseDNS(rawCfg.DNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.DNS = dnsCfg
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func parseGeneral(cfg *rawConfig) (*General, error) {
|
||||
port := cfg.Port
|
||||
socksPort := cfg.SocksPort
|
||||
redirPort := cfg.RedirPort
|
||||
allowLan := cfg.AllowLan
|
||||
externalController := cfg.ExternalController
|
||||
externalUI := cfg.ExternalUI
|
||||
secret := cfg.Secret
|
||||
mode := cfg.Mode
|
||||
logLevel := cfg.LogLevel
|
||||
|
||||
if externalUI != "" {
|
||||
if !filepath.IsAbs(externalUI) {
|
||||
externalUI = filepath.Join(C.Path.HomeDir(), externalUI)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
|
||||
}
|
||||
}
|
||||
|
||||
general := &General{
|
||||
Port: port,
|
||||
SocksPort: socksPort,
|
||||
RedirPort: redirPort,
|
||||
AllowLan: allowLan,
|
||||
Mode: mode,
|
||||
LogLevel: logLevel,
|
||||
ExternalController: externalController,
|
||||
ExternalUI: externalUI,
|
||||
Secret: secret,
|
||||
}
|
||||
return general, nil
|
||||
}
|
||||
|
||||
func parseProxies(cfg *rawConfig) (map[string]C.Proxy, error) {
|
||||
proxies := make(map[string]C.Proxy)
|
||||
proxiesConfig := cfg.Proxy
|
||||
groupsConfig := cfg.ProxyGroup
|
||||
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||
|
||||
proxies["DIRECT"] = adapters.NewProxy(adapters.NewDirect())
|
||||
proxies["REJECT"] = adapters.NewProxy(adapters.NewReject())
|
||||
|
||||
// parse proxy
|
||||
for idx, mapping := range proxiesConfig {
|
||||
proxyType, existType := mapping["type"].(string)
|
||||
if !existType {
|
||||
return nil, fmt.Errorf("Proxy %d missing type", idx)
|
||||
}
|
||||
|
||||
var proxy C.ProxyAdapter
|
||||
err := fmt.Errorf("can't parse")
|
||||
switch proxyType {
|
||||
case "ss":
|
||||
ssOption := &adapters.ShadowSocksOption{}
|
||||
err = decoder.Decode(mapping, ssOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = adapters.NewShadowSocks(*ssOption)
|
||||
case "socks5":
|
||||
socksOption := &adapters.Socks5Option{}
|
||||
err = decoder.Decode(mapping, socksOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = adapters.NewSocks5(*socksOption)
|
||||
case "http":
|
||||
httpOption := &adapters.HttpOption{}
|
||||
err = decoder.Decode(mapping, httpOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = adapters.NewHttp(*httpOption)
|
||||
case "vmess":
|
||||
vmessOption := &adapters.VmessOption{}
|
||||
err = decoder.Decode(mapping, vmessOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = adapters.NewVmess(*vmessOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Proxy [%d]: %s", idx, err.Error())
|
||||
}
|
||||
|
||||
if _, exist := proxies[proxy.Name()]; exist {
|
||||
return nil, fmt.Errorf("Proxy %s is the duplicate name", proxy.Name())
|
||||
}
|
||||
proxies[proxy.Name()] = adapters.NewProxy(proxy)
|
||||
}
|
||||
|
||||
// parse proxy group
|
||||
for idx, mapping := range groupsConfig {
|
||||
groupType, existType := mapping["type"].(string)
|
||||
groupName, existName := mapping["name"].(string)
|
||||
if !existType && existName {
|
||||
return nil, fmt.Errorf("ProxyGroup %d: missing type or name", idx)
|
||||
}
|
||||
|
||||
if _, exist := proxies[groupName]; exist {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: the duplicate name", groupName)
|
||||
}
|
||||
var group C.ProxyAdapter
|
||||
ps := []C.Proxy{}
|
||||
|
||||
err := fmt.Errorf("can't parse")
|
||||
switch groupType {
|
||||
case "url-test":
|
||||
urlTestOption := &adapters.URLTestOption{}
|
||||
err = decoder.Decode(mapping, urlTestOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ps, err = getProxies(proxies, urlTestOption.Proxies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
||||
}
|
||||
group, err = adapters.NewURLTest(*urlTestOption, ps)
|
||||
case "select":
|
||||
selectorOption := &adapters.SelectorOption{}
|
||||
err = decoder.Decode(mapping, selectorOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ps, err = getProxies(proxies, selectorOption.Proxies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
||||
}
|
||||
group, err = adapters.NewSelector(selectorOption.Name, ps)
|
||||
case "fallback":
|
||||
fallbackOption := &adapters.FallbackOption{}
|
||||
err = decoder.Decode(mapping, fallbackOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ps, err = getProxies(proxies, fallbackOption.Proxies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
||||
}
|
||||
group, err = adapters.NewFallback(*fallbackOption, ps)
|
||||
case "load-balance":
|
||||
loadBalanceOption := &adapters.LoadBalanceOption{}
|
||||
err = decoder.Decode(mapping, loadBalanceOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
ps, err = getProxies(proxies, loadBalanceOption.Proxies)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ProxyGroup %s: %s", groupName, err.Error())
|
||||
}
|
||||
group, err = adapters.NewLoadBalance(*loadBalanceOption, ps)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Proxy %s: %s", groupName, err.Error())
|
||||
}
|
||||
proxies[groupName] = adapters.NewProxy(group)
|
||||
}
|
||||
|
||||
ps := []C.Proxy{}
|
||||
for _, v := range proxies {
|
||||
ps = append(ps, v)
|
||||
}
|
||||
|
||||
global, _ := adapters.NewSelector("GLOBAL", ps)
|
||||
proxies["GLOBAL"] = adapters.NewProxy(global)
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func parseRules(cfg *rawConfig) ([]C.Rule, error) {
|
||||
rules := []C.Rule{}
|
||||
|
||||
rulesConfig := cfg.Rule
|
||||
// parse rules
|
||||
for idx, line := range rulesConfig {
|
||||
rule := trimArr(strings.Split(line, ","))
|
||||
var (
|
||||
payload string
|
||||
target string
|
||||
)
|
||||
|
||||
switch len(rule) {
|
||||
case 2:
|
||||
target = rule[1]
|
||||
case 3:
|
||||
payload = rule[1]
|
||||
target = rule[2]
|
||||
default:
|
||||
return nil, fmt.Errorf("Rules[%d] [%s] error: format invalid", idx, line)
|
||||
}
|
||||
|
||||
rule = trimArr(rule)
|
||||
var parsed C.Rule
|
||||
switch rule[0] {
|
||||
case "DOMAIN":
|
||||
parsed = R.NewDomain(payload, target)
|
||||
case "DOMAIN-SUFFIX":
|
||||
parsed = R.NewDomainSuffix(payload, target)
|
||||
case "DOMAIN-KEYWORD":
|
||||
parsed = R.NewDomainKeyword(payload, target)
|
||||
case "GEOIP":
|
||||
parsed = R.NewGEOIP(payload, target)
|
||||
case "IP-CIDR", "IP-CIDR6":
|
||||
if rule := R.NewIPCIDR(payload, target, false); rule != nil {
|
||||
parsed = rule
|
||||
}
|
||||
case "SOURCE-IP-CIDR":
|
||||
if rule := R.NewIPCIDR(payload, target, true); rule != nil {
|
||||
parsed = rule
|
||||
}
|
||||
case "MATCH":
|
||||
fallthrough
|
||||
case "FINAL":
|
||||
parsed = R.NewMatch(target)
|
||||
}
|
||||
|
||||
if parsed == nil {
|
||||
return nil, fmt.Errorf("Rules[%d] [%s] error: payload invalid", idx, line)
|
||||
}
|
||||
|
||||
rules = append(rules, parsed)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func hostWithDefaultPort(host string, defPort string) (string, error) {
|
||||
if !strings.Contains(host, ":") {
|
||||
host += ":"
|
||||
}
|
||||
|
||||
hostname, port, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
port = defPort
|
||||
}
|
||||
|
||||
return net.JoinHostPort(hostname, port), nil
|
||||
}
|
||||
|
||||
func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
||||
nameservers := []dns.NameServer{}
|
||||
|
||||
for idx, server := range servers {
|
||||
// parse without scheme .e.g 8.8.8.8:53
|
||||
if !strings.Contains(server, "://") {
|
||||
server = "udp://" + server
|
||||
}
|
||||
u, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
||||
}
|
||||
|
||||
var host, dnsNetType string
|
||||
switch u.Scheme {
|
||||
case "udp":
|
||||
host, err = hostWithDefaultPort(u.Host, "53")
|
||||
dnsNetType = "" // UDP
|
||||
case "tcp":
|
||||
host, err = hostWithDefaultPort(u.Host, "53")
|
||||
dnsNetType = "tcp" // TCP
|
||||
case "tls":
|
||||
host, err = hostWithDefaultPort(u.Host, "853")
|
||||
dnsNetType = "tcp-tls" // DNS over TLS
|
||||
default:
|
||||
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
||||
}
|
||||
|
||||
nameservers = append(
|
||||
nameservers,
|
||||
dns.NameServer{
|
||||
Net: dnsNetType,
|
||||
Addr: host,
|
||||
},
|
||||
)
|
||||
}
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
func parseDNS(cfg rawDNS) (*DNS, error) {
|
||||
if cfg.Enable && len(cfg.NameServer) == 0 {
|
||||
return nil, fmt.Errorf("If DNS configuration is turned on, NameServer cannot be empty")
|
||||
}
|
||||
|
||||
dnsCfg := &DNS{
|
||||
Enable: cfg.Enable,
|
||||
Listen: cfg.Listen,
|
||||
EnhancedMode: cfg.EnhancedMode,
|
||||
}
|
||||
var err error
|
||||
if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if dnsCfg.Fallback, err = parseNameServer(cfg.Fallback); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dnsCfg, nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"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(dir string) error {
|
||||
// initial homedir
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
return fmt.Errorf("Can't create config directory %s: %s", dir, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// initial config.yml
|
||||
if _, err := os.Stat(C.Path.Config()); os.IsNotExist(err) {
|
||||
log.Info("Can't find config, create a empty file")
|
||||
os.OpenFile(C.Path.Config(), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
// initial mmdb
|
||||
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
|
||||
log.Info("Can't find MMDB, start download")
|
||||
err := downloadMMDB(C.Path.MMDB())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Can't download MMDB: %s", err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
r = append(r, strings.Trim(e, " "))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
|
||||
var ps []C.Proxy
|
||||
for _, name := range list {
|
||||
p, ok := mapping[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' not found", name)
|
||||
}
|
||||
ps = append(ps, p)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func or(pointers ...*int) *int {
|
||||
for _, p := range pointers {
|
||||
if p != nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return pointers[len(pointers)-1]
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Adapter Type
|
||||
const (
|
||||
Direct AdapterType = iota
|
||||
Fallback
|
||||
Reject
|
||||
Selector
|
||||
Shadowsocks
|
||||
Socks5
|
||||
Http
|
||||
URLTest
|
||||
Vmess
|
||||
LoadBalance
|
||||
)
|
||||
|
||||
type ServerAdapter interface {
|
||||
Metadata() *Metadata
|
||||
Close()
|
||||
}
|
||||
|
||||
type ProxyAdapter interface {
|
||||
Name() string
|
||||
Type() AdapterType
|
||||
Dial(metadata *Metadata) (net.Conn, error)
|
||||
Destroy()
|
||||
MarshalJSON() ([]byte, error)
|
||||
}
|
||||
|
||||
type DelayHistory struct {
|
||||
Time time.Time `json:"time"`
|
||||
Delay uint16 `json:"delay"`
|
||||
}
|
||||
|
||||
type Proxy interface {
|
||||
ProxyAdapter
|
||||
Alive() bool
|
||||
DelayHistory() []DelayHistory
|
||||
LastDelay() uint16
|
||||
URLTest(url string) (uint16, error)
|
||||
}
|
||||
|
||||
// AdapterType is enum of adapter type
|
||||
type AdapterType int
|
||||
|
||||
func (at AdapterType) String() string {
|
||||
switch at {
|
||||
case Direct:
|
||||
return "Direct"
|
||||
case Fallback:
|
||||
return "Fallback"
|
||||
case Reject:
|
||||
return "Reject"
|
||||
case Selector:
|
||||
return "Selector"
|
||||
case Shadowsocks:
|
||||
return "Shadowsocks"
|
||||
case Socks5:
|
||||
return "Socks5"
|
||||
case Http:
|
||||
return "Http"
|
||||
case URLTest:
|
||||
return "URLTest"
|
||||
case Vmess:
|
||||
return "Vmess"
|
||||
case LoadBalance:
|
||||
return "LoadBalance"
|
||||
default:
|
||||
return "Unknow"
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Socks addr type
|
||||
const (
|
||||
AtypIPv4 = 1
|
||||
AtypDomainName = 3
|
||||
AtypIPv6 = 4
|
||||
|
||||
TCP NetWork = iota
|
||||
UDP
|
||||
|
||||
HTTP SourceType = iota
|
||||
SOCKS
|
||||
REDIR
|
||||
)
|
||||
|
||||
type NetWork int
|
||||
|
||||
func (n *NetWork) String() string {
|
||||
if *n == TCP {
|
||||
return "tcp"
|
||||
}
|
||||
return "udp"
|
||||
}
|
||||
|
||||
type SourceType int
|
||||
|
||||
// Metadata is used to store connection address
|
||||
type Metadata struct {
|
||||
NetWork NetWork
|
||||
Source SourceType
|
||||
SourceIP *net.IP
|
||||
AddrType int
|
||||
Host string
|
||||
IP *net.IP
|
||||
Port string
|
||||
}
|
||||
|
||||
func (m *Metadata) String() string {
|
||||
if m.Host == "" {
|
||||
return m.IP.String()
|
||||
}
|
||||
return m.Host
|
||||
}
|
||||
|
||||
func (m *Metadata) Valid() bool {
|
||||
return m.Host != "" || m.IP != nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
P "path"
|
||||
)
|
||||
|
||||
const Name = "clash"
|
||||
|
||||
// Path is used to get the configuration path
|
||||
var Path *path
|
||||
|
||||
type path struct {
|
||||
homedir string
|
||||
}
|
||||
|
||||
func init() {
|
||||
currentUser, err := user.Current()
|
||||
var homedir string
|
||||
if err != nil {
|
||||
dir := os.Getenv("HOME")
|
||||
if dir == "" {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
homedir = dir
|
||||
} else {
|
||||
homedir = currentUser.HomeDir
|
||||
}
|
||||
|
||||
homedir = P.Join(homedir, ".config", Name)
|
||||
Path = &path{homedir: homedir}
|
||||
}
|
||||
|
||||
// SetHomeDir is used to set the configuration path
|
||||
func SetHomeDir(root string) {
|
||||
Path = &path{homedir: root}
|
||||
}
|
||||
|
||||
func (p *path) HomeDir() string {
|
||||
return p.homedir
|
||||
}
|
||||
|
||||
func (p *path) Config() string {
|
||||
return P.Join(p.homedir, "config.yml")
|
||||
}
|
||||
|
||||
func (p *path) MMDB() string {
|
||||
return P.Join(p.homedir, "Country.mmdb")
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package constant
|
||||
|
||||
// Rule Type
|
||||
const (
|
||||
Domain RuleType = iota
|
||||
DomainSuffix
|
||||
DomainKeyword
|
||||
GEOIP
|
||||
IPCIDR
|
||||
SourceIPCIDR
|
||||
MATCH
|
||||
)
|
||||
|
||||
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 SourceIPCIDR:
|
||||
return "SourceIPCIDR"
|
||||
case MATCH:
|
||||
return "MATCH"
|
||||
default:
|
||||
return "Unknow"
|
||||
}
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
RuleType() RuleType
|
||||
IsMatch(metadata *Metadata) bool
|
||||
Adapter() string
|
||||
Payload() string
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
Version = "unknown version"
|
||||
BuildTime = "unknown time"
|
||||
)
|
||||
-260
@@ -1,260 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/common/picker"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
geoip2 "github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
var (
|
||||
globalSessionCache = tls.NewLRUClientSessionCache(64)
|
||||
|
||||
mmdb *geoip2.Reader
|
||||
once sync.Once
|
||||
resolver *Resolver
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
ipv6 bool
|
||||
mapping bool
|
||||
fallback []*nameserver
|
||||
main []*nameserver
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Msg *D.Msg
|
||||
Error error
|
||||
}
|
||||
|
||||
func isIPRequest(q D.Question) bool {
|
||||
if q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Resolver) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
||||
if len(m.Question) == 0 {
|
||||
return nil, errors.New("should have one question at least")
|
||||
}
|
||||
|
||||
q := m.Question[0]
|
||||
cache, expireTime := r.cache.GetWithExpire(q.String())
|
||||
if cache != nil {
|
||||
msg = cache.(*D.Msg).Copy()
|
||||
setMsgTTL(msg, uint32(expireTime.Sub(time.Now()).Seconds()))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if msg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
putMsgToCache(r.cache, q.String(), msg)
|
||||
if r.mapping {
|
||||
ips := r.msgToIP(msg)
|
||||
for _, ip := range ips {
|
||||
putMsgToCache(r.cache, ip.String(), msg)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
isIPReq := isIPRequest(q)
|
||||
if isIPReq {
|
||||
msg, err = r.resolveIP(m)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err = r.exchange(r.main, m)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resolver) exchange(servers []*nameserver, m *D.Msg) (msg *D.Msg, err error) {
|
||||
in := make(chan interface{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
fast := picker.SelectFast(ctx, in)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(servers))
|
||||
for _, server := range servers {
|
||||
go func(s *nameserver) {
|
||||
defer wg.Done()
|
||||
msg, _, err := s.Client.Exchange(m, s.Address)
|
||||
if err != nil || msg.Rcode != D.RcodeSuccess {
|
||||
return
|
||||
}
|
||||
in <- &result{Msg: msg, Error: err}
|
||||
}(server)
|
||||
}
|
||||
|
||||
// release in channel
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(in)
|
||||
}()
|
||||
|
||||
elm, exist := <-fast
|
||||
if !exist {
|
||||
return nil, errors.New("All DNS requests failed")
|
||||
}
|
||||
|
||||
resp := elm.(*result)
|
||||
msg, err = resp.Msg, resp.Error
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resolver) resolveIP(m *D.Msg) (msg *D.Msg, err error) {
|
||||
msgCh := r.resolve(r.main, m)
|
||||
if r.fallback == nil {
|
||||
res := <-msgCh
|
||||
msg, err = res.Msg, res.Error
|
||||
return
|
||||
}
|
||||
fallbackMsg := r.resolve(r.fallback, m)
|
||||
res := <-msgCh
|
||||
if res.Error == nil {
|
||||
if mmdb == nil {
|
||||
return nil, errors.New("GeoIP can't use")
|
||||
}
|
||||
|
||||
if ips := r.msgToIP(res.Msg); len(ips) != 0 {
|
||||
if record, _ := mmdb.Country(ips[0]); record.Country.IsoCode == "CN" || record.Country.IsoCode == "" {
|
||||
// release channel
|
||||
go func() { <-fallbackMsg }()
|
||||
msg = res.Msg
|
||||
return msg, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = <-fallbackMsg
|
||||
msg, err = res.Msg, res.Error
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
||||
ip = net.ParseIP(host)
|
||||
if ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
query := &D.Msg{}
|
||||
dnsType := D.TypeA
|
||||
if r.ipv6 {
|
||||
dnsType = D.TypeAAAA
|
||||
}
|
||||
query.SetQuestion(D.Fqdn(host), dnsType)
|
||||
|
||||
msg, err := r.Exchange(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ips := r.msgToIP(msg)
|
||||
if len(ips) == 0 {
|
||||
return nil, errors.New("can't found ip")
|
||||
}
|
||||
|
||||
ip = ips[0]
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Resolver) msgToIP(msg *D.Msg) []net.IP {
|
||||
ips := []net.IP{}
|
||||
|
||||
for _, answer := range msg.Answer {
|
||||
switch ans := answer.(type) {
|
||||
case *D.AAAA:
|
||||
ips = append(ips, ans.AAAA)
|
||||
case *D.A:
|
||||
ips = append(ips, ans.A)
|
||||
}
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
func (r *Resolver) IPToHost(ip net.IP) (string, bool) {
|
||||
cache := r.cache.Get(ip.String())
|
||||
if cache == nil {
|
||||
return "", false
|
||||
}
|
||||
fqdn := cache.(*D.Msg).Question[0].Name
|
||||
return strings.TrimRight(fqdn, "."), true
|
||||
}
|
||||
|
||||
func (r *Resolver) resolve(client []*nameserver, msg *D.Msg) <-chan *result {
|
||||
ch := make(chan *result)
|
||||
go func() {
|
||||
res, err := r.exchange(client, msg)
|
||||
ch <- &result{Msg: res, Error: err}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (r *Resolver) IsMapping() bool {
|
||||
return r.mapping
|
||||
}
|
||||
|
||||
type NameServer struct {
|
||||
Net string
|
||||
Addr string
|
||||
}
|
||||
|
||||
type nameserver struct {
|
||||
Client *D.Client
|
||||
Address string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Main, Fallback []NameServer
|
||||
IPv6 bool
|
||||
EnhancedMode EnhancedMode
|
||||
}
|
||||
|
||||
func transform(servers []NameServer) []*nameserver {
|
||||
var ret []*nameserver
|
||||
for _, s := range servers {
|
||||
ret = append(ret, &nameserver{
|
||||
Client: &D.Client{
|
||||
Net: s.Net,
|
||||
TLSConfig: &tls.Config{
|
||||
ClientSessionCache: globalSessionCache,
|
||||
},
|
||||
UDPSize: 4096,
|
||||
},
|
||||
Address: s.Addr,
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func New(config Config) *Resolver {
|
||||
once.Do(func() {
|
||||
mmdb, _ = geoip2.Open(C.Path.MMDB())
|
||||
})
|
||||
|
||||
r := &Resolver{
|
||||
main: transform(config.Main),
|
||||
ipv6: config.IPv6,
|
||||
cache: cache.New(time.Second * 60),
|
||||
mapping: config.EnhancedMode == MAPPING,
|
||||
}
|
||||
if config.Fallback != nil {
|
||||
r.fallback = transform(config.Fallback)
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
address string
|
||||
server = &Server{}
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
*D.Server
|
||||
r *Resolver
|
||||
}
|
||||
|
||||
func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) {
|
||||
msg, err := s.r.Exchange(r)
|
||||
|
||||
if err != nil {
|
||||
if len(r.Question) > 0 {
|
||||
q := r.Question[0]
|
||||
qString := fmt.Sprintf("%s %s %s", q.Name, D.Class(q.Qclass).String(), D.Type(q.Qtype).String())
|
||||
log.Debugln("[DNS Server] Exchange %s failed: %v", qString, err)
|
||||
}
|
||||
D.HandleFailed(w, r)
|
||||
return
|
||||
}
|
||||
msg.SetReply(r)
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
|
||||
func (s *Server) setReslover(r *Resolver) {
|
||||
s.r = r
|
||||
}
|
||||
|
||||
func ReCreateServer(addr string, resolver *Resolver) error {
|
||||
if addr == address {
|
||||
server.setReslover(resolver)
|
||||
return nil
|
||||
}
|
||||
|
||||
if server.Server != nil {
|
||||
server.Shutdown()
|
||||
}
|
||||
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if port == "0" || port == "" || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address = addr
|
||||
server = &Server{r: resolver}
|
||||
server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server}
|
||||
|
||||
go func() {
|
||||
server.ActivateAndServe()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
// EnhancedModeMapping is a mapping for EnhancedMode enum
|
||||
EnhancedModeMapping = map[string]EnhancedMode{
|
||||
NORMAL.String(): NORMAL,
|
||||
FAKEIP.String(): FAKEIP,
|
||||
MAPPING.String(): MAPPING,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
NORMAL EnhancedMode = iota
|
||||
FAKEIP
|
||||
MAPPING
|
||||
)
|
||||
|
||||
type EnhancedMode int
|
||||
|
||||
// UnmarshalYAML unserialize EnhancedMode with yaml
|
||||
func (e *EnhancedMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tp string
|
||||
if err := unmarshal(&tp); err != nil {
|
||||
return err
|
||||
}
|
||||
mode, exist := EnhancedModeMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*e = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalYAML serialize EnhancedMode with yaml
|
||||
func (e EnhancedMode) MarshalYAML() ([]byte, error) {
|
||||
return yaml.Marshal(e.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON unserialize EnhancedMode with json
|
||||
func (e *EnhancedMode) UnmarshalJSON(data []byte) error {
|
||||
var tp string
|
||||
json.Unmarshal(data, &tp)
|
||||
mode, exist := EnhancedModeMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*e = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON serialize EnhancedMode with json
|
||||
func (e EnhancedMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(e.String())
|
||||
}
|
||||
|
||||
func (e EnhancedMode) String() string {
|
||||
switch e {
|
||||
case NORMAL:
|
||||
return "normal"
|
||||
case FAKEIP:
|
||||
return "fakeip"
|
||||
case MAPPING:
|
||||
return "redir-host"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func putMsgToCache(c *cache.Cache, key string, msg *D.Msg) {
|
||||
var ttl time.Duration
|
||||
if len(msg.Answer) != 0 {
|
||||
ttl = time.Duration(msg.Answer[0].Header().Ttl) * time.Second
|
||||
} else if len(msg.Ns) != 0 {
|
||||
ttl = time.Duration(msg.Ns[0].Header().Ttl) * time.Second
|
||||
} else if len(msg.Extra) != 0 {
|
||||
ttl = time.Duration(msg.Extra[0].Header().Ttl) * time.Second
|
||||
} else {
|
||||
log.Debugln("[DNS] response msg error: %#v", msg)
|
||||
return
|
||||
}
|
||||
|
||||
c.Put(key, msg.Copy(), ttl)
|
||||
}
|
||||
|
||||
func setMsgTTL(msg *D.Msg, ttl uint32) {
|
||||
for _, answer := range msg.Answer {
|
||||
answer.Header().Ttl = ttl
|
||||
}
|
||||
|
||||
for _, ns := range msg.Ns {
|
||||
ns.Header().Ttl = ttl
|
||||
}
|
||||
|
||||
for _, extra := range msg.Extra {
|
||||
extra.Header().Ttl = ttl
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,20 +0,0 @@
|
||||
module github.com/Dreamacro/clash
|
||||
|
||||
require (
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3-0.20190202135136-da4602d8f112
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/go-chi/chi v4.0.1+incompatible
|
||||
github.com/go-chi/cors v1.0.0
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/websocket v1.4.0
|
||||
github.com/miekg/dns v1.1.4
|
||||
github.com/oschwald/geoip2-golang v1.2.1
|
||||
github.com/oschwald/maxminddb-golang v1.3.0 // indirect
|
||||
github.com/sirupsen/logrus v1.3.0
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613
|
||||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
|
||||
gopkg.in/eapache/channels.v1 v1.1.0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3-0.20190202135136-da4602d8f112 h1:1axYxE0ZLJy40+ulq46XQt7MaJDJr4iGer1NQz7jmKw=
|
||||
github.com/Dreamacro/go-shadowsocks2 v0.1.3-0.20190202135136-da4602d8f112/go.mod h1:giIuN+TuUudTxHc1jjTOyyQYiJ3VXp1pWOHdJbSCAPo=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/go-chi/chi v4.0.1+incompatible h1:RSRC5qmFPtO90t7pTL0DBMNpZFsb/sHF3RXVlDgFisA=
|
||||
github.com/go-chi/chi v4.0.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
|
||||
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
|
||||
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU=
|
||||
github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
|
||||
github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE=
|
||||
github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg=
|
||||
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e h1:EfdBzeKbFSvOjoIqSZcfS8wp0FBLokGBEs9lz1OtSg0=
|
||||
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4=
|
||||
gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -1,98 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
P "github.com/Dreamacro/clash/proxy"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
// Parse config with default config path
|
||||
func Parse() (*config.Config, error) {
|
||||
return ParseWithPath(C.Path.Config())
|
||||
}
|
||||
|
||||
// ParseWithPath parse config with custom config path
|
||||
func ParseWithPath(path string) (*config.Config, error) {
|
||||
return config.Parse(path)
|
||||
}
|
||||
|
||||
// ApplyConfig dispatch configure to all parts
|
||||
func ApplyConfig(cfg *config.Config, force bool) {
|
||||
if force {
|
||||
updateGeneral(cfg.General)
|
||||
}
|
||||
updateProxies(cfg.Proxies)
|
||||
updateRules(cfg.Rules)
|
||||
updateDNS(cfg.DNS)
|
||||
}
|
||||
|
||||
func GetGeneral() *config.General {
|
||||
ports := P.GetPorts()
|
||||
return &config.General{
|
||||
Port: ports.Port,
|
||||
SocksPort: ports.SocksPort,
|
||||
RedirPort: ports.RedirPort,
|
||||
AllowLan: P.AllowLan(),
|
||||
Mode: T.Instance().Mode(),
|
||||
LogLevel: log.Level(),
|
||||
}
|
||||
}
|
||||
|
||||
func updateDNS(c *config.DNS) {
|
||||
if c.Enable == false {
|
||||
T.Instance().SetResolver(nil)
|
||||
dns.ReCreateServer("", nil)
|
||||
return
|
||||
}
|
||||
r := dns.New(dns.Config{
|
||||
Main: c.NameServer,
|
||||
Fallback: c.Fallback,
|
||||
IPv6: c.IPv6,
|
||||
EnhancedMode: c.EnhancedMode,
|
||||
})
|
||||
T.Instance().SetResolver(r)
|
||||
if err := dns.ReCreateServer(c.Listen, r); err != nil {
|
||||
log.Errorln("Start DNS server error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
log.Infoln("DNS server listening at: %s", c.Listen)
|
||||
}
|
||||
|
||||
func updateProxies(proxies map[string]C.Proxy) {
|
||||
tunnel := T.Instance()
|
||||
oldProxies := tunnel.Proxies()
|
||||
|
||||
// close proxy group goroutine
|
||||
for _, proxy := range oldProxies {
|
||||
proxy.Destroy()
|
||||
}
|
||||
|
||||
tunnel.UpdateProxies(proxies)
|
||||
}
|
||||
|
||||
func updateRules(rules []C.Rule) {
|
||||
T.Instance().UpdateRules(rules)
|
||||
}
|
||||
|
||||
func updateGeneral(general *config.General) {
|
||||
log.SetLevel(general.LogLevel)
|
||||
T.Instance().SetMode(general.Mode)
|
||||
|
||||
allowLan := general.AllowLan
|
||||
|
||||
P.SetAllowLan(allowLan)
|
||||
if err := P.ReCreateHTTP(general.Port); err != nil {
|
||||
log.Errorln("Start HTTP server error: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := P.ReCreateSocks(general.SocksPort); err != nil {
|
||||
log.Errorln("Start SOCKS5 server error: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := P.ReCreateRedir(general.RedirPort); err != nil {
|
||||
log.Errorln("Start Redir server error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/hub/executor"
|
||||
"github.com/Dreamacro/clash/hub/route"
|
||||
)
|
||||
|
||||
// Parse call at the beginning of clash
|
||||
func Parse() error {
|
||||
cfg, err := executor.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.General.ExternalUI != "" {
|
||||
route.SetUIPath(cfg.General.ExternalUI)
|
||||
}
|
||||
|
||||
if cfg.General.ExternalController != "" {
|
||||
go route.Start(cfg.General.ExternalController, cfg.General.Secret)
|
||||
}
|
||||
|
||||
executor.ApplyConfig(cfg, true)
|
||||
return nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Dreamacro/clash/hub/executor"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
P "github.com/Dreamacro/clash/proxy"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func configRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getConfigs)
|
||||
r.Put("/", updateConfigs)
|
||||
r.Patch("/", patchConfigs)
|
||||
return r
|
||||
}
|
||||
|
||||
type configSchema struct {
|
||||
Port *int `json:"port"`
|
||||
SocksPort *int `json:"socks-port"`
|
||||
RedirPort *int `json:"redir-port"`
|
||||
AllowLan *bool `json:"allow-lan"`
|
||||
Mode *T.Mode `json:"mode"`
|
||||
LogLevel *log.LogLevel `json:"log-level"`
|
||||
}
|
||||
|
||||
func getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
general := executor.GetGeneral()
|
||||
render.JSON(w, r, general)
|
||||
}
|
||||
|
||||
func pointerOrDefault(p *int, def int) int {
|
||||
if p != nil {
|
||||
return *p
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
func patchConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
general := &configSchema{}
|
||||
if err := render.DecodeJSON(r.Body, general); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if general.AllowLan != nil {
|
||||
P.SetAllowLan(*general.AllowLan)
|
||||
}
|
||||
|
||||
ports := P.GetPorts()
|
||||
P.ReCreateHTTP(pointerOrDefault(general.Port, ports.Port))
|
||||
P.ReCreateSocks(pointerOrDefault(general.SocksPort, ports.SocksPort))
|
||||
P.ReCreateRedir(pointerOrDefault(general.RedirPort, ports.RedirPort))
|
||||
|
||||
if general.Mode != nil {
|
||||
T.Instance().SetMode(*general.Mode)
|
||||
}
|
||||
|
||||
if general.LogLevel != nil {
|
||||
log.SetLevel(*general.LogLevel)
|
||||
}
|
||||
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
type updateConfigRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func updateConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
req := updateConfigRequest{}
|
||||
if err := render.DecodeJSON(r.Body, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(req.Path) {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, newError("path is not a absoluted path"))
|
||||
return
|
||||
}
|
||||
|
||||
force := r.URL.Query().Get("force") == "true"
|
||||
cfg, err := executor.ParseWithPath(req.Path)
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
executor.ApplyConfig(cfg, force)
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package route
|
||||
|
||||
var (
|
||||
CtxKeyProxyName = contextKey("proxy name")
|
||||
CtxKeyProxy = contextKey("proxy")
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
func (c contextKey) String() string {
|
||||
return "clash context key " + string(c)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package route
|
||||
|
||||
var (
|
||||
ErrUnauthorized = newError("Unauthorized")
|
||||
ErrBadRequest = newError("Body invalid")
|
||||
ErrForbidden = newError("Forbidden")
|
||||
ErrNotFound = newError("Resource not found")
|
||||
ErrRequestTimeout = newError("Timeout")
|
||||
)
|
||||
|
||||
// HTTPError is custom HTTP error for API
|
||||
type HTTPError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *HTTPError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func newError(msg string) *HTTPError {
|
||||
return &HTTPError{Message: msg}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
A "github.com/Dreamacro/clash/adapters/outbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func proxyRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getProxies)
|
||||
|
||||
r.Route("/{name}", func(r chi.Router) {
|
||||
r.Use(parseProxyName, findProxyByName)
|
||||
r.Get("/", getProxy)
|
||||
r.Get("/delay", getProxyDelay)
|
||||
r.Put("/", updateProxy)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// When name is composed of a partial escape string, Golang does not unescape it
|
||||
func parseProxyName(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if newName, err := url.PathUnescape(name); err == nil {
|
||||
name = newName
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), CtxKeyProxyName, name)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func findProxyByName(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.Context().Value(CtxKeyProxyName).(string)
|
||||
proxies := T.Instance().Proxies()
|
||||
proxy, exist := proxies[name]
|
||||
if !exist {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, ErrNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func getProxies(w http.ResponseWriter, r *http.Request) {
|
||||
proxies := T.Instance().Proxies()
|
||||
render.JSON(w, r, render.M{
|
||||
"proxies": proxies,
|
||||
})
|
||||
}
|
||||
|
||||
func getProxy(w http.ResponseWriter, r *http.Request) {
|
||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||
render.JSON(w, r, 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 {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := r.Context().Value(CtxKeyProxy).(*A.Proxy)
|
||||
selector, ok := proxy.ProxyAdapter.(*A.Selector)
|
||||
if !ok {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, newError("Must be a Selector"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := selector.Set(req.Name); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
|
||||
return
|
||||
}
|
||||
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
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 {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||
|
||||
sigCh := make(chan uint16)
|
||||
go func() {
|
||||
t, err := proxy.URLTest(url)
|
||||
if err != nil {
|
||||
sigCh <- 0
|
||||
}
|
||||
sigCh <- t
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Millisecond * time.Duration(timeout)):
|
||||
render.Status(r, http.StatusRequestTimeout)
|
||||
render.JSON(w, r, ErrRequestTimeout)
|
||||
case t := <-sigCh:
|
||||
if t == 0 {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError("An error occurred in the delay test"))
|
||||
} else {
|
||||
render.JSON(w, r, render.M{
|
||||
"delay": t,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
func ruleRouter() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getRules)
|
||||
return r
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Type string `json:"type"`
|
||||
Payload string `json:"payload"`
|
||||
Proxy string `json:"proxy"`
|
||||
}
|
||||
|
||||
func getRules(w http.ResponseWriter, r *http.Request) {
|
||||
rawRules := T.Instance().Rules()
|
||||
|
||||
rules := []Rule{}
|
||||
for _, rule := range rawRules {
|
||||
rules = append(rules, Rule{
|
||||
Type: rule.RuleType().String(),
|
||||
Payload: rule.Payload(),
|
||||
Proxy: rule.Adapter(),
|
||||
})
|
||||
}
|
||||
|
||||
render.JSON(w, r, render.M{
|
||||
"rules": rules,
|
||||
})
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
T "github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
var (
|
||||
serverSecret = ""
|
||||
serverAddr = ""
|
||||
|
||||
uiPath = ""
|
||||
)
|
||||
|
||||
type Traffic struct {
|
||||
Up int64 `json:"up"`
|
||||
Down int64 `json:"down"`
|
||||
}
|
||||
|
||||
func SetUIPath(path string) {
|
||||
uiPath = path
|
||||
}
|
||||
|
||||
func Start(addr string, secret string) {
|
||||
if serverAddr != "" {
|
||||
return
|
||||
}
|
||||
|
||||
serverAddr = addr
|
||||
serverSecret = secret
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
MaxAge: 300,
|
||||
})
|
||||
|
||||
root := chi.NewRouter().With(jsonContentType)
|
||||
root.Get("/traffic", traffic)
|
||||
root.Get("/logs", getLogs)
|
||||
|
||||
r.Get("/", hello)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(cors.Handler, authentication)
|
||||
|
||||
r.Mount("/", root)
|
||||
r.Mount("/configs", configRouter())
|
||||
r.Mount("/proxies", proxyRouter())
|
||||
r.Mount("/rules", ruleRouter())
|
||||
})
|
||||
|
||||
if uiPath != "" {
|
||||
r.Group(func(r chi.Router) {
|
||||
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(uiPath)))
|
||||
r.Get("/ui", http.RedirectHandler("/ui/", 301).ServeHTTP)
|
||||
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
log.Infoln("RESTful API listening at: %s", addr)
|
||||
err := http.ListenAndServe(addr, r)
|
||||
if err != nil {
|
||||
log.Errorln("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 authentication(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
text := strings.SplitN(header, " ", 2)
|
||||
|
||||
if serverSecret == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
hasUnvalidHeader := text[0] != "Bearer"
|
||||
hasUnvalidSecret := len(text) == 2 && text[1] != serverSecret
|
||||
if hasUnvalidHeader || hasUnvalidSecret {
|
||||
render.Status(r, http.StatusUnauthorized)
|
||||
render.JSON(w, r, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func hello(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, render.M{"hello": "clash"})
|
||||
}
|
||||
|
||||
func traffic(w http.ResponseWriter, r *http.Request) {
|
||||
render.Status(r, http.StatusOK)
|
||||
|
||||
tick := time.NewTicker(time.Second)
|
||||
t := T.Instance().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 := log.LogLevelMapping[levelText]
|
||||
if !ok {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
sub := log.Subscribe()
|
||||
render.Status(r, http.StatusOK)
|
||||
for elm := range sub {
|
||||
log := elm.(*log.Event)
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// LogLevelMapping is a mapping for LogLevel enum
|
||||
LogLevelMapping = map[string]LogLevel{
|
||||
ERROR.String(): ERROR,
|
||||
WARNING.String(): WARNING,
|
||||
INFO.String(): INFO,
|
||||
DEBUG.String(): DEBUG,
|
||||
SILENT.String(): SILENT,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DEBUG LogLevel = iota
|
||||
INFO
|
||||
WARNING
|
||||
ERROR
|
||||
SILENT
|
||||
)
|
||||
|
||||
type LogLevel int
|
||||
|
||||
// UnmarshalYAML unserialize LogLevel with yaml
|
||||
func (l *LogLevel) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tp string
|
||||
unmarshal(&tp)
|
||||
level, exist := LogLevelMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*l = level
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unserialize LogLevel with json
|
||||
func (l *LogLevel) UnmarshalJSON(data []byte) error {
|
||||
var tp string
|
||||
json.Unmarshal(data, &tp)
|
||||
level, exist := LogLevelMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*l = level
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON serialize LogLevel with json
|
||||
func (l LogLevel) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
case INFO:
|
||||
return "info"
|
||||
case WARNING:
|
||||
return "warning"
|
||||
case ERROR:
|
||||
return "error"
|
||||
case DEBUG:
|
||||
return "debug"
|
||||
case SILENT:
|
||||
return "silent"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/common/observable"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
logCh = make(chan interface{})
|
||||
source = observable.NewObservable(logCh)
|
||||
level = INFO
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
LogLevel LogLevel
|
||||
Payload string
|
||||
}
|
||||
|
||||
func (e *Event) Type() string {
|
||||
return e.LogLevel.String()
|
||||
}
|
||||
|
||||
func Infoln(format string, v ...interface{}) {
|
||||
event := newLog(INFO, format, v...)
|
||||
logCh <- event
|
||||
print(event)
|
||||
}
|
||||
|
||||
func Warnln(format string, v ...interface{}) {
|
||||
event := newLog(WARNING, format, v...)
|
||||
logCh <- event
|
||||
print(event)
|
||||
}
|
||||
|
||||
func Errorln(format string, v ...interface{}) {
|
||||
event := newLog(ERROR, format, v...)
|
||||
logCh <- event
|
||||
print(event)
|
||||
}
|
||||
|
||||
func Debugln(format string, v ...interface{}) {
|
||||
event := newLog(DEBUG, format, v...)
|
||||
logCh <- event
|
||||
print(event)
|
||||
}
|
||||
|
||||
func Fatalln(format string, v ...interface{}) {
|
||||
log.Fatalf(format, v...)
|
||||
}
|
||||
|
||||
func Subscribe() observable.Subscription {
|
||||
sub, _ := source.Subscribe()
|
||||
return sub
|
||||
}
|
||||
|
||||
func Level() LogLevel {
|
||||
return level
|
||||
}
|
||||
|
||||
func SetLevel(newLevel LogLevel) {
|
||||
level = newLevel
|
||||
}
|
||||
|
||||
func print(data *Event) {
|
||||
if data.LogLevel < level {
|
||||
return
|
||||
}
|
||||
|
||||
switch data.LogLevel {
|
||||
case INFO:
|
||||
log.Infoln(data.Payload)
|
||||
case WARNING:
|
||||
log.Warnln(data.Payload)
|
||||
case ERROR:
|
||||
log.Errorln(data.Payload)
|
||||
case DEBUG:
|
||||
log.Debugln(data.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
func newLog(logLevel LogLevel, format string, v ...interface{}) *Event {
|
||||
return &Event{
|
||||
LogLevel: logLevel,
|
||||
Payload: fmt.Sprintf(format, v...),
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/Dreamacro/clash/config"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/hub"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
version bool
|
||||
homedir string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&homedir, "d", "", "set configuration directory")
|
||||
flag.BoolVar(&version, "v", false, "show current version of clash")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func main() {
|
||||
if version {
|
||||
fmt.Printf("Clash %s %s %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, C.BuildTime)
|
||||
return
|
||||
}
|
||||
|
||||
// enable tls 1.3 and remove when go 1.13
|
||||
os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
|
||||
|
||||
if homedir != "" {
|
||||
if !filepath.IsAbs(homedir) {
|
||||
currentDir, _ := os.Getwd()
|
||||
homedir = filepath.Join(currentDir, homedir)
|
||||
}
|
||||
C.SetHomeDir(homedir)
|
||||
}
|
||||
|
||||
if err := config.Init(C.Path.HomeDir()); err != nil {
|
||||
log.Fatalf("Initial configuration directory error: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := hub.Parse(); err != nil {
|
||||
log.Fatalf("Parse config error: %s", err.Error())
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/inbound"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
var (
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
type HttpListener struct {
|
||||
net.Listener
|
||||
address string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewHttpProxy(addr string) (*HttpListener, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hl := &HttpListener{l, addr, false}
|
||||
|
||||
go func() {
|
||||
log.Infoln("HTTP proxy listening at: %s", addr)
|
||||
for {
|
||||
c, err := hl.Accept()
|
||||
if err != nil {
|
||||
if hl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go handleConn(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return hl, nil
|
||||
}
|
||||
|
||||
func (l *HttpListener) Close() {
|
||||
l.closed = true
|
||||
l.Listener.Close()
|
||||
}
|
||||
|
||||
func (l *HttpListener) Address() string {
|
||||
return l.address
|
||||
}
|
||||
|
||||
func handleConn(conn net.Conn) {
|
||||
br := bufio.NewReader(conn)
|
||||
request, err := http.ReadRequest(br)
|
||||
if err != nil || request.URL.Host == "" {
|
||||
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))
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/proxy/http"
|
||||
"github.com/Dreamacro/clash/proxy/redir"
|
||||
"github.com/Dreamacro/clash/proxy/socks"
|
||||
)
|
||||
|
||||
var (
|
||||
allowLan = false
|
||||
|
||||
socksListener *socks.SockListener
|
||||
httpListener *http.HttpListener
|
||||
redirListener *redir.RedirListener
|
||||
)
|
||||
|
||||
type listener interface {
|
||||
Close()
|
||||
Address() string
|
||||
}
|
||||
|
||||
type Ports struct {
|
||||
Port int `json:"port"`
|
||||
SocksPort int `json:"socks-port"`
|
||||
RedirPort int `json:"redir-port"`
|
||||
}
|
||||
|
||||
func AllowLan() bool {
|
||||
return allowLan
|
||||
}
|
||||
|
||||
func SetAllowLan(al bool) {
|
||||
allowLan = al
|
||||
}
|
||||
|
||||
func ReCreateHTTP(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
|
||||
if httpListener != nil {
|
||||
if httpListener.Address() == addr {
|
||||
return nil
|
||||
}
|
||||
httpListener.Close()
|
||||
httpListener = nil
|
||||
}
|
||||
|
||||
if portIsZero(addr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
httpListener, err = http.NewHttpProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReCreateSocks(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
|
||||
if socksListener != nil {
|
||||
if socksListener.Address() == addr {
|
||||
return nil
|
||||
}
|
||||
socksListener.Close()
|
||||
socksListener = nil
|
||||
}
|
||||
|
||||
if portIsZero(addr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
socksListener, err = socks.NewSocksProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReCreateRedir(port int) error {
|
||||
addr := genAddr(port, allowLan)
|
||||
|
||||
if redirListener != nil {
|
||||
if redirListener.Address() == addr {
|
||||
return nil
|
||||
}
|
||||
redirListener.Close()
|
||||
redirListener = nil
|
||||
}
|
||||
|
||||
if portIsZero(addr) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
redirListener, err = redir.NewRedirProxy(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPorts return the ports of proxy servers
|
||||
func GetPorts() *Ports {
|
||||
ports := &Ports{}
|
||||
|
||||
if httpListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(httpListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.Port = port
|
||||
}
|
||||
|
||||
if socksListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(socksListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.SocksPort = port
|
||||
}
|
||||
|
||||
if redirListener != nil {
|
||||
_, portStr, _ := net.SplitHostPort(redirListener.Address())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
ports.RedirPort = port
|
||||
}
|
||||
|
||||
return ports
|
||||
}
|
||||
|
||||
func portIsZero(addr string) bool {
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if port == "0" || port == "" || err != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func genAddr(port int, allowLan bool) string {
|
||||
if allowLan {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
return fmt.Sprintf("127.0.0.1:%d", port)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package redir
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/inbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
var (
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
type RedirListener struct {
|
||||
net.Listener
|
||||
address string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewRedirProxy(addr string) (*RedirListener, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rl := &RedirListener{l, addr, false}
|
||||
|
||||
go func() {
|
||||
log.Infoln("Redir proxy listening at: %s", addr)
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if rl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go handleRedir(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
func (l *RedirListener) Close() {
|
||||
l.closed = true
|
||||
l.Listener.Close()
|
||||
}
|
||||
|
||||
func (l *RedirListener) Address() string {
|
||||
return l.address
|
||||
}
|
||||
|
||||
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, C.REDIR))
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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)
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0)
|
||||
if err != 0 {
|
||||
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
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// +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
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package socks
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/inbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/socks"
|
||||
)
|
||||
|
||||
var (
|
||||
tun = tunnel.Instance()
|
||||
)
|
||||
|
||||
type SockListener struct {
|
||||
net.Listener
|
||||
address string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewSocksProxy(addr string) (*SockListener, error) {
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sl := &SockListener{l, addr, false}
|
||||
go func() {
|
||||
log.Infoln("SOCKS proxy listening at: %s", addr)
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if sl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go handleSocks(c)
|
||||
}
|
||||
}()
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (l *SockListener) Close() {
|
||||
l.closed = true
|
||||
l.Listener.Close()
|
||||
}
|
||||
|
||||
func (l *SockListener) Address() string {
|
||||
return l.address
|
||||
}
|
||||
|
||||
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, C.SOCKS))
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package socks
|
||||
@@ -1,38 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
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(metadata *C.Metadata) bool {
|
||||
if metadata.AddrType != C.AtypDomainName {
|
||||
return false
|
||||
}
|
||||
return metadata.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: strings.ToLower(domain),
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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(metadata *C.Metadata) bool {
|
||||
if metadata.AddrType != C.AtypDomainName {
|
||||
return false
|
||||
}
|
||||
domain := metadata.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: strings.ToLower(keyword),
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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(metadata *C.Metadata) bool {
|
||||
if metadata.AddrType != C.AtypDomainName {
|
||||
return false
|
||||
}
|
||||
domain := metadata.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: strings.ToLower(suffix),
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Match struct {
|
||||
adapter string
|
||||
}
|
||||
|
||||
func (f *Match) RuleType() C.RuleType {
|
||||
return C.MATCH
|
||||
}
|
||||
|
||||
func (f *Match) IsMatch(metadata *C.Metadata) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *Match) Adapter() string {
|
||||
return f.adapter
|
||||
}
|
||||
|
||||
func (f *Match) Payload() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func NewMatch(adapter string) *Match {
|
||||
return &Match{
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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(metadata *C.Metadata) bool {
|
||||
if metadata.IP == nil {
|
||||
return false
|
||||
}
|
||||
record, _ := mmdb.Country(*metadata.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.Path.MMDB())
|
||||
if err != nil {
|
||||
log.Fatalf("Can't load mmdb: %s", err.Error())
|
||||
}
|
||||
})
|
||||
return &GEOIP{
|
||||
country: country,
|
||||
adapter: adapter,
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type IPCIDR struct {
|
||||
ipnet *net.IPNet
|
||||
adapter string
|
||||
isSourceIP bool
|
||||
}
|
||||
|
||||
func (i *IPCIDR) RuleType() C.RuleType {
|
||||
if i.isSourceIP {
|
||||
return C.SourceIPCIDR
|
||||
}
|
||||
return C.IPCIDR
|
||||
}
|
||||
|
||||
func (i *IPCIDR) IsMatch(metadata *C.Metadata) bool {
|
||||
ip := metadata.IP
|
||||
if i.isSourceIP {
|
||||
ip = metadata.SourceIP
|
||||
}
|
||||
return ip != nil && i.ipnet.Contains(*ip)
|
||||
}
|
||||
|
||||
func (i *IPCIDR) Adapter() string {
|
||||
return i.adapter
|
||||
}
|
||||
|
||||
func (i *IPCIDR) Payload() string {
|
||||
return i.ipnet.String()
|
||||
}
|
||||
|
||||
func NewIPCIDR(s string, adapter string, isSourceIP bool) *IPCIDR {
|
||||
_, ipnet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &IPCIDR{
|
||||
ipnet: ipnet,
|
||||
adapter: adapter,
|
||||
isSourceIP: isSourceIP,
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
adapters "github.com/Dreamacro/clash/adapters/inbound"
|
||||
)
|
||||
|
||||
const (
|
||||
// io.Copy default buffer size is 32 KiB
|
||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
||||
bufferSize = 20 * 1024
|
||||
)
|
||||
|
||||
var bufPool = sync.Pool{New: func() interface{} { return make([]byte, bufferSize) }}
|
||||
|
||||
func (t *Tunnel) handleHTTP(request *adapters.HTTPAdapter, outbound net.Conn) {
|
||||
conn := newTrafficTrack(outbound, t.traffic)
|
||||
req := request.R
|
||||
host := req.Host
|
||||
|
||||
for {
|
||||
keepAlive := strings.TrimSpace(strings.ToLower(req.Header.Get("Proxy-Connection"))) == "keep-alive"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !keepAlive {
|
||||
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, outbound net.Conn) {
|
||||
conn := newTrafficTrack(outbound, 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() {
|
||||
buf := bufPool.Get().([]byte)
|
||||
_, err := io.CopyBuffer(leftConn, rightConn, buf)
|
||||
bufPool.Put(buf[:cap(buf)])
|
||||
leftConn.SetReadDeadline(time.Now())
|
||||
ch <- err
|
||||
}()
|
||||
|
||||
buf := bufPool.Get().([]byte)
|
||||
io.CopyBuffer(rightConn, leftConn, buf)
|
||||
bufPool.Put(buf[:cap(buf)])
|
||||
rightConn.SetReadDeadline(time.Now())
|
||||
<-ch
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Mode int
|
||||
|
||||
var (
|
||||
// ModeMapping is a mapping for Mode enum
|
||||
ModeMapping = map[string]Mode{
|
||||
Global.String(): Global,
|
||||
Rule.String(): Rule,
|
||||
Direct.String(): Direct,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
Global Mode = iota
|
||||
Rule
|
||||
Direct
|
||||
)
|
||||
|
||||
// UnmarshalJSON unserialize Mode
|
||||
func (m *Mode) UnmarshalJSON(data []byte) error {
|
||||
var tp string
|
||||
json.Unmarshal(data, &tp)
|
||||
mode, exist := ModeMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*m = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML unserialize Mode with yaml
|
||||
func (m *Mode) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var tp string
|
||||
unmarshal(&tp)
|
||||
mode, exist := ModeMapping[tp]
|
||||
if !exist {
|
||||
return errors.New("invalid mode")
|
||||
}
|
||||
*m = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON serialize Mode
|
||||
func (m Mode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.String())
|
||||
}
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case Global:
|
||||
return "Global"
|
||||
case Rule:
|
||||
return "Rule"
|
||||
case Direct:
|
||||
return "Direct"
|
||||
default:
|
||||
return "Unknow"
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
InboundAdapter "github.com/Dreamacro/clash/adapters/inbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/dns"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
channels "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
|
||||
resolver *dns.Resolver
|
||||
|
||||
// Outbound Rule
|
||||
mode Mode
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Rules return all rules
|
||||
func (t *Tunnel) Rules() []C.Rule {
|
||||
return t.rules
|
||||
}
|
||||
|
||||
// UpdateRules handle update rules
|
||||
func (t *Tunnel) UpdateRules(rules []C.Rule) {
|
||||
t.configLock.Lock()
|
||||
t.rules = rules
|
||||
t.configLock.Unlock()
|
||||
}
|
||||
|
||||
// Proxies return all proxies
|
||||
func (t *Tunnel) Proxies() map[string]C.Proxy {
|
||||
return t.proxies
|
||||
}
|
||||
|
||||
// UpdateProxies handle update proxies
|
||||
func (t *Tunnel) UpdateProxies(proxies map[string]C.Proxy) {
|
||||
t.configLock.Lock()
|
||||
t.proxies = proxies
|
||||
t.configLock.Unlock()
|
||||
}
|
||||
|
||||
// Mode return current mode
|
||||
func (t *Tunnel) Mode() Mode {
|
||||
return t.mode
|
||||
}
|
||||
|
||||
// SetMode change the mode of tunnel
|
||||
func (t *Tunnel) SetMode(mode Mode) {
|
||||
t.mode = mode
|
||||
}
|
||||
|
||||
// SetResolver change the resolver of tunnel
|
||||
func (t *Tunnel) SetResolver(resolver *dns.Resolver) {
|
||||
t.resolver = resolver
|
||||
}
|
||||
|
||||
func (t *Tunnel) hasResolver() bool {
|
||||
return t.resolver != nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) process() {
|
||||
queue := t.queue.Out()
|
||||
for {
|
||||
elm := <-queue
|
||||
conn := elm.(C.ServerAdapter)
|
||||
go t.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) resolveIP(host string) (net.IP, error) {
|
||||
if t.resolver == nil {
|
||||
ipAddr, err := net.ResolveIPAddr("ip", host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ipAddr.IP, nil
|
||||
}
|
||||
|
||||
return t.resolver.ResolveIP(host)
|
||||
}
|
||||
|
||||
func (t *Tunnel) needLookupIP(metadata *C.Metadata) bool {
|
||||
return t.hasResolver() && t.resolver.IsMapping() && metadata.Host == "" && metadata.IP != nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) handleConn(localConn C.ServerAdapter) {
|
||||
defer localConn.Close()
|
||||
metadata := localConn.Metadata()
|
||||
|
||||
if !metadata.Valid() {
|
||||
log.Warnln("[Metadata] not valid: %#v", metadata)
|
||||
return
|
||||
}
|
||||
|
||||
if t.needLookupIP(metadata) {
|
||||
host, exist := t.resolver.IPToHost(*metadata.IP)
|
||||
if exist {
|
||||
metadata.Host = host
|
||||
metadata.AddrType = C.AtypDomainName
|
||||
}
|
||||
}
|
||||
|
||||
var proxy C.Proxy
|
||||
switch t.mode {
|
||||
case Direct:
|
||||
proxy = t.proxies["DIRECT"]
|
||||
case Global:
|
||||
proxy = t.proxies["GLOBAL"]
|
||||
// Rule
|
||||
default:
|
||||
var err error
|
||||
proxy, err = t.match(metadata)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
remoConn, err := proxy.Dial(metadata)
|
||||
if err != nil {
|
||||
log.Warnln("Proxy[%s] connect [%s --> %s] error: %s", proxy.Name(), metadata.SourceIP.String(), metadata.String(), err.Error())
|
||||
return
|
||||
}
|
||||
defer remoConn.Close()
|
||||
|
||||
switch adapter := localConn.(type) {
|
||||
case *InboundAdapter.HTTPAdapter:
|
||||
t.handleHTTP(adapter, remoConn)
|
||||
case *InboundAdapter.SocketAdapter:
|
||||
t.handleSOCKS(adapter, remoConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool {
|
||||
return (rule.RuleType() == C.GEOIP || rule.RuleType() == C.IPCIDR) && metadata.Host != "" && metadata.IP == nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) match(metadata *C.Metadata) (C.Proxy, error) {
|
||||
t.configLock.RLock()
|
||||
defer t.configLock.RUnlock()
|
||||
|
||||
for _, rule := range t.rules {
|
||||
if t.shouldResolveIP(rule, metadata) {
|
||||
ip, err := t.resolveIP(metadata.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[DNS] resolve %s error: %s", metadata.Host, err.Error())
|
||||
}
|
||||
log.Debugln("[DNS] %s --> %s", metadata.Host, ip.String())
|
||||
metadata.IP = &ip
|
||||
}
|
||||
|
||||
if rule.IsMatch(metadata) {
|
||||
if a, ok := t.proxies[rule.Adapter()]; ok {
|
||||
log.Infoln("%s --> %v match %s using %s", metadata.SourceIP.String(), metadata.String(), rule.RuleType().String(), rule.Adapter())
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infoln("%s --> %v doesn't match any rule using DIRECT", metadata.SourceIP.String(), metadata.String())
|
||||
return t.proxies["DIRECT"], nil
|
||||
}
|
||||
|
||||
func newTunnel() *Tunnel {
|
||||
return &Tunnel{
|
||||
queue: channels.NewInfiniteChannel(),
|
||||
proxies: make(map[string]C.Proxy),
|
||||
configLock: &sync.RWMutex{},
|
||||
traffic: C.NewTraffic(time.Second),
|
||||
mode: Rule,
|
||||
}
|
||||
}
|
||||
|
||||
// Instance return singleton instance of Tunnel
|
||||
func Instance() *Tunnel {
|
||||
once.Do(func() {
|
||||
tunnel = newTunnel()
|
||||
go tunnel.process()
|
||||
})
|
||||
return tunnel
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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}
|
||||
}
|
||||
Reference in new issue
Block a user