Compare commits
478 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c7e34bdc11 | ||
![]() |
24186a488a | ||
![]() |
5212aaf445 | ||
![]() |
e26bed43de | ||
![]() |
700ceed194 | ||
![]() |
154cb1d1f0 | ||
![]() |
295b0da0e5 | ||
![]() |
31fe77ee69 | ||
![]() |
9177645a89 | ||
![]() |
355eb491ad | ||
![]() |
18c666a1ab | ||
![]() |
13d9e960f7 | ||
![]() |
289025c6ee | ||
![]() |
369f2735a0 | ||
![]() |
2b6dd2a909 | ||
![]() |
ccd6d321cd | ||
![]() |
4d66da2277 | ||
![]() |
1ab615852e | ||
![]() |
46bb6c38ff | ||
![]() |
c244229ffb | ||
![]() |
e8b2d0ecc8 | ||
![]() |
acec0f5c89 | ||
![]() |
4655bd4da8 | ||
![]() |
6b17fd2595 | ||
![]() |
cbcbd0e085 | ||
![]() |
75c0254703 | ||
![]() |
e02d556bf4 | ||
![]() |
d006b0f2b4 | ||
![]() |
d9753efe23 | ||
![]() |
6ecd96e5ac | ||
![]() |
fdb1456c69 | ||
![]() |
7d9723662c | ||
![]() |
ca42ca2ca8 | ||
![]() |
10dcb7a3ad | ||
![]() |
4c3c64a34a | ||
![]() |
257fcef0b8 | ||
![]() |
7f1b50f4a7 | ||
![]() |
4f5e74dad9 | ||
![]() |
48b77b2847 | ||
![]() |
6eee226965 | ||
![]() |
765982e86a | ||
![]() |
c5fe5235f7 | ||
![]() |
63770b328f | ||
![]() |
85f4cb23fc | ||
![]() |
d71324069d | ||
![]() |
b7aade5e11 | ||
![]() |
df61a586c9 | ||
![]() |
8e05fbfd6d | ||
![]() |
9b2b7c662d | ||
![]() |
20a521f02d | ||
![]() |
95bbfe3945 | ||
![]() |
4cd4912749 | ||
![]() |
5045ca4574 | ||
![]() |
a7252a1576 | ||
![]() |
7e2974f02f | ||
![]() |
8f9b39c62e | ||
![]() |
d808576f98 | ||
![]() |
fcbe2f06cc | ||
![]() |
148ebccb60 | ||
![]() |
3b1d319820 | ||
![]() |
ff2f2b667b | ||
![]() |
e5a2dbd9b5 | ||
![]() |
4d14dd65fa | ||
![]() |
4ffc999617 | ||
![]() |
71f8f0667f | ||
![]() |
f78a7cb2cb | ||
![]() |
8173d6681b | ||
![]() |
fbf2f26516 | ||
![]() |
9af6d498e7 | ||
![]() |
81b1e9f931 | ||
![]() |
58732ee8b1 | ||
![]() |
d16727e2bd | ||
![]() |
876653ebc8 | ||
![]() |
a26b670420 | ||
![]() |
0489a7391b | ||
![]() |
0c0d18a01c | ||
![]() |
e1fa343088 | ||
![]() |
a5d54884e0 | ||
![]() |
2301b909d2 | ||
![]() |
fbca37c42b | ||
![]() |
4a57917783 | ||
![]() |
cdc7d449a6 | ||
![]() |
d8ac82be36 | ||
![]() |
a6c144038b | ||
![]() |
90b40a8e5a | ||
![]() |
ed988dcdc5 | ||
![]() |
efa4b9e0b8 | ||
![]() |
8c6e205c5a | ||
![]() |
5b07d7b776 | ||
![]() |
de264c42a8 | ||
![]() |
c2469162fb | ||
![]() |
19b7c7f52a | ||
![]() |
c8bc11d61d | ||
![]() |
f29b54898f | ||
![]() |
3e2b08f9d0 | ||
![]() |
fb85691fb9 | ||
![]() |
d411394482 | ||
![]() |
827d5289bc | ||
![]() |
6995e98181 | ||
![]() |
4f291fa513 | ||
![]() |
22b9befbda | ||
![]() |
425b6e0dc0 | ||
![]() |
2516169f61 | ||
![]() |
a3281712e2 | ||
![]() |
bf079742cb | ||
![]() |
6e058f8581 | ||
![]() |
3946d771e5 | ||
![]() |
5940f62794 | ||
![]() |
71cad51e8f | ||
![]() |
50105f0559 | ||
![]() |
6648793e40 | ||
![]() |
95e3a88608 | ||
![]() |
bec4df7b12 | ||
![]() |
93400cf44d | ||
![]() |
a794819869 | ||
![]() |
be8d63ba8f | ||
![]() |
3b90e18047 | ||
![]() |
f0952b55d0 | ||
![]() |
8c7c8f4374 | ||
![]() |
65a8e8f59c | ||
![]() |
5497adaba1 | ||
![]() |
aaf08dadff | ||
![]() |
557297ac9a | ||
![]() |
77a1e3a653 | ||
![]() |
27e1d6cdae | ||
![]() |
91c22b16bf | ||
![]() |
fc5c9b931b | ||
![]() |
c231fd1466 | ||
![]() |
fbb27b84d1 | ||
![]() |
e0c5a85314 | ||
![]() |
2fa1a5c4b9 | ||
![]() |
06d75da257 | ||
![]() |
09d49bac95 | ||
![]() |
3360839fe3 | ||
![]() |
c1285adbf8 | ||
![]() |
9d2fc976e2 | ||
![]() |
7f41f94fff | ||
![]() |
d1f0dac302 | ||
![]() |
afb3e00067 | ||
![]() |
9a31ad6151 | ||
![]() |
09cc6b69e3 | ||
![]() |
8603ac40a1 | ||
![]() |
b384449717 | ||
![]() |
da7ffc0da9 | ||
![]() |
5dd94c8298 | ||
![]() |
412b44a981 | ||
![]() |
aef4dd3fe7 | ||
![]() |
6a92c6af4e | ||
![]() |
e010940b61 | ||
![]() |
2c9a4d276a | ||
![]() |
4dfba73e5c | ||
![]() |
c282d662ca | ||
![]() |
b3d7594813 | ||
![]() |
dd9bdf4e2f | ||
![]() |
275cc7edf3 | ||
![]() |
8c9e0b3884 | ||
![]() |
30d4668008 | ||
![]() |
02333a859a | ||
![]() |
f9cc1cc363 | ||
![]() |
fb7d340233 | ||
![]() |
6a661bff0c | ||
![]() |
d1dd21417b | ||
![]() |
b866f06414 | ||
![]() |
9683c297a7 | ||
![]() |
f6c7281bb7 | ||
![]() |
83bfe521b1 | ||
![]() |
b52d0c16e9 | ||
![]() |
132a6a6a2f | ||
![]() |
03e4b5d525 | ||
![]() |
a0221bf897 | ||
![]() |
b1a639feae | ||
![]() |
cfe7354c07 | ||
![]() |
9732efe938 | ||
![]() |
8f3385bbb6 | ||
![]() |
d237b041b3 | ||
![]() |
3cb87e083c | ||
![]() |
8c6d0c6757 | ||
![]() |
cb95326aca | ||
![]() |
8679968ab0 | ||
![]() |
204a72bbd3 | ||
![]() |
7267c58913 | ||
![]() |
14ae87fcd0 | ||
![]() |
ee6fc12709 | ||
![]() |
78e105f3b2 | ||
![]() |
08607fb6b4 | ||
![]() |
075d8ed094 | ||
![]() |
b1bed7623d | ||
![]() |
1401a82bb0 | ||
![]() |
4524cf4418 | ||
![]() |
0db15d46c3 | ||
![]() |
08c43b8876 | ||
![]() |
499beb7344 | ||
![]() |
c9be614821 | ||
![]() |
b56d35040d | ||
![]() |
bd2ea2b917 | ||
![]() |
e622d8dd38 | ||
![]() |
d40e5e4fe6 | ||
![]() |
1a7830f18e | ||
![]() |
bcb301b730 | ||
![]() |
ebbc9604ce | ||
![]() |
a7aea12aa6 | ||
![]() |
c6cceeb0c5 | ||
![]() |
967932d02c | ||
![]() |
81d5da51a3 | ||
![]() |
fea9d1c5e2 | ||
![]() |
df3a491d40 | ||
![]() |
68753b4ae1 | ||
![]() |
583b2a5ace | ||
![]() |
13bd601cac | ||
![]() |
3d5681cffd | ||
![]() |
a1c2478e74 | ||
![]() |
f1cf7e9269 | ||
![]() |
4ce35870fe | ||
![]() |
1996bef9e6 | ||
![]() |
66cb0b1218 | ||
![]() |
b9d470cf79 | ||
![]() |
4f1fac02ab | ||
![]() |
537b672fcf | ||
![]() |
ced9749104 | ||
![]() |
9aeb4c8cfe | ||
![]() |
70c8605cca | ||
![]() |
5b1a0a523f | ||
![]() |
b398f1e6f3 | ||
![]() |
b3cd4ebbd3 | ||
![]() |
b0f83e401f | ||
![]() |
f5806d9263 | ||
![]() |
55600c49c9 | ||
![]() |
beb88cc46f | ||
![]() |
d49b38b00f | ||
![]() |
0c79d1207e | ||
![]() |
400dc923e0 | ||
![]() |
5b7f0de48b | ||
![]() |
a5b950a779 | ||
![]() |
a2d59d6ef5 | ||
![]() |
8ef5cdb8be | ||
![]() |
c7b718f651 | ||
![]() |
ff56e5c5de | ||
![]() |
661c417fce | ||
![]() |
7d20097465 | ||
![]() |
a20b9a3960 | ||
![]() |
e0d3f926b7 | ||
![]() |
121bc910f6 | ||
![]() |
4522cdc551 | ||
![]() |
410772e81c | ||
![]() |
0267b2efad | ||
![]() |
c6d375eda2 | ||
![]() |
847f41952e | ||
![]() |
47044ec0d8 | ||
![]() |
426ca36118 | ||
![]() |
571d2a0075 | ||
![]() |
1be09f5751 | ||
![]() |
2663cb2e6e | ||
![]() |
9b0bbb90ff | ||
![]() |
588645a2c3 | ||
![]() |
1bfebd0d03 | ||
![]() |
3705996974 | ||
![]() |
09697b7679 | ||
![]() |
4578b2c826 | ||
![]() |
b3a293ab07 | ||
![]() |
507ba16065 | ||
![]() |
aa9f8a39a3 | ||
![]() |
8d37220566 | ||
![]() |
53e17a916b | ||
![]() |
247dd84970 | ||
![]() |
c2f3111922 | ||
![]() |
44872300e9 | ||
![]() |
91ed0118f6 | ||
![]() |
a461c2306a | ||
![]() |
46f4f84442 | ||
![]() |
250a9f4f84 | ||
![]() |
b4292d0972 | ||
![]() |
d755383e39 | ||
![]() |
dff1e8f1ce | ||
![]() |
995aa7a8fc | ||
![]() |
3ca5d17c40 | ||
![]() |
244cb370a4 | ||
![]() |
c35cb24bda | ||
![]() |
b6ff08074c | ||
![]() |
70d53fd45a | ||
![]() |
f231a63e93 | ||
![]() |
6091fcdfec | ||
![]() |
bcfc15e398 | ||
![]() |
045edc188c | ||
![]() |
0778591524 | ||
![]() |
d5e52bed43 | ||
![]() |
06fdd3abe0 | ||
![]() |
4e5898197a | ||
![]() |
f96ebab99f | ||
![]() |
3c54f99fea | ||
![]() |
824f5bd731 | ||
![]() |
3f3db8476e | ||
![]() |
f375f080da | ||
![]() |
e19e9ef5a4 | ||
![]() |
682e65cb54 | ||
![]() |
16a6d409d9 | ||
![]() |
4186bcf1b2 | ||
![]() |
df5112175f | ||
![]() |
d9341a49ea | ||
![]() |
4e9e4b6cde | ||
![]() |
936b7012ba | ||
![]() |
a9cbd9ec98 | ||
![]() |
c9943fb857 | ||
![]() |
a40274e2a2 | ||
![]() |
b59d45c660 | ||
![]() |
7b01e103c2 | ||
![]() |
93a8acecce | ||
![]() |
586bb91c0c | ||
![]() |
baf03b81e3 | ||
![]() |
9807e1189c | ||
![]() |
3d5a0d9f73 | ||
![]() |
cc96187f58 | ||
![]() |
3aefa1d924 | ||
![]() |
42e21b3733 | ||
![]() |
0a35237915 | ||
![]() |
a1f3a5ea26 | ||
![]() |
e63f995258 | ||
![]() |
d0c829c578 | ||
![]() |
4ad9761b32 | ||
![]() |
1f593d37fb | ||
![]() |
109bfcb0f9 | ||
![]() |
7ee49f5171 | ||
![]() |
d759d16944 | ||
![]() |
807d53c1e7 | ||
![]() |
1355196b7c | ||
![]() |
573316bcde | ||
![]() |
784c28266c | ||
![]() |
5da1b2a8aa | ||
![]() |
0976d27cb1 | ||
![]() |
6c83ff3496 | ||
![]() |
f7f97ef625 | ||
![]() |
5acdd72a1d | ||
![]() |
f53686103d | ||
![]() |
f63c9eb22f | ||
![]() |
a37243cf30 | ||
![]() |
b3c1b4a840 | ||
![]() |
14bbf6eedc | ||
![]() |
aa81193d5b | ||
![]() |
9eb98e399d | ||
![]() |
d48cfecf60 | ||
![]() |
6036fb63ba | ||
![]() |
cd48f69b1f | ||
![]() |
fcc594ae26 | ||
![]() |
f4de055aa1 | ||
![]() |
35925cb3da | ||
![]() |
ff430df845 | ||
![]() |
e4cdea2111 | ||
![]() |
b6ee47a541 | ||
![]() |
b25009cde7 | ||
![]() |
6fedd7ec84 | ||
![]() |
9619c3fb20 | ||
![]() |
02d029dd2d | ||
![]() |
09c28e0355 | ||
![]() |
3600077f3b | ||
![]() |
de7656a787 | ||
![]() |
5dfe7f8561 | ||
![]() |
ed27898a33 | ||
![]() |
532396d25c | ||
![]() |
4b1b494164 | ||
![]() |
0d33dc3eb9 | ||
![]() |
994cbff215 | ||
![]() |
bea2ee8bf2 | ||
![]() |
1e5593f1a9 | ||
![]() |
34febc4579 | ||
![]() |
97581148b5 | ||
![]() |
0402878daa | ||
![]() |
4735f61fd1 | ||
![]() |
16ae107e70 | ||
![]() |
83efe2ae57 | ||
![]() |
87e4d94290 | ||
![]() |
b98e9ea202 | ||
![]() |
9a62b1081d | ||
![]() |
2cd1b890ce | ||
![]() |
ba060bd0ee | ||
![]() |
b1795b1e3d | ||
![]() |
76c9820065 | ||
![]() |
2db4ce57ef | ||
![]() |
50b3d497f6 | ||
![]() |
2321e9139d | ||
![]() |
baabf21340 | ||
![]() |
d3bb4c65a8 | ||
![]() |
8c3e2a7559 | ||
![]() |
bc52f8e4fd | ||
![]() |
d3b14c325f | ||
![]() |
4859b158b4 | ||
![]() |
d65b51c62b | ||
![]() |
a6444bb449 | ||
![]() |
e09931dcf7 | ||
![]() |
5bd189f2d0 | ||
![]() |
8766287e72 | ||
![]() |
10f9571c9e | ||
![]() |
96a8259c42 | ||
![]() |
68dd0622b8 | ||
![]() |
558ac6b965 | ||
![]() |
e773f95f21 | ||
![]() |
314ce1c249 | ||
![]() |
13275b1aa6 | ||
![]() |
02d9169b5d | ||
![]() |
7631bcc99e | ||
![]() |
a32ee13fc9 | ||
![]() |
b8ed738238 | ||
![]() |
687c2a21cf | ||
![]() |
ad18064e6b | ||
![]() |
c9735ef75b | ||
![]() |
b70882f01a | ||
![]() |
5805334ccd | ||
![]() |
c1b4382fe8 | ||
![]() |
008743f20b | ||
![]() |
50d778da3c | ||
![]() |
8b7c731fd6 | ||
![]() |
0b7918de9c | ||
![]() |
4f61c04519 | ||
![]() |
89cf06036d | ||
![]() |
4ba6f248bc | ||
![]() |
83a684c551 | ||
![]() |
92a23f1eab | ||
![]() |
622ac45258 | ||
![]() |
791d203b5f | ||
![]() |
77d6f9ae6f | ||
![]() |
b1d9dfd6bf | ||
![]() |
6532947e71 | ||
![]() |
6c5f23f552 | ||
![]() |
78c3034158 | ||
![]() |
8f0098092d | ||
![]() |
33a6579a3a | ||
![]() |
b4221d4b74 | ||
![]() |
0e4b9daaad | ||
![]() |
ee72865f48 | ||
![]() |
6521acf8f1 | ||
![]() |
4f73410618 | ||
![]() |
20eff200b1 | ||
![]() |
ae1e1dc9f6 | ||
![]() |
cf9e1545a4 | ||
![]() |
6c7a8fffe0 | ||
![]() |
3a3e2c05af | ||
![]() |
02c7fd8d70 | ||
![]() |
e6aa452b51 | ||
![]() |
35449bfa17 | ||
![]() |
acd51bbc90 | ||
![]() |
f44cd9180c | ||
![]() |
93c987a6cb | ||
![]() |
3f0584ac09 | ||
![]() |
59968fff1c | ||
![]() |
7c62fe41b4 | ||
![]() |
2781090405 | ||
![]() |
14c9cf1b97 | ||
![]() |
3dfff84cc3 | ||
![]() |
5f3db72422 | ||
![]() |
18bb285a90 | ||
![]() |
60bad66bc3 | ||
![]() |
99b34e8d8b | ||
![]() |
9f1d85ab6e | ||
![]() |
4323dd24d0 | ||
![]() |
59bda1d547 | ||
![]() |
1c760935f4 | ||
![]() |
4f674755ce | ||
![]() |
f1b792bd26 | ||
![]() |
58c077b45e | ||
![]() |
1854199c47 | ||
![]() |
ecac8eb8e5 | ||
![]() |
48cff50a4c | ||
![]() |
fb628e9c62 | ||
![]() |
2dece02df6 | ||
![]() |
8f32e6a60f | ||
![]() |
98614a1f3f | ||
![]() |
c1b4c94b9c | ||
![]() |
7ddbc12cdb | ||
![]() |
1a217e21e9 | ||
![]() |
147a7ce779 | ||
![]() |
fb0289bb4c | ||
![]() |
3e7970612a | ||
![]() |
46244a6496 | ||
![]() |
71d30e6654 | ||
![]() |
008731c249 | ||
![]() |
5628f97da1 | ||
![]() |
8d0c6c6e66 | ||
![]() |
5073c3cde8 | ||
![]() |
3a27cfc4a1 | ||
![]() |
3638b077cd |
96
.github/ISSUE_TEMPLATE/bug_report.md
vendored
96
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,96 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: "[Bug]"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- The English version is available. -->
|
|
||||||
感谢你向 Clash Core 提交 issue!
|
|
||||||
在提交之前,请确认:
|
|
||||||
|
|
||||||
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
|
||||||
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 Openclash、Koolclash 等)的特定问题
|
|
||||||
- [ ] 我已经使用 Clash core 的 dev 分支版本测试过,问题依旧存在
|
|
||||||
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
|
|
||||||
|
|
||||||
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thanks for submitting an issue towards the Clash core!
|
|
||||||
But before so, please do the following checklist:
|
|
||||||
|
|
||||||
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
|
||||||
- [ ] Your issue may already be reported! Please search on the [issue tracker](……/) before creating one.
|
|
||||||
- [ ] I have tested using the dev branch, and the issue still exists.
|
|
||||||
- [ ] This is an issue related to the Clash core, not to the derivatives of Clash, like Openclash or Koolclash
|
|
||||||
|
|
||||||
Please understand that we close issues that fail to follow the issue template.
|
|
||||||
-->
|
|
||||||
|
|
||||||
我都确认过了,我要继续提交。
|
|
||||||
<!-- None of the above, create a bug report -->
|
|
||||||
------------------------------------------------------------------
|
|
||||||
|
|
||||||
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
|
|
||||||
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
|
|
||||||
|
|
||||||
### clash core config
|
|
||||||
<!--
|
|
||||||
在下方附上 Clash core 脱敏后配置文件的内容
|
|
||||||
Paste the Clash core configuration below.
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
……
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clash log
|
|
||||||
<!--
|
|
||||||
在下方附上 Clash Core 的日志,log level 最好使用 DEBUG
|
|
||||||
Paste the Clash core log below with the log level set to `DEBUG`.
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
……
|
|
||||||
```
|
|
||||||
|
|
||||||
### 环境 Environment
|
|
||||||
|
|
||||||
* Clash Core 的操作系统 (the OS that the Clash core is running on)
|
|
||||||
……
|
|
||||||
* 使用者的操作系统 (the OS running on the client)
|
|
||||||
……
|
|
||||||
* 网路环境或拓扑 (network conditions/topology)
|
|
||||||
……
|
|
||||||
* iptables,如果适用 (if applicable)
|
|
||||||
……
|
|
||||||
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
|
||||||
……
|
|
||||||
* 其他
|
|
||||||
……
|
|
||||||
|
|
||||||
### 说明 Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
|
|
||||||
-->
|
|
||||||
|
|
||||||
### 重现问题的具体布骤 Steps to Reproduce
|
|
||||||
|
|
||||||
1. [First Step]
|
|
||||||
2. [Second Step]
|
|
||||||
3. ……
|
|
||||||
|
|
||||||
**我预期会发生……?**
|
|
||||||
<!-- **Expected behavior:** [What you expected to happen] -->
|
|
||||||
|
|
||||||
**实际上发生了什麽?**
|
|
||||||
<!-- **Actual behavior:** [What actually happened] -->
|
|
||||||
|
|
||||||
### 可能的解决方案 Possible Solution
|
|
||||||
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
|
|
||||||
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
|
|
||||||
<!-- or ideas how to implement the addition or change -->
|
|
||||||
|
|
||||||
### 更多信息 More Information
|
|
124
.github/ISSUE_TEMPLATE/bug_report_en.yml
vendored
Normal file
124
.github/ISSUE_TEMPLATE/bug_report_en.yml
vendored
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
name: (English) Report a bug of the Clash core
|
||||||
|
description: Create a bug report to help us improve
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
title: "[Bug] <issue title>"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Welcome to the official Clash open-source community"
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to report an issue with the Clash core.
|
||||||
|
|
||||||
|
Prior to submitting this issue, please read and follow the guidelines below to ensure that your issue can be resolved as quickly as possible. Options marked with an asterisk (*) are required, while others are optional. If the information you provide does not comply with the requirements, the maintainers may not respond and may directly close the issue.
|
||||||
|
|
||||||
|
If you can debug and fix the issue yourself, we welcome you to submit a pull request to merge your changes upstream.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
description: "If any of the following options do not apply, please do not submit this issue as we will close it"
|
||||||
|
options:
|
||||||
|
- label: "I understand that this is the official open-source version of the Clash core, **only providing support for the open-source version or Premium version**"
|
||||||
|
required: true
|
||||||
|
- label: "I am submitting an issue with the Clash core, not Clash.Meta / OpenClash / ClashX / Clash For Windows or any other derivative version"
|
||||||
|
required: true
|
||||||
|
- label: "I am using the latest version of the Clash or Clash Premium core **in this repository**"
|
||||||
|
required: true
|
||||||
|
- label: "I have searched at the [Issue Tracker](……/) **and have not found any related issues**"
|
||||||
|
required: true
|
||||||
|
- label: "I have read the [official Wiki](https://dreamacro.github.io/clash/) **and was unable to solve the issue**"
|
||||||
|
required: true
|
||||||
|
- label: "(required for Premium core) I've tried the `dev` branch and the issue still exists"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Environment"
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please provide the following information to help us locate the issue.
|
||||||
|
The issue might be closed if there's not enough information provided.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: "Run `clash -v` or look at the bottom-left corner of the Clash Dashboard to find out"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: "Select all operating systems that apply to this issue"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- Windows
|
||||||
|
- macOS (darwin)
|
||||||
|
- Android
|
||||||
|
- OpenBSD / FreeBSD
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: arch
|
||||||
|
attributes:
|
||||||
|
label: Architecture
|
||||||
|
description: "Select all architectures that apply to this issue"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- amd64
|
||||||
|
- amd64-v3
|
||||||
|
- arm64
|
||||||
|
- "386"
|
||||||
|
- armv5
|
||||||
|
- armv6
|
||||||
|
- armv7
|
||||||
|
- mips-softfloat
|
||||||
|
- mips-hardfloat
|
||||||
|
- mipsle-softfloat
|
||||||
|
- mipsle-hardfloat
|
||||||
|
- mips64
|
||||||
|
- mips64le
|
||||||
|
- riscv64
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Clash related information"
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Please provide relevant information about your Clash instance here. If you
|
||||||
|
do not provide enough information, the issue may be closed.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: YAML
|
||||||
|
label: Configuration File
|
||||||
|
placeholder: "Ensure that there is no sensitive information (such as server addresses, passwords, or ports) in the configuration file, and provide the minimum reproducible configuration. Do not post configurations with thousands of lines."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: Text
|
||||||
|
label: Log
|
||||||
|
placeholder: "Please attach the corresponding core outout (setting `log-level: debug` in the configuration provides debugging information)."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
placeholder: "Please describe your issue in detail here to help us understand (supports Markdown syntax)."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction Steps
|
||||||
|
placeholder: "Please provide the specific steps to reproduce the issue here (supports Markdown syntax)."
|
||||||
|
|
121
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal file
121
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
name: (中文)提交 Clash 核心的问题
|
||||||
|
description: 如果 Clash 核心运作不符合预期,在这里提交问题
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
title: "[Bug] <问题标题>"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## 欢迎来到 Clash 官方开源社区!"
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢你拨冗提交 Clash 内核的问题。在提交之前,请仔细阅读并遵守以下指引,以确保你的问题能够被尽快解决。
|
||||||
|
带有星号(*)的选项为必填,其他可选填。**如果你填写的资料不符合规范,维护者可能不予回复,并直接关闭这个 issue。**
|
||||||
|
如果你可以自行 debug 并且修正,我们随时欢迎你提交 Pull Request,将你的修改合并到上游。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: 先决条件
|
||||||
|
description: "若以下任意选项不适用,请勿提交这个 issue,因为我们会把它关闭"
|
||||||
|
options:
|
||||||
|
- label: "我了解这里是官方开源版 Clash 核心仓库,**只提供开源版或者 Premium 内核的支持**"
|
||||||
|
required: true
|
||||||
|
- label: "我要提交 Clash 核心的问题,并非 Clash.Meta / OpenClash / ClashX / Clash For Windows 或其他任何衍生版本的问题"
|
||||||
|
required: true
|
||||||
|
- label: "我使用的是**本仓库**最新版本的 Clash 或 Clash Premium 内核"
|
||||||
|
required: true
|
||||||
|
- label: "我已经在 [Issue Tracker](……/) 中找过我要提出的 bug,**并且没有找到相关问题**"
|
||||||
|
required: true
|
||||||
|
- label: "我已经仔细阅读 [官方 Wiki](https://dreamacro.github.io/clash/) 并无法自行解决问题"
|
||||||
|
required: true
|
||||||
|
- label: "(非 Premium 内核必填)我已经使用 dev 分支版本测试过,问题依旧存在"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## 系统环境"
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
请附上这个问题适用的环境,以帮助我们迅速定位问题并解决。若你提供的信息不足,我们将关闭
|
||||||
|
这个 issue 并要求你提供更多信息。
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: 版本
|
||||||
|
description: "运行 `clash -v` 或者查看 Clash Dashboard 的左下角来找到你现在使用的版本"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: 适用的作业系统
|
||||||
|
description: "勾选所有适用于这个 issue 的系统"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- Windows
|
||||||
|
- macOS (darwin)
|
||||||
|
- Android
|
||||||
|
- OpenBSD / FreeBSD
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: arch
|
||||||
|
attributes:
|
||||||
|
label: 适用的硬件架构
|
||||||
|
description: "勾选所有适用于这个 issue 的架构"
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- amd64
|
||||||
|
- amd64-v3
|
||||||
|
- arm64
|
||||||
|
- "386"
|
||||||
|
- armv5
|
||||||
|
- armv6
|
||||||
|
- armv7
|
||||||
|
- mips-softfloat
|
||||||
|
- mips-hardfloat
|
||||||
|
- mipsle-softfloat
|
||||||
|
- mipsle-hardfloat
|
||||||
|
- mips64
|
||||||
|
- mips64le
|
||||||
|
- riscv64
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Clash 相关信息"
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
请附上与这个问题直接相关的相应信息,以帮助我们迅速定位问题并解决。
|
||||||
|
若你提供的信息不足,我们将关闭这个 issue 并要求你提供更多信息。
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: YAML
|
||||||
|
label: "配置文件"
|
||||||
|
placeholder: "确保配置文件中没有敏感信息(如:服务器地址、密码、端口),并且提供最小可复现配置,严禁贴上上千行的配置"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
render: Text
|
||||||
|
label: 日志输出
|
||||||
|
placeholder: "在这里附上问题对应的内核日志(在配置中设置 `log-level: debug` 可获得调试信息)"
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 问题描述
|
||||||
|
placeholder: "在这里详细叙述你的问题,帮助我们理解(支持 Markdown 语法)"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 复现步骤
|
||||||
|
placeholder: "在这里提供问题的具体重现步骤(支持 Markdown 语法)"
|
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
9
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
|
||||||
|
contact_links:
|
||||||
|
- name: (中文)阅读 Wiki
|
||||||
|
url: https://dreamacro.github.io/clash/zh_CN/
|
||||||
|
about: 如果你是新手,或者想要了解 Clash 的更多信息,请阅读我们撰写的官方 Wiki
|
||||||
|
- name: (English) Read our Wiki page
|
||||||
|
url: https://dreamacro.github.io/clash/
|
||||||
|
about: If you are new to Clash, or want to know more about Clash, please read our Wiki page
|
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
78
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,78 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: "[Feature]"
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- The English version is available. -->
|
|
||||||
感谢你向 Clash Core 提交 Feature Request!
|
|
||||||
在提交之前,请确认:
|
|
||||||
|
|
||||||
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求
|
|
||||||
|
|
||||||
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thanks for submitting a feature request towards the Clash core!
|
|
||||||
But before so, please do the following checklist:
|
|
||||||
|
|
||||||
- [ ] I have searched on the [issue tracker](……/) before creating the issue.
|
|
||||||
|
|
||||||
Please understand that we close issues that fail to follow the issue template.
|
|
||||||
-->
|
|
||||||
|
|
||||||
我都确认过了,我要继续提交。
|
|
||||||
<!-- None of the above, create a feature request -->
|
|
||||||
------------------------------------------------------------------
|
|
||||||
|
|
||||||
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
|
|
||||||
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
|
|
||||||
|
|
||||||
### Clash core config
|
|
||||||
<!--
|
|
||||||
在下方附上 Clash Core 脱敏后的配置内容
|
|
||||||
Paste the Clash core configuration below.
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
……
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clash log
|
|
||||||
<!--
|
|
||||||
在下方附上 Clash Core 的日志,log level 请使用 DEBUG
|
|
||||||
Paste the Clash core log below with the log level set to `DEBUG`.
|
|
||||||
-->
|
|
||||||
```
|
|
||||||
……
|
|
||||||
```
|
|
||||||
|
|
||||||
### 环境 Environment
|
|
||||||
|
|
||||||
* Clash Core 的操作系统 (the OS that the Clash core is running on)
|
|
||||||
……
|
|
||||||
* 使用者的操作系统 (the OS running on the client)
|
|
||||||
……
|
|
||||||
* 网路环境或拓扑 (network conditions/topology)
|
|
||||||
……
|
|
||||||
* iptables,如果适用 (if applicable)
|
|
||||||
……
|
|
||||||
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
|
||||||
……
|
|
||||||
* 其他
|
|
||||||
……
|
|
||||||
|
|
||||||
### 说明 Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
|
|
||||||
-->
|
|
||||||
|
|
||||||
### 可能的解决方案 Possible Solution
|
|
||||||
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
|
|
||||||
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
|
|
||||||
<!-- or ideas how to implement the addition or change -->
|
|
||||||
|
|
||||||
### 更多信息 More Information
|
|
43
.github/ISSUE_TEMPLATE/feature_request_en.yml
vendored
Normal file
43
.github/ISSUE_TEMPLATE/feature_request_en.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: (English) Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
title: "[Feature] <title>"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Welcome to the official Clash open-source community"
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for taking the time to make a suggestion to the Clash core.
|
||||||
|
|
||||||
|
Prior to submitting this issue, please read and follow the guidelines below to ensure that your issue can be resolved as quickly as possible. Options marked with an asterisk (*) are required, while others are optional. If the information you provide does not comply with the requirements, the maintainers may not respond and may directly close the issue.
|
||||||
|
|
||||||
|
If you can implement your idea by yourself, we welcome you to submit a pull request to merge your changes upstream.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: Prerequisites
|
||||||
|
description: "If any of the following options do not apply, please do not submit this issue as we will close it"
|
||||||
|
options:
|
||||||
|
- label: "I understand that this is the official open-source version of the Clash core, **only providing support for the open-source version or Premium version**"
|
||||||
|
required: true
|
||||||
|
- label: "I have looked for my idea in [the issue tracker](https://github.com/Dreamacro/clash/issues?q=is%3Aissue+label%3Aenhancement), **and found none of which being related**"
|
||||||
|
required: true
|
||||||
|
- label: "I have read the [official Wiki](https://dreamacro.github.io/clash/)"
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
placeholder: "Please explain your suggestions in detail and in a clear manner. For instance, how does this issue impact you? What specific functionality are you hoping to achieve? Also, let us know what Clash Core is currently doing in terms of your suggestion, and what you would like it to do instead."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Possible Solution
|
||||||
|
placeholder: "Do you have any ideas on the implementation details?"
|
41
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: (中文)建议一个新功能
|
||||||
|
description: 在这里提供一个的想法或建议
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
title: "[Feature] <标题>"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## 欢迎来到 Clash 官方开源社区!"
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
感谢你拨冗为 Clash 内核提供建议。在提交之前,请仔细阅读并遵守以下指引,以确保你的建议能够被顺利采纳。
|
||||||
|
带有星号(*)的选项为必填,其他可选填。**如果你填写的资料不符合规范,维护者可能不予回复,并直接关闭这个 issue。**
|
||||||
|
如果你可以自行添加这个功能,我们随时欢迎你提交 Pull Request,并将你的修改合并到上游。
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: ensure
|
||||||
|
attributes:
|
||||||
|
label: 先决条件
|
||||||
|
description: "若以下任意选项不适用,请勿提交这个 issue,因为我们会把它关闭"
|
||||||
|
options:
|
||||||
|
- label: "我了解这里是 Clash 官方仓库,并非 Clash.Meta / OpenClash / ClashX / Clash For Windows 或其他任何衍生版本"
|
||||||
|
required: true
|
||||||
|
- label: "我已经在[这里](https://github.com/Dreamacro/clash/issues?q=is%3Aissue+label%3Aenhancement)找过我要提出的建议,**并且没有找到相关问题**"
|
||||||
|
required: true
|
||||||
|
- label: "我已经仔细阅读 [官方 Wiki](https://dreamacro.github.io/clash/) "
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 描述
|
||||||
|
placeholder: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什么?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: 可能的解决方案
|
||||||
|
placeholder: 此项非必须,但是如果你有想法的话欢迎提出。
|
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
30
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['go']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
42
.github/workflows/deploy-docs.yml
vendored
Normal file
42
.github/workflows/deploy-docs.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20]
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: docs
|
||||||
|
run: pnpm install --frozen-lockfile=false
|
||||||
|
- name: Build
|
||||||
|
working-directory: docs
|
||||||
|
run: pnpm run docs:build
|
||||||
|
- uses: actions/configure-pages@v2
|
||||||
|
- uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: docs/.vitepress/dist
|
||||||
|
- name: Deploy
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v2
|
80
.github/workflows/docker.yml
vendored
Normal file
80
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
name: Publish Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
with:
|
||||||
|
platforms: all
|
||||||
|
|
||||||
|
- name: Set up docker buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to Github Package
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: Dreamacro
|
||||||
|
password: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build dev branch and push
|
||||||
|
if: github.ref == 'refs/heads/dev'
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Get all docker tags
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
id: tags
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
|
||||||
|
const tags = [
|
||||||
|
'dreamacro/clash:latest',
|
||||||
|
`dreamacro/clash:${ref}`,
|
||||||
|
'ghcr.io/dreamacro/clash:latest',
|
||||||
|
`ghcr.io/dreamacro/clash:${ref}`
|
||||||
|
]
|
||||||
|
return tags.join(',')
|
||||||
|
result-encoding: string
|
||||||
|
|
||||||
|
- name: Build release and push
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{steps.tags.outputs.result}}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
18
.github/workflows/linter.yml
vendored
Normal file
18
.github/workflows/linter.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
name: Linter
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
check-latest: true
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
@ -1,28 +1,29 @@
|
|||||||
name: Go
|
name: Release
|
||||||
on: [push, pull_request]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v1
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.14.x
|
check-latest: true
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Get dependencies and run test
|
- name: Get dependencies, run test
|
||||||
run: |
|
run: |
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
@ -31,14 +32,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NAME: clash
|
NAME: clash
|
||||||
BINDIR: bin
|
BINDIR: bin
|
||||||
run: make -j releases
|
run: make -j $(go run ./test/main.go) releases
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
files: bin/*
|
files: bin/*
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: true
|
|
18
.github/workflows/stale.yml
vendored
Normal file
18
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v7
|
||||||
|
with:
|
||||||
|
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-close: 5
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -12,7 +12,7 @@ bin/*
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# dep
|
# go mod vendor
|
||||||
vendor
|
vendor
|
||||||
|
|
||||||
# GoLand
|
# GoLand
|
||||||
@ -20,3 +20,17 @@ vendor
|
|||||||
|
|
||||||
# macOS file
|
# macOS file
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# test suite
|
||||||
|
test/config/cache*
|
||||||
|
|
||||||
|
# docs site generator
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# docs site cache
|
||||||
|
docs/.vitepress/cache
|
||||||
|
|
||||||
|
# docs site build files
|
||||||
|
docs/.vitepress/dist
|
||||||
|
23
.golangci.yaml
Normal file
23
.golangci.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gci:
|
||||||
|
custom-order: true
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- prefix(github.com/Dreamacro/clash)
|
||||||
|
- default
|
||||||
|
staticcheck:
|
||||||
|
go: '1.20'
|
22
Dockerfile
22
Dockerfile
@ -1,16 +1,22 @@
|
|||||||
FROM golang:alpine as builder
|
FROM --platform=${BUILDPLATFORM} golang:alpine as builder
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
RUN apk add --no-cache make git ca-certificates tzdata && \
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
|
||||||
WORKDIR /clash-src
|
WORKDIR /workdir
|
||||||
COPY . /clash-src
|
COPY --from=tonistiigi/xx:golang / /
|
||||||
RUN go mod download && \
|
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||||
make linux-amd64 && \
|
|
||||||
mv ./bin/clash-linux-amd64 /clash
|
RUN --mount=target=. \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
make BINDIR= ${TARGETOS}-${TARGETARCH}${TARGETVARIANT} && \
|
||||||
|
mv /clash* /clash
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||||
COPY --from=builder /clash /
|
COPY --from=builder /clash /
|
||||||
ENTRYPOINT ["/clash"]
|
ENTRYPOINT ["/clash"]
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
FROM golang:alpine as builder
|
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
|
|
||||||
wget -O /qemu-arm-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-arm-static && \
|
|
||||||
chmod +x /qemu-arm-static
|
|
||||||
|
|
||||||
WORKDIR /clash-src
|
|
||||||
COPY . /clash-src
|
|
||||||
RUN go mod download && \
|
|
||||||
make linux-armv7 && \
|
|
||||||
mv ./bin/clash-linux-armv7 /clash
|
|
||||||
|
|
||||||
FROM arm32v7/alpine:latest
|
|
||||||
|
|
||||||
COPY --from=builder /qemu-arm-static /usr/bin/
|
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
|
||||||
COPY --from=builder /clash /
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
ENTRYPOINT ["/clash"]
|
|
@ -1,20 +0,0 @@
|
|||||||
FROM golang:alpine as builder
|
|
||||||
|
|
||||||
RUN apk add --no-cache make git && \
|
|
||||||
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb && \
|
|
||||||
wget -O /qemu-aarch64-static https://github.com/multiarch/qemu-user-static/releases/latest/download/qemu-aarch64-static && \
|
|
||||||
chmod +x /qemu-aarch64-static
|
|
||||||
|
|
||||||
WORKDIR /clash-src
|
|
||||||
COPY . /clash-src
|
|
||||||
RUN go mod download && \
|
|
||||||
make linux-armv8 && \
|
|
||||||
mv ./bin/clash-linux-armv8 /clash
|
|
||||||
|
|
||||||
FROM arm64v8/alpine:latest
|
|
||||||
|
|
||||||
COPY --from=builder /qemu-aarch64-static /usr/bin/
|
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
|
||||||
COPY --from=builder /clash /
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
ENTRYPOINT ["/clash"]
|
|
63
Makefile
63
Makefile
@ -2,42 +2,61 @@ NAME=clash
|
|||||||
BINDIR=bin
|
BINDIR=bin
|
||||||
VERSION=$(shell git describe --tags || echo "unknown version")
|
VERSION=$(shell git describe --tags || echo "unknown version")
|
||||||
BUILDTIME=$(shell date -u)
|
BUILDTIME=$(shell date -u)
|
||||||
GOBUILD=CGO_ENABLED=0 go build -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
|
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
|
||||||
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
|
||||||
-w -s'
|
-w -s -buildid='
|
||||||
|
|
||||||
PLATFORM_LIST = \
|
PLATFORM_LIST = \
|
||||||
darwin-amd64 \
|
darwin-amd64 \
|
||||||
|
darwin-amd64-v3 \
|
||||||
|
darwin-arm64 \
|
||||||
linux-386 \
|
linux-386 \
|
||||||
linux-amd64 \
|
linux-amd64 \
|
||||||
|
linux-amd64-v3 \
|
||||||
linux-armv5 \
|
linux-armv5 \
|
||||||
linux-armv6 \
|
linux-armv6 \
|
||||||
linux-armv7 \
|
linux-armv7 \
|
||||||
linux-armv8 \
|
linux-arm64 \
|
||||||
linux-mips-softfloat \
|
linux-mips-softfloat \
|
||||||
linux-mips-hardfloat \
|
linux-mips-hardfloat \
|
||||||
linux-mipsle-softfloat \
|
linux-mipsle-softfloat \
|
||||||
linux-mipsle-hardfloat \
|
linux-mipsle-hardfloat \
|
||||||
linux-mips64 \
|
linux-mips64 \
|
||||||
linux-mips64le \
|
linux-mips64le \
|
||||||
|
linux-riscv64 \
|
||||||
|
linux-loong64 \
|
||||||
freebsd-386 \
|
freebsd-386 \
|
||||||
freebsd-amd64
|
freebsd-amd64 \
|
||||||
|
freebsd-amd64-v3 \
|
||||||
|
freebsd-arm64
|
||||||
|
|
||||||
WINDOWS_ARCH_LIST = \
|
WINDOWS_ARCH_LIST = \
|
||||||
windows-386 \
|
windows-386 \
|
||||||
windows-amd64
|
windows-amd64 \
|
||||||
|
windows-amd64-v3 \
|
||||||
|
windows-arm64 \
|
||||||
|
windows-armv7
|
||||||
|
|
||||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||||
|
|
||||||
darwin-amd64:
|
darwin-amd64:
|
||||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
darwin-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
darwin-arm64:
|
||||||
|
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-386:
|
linux-386:
|
||||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-amd64:
|
linux-amd64:
|
||||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-armv5:
|
linux-armv5:
|
||||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -47,7 +66,7 @@ linux-armv6:
|
|||||||
linux-armv7:
|
linux-armv7:
|
||||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-armv8:
|
linux-arm64:
|
||||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-mips-softfloat:
|
linux-mips-softfloat:
|
||||||
@ -68,18 +87,39 @@ linux-mips64:
|
|||||||
linux-mips64le:
|
linux-mips64le:
|
||||||
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-riscv64:
|
||||||
|
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-loong64:
|
||||||
|
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-386:
|
freebsd-386:
|
||||||
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-amd64:
|
freebsd-amd64:
|
||||||
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
freebsd-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
freebsd-arm64:
|
||||||
|
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
windows-386:
|
windows-386:
|
||||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-amd64:
|
windows-amd64:
|
||||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
|
windows-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
|
windows-arm64:
|
||||||
|
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
|
windows-armv7:
|
||||||
|
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
||||||
|
|
||||||
@ -93,5 +133,16 @@ $(zip_releases): %.zip : %
|
|||||||
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
||||||
|
|
||||||
releases: $(gz_releases) $(zip_releases)
|
releases: $(gz_releases) $(zip_releases)
|
||||||
|
|
||||||
|
LINT_OS_LIST := darwin windows linux freebsd openbsd
|
||||||
|
|
||||||
|
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
|
||||||
|
%-lint:
|
||||||
|
GOOS=$* golangci-lint run ./...
|
||||||
|
|
||||||
|
lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
|
||||||
|
%-lint-fix:
|
||||||
|
GOOS=$* golangci-lint run --fix ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm $(BINDIR)/*
|
rm $(BINDIR)/*
|
||||||
|
384
README.md
384
README.md
@ -7,387 +7,47 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Dreamacro/clash/actions">
|
<a href="https://github.com/Dreamacro/clash/actions">
|
||||||
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions">
|
<img src="https://img.shields.io/github/actions/workflow/status/Dreamacro/clash/release.yml?branch=master&style=flat-square" alt="Github Actions">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
||||||
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
|
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
|
||||||
<a href="https://github.com/Dreamacro/clash/releases">
|
<a href="https://github.com/Dreamacro/clash/releases">
|
||||||
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
|
||||||
|
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Local HTTP/HTTPS/SOCKS server with/without authentication
|
This is a general overview of the features that comes with Clash.
|
||||||
- VMess, Shadowsocks, Trojan (experimental), Snell protocol support for remote connections. UDP is supported.
|
|
||||||
- Built-in DNS server that aims to minimize DNS pollution attacks, supports DoH/DoT upstream. Fake IP is also supported.
|
|
||||||
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
|
|
||||||
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
|
|
||||||
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
|
|
||||||
- Netfilter TCP redirecting. You can deploy Clash on your Internet gateway with `iptables`.
|
|
||||||
- Comprehensive HTTP API controller
|
|
||||||
|
|
||||||
## Install
|
- Inbound: HTTP, HTTPS, SOCKS5 server, TUN device
|
||||||
|
- Outbound: Shadowsocks(R), VMess, Trojan, Snell, SOCKS5, HTTP(S), Wireguard
|
||||||
|
- Rule-based Routing: dynamic scripting, domain, IP addresses, process name and more
|
||||||
|
- Fake-IP DNS: minimises impact on DNS pollution and improves network performance
|
||||||
|
- Transparent Proxy: Redirect TCP and TProxy TCP/UDP with automatic route table/rule management
|
||||||
|
- Proxy Groups: automatic fallback, load balancing or latency testing
|
||||||
|
- Remote Providers: load remote proxy lists dynamically
|
||||||
|
- RESTful API: update configuration in-place via a comprehensive API
|
||||||
|
|
||||||
Clash requires Go >= 1.13. You can build it from source:
|
*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).*
|
||||||
|
|
||||||
```sh
|
## Documentation
|
||||||
$ go get -u -v github.com/Dreamacro/clash
|
|
||||||
```
|
|
||||||
|
|
||||||
Pre-built binaries are available here: [release](https://github.com/Dreamacro/clash/releases)
|
You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/).
|
||||||
Pre-built TUN mode binaries are available here: [TUN release](https://github.com/Dreamacro/clash/releases/tag/TUN). Source is not currently available.
|
|
||||||
|
|
||||||
Check Clash version with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ clash -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Daemonize Clash
|
|
||||||
|
|
||||||
Unfortunately, there is no native or elegant way to implement daemons on Golang. We recommend using third-party daemon management tools like PM2, Supervisor or the like to keep Clash running as a service.
|
|
||||||
|
|
||||||
In the case of [pm2](https://github.com/Unitech/pm2), start the daemon this way:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ pm2 start clash
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have Docker installed, it's recommended to deploy Clash directly using `docker-compose`: [run Clash in Docker](https://github.com/Dreamacro/clash/wiki/Run-clash-in-docker)
|
|
||||||
|
|
||||||
## Config
|
|
||||||
|
|
||||||
The default configuration directory is `$HOME/.config/clash`.
|
|
||||||
|
|
||||||
The name of the configuration file is `config.yaml`.
|
|
||||||
|
|
||||||
If you want to use another directory, use `-d` to control the configuration directory.
|
|
||||||
|
|
||||||
For example, you can use the current directory as the configuration directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ clash -d .
|
|
||||||
```
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>This is an example configuration file (click to expand)</summary>
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# port of HTTP
|
|
||||||
port: 7890
|
|
||||||
|
|
||||||
# port of SOCKS5
|
|
||||||
socks-port: 7891
|
|
||||||
|
|
||||||
# redir port for Linux and macOS
|
|
||||||
# redir-port: 7892
|
|
||||||
|
|
||||||
allow-lan: false
|
|
||||||
|
|
||||||
# Only applicable when setting allow-lan to true
|
|
||||||
# "*": bind all IP addresses
|
|
||||||
# 192.168.122.11: bind a single IPv4 address
|
|
||||||
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
|
|
||||||
# bind-address: "*"
|
|
||||||
|
|
||||||
# Rule / Global / Direct (default is Rule)
|
|
||||||
mode: Rule
|
|
||||||
|
|
||||||
# set log level to stdout (default is info)
|
|
||||||
# info / warning / error / debug / silent
|
|
||||||
log-level: info
|
|
||||||
|
|
||||||
# RESTful API for clash
|
|
||||||
external-controller: 127.0.0.1:9090
|
|
||||||
|
|
||||||
# you can put the static web resource (such as clash-dashboard) to a directory, and clash would serve in `${API}/ui`
|
|
||||||
# input is a relative path to the configuration directory or an absolute path
|
|
||||||
# external-ui: folder
|
|
||||||
|
|
||||||
# Secret for RESTful API (Optional)
|
|
||||||
# secret: ""
|
|
||||||
|
|
||||||
# experimental feature
|
|
||||||
experimental:
|
|
||||||
ignore-resolve-fail: true # ignore dns resolve fail, default value is true
|
|
||||||
# interface-name: en0 # outbound interface name
|
|
||||||
|
|
||||||
# authentication of local SOCKS5/HTTP(S) server
|
|
||||||
# authentication:
|
|
||||||
# - "user1:pass1"
|
|
||||||
# - "user2:pass2"
|
|
||||||
|
|
||||||
# # experimental hosts, support wildcard (e.g. *.clash.dev Even *.foo.*.example.com)
|
|
||||||
# # static domain has a higher priority than wildcard domain (foo.example.com > *.example.com > .example.com)
|
|
||||||
# hosts:
|
|
||||||
# '*.clash.dev': 127.0.0.1
|
|
||||||
# '.dev': 127.0.0.1
|
|
||||||
# 'alpha.clash.dev': '::1'
|
|
||||||
|
|
||||||
# dns:
|
|
||||||
# enable: true # set true to enable dns (default is false)
|
|
||||||
# ipv6: false # default is false
|
|
||||||
# listen: 0.0.0.0:53
|
|
||||||
# # default-nameserver: # resolve dns nameserver host, should fill pure IP
|
|
||||||
# # - 114.114.114.114
|
|
||||||
# # - 8.8.8.8
|
|
||||||
# enhanced-mode: redir-host # or fake-ip
|
|
||||||
# # fake-ip-range: 198.18.0.1/16 # if you don't know what it is, don't change it
|
|
||||||
# fake-ip-filter: # fake ip white domain list
|
|
||||||
# - '*.lan'
|
|
||||||
# - localhost.ptlogin2.qq.com
|
|
||||||
# nameserver:
|
|
||||||
# - 114.114.114.114
|
|
||||||
# - tls://dns.rubyfish.cn:853 # dns over tls
|
|
||||||
# - https://1.1.1.1/dns-query # dns over https
|
|
||||||
# fallback: # concurrent request with nameserver, fallback used when GEOIP country isn't CN
|
|
||||||
# - tcp://1.1.1.1
|
|
||||||
# fallback-filter:
|
|
||||||
# geoip: true # default
|
|
||||||
# ipcidr: # ips in these subnets will be considered polluted
|
|
||||||
# - 240.0.0.0/4
|
|
||||||
|
|
||||||
proxies:
|
|
||||||
# shadowsocks
|
|
||||||
# The supported ciphers(encrypt methods):
|
|
||||||
# aes-128-gcm aes-192-gcm aes-256-gcm
|
|
||||||
# aes-128-cfb aes-192-cfb aes-256-cfb
|
|
||||||
# aes-128-ctr aes-192-ctr aes-256-ctr
|
|
||||||
# rc4-md5 chacha20-ietf xchacha20
|
|
||||||
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
|
|
||||||
- name: "ss1"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
# udp: true
|
|
||||||
|
|
||||||
# old obfs configuration format remove after prerelease
|
|
||||||
- name: "ss2"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
plugin: obfs
|
|
||||||
plugin-opts:
|
|
||||||
mode: tls # or http
|
|
||||||
# host: bing.com
|
|
||||||
|
|
||||||
- name: "ss3"
|
|
||||||
type: ss
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
cipher: chacha20-ietf-poly1305
|
|
||||||
password: "password"
|
|
||||||
plugin: v2ray-plugin
|
|
||||||
plugin-opts:
|
|
||||||
mode: websocket # no QUIC now
|
|
||||||
# tls: true # wss
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# host: bing.com
|
|
||||||
# path: "/"
|
|
||||||
# mux: true
|
|
||||||
# headers:
|
|
||||||
# custom: value
|
|
||||||
|
|
||||||
# vmess
|
|
||||||
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
|
|
||||||
- name: "vmess"
|
|
||||||
type: vmess
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
alterId: 32
|
|
||||||
cipher: auto
|
|
||||||
# udp: true
|
|
||||||
# tls: true
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# network: ws
|
|
||||||
# ws-path: /path
|
|
||||||
# ws-headers:
|
|
||||||
# Host: v2ray.com
|
|
||||||
|
|
||||||
- name: "vmess-http"
|
|
||||||
type: vmess
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
uuid: uuid
|
|
||||||
alterId: 32
|
|
||||||
cipher: auto
|
|
||||||
# udp: true
|
|
||||||
# network: http
|
|
||||||
# http-opts:
|
|
||||||
# # method: "GET"
|
|
||||||
# # path:
|
|
||||||
# # - '/'
|
|
||||||
# # - '/video'
|
|
||||||
# # headers:
|
|
||||||
# # Connection:
|
|
||||||
# # - keep-alive
|
|
||||||
|
|
||||||
# socks5
|
|
||||||
- name: "socks"
|
|
||||||
type: socks5
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
# username: username
|
|
||||||
# password: password
|
|
||||||
# tls: true
|
|
||||||
# skip-cert-verify: true
|
|
||||||
# udp: true
|
|
||||||
|
|
||||||
# http
|
|
||||||
- name: "http"
|
|
||||||
type: http
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
# username: username
|
|
||||||
# password: password
|
|
||||||
# tls: true # https
|
|
||||||
# skip-cert-verify: true
|
|
||||||
|
|
||||||
# snell
|
|
||||||
- name: "snell"
|
|
||||||
type: snell
|
|
||||||
server: server
|
|
||||||
port: 44046
|
|
||||||
psk: yourpsk
|
|
||||||
# obfs-opts:
|
|
||||||
# mode: http # or tls
|
|
||||||
# host: bing.com
|
|
||||||
|
|
||||||
# trojan
|
|
||||||
- name: "trojan"
|
|
||||||
type: trojan
|
|
||||||
server: server
|
|
||||||
port: 443
|
|
||||||
password: yourpsk
|
|
||||||
# udp: true
|
|
||||||
# sni: example.com # aka server name
|
|
||||||
# alpn:
|
|
||||||
# - h2
|
|
||||||
# - http/1.1
|
|
||||||
# skip-cert-verify: true
|
|
||||||
|
|
||||||
proxy-groups:
|
|
||||||
# relay chains the proxies. proxies shall not contain a relay. No UDP support.
|
|
||||||
# Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet
|
|
||||||
- name: "relay"
|
|
||||||
type: relay
|
|
||||||
proxies:
|
|
||||||
- http
|
|
||||||
- vmess
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
|
|
||||||
# url-test select which proxy will be used by benchmarking speed to a URL.
|
|
||||||
- name: "auto"
|
|
||||||
type: url-test
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# fallback select an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
|
|
||||||
- name: "fallback-auto"
|
|
||||||
type: fallback
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# load-balance: The request of the same eTLD will be dial on the same proxy.
|
|
||||||
- name: "load-balance"
|
|
||||||
type: load-balance
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
url: 'http://www.gstatic.com/generate_204'
|
|
||||||
interval: 300
|
|
||||||
|
|
||||||
# select is used for selecting proxy or proxy group
|
|
||||||
# you can use RESTful API to switch proxy, is recommended for use in GUI.
|
|
||||||
- name: Proxy
|
|
||||||
type: select
|
|
||||||
proxies:
|
|
||||||
- ss1
|
|
||||||
- ss2
|
|
||||||
- vmess1
|
|
||||||
- auto
|
|
||||||
|
|
||||||
- name: UseProvider
|
|
||||||
type: select
|
|
||||||
use:
|
|
||||||
- provider1
|
|
||||||
proxies:
|
|
||||||
- Proxy
|
|
||||||
- DIRECT
|
|
||||||
|
|
||||||
proxy-providers:
|
|
||||||
provider1:
|
|
||||||
type: http
|
|
||||||
url: "url"
|
|
||||||
interval: 3600
|
|
||||||
path: ./hk.yaml
|
|
||||||
health-check:
|
|
||||||
enable: true
|
|
||||||
interval: 600
|
|
||||||
url: http://www.gstatic.com/generate_204
|
|
||||||
test:
|
|
||||||
type: file
|
|
||||||
path: /test.yaml
|
|
||||||
health-check:
|
|
||||||
enable: true
|
|
||||||
interval: 36000
|
|
||||||
url: http://www.gstatic.com/generate_204
|
|
||||||
|
|
||||||
rules:
|
|
||||||
- DOMAIN-SUFFIX,google.com,auto
|
|
||||||
- DOMAIN-KEYWORD,google,auto
|
|
||||||
- DOMAIN,google.com,auto
|
|
||||||
- DOMAIN-SUFFIX,ad.com,REJECT
|
|
||||||
# rename SOURCE-IP-CIDR and would remove after prerelease
|
|
||||||
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
|
|
||||||
# optional param "no-resolve" for IP rules (GEOIP IP-CIDR)
|
|
||||||
- IP-CIDR,127.0.0.0/8,DIRECT
|
|
||||||
- GEOIP,CN,DIRECT
|
|
||||||
- DST-PORT,80,DIRECT
|
|
||||||
- SRC-PORT,7777,DIRECT
|
|
||||||
# FINAL would remove after prerelease
|
|
||||||
# you also can use `FINAL,Proxy` or `FINAL,,Proxy` now
|
|
||||||
- MATCH,auto
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Advanced
|
|
||||||
[Provider](https://github.com/Dreamacro/clash/wiki/Provider)
|
|
||||||
|
|
||||||
## Documentations
|
|
||||||
https://clash.gitbook.io/
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
[riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||||
|
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||||
[v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
This software is released under the GPL-3.0 license.
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- [x] Complementing the necessary rule operators
|
|
||||||
- [x] Redir proxy
|
|
||||||
- [x] UDP support
|
|
||||||
- [x] Connection manager
|
|
||||||
- [ ] Event API
|
|
||||||
|
@ -1,129 +1,61 @@
|
|||||||
package outbound
|
package adapter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
defaultURLTestTimeout = time.Second * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type Base struct {
|
|
||||||
name string
|
|
||||||
addr string
|
|
||||||
tp C.AdapterType
|
|
||||||
udp bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) Name() string {
|
|
||||||
return b.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) Type() C.AdapterType {
|
|
||||||
return b.tp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|
||||||
return c, errors.New("no support")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
return nil, errors.New("no support")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) SupportUDP() bool {
|
|
||||||
return b.udp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]string{
|
|
||||||
"type": b.Type().String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) Addr() string {
|
|
||||||
return b.addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
|
|
||||||
return &Base{name, addr, tp, udp}
|
|
||||||
}
|
|
||||||
|
|
||||||
type conn struct {
|
|
||||||
net.Conn
|
|
||||||
chain C.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *conn) Chains() C.Chain {
|
|
||||||
return c.chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
|
||||||
c.chain = append(c.chain, a.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
|
||||||
return &conn{c, []string{a.Name()}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketConn interface {
|
|
||||||
net.PacketConn
|
|
||||||
WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type packetConn struct {
|
|
||||||
PacketConn
|
|
||||||
chain C.Chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *packetConn) Chains() C.Chain {
|
|
||||||
return c.chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
|
||||||
c.chain = append(c.chain, a.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPacketConn(pc PacketConn, a C.ProxyAdapter) C.PacketConn {
|
|
||||||
return &packetConn{pc, []string{a.Name()}}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
history *queue.Queue
|
history *queue.Queue
|
||||||
alive bool
|
alive *atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alive implements C.Proxy
|
||||||
func (p *Proxy) Alive() bool {
|
func (p *Proxy) Alive() bool {
|
||||||
return p.alive
|
return p.alive.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dial implements C.Proxy
|
||||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return p.DialContext(ctx, metadata)
|
return p.DialContext(ctx, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
if err != nil {
|
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
||||||
p.alive = false
|
p.alive.Store(err == nil)
|
||||||
}
|
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
|
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return p.ListenPacketContext(ctx, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
||||||
|
p.alive.Store(err == nil)
|
||||||
|
return pc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelayHistory implements C.Proxy
|
||||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||||
queue := p.history.Copy()
|
queue := p.history.Copy()
|
||||||
histories := []C.DelayHistory{}
|
histories := []C.DelayHistory{}
|
||||||
@ -134,9 +66,10 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||||
|
// implements C.Proxy
|
||||||
func (p *Proxy) LastDelay() (delay uint16) {
|
func (p *Proxy) LastDelay() (delay uint16) {
|
||||||
var max uint16 = 0xffff
|
var max uint16 = 0xffff
|
||||||
if !p.alive {
|
if !p.alive.Load() {
|
||||||
return max
|
return max
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,26 +84,31 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
|||||||
return history.Delay
|
return history.Delay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return inner, err
|
return inner, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping := map[string]interface{}{}
|
mapping := map[string]any{}
|
||||||
json.Unmarshal(inner, &mapping)
|
json.Unmarshal(inner, &mapping)
|
||||||
mapping["history"] = p.DelayHistory()
|
mapping["history"] = p.DelayHistory()
|
||||||
|
mapping["alive"] = p.Alive()
|
||||||
mapping["name"] = p.Name()
|
mapping["name"] = p.Name()
|
||||||
|
mapping["udp"] = p.SupportUDP()
|
||||||
return json.Marshal(mapping)
|
return json.Marshal(mapping)
|
||||||
}
|
}
|
||||||
|
|
||||||
// URLTest get the delay for the specified URL
|
// URLTest get the delay for the specified URL
|
||||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
// implements C.Proxy
|
||||||
|
func (p *Proxy) URLTest(ctx context.Context, url string) (delay, meanDelay uint16, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.alive = err == nil
|
p.alive.Store(err == nil)
|
||||||
record := C.DelayHistory{Time: time.Now()}
|
record := C.DelayHistory{Time: time.Now()}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
record.Delay = t
|
record.Delay = delay
|
||||||
|
record.MeanDelay = meanDelay
|
||||||
}
|
}
|
||||||
p.history.Put(record)
|
p.history.Put(record)
|
||||||
if p.history.Len() > 10 {
|
if p.history.Len() > 10 {
|
||||||
@ -213,15 +151,53 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
defer client.CloseIdleConnections()
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
t = uint16(time.Since(start) / time.Millisecond)
|
delay = uint16(time.Since(start) / time.Millisecond)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// ignore error because some server will hijack the connection and close immediately
|
||||||
|
return delay, 0, nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
meanDelay = uint16(time.Since(start) / time.Millisecond / 2)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||||
return &Proxy{adapter, queue.New(10), true}
|
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
port := u.Port()
|
||||||
|
if port == "" {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "https":
|
||||||
|
port = "443"
|
||||||
|
case "http":
|
||||||
|
port = "80"
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("%s scheme not Support", rawURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = C.Metadata{
|
||||||
|
Host: u.Hostname(),
|
||||||
|
DstIP: nil,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
27
adapter/inbound/http.go
Normal file
27
adapter/inbound/http.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHTTP receive normal http request and return HTTPContext
|
||||||
|
func NewHTTP(target socks5.Addr, source net.Addr, originTarget net.Addr, conn net.Conn) *context.ConnContext {
|
||||||
|
metadata := parseSocksAddr(target)
|
||||||
|
metadata.NetWork = C.TCP
|
||||||
|
metadata.Type = C.HTTP
|
||||||
|
if ip, port, err := parseAddr(source.String()); err == nil {
|
||||||
|
metadata.SrcIP = ip
|
||||||
|
metadata.SrcPort = port
|
||||||
|
}
|
||||||
|
if originTarget != nil {
|
||||||
|
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context.NewConnContext(conn, metadata)
|
||||||
|
}
|
24
adapter/inbound/https.go
Normal file
24
adapter/inbound/https.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHTTPS receive CONNECT request and return ConnContext
|
||||||
|
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
||||||
|
metadata := parseHTTPAddr(request)
|
||||||
|
metadata.Type = C.HTTPCONNECT
|
||||||
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
|
metadata.SrcIP = ip
|
||||||
|
metadata.SrcPort = port
|
||||||
|
}
|
||||||
|
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
return context.NewConnContext(conn, metadata)
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
package inbound
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
|
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
|
||||||
@ -17,7 +20,7 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPacket is PacketAdapter generator
|
// NewPacket is PacketAdapter generator
|
||||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.UDP
|
metadata.NetWork = C.UDP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
@ -25,7 +28,11 @@ func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAda
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
if originTarget != nil {
|
||||||
|
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
}
|
||||||
return &PacketAdapter{
|
return &PacketAdapter{
|
||||||
UDPPacket: packet,
|
UDPPacket: packet,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
25
adapter/inbound/socket.go
Normal file
25
adapter/inbound/socket.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/context"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSocket receive TCP inbound and return ConnContext
|
||||||
|
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
|
||||||
|
metadata := parseSocksAddr(target)
|
||||||
|
metadata.NetWork = C.TCP
|
||||||
|
metadata.Type = source
|
||||||
|
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||||
|
metadata.SrcIP = ip
|
||||||
|
metadata.SrcPort = port
|
||||||
|
}
|
||||||
|
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
return context.NewConnContext(conn, metadata)
|
||||||
|
}
|
@ -4,19 +4,19 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{}
|
||||||
AddrType: int(target[0]),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch target[0] {
|
switch target[0] {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
metadata.Host = string(target[2 : 2+target[1]])
|
// trim for FQDN
|
||||||
|
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
|
||||||
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
||||||
case socks5.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
ip := net.IP(target[1 : 1+net.IPv4len])
|
ip := net.IP(target[1 : 1+net.IPv4len])
|
||||||
@ -38,22 +38,17 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
port = "80"
|
port = "80"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim FQDN (#737)
|
||||||
|
host = strings.TrimRight(host, ".")
|
||||||
|
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{
|
||||||
NetWork: C.TCP,
|
NetWork: C.TCP,
|
||||||
AddrType: C.AtypDomainName,
|
Host: host,
|
||||||
Host: host,
|
DstIP: nil,
|
||||||
DstIP: nil,
|
DstPort: port,
|
||||||
DstPort: port,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
if ip != nil {
|
|
||||||
switch {
|
|
||||||
case ip.To4() == nil:
|
|
||||||
metadata.AddrType = C.AtypIPv6
|
|
||||||
default:
|
|
||||||
metadata.AddrType = C.AtypIPv4
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
metadata.DstIP = ip
|
||||||
}
|
}
|
||||||
|
|
138
adapter/outbound/base.go
Normal file
138
adapter/outbound/base.go
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Base struct {
|
||||||
|
name string
|
||||||
|
addr string
|
||||||
|
iface string
|
||||||
|
tp C.AdapterType
|
||||||
|
udp bool
|
||||||
|
rmark int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements C.ProxyAdapter
|
||||||
|
func (b *Base) Name() string {
|
||||||
|
return b.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type implements C.ProxyAdapter
|
||||||
|
func (b *Base) Type() C.AdapterType {
|
||||||
|
return b.tp
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
return c, errors.New("no support")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
return nil, errors.New("no support")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
|
func (b *Base) SupportUDP() bool {
|
||||||
|
return b.udp
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]string{
|
||||||
|
"type": b.Type().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr implements C.ProxyAdapter
|
||||||
|
func (b *Base) Addr() string {
|
||||||
|
return b.addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialOptions return []dialer.Option from struct
|
||||||
|
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
|
||||||
|
if b.iface != "" {
|
||||||
|
opts = append(opts, dialer.WithInterface(b.iface))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.rmark != 0 {
|
||||||
|
opts = append(opts, dialer.WithRoutingMark(b.rmark))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasicOption struct {
|
||||||
|
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
|
||||||
|
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseOption struct {
|
||||||
|
Name string
|
||||||
|
Addr string
|
||||||
|
Type C.AdapterType
|
||||||
|
UDP bool
|
||||||
|
Interface string
|
||||||
|
RoutingMark int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBase(opt BaseOption) *Base {
|
||||||
|
return &Base{
|
||||||
|
name: opt.Name,
|
||||||
|
addr: opt.Addr,
|
||||||
|
tp: opt.Type,
|
||||||
|
udp: opt.UDP,
|
||||||
|
iface: opt.Interface,
|
||||||
|
rmark: opt.RoutingMark,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type conn struct {
|
||||||
|
net.Conn
|
||||||
|
chain C.Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chains implements C.Connection
|
||||||
|
func (c *conn) Chains() C.Chain {
|
||||||
|
return c.chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendToChains implements C.Connection
|
||||||
|
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
||||||
|
c.chain = append(c.chain, a.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||||
|
return &conn{c, []string{a.Name()}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type packetConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
chain C.Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chains implements C.Connection
|
||||||
|
func (c *packetConn) Chains() C.Chain {
|
||||||
|
return c.chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendToChains implements C.Connection
|
||||||
|
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
||||||
|
c.chain = append(c.chain, a.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||||
|
return &packetConn{pc, []string{a.Name()}}
|
||||||
|
}
|
46
adapter/outbound/direct.go
Normal file
46
adapter/outbound/direct.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Direct struct {
|
||||||
|
*Base
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return NewConn(c, d), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newPacketConn(&directPacketConn{pc}, d), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type directPacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirect() *Direct {
|
||||||
|
return &Direct{
|
||||||
|
Base: &Base{
|
||||||
|
name: "DIRECT",
|
||||||
|
tp: C.Direct,
|
||||||
|
udp: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -22,22 +22,29 @@ type Http struct {
|
|||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
Headers http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpOption struct {
|
type HttpOption struct {
|
||||||
Name string `proxy:"name"`
|
BasicOption
|
||||||
Server string `proxy:"server"`
|
Name string `proxy:"name"`
|
||||||
Port int `proxy:"port"`
|
Server string `proxy:"server"`
|
||||||
UserName string `proxy:"username,omitempty"`
|
Port int `proxy:"port"`
|
||||||
Password string `proxy:"password,omitempty"`
|
UserName string `proxy:"username,omitempty"`
|
||||||
TLS bool `proxy:"tls,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
|
SNI string `proxy:"sni,omitempty"`
|
||||||
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
Headers map[string]string `proxy:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if h.tlsConfig != nil {
|
if h.tlsConfig != nil {
|
||||||
cc := tls.Client(c, h.tlsConfig)
|
cc := tls.Client(c, h.tlsConfig)
|
||||||
err := cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err := cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
@ -50,13 +57,18 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
c, err := dialer.DialContext(ctx, "tcp", h.addr)
|
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = h.StreamConn(c, metadata)
|
c, err = h.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -72,12 +84,12 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Host: addr,
|
Host: addr,
|
||||||
},
|
},
|
||||||
Host: addr,
|
Host: addr,
|
||||||
Header: http.Header{
|
Header: h.Headers.Clone(),
|
||||||
"Proxy-Connection": []string{"Keep-Alive"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Proxy-Connection", "Keep-Alive")
|
||||||
|
|
||||||
if h.user != "" && h.pass != "" {
|
if h.user != "" && h.pass != "" {
|
||||||
auth := h.user + ":" + h.pass
|
auth := h.user + ":" + h.pass
|
||||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||||
@ -114,21 +126,32 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
func NewHttp(option HttpOption) *Http {
|
func NewHttp(option HttpOption) *Http {
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
|
sni := option.Server
|
||||||
|
if option.SNI != "" {
|
||||||
|
sni = option.SNI
|
||||||
|
}
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
InsecureSkipVerify: option.SkipCertVerify,
|
||||||
ClientSessionCache: getClientSessionCache(),
|
ServerName: sni,
|
||||||
ServerName: option.Server,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers := http.Header{}
|
||||||
|
for name, value := range option.Headers {
|
||||||
|
headers.Add(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
return &Http{
|
return &Http{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Http,
|
tp: C.Http,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
Headers: headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
62
adapter/outbound/reject.go
Normal file
62
adapter/outbound/reject.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reject struct {
|
||||||
|
*Base
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
|
return NewConn(&nopConn{}, r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
return newPacketConn(&nopPacketConn{}, r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReject() *Reject {
|
||||||
|
return &Reject{
|
||||||
|
Base: &Base{
|
||||||
|
name: "REJECT",
|
||||||
|
tp: C.Reject,
|
||||||
|
udp: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *nopConn) Close() error { return nil }
|
||||||
|
func (rw *nopConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (rw *nopConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (rw *nopConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
type nopPacketConn struct{}
|
||||||
|
|
||||||
|
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
|
||||||
|
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
|
||||||
|
func (npc *nopPacketConn) Close() error { return nil }
|
||||||
|
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
|
||||||
|
func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
|
@ -2,7 +2,6 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -10,12 +9,11 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
|
||||||
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/shadowsocks/core"
|
||||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
@ -29,22 +27,19 @@ type ShadowSocks struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksOption struct {
|
type ShadowSocksOption struct {
|
||||||
Name string `proxy:"name"`
|
BasicOption
|
||||||
Server string `proxy:"server"`
|
Name string `proxy:"name"`
|
||||||
Port int `proxy:"port"`
|
Server string `proxy:"server"`
|
||||||
Password string `proxy:"password"`
|
Port int `proxy:"port"`
|
||||||
Cipher string `proxy:"cipher"`
|
Password string `proxy:"password"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
Cipher string `proxy:"cipher"`
|
||||||
Plugin string `proxy:"plugin,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
Plugin string `proxy:"plugin,omitempty"`
|
||||||
|
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
||||||
// deprecated when bump to 1.0
|
|
||||||
Obfs string `proxy:"obfs,omitempty"`
|
|
||||||
ObfsHost string `proxy:"obfs-host,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type simpleObfsOption struct {
|
type simpleObfsOption struct {
|
||||||
Mode string `obfs:"mode"`
|
Mode string `obfs:"mode,omitempty"`
|
||||||
Host string `obfs:"host,omitempty"`
|
Host string `obfs:"host,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +53,7 @@ type v2rayObfsOption struct {
|
|||||||
Mux bool `obfs:"mux,omitempty"`
|
Mux bool `obfs:"mux,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
switch ss.obfsMode {
|
switch ss.obfsMode {
|
||||||
case "tls":
|
case "tls":
|
||||||
@ -77,25 +73,32 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
|||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
return NewConn(c, ss), err
|
return NewConn(c, ss), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
pc, err := dialer.ListenPacket("udp", "")
|
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := resolveUDPAddr("udp", ss.addr)
|
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
pc.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +106,6 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]string{
|
|
||||||
"type": ss.Type().String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
cipher := option.Cipher
|
cipher := option.Cipher
|
||||||
@ -122,17 +119,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
var obfsOption *simpleObfsOption
|
var obfsOption *simpleObfsOption
|
||||||
obfsMode := ""
|
obfsMode := ""
|
||||||
|
|
||||||
// forward compatibility before 1.0
|
|
||||||
if option.Obfs != "" {
|
|
||||||
obfsMode = option.Obfs
|
|
||||||
obfsOption = &simpleObfsOption{
|
|
||||||
Host: "bing.com",
|
|
||||||
}
|
|
||||||
if option.ObfsHost != "" {
|
|
||||||
obfsOption.Host = option.ObfsHost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
if option.Plugin == "obfs" {
|
if option.Plugin == "obfs" {
|
||||||
opts := simpleObfsOption{Host: "bing.com"}
|
opts := simpleObfsOption{Host: "bing.com"}
|
||||||
@ -165,16 +151,17 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
if opts.TLS {
|
if opts.TLS {
|
||||||
v2rayOption.TLS = true
|
v2rayOption.TLS = true
|
||||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||||
v2rayOption.SessionCache = getClientSessionCache()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ShadowSocks{
|
return &ShadowSocks{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Shadowsocks,
|
tp: C.Shadowsocks,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
cipher: ciph,
|
cipher: ciph,
|
||||||
|
|
||||||
@ -197,14 +184,6 @@ func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|||||||
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (spc *ssPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, _, e := spc.PacketConn.ReadFrom(b)
|
n, _, e := spc.PacketConn.ReadFrom(b)
|
||||||
if e != nil {
|
if e != nil {
|
159
adapter/outbound/shadowsocksr.go
Normal file
159
adapter/outbound/shadowsocksr.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/shadowsocks/core"
|
||||||
|
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
|
||||||
|
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
|
||||||
|
"github.com/Dreamacro/clash/transport/ssr/obfs"
|
||||||
|
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShadowSocksR struct {
|
||||||
|
*Base
|
||||||
|
cipher core.Cipher
|
||||||
|
obfs obfs.Obfs
|
||||||
|
protocol protocol.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShadowSocksROption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
Password string `proxy:"password"`
|
||||||
|
Cipher string `proxy:"cipher"`
|
||||||
|
Obfs string `proxy:"obfs"`
|
||||||
|
ObfsParam string `proxy:"obfs-param,omitempty"`
|
||||||
|
Protocol string `proxy:"protocol"`
|
||||||
|
ProtocolParam string `proxy:"protocol-param,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
c = ssr.obfs.StreamConn(c)
|
||||||
|
c = ssr.cipher.StreamConn(c)
|
||||||
|
var (
|
||||||
|
iv []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch conn := c.(type) {
|
||||||
|
case *shadowstream.Conn:
|
||||||
|
iv, err = conn.ObtainWriteIV()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case *shadowaead.Conn:
|
||||||
|
return nil, fmt.Errorf("invalid connection type")
|
||||||
|
}
|
||||||
|
c = ssr.protocol.StreamConn(c, iv)
|
||||||
|
_, err = c.Write(serializesSocksAddr(metadata))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = ssr.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, ssr), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := resolveUDPAddr("udp", ssr.addr)
|
||||||
|
if err != nil {
|
||||||
|
pc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = ssr.cipher.PacketConn(pc)
|
||||||
|
pc = ssr.protocol.PacketConn(pc)
|
||||||
|
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||||
|
// SSR protocol compatibility
|
||||||
|
// https://github.com/Dreamacro/clash/pull/2056
|
||||||
|
if option.Cipher == "none" {
|
||||||
|
option.Cipher = "dummy"
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
cipher := option.Cipher
|
||||||
|
password := option.Password
|
||||||
|
coreCiph, err := core.PickCipher(cipher, nil, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
ivSize int
|
||||||
|
key []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if option.Cipher == "dummy" {
|
||||||
|
ivSize = 0
|
||||||
|
key = core.Kdf(option.Password, 16)
|
||||||
|
} else {
|
||||||
|
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
|
||||||
|
}
|
||||||
|
ivSize = ciph.IVSize()
|
||||||
|
key = ciph.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
|
||||||
|
Host: option.Server,
|
||||||
|
Port: option.Port,
|
||||||
|
Key: key,
|
||||||
|
IVSize: ivSize,
|
||||||
|
Param: option.ObfsParam,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
|
||||||
|
Key: key,
|
||||||
|
Overhead: obfsOverhead,
|
||||||
|
Param: option.ProtocolParam,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ShadowSocksR{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.ShadowsocksR,
|
||||||
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
},
|
||||||
|
cipher: coreCiph,
|
||||||
|
obfs: obfs,
|
||||||
|
protocol: protocol,
|
||||||
|
}, nil
|
||||||
|
}
|
166
adapter/outbound/snell.go
Normal file
166
adapter/outbound/snell.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
|
"github.com/Dreamacro/clash/transport/snell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Snell struct {
|
||||||
|
*Base
|
||||||
|
psk []byte
|
||||||
|
pool *snell.Pool
|
||||||
|
obfsOption *simpleObfsOption
|
||||||
|
version int
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnellOption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
Psk string `proxy:"psk"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
Version int `proxy:"version,omitempty"`
|
||||||
|
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamOption struct {
|
||||||
|
psk []byte
|
||||||
|
version int
|
||||||
|
addr string
|
||||||
|
obfsOption *simpleObfsOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
||||||
|
switch option.obfsOption.Mode {
|
||||||
|
case "tls":
|
||||||
|
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
|
||||||
|
case "http":
|
||||||
|
_, port, _ := net.SplitHostPort(option.addr)
|
||||||
|
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
|
||||||
|
}
|
||||||
|
return snell.StreamConn(c, option.psk, option.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||||
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
|
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
if s.version == snell.Version2 && len(opts) == 0 {
|
||||||
|
c, err := s.pool.Get()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
|
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c, s), err
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = s.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, s), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||||
|
|
||||||
|
err = snell.WriteUDPHeader(c, s.version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := snell.PacketConn(c)
|
||||||
|
return newPacketConn(pc, s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnell(option SnellOption) (*Snell, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
psk := []byte(option.Psk)
|
||||||
|
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||||
|
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
||||||
|
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
||||||
|
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch obfsOption.Mode {
|
||||||
|
case "tls", "http", "":
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backward compatible
|
||||||
|
if option.Version == 0 {
|
||||||
|
option.Version = snell.DefaultSnellVersion
|
||||||
|
}
|
||||||
|
switch option.Version {
|
||||||
|
case snell.Version1, snell.Version2:
|
||||||
|
if option.UDP {
|
||||||
|
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
|
||||||
|
}
|
||||||
|
case snell.Version3:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("snell version error: %d", option.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Snell{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.Snell,
|
||||||
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
},
|
||||||
|
psk: psk,
|
||||||
|
obfsOption: obfsOption,
|
||||||
|
version: option.Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.Version == snell.Version2 {
|
||||||
|
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
@ -6,13 +6,12 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Socks5 struct {
|
type Socks5 struct {
|
||||||
@ -25,6 +24,7 @@ type Socks5 struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Socks5Option struct {
|
type Socks5Option struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -35,10 +35,13 @@ type Socks5Option struct {
|
|||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if ss.tls {
|
if ss.tls {
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
err := cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err := cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
@ -58,13 +61,18 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -73,10 +81,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
|
|||||||
return NewConn(c, ss), nil
|
return NewConn(c, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||||
defer cancel()
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
return
|
return
|
||||||
@ -84,15 +91,15 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
|
|
||||||
if ss.tls {
|
if ss.tls {
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
err = cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func(c net.Conn) {
|
||||||
if err != nil {
|
safeConnClose(c, err)
|
||||||
c.Close()
|
}(c)
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
var user *socks5.User
|
var user *socks5.User
|
||||||
@ -109,20 +116,34 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := dialer.ListenPacket("udp", "")
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(ioutil.Discard, c)
|
io.Copy(io.Discard, c)
|
||||||
c.Close()
|
c.Close()
|
||||||
// A UDP association terminates when the TCP connection that the UDP
|
// A UDP association terminates when the TCP connection that the UDP
|
||||||
// ASSOCIATE request arrived on terminates. RFC1928
|
// ASSOCIATE request arrived on terminates. RFC1928
|
||||||
pc.Close()
|
pc.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindAddr.UDPAddr(), tcpConn: c}, ss), nil
|
// Support unspecified UDP bind address.
|
||||||
|
bindUDPAddr := bindAddr.UDPAddr()
|
||||||
|
if bindUDPAddr == nil {
|
||||||
|
err = errors.New("invalid UDP bind address")
|
||||||
|
return
|
||||||
|
} else if bindUDPAddr.IP.IsUnspecified() {
|
||||||
|
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bindUDPAddr.IP = serverAddr.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSocks5(option Socks5Option) *Socks5 {
|
func NewSocks5(option Socks5Option) *Socks5 {
|
||||||
@ -130,17 +151,18 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
|||||||
if option.TLS {
|
if option.TLS {
|
||||||
tlsConfig = &tls.Config{
|
tlsConfig = &tls.Config{
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
InsecureSkipVerify: option.SkipCertVerify,
|
||||||
ClientSessionCache: getClientSessionCache(),
|
|
||||||
ServerName: option.Server,
|
ServerName: option.Server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Socks5{
|
return &Socks5{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Socks5,
|
tp: C.Socks5,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
@ -164,14 +186,6 @@ func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
|||||||
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *socksPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddr(metadata.RemoteAddress()), p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
n, _, e := uc.PacketConn.ReadFrom(b)
|
n, _, e := uc.PacketConn.ReadFrom(b)
|
||||||
if e != nil {
|
if e != nil {
|
214
adapter/outbound/trojan.go
Normal file
214
adapter/outbound/trojan.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/gun"
|
||||||
|
"github.com/Dreamacro/clash/transport/trojan"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Trojan struct {
|
||||||
|
*Base
|
||||||
|
instance *trojan.Trojan
|
||||||
|
option *TrojanOption
|
||||||
|
|
||||||
|
// for gun mux
|
||||||
|
gunTLSConfig *tls.Config
|
||||||
|
gunConfig *gun.Config
|
||||||
|
transport *http2.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrojanOption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
Password string `proxy:"password"`
|
||||||
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
|
SNI string `proxy:"sni,omitempty"`
|
||||||
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
Network string `proxy:"network,omitempty"`
|
||||||
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
|
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
|
||||||
|
if t.option.Network == "ws" {
|
||||||
|
host, port, _ := net.SplitHostPort(t.addr)
|
||||||
|
wsOpts := &trojan.WebsocketOption{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Path: t.option.WSOpts.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.option.SNI != "" {
|
||||||
|
wsOpts.Host = t.option.SNI
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.option.WSOpts.Headers) != 0 {
|
||||||
|
header := http.Header{}
|
||||||
|
for key, value := range t.option.WSOpts.Headers {
|
||||||
|
header.Add(key, value)
|
||||||
|
}
|
||||||
|
wsOpts.Headers = header
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.instance.StreamWebsocketConn(c, wsOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.instance.StreamConn(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
var err error
|
||||||
|
if t.transport != nil {
|
||||||
|
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
||||||
|
} else {
|
||||||
|
c, err = t.plainStream(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
// gun transport
|
||||||
|
if t.transport != nil && len(opts) == 0 {
|
||||||
|
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = t.StreamConn(c, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, t), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||||
|
var c net.Conn
|
||||||
|
|
||||||
|
// grpc transport
|
||||||
|
if t.transport != nil && len(opts) == 0 {
|
||||||
|
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
} else {
|
||||||
|
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
c, err = t.plainStream(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := t.instance.PacketConn(c)
|
||||||
|
return newPacketConn(pc, t), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
|
||||||
|
tOption := &trojan.Option{
|
||||||
|
Password: option.Password,
|
||||||
|
ALPN: option.ALPN,
|
||||||
|
ServerName: option.Server,
|
||||||
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.SNI != "" {
|
||||||
|
tOption.ServerName = option.SNI
|
||||||
|
}
|
||||||
|
|
||||||
|
t := &Trojan{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.Trojan,
|
||||||
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
},
|
||||||
|
instance: trojan.New(tOption),
|
||||||
|
option: &option,
|
||||||
|
}
|
||||||
|
|
||||||
|
if option.Network == "grpc" {
|
||||||
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
NextProtos: option.ALPN,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
InsecureSkipVerify: tOption.SkipCertVerify,
|
||||||
|
ServerName: tOption.ServerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
t.gunTLSConfig = tlsConfig
|
||||||
|
t.gunConfig = &gun.Config{
|
||||||
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
|
Host: tOption.ServerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, nil
|
||||||
|
}
|
60
adapter/outbound/util.go
Normal file
60
adapter/outbound/util.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/protobytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tcpKeepAlive(c net.Conn) {
|
||||||
|
if tcp, ok := c.(*net.TCPConn); ok {
|
||||||
|
tcp.SetKeepAlive(true)
|
||||||
|
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||||
|
buf := protobytes.BytesWriter{}
|
||||||
|
|
||||||
|
addrType := metadata.AddrType()
|
||||||
|
buf.PutUint8(uint8(addrType))
|
||||||
|
|
||||||
|
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
|
switch addrType {
|
||||||
|
case socks5.AtypDomainName:
|
||||||
|
buf.PutUint8(uint8(len(metadata.Host)))
|
||||||
|
buf.PutString(metadata.Host)
|
||||||
|
case socks5.AtypIPv4:
|
||||||
|
buf.PutSlice(metadata.DstIP.To4())
|
||||||
|
case socks5.AtypIPv6:
|
||||||
|
buf.PutSlice(metadata.DstIP.To16())
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.PutUint16be(uint16(p))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
||||||
|
host, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := resolver.ResolveIP(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeConnClose(c net.Conn, err error) {
|
||||||
|
if err != nil {
|
||||||
|
c.Close()
|
||||||
|
}
|
||||||
|
}
|
385
adapter/outbound/vmess.go
Normal file
385
adapter/outbound/vmess.go
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/gun"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
"github.com/Dreamacro/clash/transport/vmess"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
|
||||||
|
|
||||||
|
type Vmess struct {
|
||||||
|
*Base
|
||||||
|
client *vmess.Client
|
||||||
|
option *VmessOption
|
||||||
|
|
||||||
|
// for gun mux
|
||||||
|
gunTLSConfig *tls.Config
|
||||||
|
gunConfig *gun.Config
|
||||||
|
transport *http2.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
type VmessOption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
UUID string `proxy:"uuid"`
|
||||||
|
AlterID int `proxy:"alterId"`
|
||||||
|
Cipher string `proxy:"cipher"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
Network string `proxy:"network,omitempty"`
|
||||||
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
ServerName string `proxy:"servername,omitempty"`
|
||||||
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
|
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPOptions struct {
|
||||||
|
Method string `proxy:"method,omitempty"`
|
||||||
|
Path []string `proxy:"path,omitempty"`
|
||||||
|
Headers map[string][]string `proxy:"headers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTP2Options struct {
|
||||||
|
Host []string `proxy:"host,omitempty"`
|
||||||
|
Path string `proxy:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrpcOptions struct {
|
||||||
|
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSOptions struct {
|
||||||
|
Path string `proxy:"path,omitempty"`
|
||||||
|
Headers map[string]string `proxy:"headers,omitempty"`
|
||||||
|
MaxEarlyData int `proxy:"max-early-data,omitempty"`
|
||||||
|
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamConn implements C.ProxyAdapter
|
||||||
|
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
|
var err error
|
||||||
|
switch v.option.Network {
|
||||||
|
case "ws":
|
||||||
|
host, port, _ := net.SplitHostPort(v.addr)
|
||||||
|
wsOpts := &vmess.WebsocketConfig{
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
Path: v.option.WSOpts.Path,
|
||||||
|
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
|
||||||
|
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.option.WSOpts.Headers) != 0 {
|
||||||
|
header := http.Header{}
|
||||||
|
for key, value := range v.option.WSOpts.Headers {
|
||||||
|
header.Add(key, value)
|
||||||
|
}
|
||||||
|
wsOpts.Headers = header
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.TLS {
|
||||||
|
wsOpts.TLS = true
|
||||||
|
wsOpts.TLSConfig = &tls.Config{
|
||||||
|
ServerName: host,
|
||||||
|
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||||
|
NextProtos: []string{"http/1.1"},
|
||||||
|
}
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
wsOpts.TLSConfig.ServerName = v.option.ServerName
|
||||||
|
} else if host := wsOpts.Headers.Get("Host"); host != "" {
|
||||||
|
wsOpts.TLSConfig.ServerName = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
|
case "http":
|
||||||
|
// readability first, so just copy default TLS logic
|
||||||
|
if v.option.TLS {
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsOpts := &vmess.TLSConfig{
|
||||||
|
Host: host,
|
||||||
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
httpOpts := &vmess.HTTPConfig{
|
||||||
|
Host: host,
|
||||||
|
Method: v.option.HTTPOpts.Method,
|
||||||
|
Path: v.option.HTTPOpts.Path,
|
||||||
|
Headers: v.option.HTTPOpts.Headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
c = vmess.StreamHTTPConn(c, httpOpts)
|
||||||
|
case "h2":
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsOpts := vmess.TLSConfig{
|
||||||
|
Host: host,
|
||||||
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
|
NextProtos: []string{"h2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamTLSConn(c, &tlsOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h2Opts := &vmess.H2Config{
|
||||||
|
Hosts: v.option.HTTP2Opts.Host,
|
||||||
|
Path: v.option.HTTP2Opts.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamH2Conn(c, h2Opts)
|
||||||
|
case "grpc":
|
||||||
|
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
|
||||||
|
default:
|
||||||
|
// handle TLS
|
||||||
|
if v.option.TLS {
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsOpts := &vmess.TLSConfig{
|
||||||
|
Host: host,
|
||||||
|
SkipCertVerify: v.option.SkipCertVerify,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName != "" {
|
||||||
|
tlsOpts.Host = v.option.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
|
// gun transport
|
||||||
|
if v.transport != nil && len(opts) == 0 {
|
||||||
|
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewConn(c, v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = v.StreamConn(c, metadata)
|
||||||
|
return NewConn(c, v), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||||
|
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
|
||||||
|
if !metadata.Resolved() {
|
||||||
|
ip, err := resolver.ResolveIP(metadata.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("can't resolve ip")
|
||||||
|
}
|
||||||
|
metadata.DstIP = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
var c net.Conn
|
||||||
|
// gun transport
|
||||||
|
if v.transport != nil && len(opts) == 0 {
|
||||||
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
|
} else {
|
||||||
|
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
|
c, err = v.StreamConn(c, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
HostName: option.Server,
|
||||||
|
Port: strconv.Itoa(option.Port),
|
||||||
|
IsAead: option.AlterID == 0,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch option.Network {
|
||||||
|
case "h2", "grpc":
|
||||||
|
if !option.TLS {
|
||||||
|
return nil, fmt.Errorf("TLS must be true with h2/grpc network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := &Vmess{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
|
tp: C.Vmess,
|
||||||
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
},
|
||||||
|
client: client,
|
||||||
|
option: &option,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch option.Network {
|
||||||
|
case "h2":
|
||||||
|
if len(option.HTTP2Opts.Host) == 0 {
|
||||||
|
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
||||||
|
}
|
||||||
|
case "grpc":
|
||||||
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
|
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
|
}
|
||||||
|
tcpKeepAlive(c)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gunConfig := &gun.Config{
|
||||||
|
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
||||||
|
Host: v.option.ServerName,
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||||
|
ServerName: v.option.ServerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.option.ServerName == "" {
|
||||||
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
|
tlsConfig.ServerName = host
|
||||||
|
gunConfig.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
v.gunTLSConfig = tlsConfig
|
||||||
|
v.gunConfig = gunConfig
|
||||||
|
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||||
|
var addrType byte
|
||||||
|
var addr []byte
|
||||||
|
switch metadata.AddrType() {
|
||||||
|
case socks5.AtypIPv4:
|
||||||
|
addrType = vmess.AtypIPv4
|
||||||
|
addr = make([]byte, net.IPv4len)
|
||||||
|
copy(addr[:], metadata.DstIP.To4())
|
||||||
|
case socks5.AtypIPv6:
|
||||||
|
addrType = vmess.AtypIPv6
|
||||||
|
addr = make([]byte, net.IPv6len)
|
||||||
|
copy(addr[:], metadata.DstIP.To16())
|
||||||
|
case socks5.AtypDomainName:
|
||||||
|
addrType = vmess.AtypDomainName
|
||||||
|
addr = make([]byte, len(metadata.Host)+1)
|
||||||
|
addr[0] = byte(len(metadata.Host))
|
||||||
|
copy(addr[1:], []byte(metadata.Host))
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
|
return &vmess.DstAddr{
|
||||||
|
UDP: metadata.NetWork == C.UDP,
|
||||||
|
AddrType: addrType,
|
||||||
|
Addr: addr,
|
||||||
|
Port: uint(port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type vmessPacketConn struct {
|
||||||
|
net.Conn
|
||||||
|
rAddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo implments C.PacketConn.WriteTo
|
||||||
|
// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not.
|
||||||
|
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||||
|
allowedAddr := uc.rAddr.(*net.UDPAddr)
|
||||||
|
destAddr := addr.(*net.UDPAddr)
|
||||||
|
if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) {
|
||||||
|
return 0, ErrUDPRemoteAddrMismatch
|
||||||
|
}
|
||||||
|
return uc.Conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||||
|
n, err := uc.Conn.Read(b)
|
||||||
|
return n, uc.rAddr, err
|
||||||
|
}
|
29
adapter/outboundgroup/common.go
Normal file
29
adapter/outboundgroup/common.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultGetProxiesDuration = time.Second * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
func touchProviders(providers []provider.ProxyProvider) {
|
||||||
|
for _, provider := range providers {
|
||||||
|
provider.Touch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
||||||
|
proxies := []C.Proxy{}
|
||||||
|
for _, provider := range providers {
|
||||||
|
if touch {
|
||||||
|
provider.Touch()
|
||||||
|
}
|
||||||
|
proxies = append(proxies, provider.Proxies()...)
|
||||||
|
}
|
||||||
|
return proxies
|
||||||
|
}
|
106
adapter/outboundgroup/fallback.go
Normal file
106
adapter/outboundgroup/fallback.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Fallback struct {
|
||||||
|
*outbound.Base
|
||||||
|
disableUDP bool
|
||||||
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) Now() string {
|
||||||
|
proxy := f.findAliveProxy(false)
|
||||||
|
return proxy.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
|
proxy := f.findAliveProxy(true)
|
||||||
|
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
c.AppendToChains(f)
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
proxy := f.findAliveProxy(true)
|
||||||
|
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
pc.AppendToChains(f)
|
||||||
|
}
|
||||||
|
return pc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
|
func (f *Fallback) SupportUDP() bool {
|
||||||
|
if f.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := f.findAliveProxy(false)
|
||||||
|
return proxy.SupportUDP()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||||
|
var all []string
|
||||||
|
for _, proxy := range f.proxies(false) {
|
||||||
|
all = append(all, proxy.Name())
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"type": f.Type().String(),
|
||||||
|
"now": f.Now(),
|
||||||
|
"all": all,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
proxy := f.findAliveProxy(true)
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := f.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(f.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||||
|
proxies := f.proxies(touch)
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||||
|
return &Fallback{
|
||||||
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
|
Name: option.Name,
|
||||||
|
Type: C.Fallback,
|
||||||
|
Interface: option.Interface,
|
||||||
|
RoutingMark: option.RoutingMark,
|
||||||
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
disableUDP: option.DisableUDP,
|
||||||
|
}
|
||||||
|
}
|
189
adapter/outboundgroup/loadbalance.go
Normal file
189
adapter/outboundgroup/loadbalance.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/murmur3"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
|
||||||
|
|
||||||
|
type LoadBalance struct {
|
||||||
|
*outbound.Base
|
||||||
|
disableUDP bool
|
||||||
|
single *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
|
strategyFn strategyFn
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
|
||||||
|
func parseStrategy(config map[string]any) string {
|
||||||
|
if strategy, ok := config["strategy"].(string); ok {
|
||||||
|
return strategy
|
||||||
|
}
|
||||||
|
return "consistent-hashing"
|
||||||
|
}
|
||||||
|
|
||||||
|
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.DstIP == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.DstIP.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
c.AppendToChains(lb)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := lb.Unwrap(metadata)
|
||||||
|
|
||||||
|
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
pc.AppendToChains(lb)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
proxy := lb.Unwrap(metadata)
|
||||||
|
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) SupportUDP() bool {
|
||||||
|
return !lb.disableUDP
|
||||||
|
}
|
||||||
|
|
||||||
|
func strategyRoundRobin() strategyFn {
|
||||||
|
idx := 0
|
||||||
|
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||||
|
length := len(proxies)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
idx = (idx + 1) % length
|
||||||
|
proxy := proxies[idx]
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func strategyConsistentHashing() strategyFn {
|
||||||
|
maxRetry := 5
|
||||||
|
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||||
|
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||||
|
buckets := int32(len(proxies))
|
||||||
|
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
||||||
|
idx := jumpHash(key, buckets)
|
||||||
|
proxy := proxies[idx]
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// when availability is poor, traverse the entire list to get the available nodes
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if proxy.Alive() {
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
proxies := lb.proxies(true)
|
||||||
|
return lb.strategyFn(proxies, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := lb.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(lb.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
|
var all []string
|
||||||
|
for _, proxy := range lb.proxies(false) {
|
||||||
|
all = append(all, proxy.Name())
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"type": lb.Type().String(),
|
||||||
|
"all": all,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||||
|
var strategyFn strategyFn
|
||||||
|
switch strategy {
|
||||||
|
case "consistent-hashing":
|
||||||
|
strategyFn = strategyConsistentHashing()
|
||||||
|
case "round-robin":
|
||||||
|
strategyFn = strategyRoundRobin()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||||
|
}
|
||||||
|
return &LoadBalance{
|
||||||
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
|
Name: option.Name,
|
||||||
|
Type: C.LoadBalance,
|
||||||
|
Interface: option.Interface,
|
||||||
|
RoutingMark: option.RoutingMark,
|
||||||
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
strategyFn: strategyFn,
|
||||||
|
disableUDP: option.DisableUDP,
|
||||||
|
}, nil
|
||||||
|
}
|
166
adapter/outboundgroup/parser.go
Normal file
166
adapter/outboundgroup/parser.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/adapter/provider"
|
||||||
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
|
regexp "github.com/dlclark/regexp2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errFormat = errors.New("format error")
|
||||||
|
errType = errors.New("unsupport type")
|
||||||
|
errMissProxy = errors.New("`use` or `proxies` missing")
|
||||||
|
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
||||||
|
errDuplicateProvider = errors.New("duplicate provider name")
|
||||||
|
)
|
||||||
|
|
||||||
|
type GroupCommonOption struct {
|
||||||
|
outbound.BasicOption
|
||||||
|
Name string `group:"name"`
|
||||||
|
Type string `group:"type"`
|
||||||
|
Proxies []string `group:"proxies,omitempty"`
|
||||||
|
Use []string `group:"use,omitempty"`
|
||||||
|
URL string `group:"url,omitempty"`
|
||||||
|
Interval int `group:"interval,omitempty"`
|
||||||
|
Lazy bool `group:"lazy,omitempty"`
|
||||||
|
DisableUDP bool `group:"disable-udp,omitempty"`
|
||||||
|
Filter string `group:"filter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
||||||
|
|
||||||
|
groupOption := &GroupCommonOption{
|
||||||
|
Lazy: true,
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(config, groupOption); err != nil {
|
||||||
|
return nil, errFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if groupOption.Type == "" || groupOption.Name == "" {
|
||||||
|
return nil, errFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
groupName = groupOption.Name
|
||||||
|
filterReg *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
|
if groupOption.Filter != "" {
|
||||||
|
f, err := regexp.Compile(groupOption.Filter, regexp.None)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: invalid filter regex: %w", groupName, err)
|
||||||
|
}
|
||||||
|
filterReg = f
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := []types.ProxyProvider{}
|
||||||
|
|
||||||
|
if len(groupOption.Proxies) != 0 {
|
||||||
|
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := providersMap[groupName]; ok {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// select don't need health check
|
||||||
|
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
||||||
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers = append(providers, pd)
|
||||||
|
providersMap[groupName] = pd
|
||||||
|
} else {
|
||||||
|
if groupOption.URL == "" || groupOption.Interval == 0 {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, errMissHealthCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
||||||
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
providers = append(providers, pd)
|
||||||
|
providersMap[groupName] = pd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groupOption.Use) != 0 {
|
||||||
|
list, err := getProviders(providersMap, groupOption.Use)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
|
}
|
||||||
|
if filterReg != nil {
|
||||||
|
pd := provider.NewFilterableProvider(groupName, list, filterReg)
|
||||||
|
providers = append(providers, pd)
|
||||||
|
} else {
|
||||||
|
providers = append(providers, list...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var group C.ProxyAdapter
|
||||||
|
switch groupOption.Type {
|
||||||
|
case "url-test":
|
||||||
|
opts := parseURLTestOption(config)
|
||||||
|
group = NewURLTest(groupOption, providers, opts...)
|
||||||
|
case "select":
|
||||||
|
group = NewSelector(groupOption, providers)
|
||||||
|
case "fallback":
|
||||||
|
group = NewFallback(groupOption, providers)
|
||||||
|
case "load-balance":
|
||||||
|
strategy := parseStrategy(config)
|
||||||
|
return NewLoadBalance(groupOption, providers, strategy)
|
||||||
|
case "relay":
|
||||||
|
group = NewRelay(groupOption, providers)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return group, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
|
||||||
|
var ps []types.ProxyProvider
|
||||||
|
for _, name := range list {
|
||||||
|
p, ok := mapping[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("'%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.VehicleType() == types.Compatible {
|
||||||
|
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
|
||||||
|
}
|
||||||
|
ps = append(ps, p)
|
||||||
|
}
|
||||||
|
return ps, nil
|
||||||
|
}
|
@ -3,14 +3,13 @@ package outboundgroup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Relay struct {
|
type Relay struct {
|
||||||
@ -19,15 +18,26 @@ type Relay struct {
|
|||||||
providers []provider.ProxyProvider
|
providers []provider.ProxyProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
// DialContext implements C.ProxyAdapter
|
||||||
proxies := r.proxies(metadata)
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
if len(proxies) == 0 {
|
var proxies []C.Proxy
|
||||||
return nil, errors.New("Proxy does not exist")
|
for _, proxy := range r.proxies(metadata, true) {
|
||||||
|
if proxy.Type() != C.Direct {
|
||||||
|
proxies = append(proxies, proxy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch len(proxies) {
|
||||||
|
case 0:
|
||||||
|
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
||||||
|
case 1:
|
||||||
|
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
||||||
|
}
|
||||||
|
|
||||||
first := proxies[0]
|
first := proxies[0]
|
||||||
last := proxies[len(proxies)-1]
|
last := proxies[len(proxies)-1]
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
|
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||||
}
|
}
|
||||||
@ -56,27 +66,28 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
return outbound.NewConn(c, r), nil
|
return outbound.NewConn(c, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (r *Relay) MarshalJSON() ([]byte, error) {
|
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||||
var all []string
|
var all []string
|
||||||
for _, proxy := range r.rawProxies() {
|
for _, proxy := range r.rawProxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": r.Type().String(),
|
"type": r.Type().String(),
|
||||||
"all": all,
|
"all": all,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) rawProxies() []C.Proxy {
|
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
elm, _, _ := r.single.Do(func() (any, error) {
|
||||||
return getProvidersProxies(r.providers), nil
|
return getProvidersProxies(r.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
return elm.([]C.Proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
|
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||||
proxies := r.rawProxies()
|
proxies := r.rawProxies(touch)
|
||||||
|
|
||||||
for n, proxy := range proxies {
|
for n, proxy := range proxies {
|
||||||
subproxy := proxy.Unwrap(metadata)
|
subproxy := proxy.Unwrap(metadata)
|
||||||
@ -89,9 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
|
|||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelay(name string, providers []provider.ProxyProvider) *Relay {
|
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||||
return &Relay{
|
return &Relay{
|
||||||
Base: outbound.NewBase(name, "", C.Relay, false),
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
|
Name: option.Name,
|
||||||
|
Type: C.Relay,
|
||||||
|
Interface: option.Interface,
|
||||||
|
RoutingMark: option.RoutingMark,
|
||||||
|
}),
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
}
|
}
|
114
adapter/outboundgroup/selector.go
Normal file
114
adapter/outboundgroup/selector.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Selector struct {
|
||||||
|
*outbound.Base
|
||||||
|
disableUDP bool
|
||||||
|
single *singledo.Single
|
||||||
|
selected string
|
||||||
|
providers []provider.ProxyProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
|
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
c.AppendToChains(s)
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
pc.AppendToChains(s)
|
||||||
|
}
|
||||||
|
return pc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
|
func (s *Selector) SupportUDP() bool {
|
||||||
|
if s.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.selectedProxy(false).SupportUDP()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||||
|
var all []string
|
||||||
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
|
all = append(all, proxy.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"type": s.Type().String(),
|
||||||
|
"now": s.Now(),
|
||||||
|
"all": all,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selector) Now() string {
|
||||||
|
return s.selectedProxy(false).Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selector) Set(name string) error {
|
||||||
|
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||||
|
if proxy.Name() == name {
|
||||||
|
s.selected = name
|
||||||
|
s.single.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("proxy not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return s.selectedProxy(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||||
|
elm, _, _ := s.single.Do(func() (any, error) {
|
||||||
|
proxies := getProvidersProxies(s.providers, touch)
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
if proxy.Name() == s.selected {
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies[0], nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.(C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||||
|
selected := providers[0].Proxies()[0].Name()
|
||||||
|
return &Selector{
|
||||||
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
|
Name: option.Name,
|
||||||
|
Type: C.Selector,
|
||||||
|
Interface: option.Interface,
|
||||||
|
RoutingMark: option.RoutingMark,
|
||||||
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
providers: providers,
|
||||||
|
selected: selected,
|
||||||
|
disableUDP: option.DisableUDP,
|
||||||
|
}
|
||||||
|
}
|
157
adapter/outboundgroup/urltest.go
Normal file
157
adapter/outboundgroup/urltest.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package outboundgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type urlTestOption func(*URLTest)
|
||||||
|
|
||||||
|
func urlTestWithTolerance(tolerance uint16) urlTestOption {
|
||||||
|
return func(u *URLTest) {
|
||||||
|
u.tolerance = tolerance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLTest struct {
|
||||||
|
*outbound.Base
|
||||||
|
tolerance uint16
|
||||||
|
disableUDP bool
|
||||||
|
fastNode C.Proxy
|
||||||
|
single *singledo.Single
|
||||||
|
fastSingle *singledo.Single
|
||||||
|
providers []provider.ProxyProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) Now() string {
|
||||||
|
return u.fast(false).Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialContext implements C.ProxyAdapter
|
||||||
|
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
||||||
|
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
c.AppendToChains(u)
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
||||||
|
if err == nil {
|
||||||
|
pc.AppendToChains(u)
|
||||||
|
}
|
||||||
|
return pc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap implements C.ProxyAdapter
|
||||||
|
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||||
|
return u.fast(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) proxies(touch bool) []C.Proxy {
|
||||||
|
elm, _, _ := u.single.Do(func() (any, error) {
|
||||||
|
return getProvidersProxies(u.providers, touch), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
|
elm, _, shared := u.fastSingle.Do(func() (any, error) {
|
||||||
|
proxies := u.proxies(touch)
|
||||||
|
fast := proxies[0]
|
||||||
|
min := fast.LastDelay()
|
||||||
|
fastNotExist := true
|
||||||
|
|
||||||
|
for _, proxy := range proxies[1:] {
|
||||||
|
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
|
||||||
|
fastNotExist = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !proxy.Alive() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delay := proxy.LastDelay()
|
||||||
|
if delay < min {
|
||||||
|
fast = proxy
|
||||||
|
min = delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tolerance
|
||||||
|
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
|
||||||
|
u.fastNode = fast
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fastNode, nil
|
||||||
|
})
|
||||||
|
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
|
||||||
|
touchProviders(u.providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return elm.(C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUDP implements C.ProxyAdapter
|
||||||
|
func (u *URLTest) SupportUDP() bool {
|
||||||
|
if u.disableUDP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.fast(false).SupportUDP()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
|
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||||
|
var all []string
|
||||||
|
for _, proxy := range u.proxies(false) {
|
||||||
|
all = append(all, proxy.Name())
|
||||||
|
}
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"type": u.Type().String(),
|
||||||
|
"now": u.Now(),
|
||||||
|
"all": all,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseURLTestOption(config map[string]any) []urlTestOption {
|
||||||
|
opts := []urlTestOption{}
|
||||||
|
|
||||||
|
// tolerance
|
||||||
|
if tolerance, ok := config["tolerance"].(int); ok {
|
||||||
|
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||||
|
urlTest := &URLTest{
|
||||||
|
Base: outbound.NewBase(outbound.BaseOption{
|
||||||
|
Name: option.Name,
|
||||||
|
Type: C.URLTest,
|
||||||
|
Interface: option.Interface,
|
||||||
|
RoutingMark: option.RoutingMark,
|
||||||
|
}),
|
||||||
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
|
fastSingle: singledo.NewSingle(time.Second * 10),
|
||||||
|
providers: providers,
|
||||||
|
disableUDP: option.DisableUDP,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
option(urlTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlTest
|
||||||
|
}
|
@ -16,33 +16,28 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip == nil {
|
||||||
if ip.To4() != nil {
|
|
||||||
addr = &C.Metadata{
|
|
||||||
AddrType: C.AtypIPv4,
|
|
||||||
Host: "",
|
|
||||||
DstIP: ip,
|
|
||||||
DstPort: port,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
addr = &C.Metadata{
|
|
||||||
AddrType: C.AtypIPv6,
|
|
||||||
Host: "",
|
|
||||||
DstIP: ip,
|
|
||||||
DstPort: port,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypDomainName,
|
Host: host,
|
||||||
Host: host,
|
DstIP: nil,
|
||||||
DstIP: nil,
|
DstPort: port,
|
||||||
DstPort: port,
|
}
|
||||||
|
return
|
||||||
|
} else if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
addr = &C.Metadata{
|
||||||
|
Host: "",
|
||||||
|
DstIP: ip4,
|
||||||
|
DstPort: port,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr = &C.Metadata{
|
||||||
|
Host: "",
|
||||||
|
DstIP: ip,
|
||||||
|
DstPort: port,
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
func tcpKeepAlive(c net.Conn) {
|
86
adapter/parser.go
Normal file
86
adapter/parser.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
||||||
|
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||||
|
proxyType, existType := mapping["type"].(string)
|
||||||
|
if !existType {
|
||||||
|
return nil, fmt.Errorf("missing type")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
proxy C.ProxyAdapter
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch proxyType {
|
||||||
|
case "ss":
|
||||||
|
ssOption := &outbound.ShadowSocksOption{}
|
||||||
|
err = decoder.Decode(mapping, ssOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewShadowSocks(*ssOption)
|
||||||
|
case "ssr":
|
||||||
|
ssrOption := &outbound.ShadowSocksROption{}
|
||||||
|
err = decoder.Decode(mapping, ssrOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewShadowSocksR(*ssrOption)
|
||||||
|
case "socks5":
|
||||||
|
socksOption := &outbound.Socks5Option{}
|
||||||
|
err = decoder.Decode(mapping, socksOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy = outbound.NewSocks5(*socksOption)
|
||||||
|
case "http":
|
||||||
|
httpOption := &outbound.HttpOption{}
|
||||||
|
err = decoder.Decode(mapping, httpOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy = outbound.NewHttp(*httpOption)
|
||||||
|
case "vmess":
|
||||||
|
vmessOption := &outbound.VmessOption{
|
||||||
|
HTTPOpts: outbound.HTTPOptions{
|
||||||
|
Method: "GET",
|
||||||
|
Path: []string{"/"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err = decoder.Decode(mapping, vmessOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewVmess(*vmessOption)
|
||||||
|
case "snell":
|
||||||
|
snellOption := &outbound.SnellOption{}
|
||||||
|
err = decoder.Decode(mapping, snellOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewSnell(*snellOption)
|
||||||
|
case "trojan":
|
||||||
|
trojanOption := &outbound.TrojanOption{}
|
||||||
|
err = decoder.Decode(mapping, trojanOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewTrojan(*trojanOption)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewProxy(proxy), nil
|
||||||
|
}
|
@ -3,46 +3,54 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileMode os.FileMode = 0666
|
fileMode os.FileMode = 0o666
|
||||||
|
dirMode os.FileMode = 0o755
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser = func([]byte) (interface{}, error)
|
type parser = func([]byte) (any, error)
|
||||||
|
|
||||||
type fetcher struct {
|
type fetcher struct {
|
||||||
name string
|
name string
|
||||||
vehicle Vehicle
|
vehicle types.Vehicle
|
||||||
|
interval time.Duration
|
||||||
updatedAt *time.Time
|
updatedAt *time.Time
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
hash [16]byte
|
hash [16]byte
|
||||||
parser parser
|
parser parser
|
||||||
onUpdate func(interface{})
|
onUpdate func(any)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Name() string {
|
func (f *fetcher) Name() string {
|
||||||
return f.name
|
return f.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) VehicleType() VehicleType {
|
func (f *fetcher) VehicleType() types.VehicleType {
|
||||||
return f.vehicle.Type()
|
return f.vehicle.Type()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Initial() (interface{}, error) {
|
func (f *fetcher) Initial() (any, error) {
|
||||||
var buf []byte
|
var (
|
||||||
var err error
|
buf []byte
|
||||||
var isLocal bool
|
err error
|
||||||
if stat, err := os.Stat(f.vehicle.Path()); err == nil {
|
isLocal bool
|
||||||
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
immediatelyUpdate bool
|
||||||
|
)
|
||||||
|
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||||
|
buf, err = os.ReadFile(f.vehicle.Path())
|
||||||
modTime := stat.ModTime()
|
modTime := stat.ModTime()
|
||||||
f.updatedAt = &modTime
|
f.updatedAt = &modTime
|
||||||
isLocal = true
|
isLocal = true
|
||||||
|
immediatelyUpdate = time.Since(modTime) > f.interval
|
||||||
} else {
|
} else {
|
||||||
buf, err = f.vehicle.Read()
|
buf, err = f.vehicle.Read()
|
||||||
}
|
}
|
||||||
@ -67,23 +75,27 @@ func (f *fetcher) Initial() (interface{}, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLocal = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
|
if f.vehicle.Type() != types.File && !isLocal {
|
||||||
return nil, err
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.hash = md5.Sum(buf)
|
f.hash = md5.Sum(buf)
|
||||||
|
|
||||||
// pull proxies automatically
|
// pull proxies automatically
|
||||||
if f.ticker != nil {
|
if f.ticker != nil {
|
||||||
go f.pullLoop()
|
go f.pullLoop(immediatelyUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Update() (interface{}, bool, error) {
|
func (f *fetcher) Update() (any, bool, error) {
|
||||||
buf, err := f.vehicle.Read()
|
buf, err := f.vehicle.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@ -93,6 +105,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
hash := md5.Sum(buf)
|
hash := md5.Sum(buf)
|
||||||
if bytes.Equal(f.hash[:], hash[:]) {
|
if bytes.Equal(f.hash[:], hash[:]) {
|
||||||
f.updatedAt = &now
|
f.updatedAt = &now
|
||||||
|
os.Chtimes(f.vehicle.Path(), now, now)
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,8 +114,10 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(f.vehicle.Path(), buf, fileMode); err != nil {
|
if f.vehicle.Type() != types.File {
|
||||||
return nil, false, err
|
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.updatedAt = &now
|
f.updatedAt = &now
|
||||||
@ -113,22 +128,22 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
|
|
||||||
func (f *fetcher) Destroy() error {
|
func (f *fetcher) Destroy() error {
|
||||||
if f.ticker != nil {
|
if f.ticker != nil {
|
||||||
f.ticker.Stop()
|
f.done <- struct{}{}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) pullLoop() {
|
func (f *fetcher) pullLoop(immediately bool) {
|
||||||
for range f.ticker.C {
|
update := func() {
|
||||||
elm, same, err := f.Update()
|
elm, same, err := f.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if same {
|
if same {
|
||||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
@ -136,9 +151,35 @@ func (f *fetcher) pullLoop() {
|
|||||||
f.onUpdate(elm)
|
f.onUpdate(elm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if immediately {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.ticker.C:
|
||||||
|
update()
|
||||||
|
case <-f.done:
|
||||||
|
f.ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
|
func safeWrite(path string, buf []byte) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(dir, dirMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, buf, fileMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
|
||||||
var ticker *time.Ticker
|
var ticker *time.Ticker
|
||||||
if interval != 0 {
|
if interval != 0 {
|
||||||
ticker = time.NewTicker(interval)
|
ticker = time.NewTicker(interval)
|
||||||
@ -148,7 +189,9 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
|
|||||||
name: name,
|
name: name,
|
||||||
ticker: ticker,
|
ticker: ticker,
|
||||||
vehicle: vehicle,
|
vehicle: vehicle,
|
||||||
|
interval: interval,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
}
|
}
|
||||||
}
|
}
|
100
adapter/provider/healthcheck.go
Normal file
100
adapter/provider/healthcheck.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/batch"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultURLTestTimeout = time.Second * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthCheckOption struct {
|
||||||
|
URL string
|
||||||
|
Interval uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthCheck struct {
|
||||||
|
url string
|
||||||
|
proxies []C.Proxy
|
||||||
|
interval uint
|
||||||
|
lazy bool
|
||||||
|
lastTouch *atomic.Int64
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) process() {
|
||||||
|
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||||
|
|
||||||
|
go hc.checkAll()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
now := time.Now().Unix()
|
||||||
|
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||||
|
hc.checkAll()
|
||||||
|
} else { // lazy but still need to check not alive proxies
|
||||||
|
notAliveProxies := lo.Filter(hc.proxies, func(proxy C.Proxy, _ int) bool {
|
||||||
|
return !proxy.Alive()
|
||||||
|
})
|
||||||
|
if len(notAliveProxies) != 0 {
|
||||||
|
hc.check(notAliveProxies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-hc.done:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
||||||
|
hc.proxies = proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) auto() bool {
|
||||||
|
return hc.interval != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) touch() {
|
||||||
|
hc.lastTouch.Store(time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) checkAll() {
|
||||||
|
hc.check(hc.proxies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) check(proxies []C.Proxy) {
|
||||||
|
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
|
||||||
|
for _, proxy := range proxies {
|
||||||
|
p := proxy
|
||||||
|
b.Go(p.Name(), func() (any, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
p.URLTest(ctx, hc.url)
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
b.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) close() {
|
||||||
|
hc.done <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
|
||||||
|
return &HealthCheck{
|
||||||
|
proxies: proxies,
|
||||||
|
url: url,
|
||||||
|
interval: interval,
|
||||||
|
lazy: lazy,
|
||||||
|
lastTouch: atomic.NewInt64(0),
|
||||||
|
done: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
}
|
@ -7,16 +7,19 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errVehicleType = errors.New("unsupport vehicle type")
|
errVehicleType = errors.New("unsupport vehicle type")
|
||||||
|
errSubPath = errors.New("path is not subpath of home directory")
|
||||||
)
|
)
|
||||||
|
|
||||||
type healthCheckSchema struct {
|
type healthCheckSchema struct {
|
||||||
Enable bool `provider:"enable"`
|
Enable bool `provider:"enable"`
|
||||||
URL string `provider:"url"`
|
URL string `provider:"url"`
|
||||||
Interval int `provider:"interval"`
|
Interval int `provider:"interval"`
|
||||||
|
Lazy bool `provider:"lazy,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type proxyProviderSchema struct {
|
type proxyProviderSchema struct {
|
||||||
@ -24,35 +27,44 @@ type proxyProviderSchema struct {
|
|||||||
Path string `provider:"path"`
|
Path string `provider:"path"`
|
||||||
URL string `provider:"url,omitempty"`
|
URL string `provider:"url,omitempty"`
|
||||||
Interval int `provider:"interval,omitempty"`
|
Interval int `provider:"interval,omitempty"`
|
||||||
|
Filter string `provider:"filter,omitempty"`
|
||||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
|
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||||
|
|
||||||
schema := &proxyProviderSchema{}
|
schema := &proxyProviderSchema{
|
||||||
|
HealthCheck: healthCheckSchema{
|
||||||
|
Lazy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
if err := decoder.Decode(mapping, schema); err != nil {
|
if err := decoder.Decode(mapping, schema); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var hcInterval uint = 0
|
var hcInterval uint
|
||||||
if schema.HealthCheck.Enable {
|
if schema.HealthCheck.Enable {
|
||||||
hcInterval = uint(schema.HealthCheck.Interval)
|
hcInterval = uint(schema.HealthCheck.Interval)
|
||||||
}
|
}
|
||||||
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval)
|
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
|
||||||
|
|
||||||
path := C.Path.Resolve(schema.Path)
|
path := C.Path.Resolve(schema.Path)
|
||||||
|
|
||||||
var vehicle Vehicle
|
var vehicle types.Vehicle
|
||||||
switch schema.Type {
|
switch schema.Type {
|
||||||
case "file":
|
case "file":
|
||||||
vehicle = NewFileVehicle(path)
|
vehicle = NewFileVehicle(path)
|
||||||
case "http":
|
case "http":
|
||||||
|
if !C.Path.IsSubPath(path) {
|
||||||
|
return nil, fmt.Errorf("%w: %s", errSubPath, path)
|
||||||
|
}
|
||||||
vehicle = NewHTTPVehicle(schema.URL, path)
|
vehicle = NewHTTPVehicle(schema.URL, path)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||||
return NewProxySetProvider(name, interval, vehicle, hc), nil
|
filter := schema.Filter
|
||||||
|
return NewProxySetProvider(name, interval, filter, vehicle, hc)
|
||||||
}
|
}
|
322
adapter/provider/provider.go
Normal file
322
adapter/provider/provider.go
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter"
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
|
regexp "github.com/dlclark/regexp2"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reject = adapter.NewProxy(outbound.NewReject())
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReservedName = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProxySchema struct {
|
||||||
|
Proxies []map[string]any `yaml:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// for auto gc
|
||||||
|
type ProxySetProvider struct {
|
||||||
|
*proxySetProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxySetProvider struct {
|
||||||
|
*fetcher
|
||||||
|
proxies []C.Proxy
|
||||||
|
healthCheck *HealthCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"name": pp.Name(),
|
||||||
|
"type": pp.Type().String(),
|
||||||
|
"vehicleType": pp.VehicleType().String(),
|
||||||
|
"proxies": pp.Proxies(),
|
||||||
|
"updatedAt": pp.updatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Name() string {
|
||||||
|
return pp.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) HealthCheck() {
|
||||||
|
pp.healthCheck.checkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Update() error {
|
||||||
|
elm, same, err := pp.fetcher.Update()
|
||||||
|
if err == nil && !same {
|
||||||
|
pp.onUpdate(elm)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Initial() error {
|
||||||
|
elm, err := pp.fetcher.Initial()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pp.onUpdate(elm)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Type() types.ProviderType {
|
||||||
|
return types.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Proxies() []C.Proxy {
|
||||||
|
return pp.proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) Touch() {
|
||||||
|
pp.healthCheck.touch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||||
|
pp.proxies = proxies
|
||||||
|
pp.healthCheck.setProxy(proxies)
|
||||||
|
if pp.healthCheck.auto() {
|
||||||
|
go pp.healthCheck.checkAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopProxyProvider(pd *ProxySetProvider) {
|
||||||
|
pd.healthCheck.close()
|
||||||
|
pd.fetcher.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
||||||
|
filterReg, err := regexp.Compile(filter, regexp.None)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid filter regex: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.auto() {
|
||||||
|
go hc.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pd := &proxySetProvider{
|
||||||
|
proxies: []C.Proxy{},
|
||||||
|
healthCheck: hc,
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate := func(elm any) {
|
||||||
|
ret := elm.([]C.Proxy)
|
||||||
|
pd.setProxies(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxiesParseAndFilter := func(buf []byte) (any, error) {
|
||||||
|
schema := &ProxySchema{}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(buf, schema); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.Proxies == nil {
|
||||||
|
return nil, errors.New("file must have a `proxies` field")
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies := []C.Proxy{}
|
||||||
|
for idx, mapping := range schema.Proxies {
|
||||||
|
if name, ok := mapping["name"].(string); ok && len(filter) > 0 {
|
||||||
|
matched, err := filterReg.MatchString(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("regex filter failed: %w", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxy, err := adapter.ParseProxy(mapping)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
|
||||||
|
}
|
||||||
|
proxies = append(proxies, proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(proxies) == 0 {
|
||||||
|
if len(filter) > 0 {
|
||||||
|
return nil, errors.New("doesn't match any proxy, please check your filter")
|
||||||
|
}
|
||||||
|
return nil, errors.New("file doesn't have any proxy")
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
|
||||||
|
pd.fetcher = fetcher
|
||||||
|
|
||||||
|
wrapper := &ProxySetProvider{pd}
|
||||||
|
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
||||||
|
return wrapper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// for auto gc
|
||||||
|
type CompatibleProvider struct {
|
||||||
|
*compatibleProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
type compatibleProvider struct {
|
||||||
|
name string
|
||||||
|
healthCheck *HealthCheck
|
||||||
|
proxies []C.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"name": cp.Name(),
|
||||||
|
"type": cp.Type().String(),
|
||||||
|
"vehicleType": cp.VehicleType().String(),
|
||||||
|
"proxies": cp.Proxies(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Name() string {
|
||||||
|
return cp.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) HealthCheck() {
|
||||||
|
cp.healthCheck.checkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Update() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Initial() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) VehicleType() types.VehicleType {
|
||||||
|
return types.Compatible
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Type() types.ProviderType {
|
||||||
|
return types.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Proxies() []C.Proxy {
|
||||||
|
return cp.proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) Touch() {
|
||||||
|
cp.healthCheck.touch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
|
pd.healthCheck.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
||||||
|
if len(proxies) == 0 {
|
||||||
|
return nil, errors.New("provider need one proxy at least")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.auto() {
|
||||||
|
go hc.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
pd := &compatibleProvider{
|
||||||
|
name: name,
|
||||||
|
proxies: proxies,
|
||||||
|
healthCheck: hc,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := &CompatibleProvider{pd}
|
||||||
|
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
||||||
|
return wrapper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ types.ProxyProvider = (*FilterableProvider)(nil)
|
||||||
|
|
||||||
|
type FilterableProvider struct {
|
||||||
|
name string
|
||||||
|
providers []types.ProxyProvider
|
||||||
|
filterReg *regexp.Regexp
|
||||||
|
single *singledo.Single
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"name": fp.Name(),
|
||||||
|
"type": fp.Type().String(),
|
||||||
|
"vehicleType": fp.VehicleType().String(),
|
||||||
|
"proxies": fp.Proxies(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Name() string {
|
||||||
|
return fp.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) HealthCheck() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Update() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Initial() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) VehicleType() types.VehicleType {
|
||||||
|
return types.Compatible
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Type() types.ProviderType {
|
||||||
|
return types.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Proxies() []C.Proxy {
|
||||||
|
elm, _, _ := fp.single.Do(func() (any, error) {
|
||||||
|
proxies := lo.FlatMap(
|
||||||
|
fp.providers,
|
||||||
|
func(item types.ProxyProvider, _ int) []C.Proxy {
|
||||||
|
return lo.Filter(
|
||||||
|
item.Proxies(),
|
||||||
|
func(item C.Proxy, _ int) bool {
|
||||||
|
matched, _ := fp.filterReg.MatchString(item.Name())
|
||||||
|
return matched
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(proxies) == 0 {
|
||||||
|
proxies = append(proxies, reject)
|
||||||
|
}
|
||||||
|
return proxies, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return elm.([]C.Proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fp *FilterableProvider) Touch() {
|
||||||
|
for _, provider := range fp.providers {
|
||||||
|
provider.Touch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilterableProvider(name string, providers []types.ProxyProvider, filterReg *regexp.Regexp) *FilterableProvider {
|
||||||
|
return &FilterableProvider{
|
||||||
|
name: name,
|
||||||
|
providers: providers,
|
||||||
|
filterReg: filterReg,
|
||||||
|
single: singledo.NewSingle(time.Second * 10),
|
||||||
|
}
|
||||||
|
}
|
@ -2,49 +2,23 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vehicle Type
|
|
||||||
const (
|
|
||||||
File VehicleType = iota
|
|
||||||
HTTP
|
|
||||||
Compatible
|
|
||||||
)
|
|
||||||
|
|
||||||
// VehicleType defined
|
|
||||||
type VehicleType int
|
|
||||||
|
|
||||||
func (v VehicleType) String() string {
|
|
||||||
switch v {
|
|
||||||
case File:
|
|
||||||
return "File"
|
|
||||||
case HTTP:
|
|
||||||
return "HTTP"
|
|
||||||
case Compatible:
|
|
||||||
return "Compatible"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Vehicle interface {
|
|
||||||
Read() ([]byte, error)
|
|
||||||
Path() string
|
|
||||||
Type() VehicleType
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileVehicle struct {
|
type FileVehicle struct {
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileVehicle) Type() VehicleType {
|
func (f *FileVehicle) Type() types.VehicleType {
|
||||||
return File
|
return types.File
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileVehicle) Path() string {
|
func (f *FileVehicle) Path() string {
|
||||||
@ -52,7 +26,7 @@ func (f *FileVehicle) Path() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileVehicle) Read() ([]byte, error) {
|
func (f *FileVehicle) Read() ([]byte, error) {
|
||||||
return ioutil.ReadFile(f.path)
|
return os.ReadFile(f.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileVehicle(path string) *FileVehicle {
|
func NewFileVehicle(path string) *FileVehicle {
|
||||||
@ -64,8 +38,8 @@ type HTTPVehicle struct {
|
|||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPVehicle) Type() VehicleType {
|
func (h *HTTPVehicle) Type() types.VehicleType {
|
||||||
return HTTP
|
return types.HTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPVehicle) Path() string {
|
func (h *HTTPVehicle) Path() string {
|
||||||
@ -99,7 +73,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
DialContext: dialer.DialContext,
|
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, address)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := http.Client{Transport: transport}
|
client := http.Client{Transport: transport}
|
||||||
@ -107,8 +83,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(resp.Body)
|
buf, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
@ -1,60 +0,0 @@
|
|||||||
package inbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPAdapter is a adapter for HTTP connection
|
|
||||||
type HTTPAdapter struct {
|
|
||||||
net.Conn
|
|
||||||
metadata *C.Metadata
|
|
||||||
R *http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata return destination metadata
|
|
||||||
func (h *HTTPAdapter) Metadata() *C.Metadata {
|
|
||||||
return h.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHTTP is HTTPAdapter generator
|
|
||||||
func NewHTTP(request *http.Request, conn net.Conn) *HTTPAdapter {
|
|
||||||
metadata := parseHTTPAddr(request)
|
|
||||||
metadata.Type = C.HTTP
|
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
|
||||||
metadata.SrcIP = ip
|
|
||||||
metadata.SrcPort = port
|
|
||||||
}
|
|
||||||
return &HTTPAdapter{
|
|
||||||
metadata: metadata,
|
|
||||||
R: request,
|
|
||||||
Conn: conn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,22 +0,0 @@
|
|||||||
package inbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewHTTPS is HTTPAdapter generator
|
|
||||||
func NewHTTPS(request *http.Request, conn net.Conn) *SocketAdapter {
|
|
||||||
metadata := parseHTTPAddr(request)
|
|
||||||
metadata.Type = C.HTTPCONNECT
|
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
|
||||||
metadata.SrcIP = ip
|
|
||||||
metadata.SrcPort = port
|
|
||||||
}
|
|
||||||
return &SocketAdapter{
|
|
||||||
metadata: metadata,
|
|
||||||
Conn: conn,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package inbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SocketAdapter is a adapter for socks and redir connection
|
|
||||||
type SocketAdapter struct {
|
|
||||||
net.Conn
|
|
||||||
metadata *C.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata return destination metadata
|
|
||||||
func (s *SocketAdapter) Metadata() *C.Metadata {
|
|
||||||
return s.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSocket is SocketAdapter generator
|
|
||||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *SocketAdapter {
|
|
||||||
metadata := parseSocksAddr(target)
|
|
||||||
metadata.NetWork = C.TCP
|
|
||||||
metadata.Type = source
|
|
||||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
|
||||||
metadata.SrcIP = ip
|
|
||||||
metadata.SrcPort = port
|
|
||||||
}
|
|
||||||
|
|
||||||
return &SocketAdapter{
|
|
||||||
Conn: conn,
|
|
||||||
metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Direct struct {
|
|
||||||
*Base
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
return NewConn(c, d), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
pc, err := dialer.ListenPacket("udp", "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newPacketConn(&directPacketConn{pc}, d), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type directPacketConn struct {
|
|
||||||
net.PacketConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dp *directPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
return dp.WriteTo(p, metadata.UDPAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDirect() *Direct {
|
|
||||||
return &Direct{
|
|
||||||
Base: &Base{
|
|
||||||
name: "DIRECT",
|
|
||||||
tp: C.Direct,
|
|
||||||
udp: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
|
||||||
proxyType, existType := mapping["type"].(string)
|
|
||||||
if !existType {
|
|
||||||
return nil, fmt.Errorf("Missing type")
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxy C.ProxyAdapter
|
|
||||||
err := fmt.Errorf("Cannot parse")
|
|
||||||
switch proxyType {
|
|
||||||
case "ss":
|
|
||||||
ssOption := &ShadowSocksOption{}
|
|
||||||
err = decoder.Decode(mapping, ssOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = NewShadowSocks(*ssOption)
|
|
||||||
case "socks5":
|
|
||||||
socksOption := &Socks5Option{}
|
|
||||||
err = decoder.Decode(mapping, socksOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy = NewSocks5(*socksOption)
|
|
||||||
case "http":
|
|
||||||
httpOption := &HttpOption{}
|
|
||||||
err = decoder.Decode(mapping, httpOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy = NewHttp(*httpOption)
|
|
||||||
case "vmess":
|
|
||||||
vmessOption := &VmessOption{
|
|
||||||
HTTPOpts: HTTPOptions{
|
|
||||||
Method: "GET",
|
|
||||||
Path: []string{"/"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = decoder.Decode(mapping, vmessOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = NewVmess(*vmessOption)
|
|
||||||
case "snell":
|
|
||||||
snellOption := &SnellOption{}
|
|
||||||
err = decoder.Decode(mapping, snellOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = NewSnell(*snellOption)
|
|
||||||
case "trojan":
|
|
||||||
trojanOption := &TrojanOption{}
|
|
||||||
err = decoder.Decode(mapping, trojanOption)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
proxy, err = NewTrojan(*trojanOption)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("Unsupport proxy type: %s", proxyType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewProxy(proxy), nil
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Reject struct {
|
|
||||||
*Base
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
return NewConn(&NopConn{}, r), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
return nil, errors.New("match reject rule")
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReject() *Reject {
|
|
||||||
return &Reject{
|
|
||||||
Base: &Base{
|
|
||||||
name: "REJECT",
|
|
||||||
tp: C.Reject,
|
|
||||||
udp: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,78 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
|
||||||
obfs "github.com/Dreamacro/clash/component/simple-obfs"
|
|
||||||
"github.com/Dreamacro/clash/component/snell"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Snell struct {
|
|
||||||
*Base
|
|
||||||
psk []byte
|
|
||||||
obfsOption *simpleObfsOption
|
|
||||||
}
|
|
||||||
|
|
||||||
type SnellOption struct {
|
|
||||||
Name string `proxy:"name"`
|
|
||||||
Server string `proxy:"server"`
|
|
||||||
Port int `proxy:"port"`
|
|
||||||
Psk string `proxy:"psk"`
|
|
||||||
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|
||||||
switch s.obfsOption.Mode {
|
|
||||||
case "tls":
|
|
||||||
c = obfs.NewTLSObfs(c, s.obfsOption.Host)
|
|
||||||
case "http":
|
|
||||||
_, port, _ := net.SplitHostPort(s.addr)
|
|
||||||
c = obfs.NewHTTPObfs(c, s.obfsOption.Host, port)
|
|
||||||
}
|
|
||||||
c = snell.StreamConn(c, s.psk)
|
|
||||||
port, _ := strconv.Atoi(metadata.DstPort)
|
|
||||||
err := snell.WriteHeader(c, metadata.String(), uint(port))
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
|
|
||||||
c, err = s.StreamConn(c, metadata)
|
|
||||||
return NewConn(c, s), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSnell(option SnellOption) (*Snell, error) {
|
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
|
||||||
psk := []byte(option.Psk)
|
|
||||||
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
|
||||||
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
|
||||||
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
|
||||||
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
|
||||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Snell{
|
|
||||||
Base: &Base{
|
|
||||||
name: option.Name,
|
|
||||||
addr: addr,
|
|
||||||
tp: C.Snell,
|
|
||||||
},
|
|
||||||
psk: psk,
|
|
||||||
obfsOption: obfsOption,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
|
||||||
"github.com/Dreamacro/clash/component/trojan"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Trojan struct {
|
|
||||||
*Base
|
|
||||||
instance *trojan.Trojan
|
|
||||||
}
|
|
||||||
|
|
||||||
type TrojanOption struct {
|
|
||||||
Name string `proxy:"name"`
|
|
||||||
Server string `proxy:"server"`
|
|
||||||
Port int `proxy:"port"`
|
|
||||||
Password string `proxy:"password"`
|
|
||||||
ALPN []string `proxy:"alpn,omitempty"`
|
|
||||||
SNI string `proxy:"sni,omitempty"`
|
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|
||||||
c, err := t.instance.StreamConn(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
c, err = t.StreamConn(c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewConn(c, t), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trojan) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
|
||||||
defer cancel()
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
c, err = t.instance.StreamConn(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pc := t.instance.PacketConn(c)
|
|
||||||
return newPacketConn(&trojanPacketConn{pc, c}, t), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Trojan) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]string{
|
|
||||||
"type": t.Type().String(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
|
||||||
|
|
||||||
tOption := &trojan.Option{
|
|
||||||
Password: option.Password,
|
|
||||||
ALPN: option.ALPN,
|
|
||||||
ServerName: option.Server,
|
|
||||||
SkipCertVerify: option.SkipCertVerify,
|
|
||||||
ClientSessionCache: getClientSessionCache(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if option.SNI != "" {
|
|
||||||
tOption.ServerName = option.SNI
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Trojan{
|
|
||||||
Base: &Base{
|
|
||||||
name: option.Name,
|
|
||||||
addr: addr,
|
|
||||||
tp: C.Trojan,
|
|
||||||
udp: option.UDP,
|
|
||||||
},
|
|
||||||
instance: trojan.New(tOption),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type trojanPacketConn struct {
|
|
||||||
net.PacketConn
|
|
||||||
conn net.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tpc *trojanPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
return trojan.WritePacket(tpc.conn, serializesSocksAddr(metadata), p)
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
"github.com/Dreamacro/clash/component/socks5"
|
|
||||||
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 == "" {
|
|
||||||
switch u.Scheme {
|
|
||||||
case "https":
|
|
||||||
port = "443"
|
|
||||||
case "http":
|
|
||||||
port = "80"
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("%s scheme not Support", rawURL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addr = C.Metadata{
|
|
||||||
AddrType: C.AtypDomainName,
|
|
||||||
Host: u.Hostname(),
|
|
||||||
DstIP: nil,
|
|
||||||
DstPort: port,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
|
||||||
if tcp, ok := c.(*net.TCPConn); ok {
|
|
||||||
tcp.SetKeepAlive(true)
|
|
||||||
tcp.SetKeepAlivePeriod(30 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClientSessionCache() tls.ClientSessionCache {
|
|
||||||
once.Do(func() {
|
|
||||||
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
|
|
||||||
})
|
|
||||||
return globalClientSessionCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
|
||||||
var buf [][]byte
|
|
||||||
aType := uint8(metadata.AddrType)
|
|
||||||
p, _ := strconv.Atoi(metadata.DstPort)
|
|
||||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
|
||||||
switch metadata.AddrType {
|
|
||||||
case socks5.AtypDomainName:
|
|
||||||
len := uint8(len(metadata.Host))
|
|
||||||
host := []byte(metadata.Host)
|
|
||||||
buf = [][]byte{{aType, len}, host, port}
|
|
||||||
case socks5.AtypIPv4:
|
|
||||||
host := metadata.DstIP.To4()
|
|
||||||
buf = [][]byte{{aType}, host, port}
|
|
||||||
case socks5.AtypIPv6:
|
|
||||||
host := metadata.DstIP.To16()
|
|
||||||
buf = [][]byte{{aType}, host, port}
|
|
||||||
}
|
|
||||||
return bytes.Join(buf, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
|
||||||
host, port, err := net.SplitHostPort(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := resolver.ResolveIP(host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
|
||||||
}
|
|
@ -1,205 +0,0 @@
|
|||||||
package outbound
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
|
||||||
"github.com/Dreamacro/clash/component/vmess"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Vmess struct {
|
|
||||||
*Base
|
|
||||||
client *vmess.Client
|
|
||||||
option *VmessOption
|
|
||||||
}
|
|
||||||
|
|
||||||
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"`
|
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
|
||||||
Network string `proxy:"network,omitempty"`
|
|
||||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
|
||||||
WSPath string `proxy:"ws-path,omitempty"`
|
|
||||||
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPOptions struct {
|
|
||||||
Method string `proxy:"method,omitempty"`
|
|
||||||
Path []string `proxy:"path,omitempty"`
|
|
||||||
Headers map[string][]string `proxy:"headers,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|
||||||
var err error
|
|
||||||
switch v.option.Network {
|
|
||||||
case "ws":
|
|
||||||
host, port, _ := net.SplitHostPort(v.addr)
|
|
||||||
wsOpts := &vmess.WebsocketConfig{
|
|
||||||
Host: host,
|
|
||||||
Port: port,
|
|
||||||
Path: v.option.WSPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(v.option.WSHeaders) != 0 {
|
|
||||||
header := http.Header{}
|
|
||||||
for key, value := range v.option.WSHeaders {
|
|
||||||
header.Add(key, value)
|
|
||||||
}
|
|
||||||
wsOpts.Headers = header
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.option.TLS {
|
|
||||||
wsOpts.TLS = true
|
|
||||||
wsOpts.SessionCache = getClientSessionCache()
|
|
||||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
|
||||||
}
|
|
||||||
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
|
||||||
case "http":
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
|
||||||
httpOpts := &vmess.HTTPConfig{
|
|
||||||
Host: host,
|
|
||||||
Method: v.option.HTTPOpts.Method,
|
|
||||||
Path: v.option.HTTPOpts.Path,
|
|
||||||
Headers: v.option.HTTPOpts.Headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
c = vmess.StreamHTTPConn(c, httpOpts)
|
|
||||||
default:
|
|
||||||
// handle TLS
|
|
||||||
if v.option.TLS {
|
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
|
||||||
tlsOpts := &vmess.TLSConfig{
|
|
||||||
Host: host,
|
|
||||||
SkipCertVerify: v.option.SkipCertVerify,
|
|
||||||
SessionCache: getClientSessionCache(),
|
|
||||||
}
|
|
||||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error", v.addr)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
|
||||||
return NewConn(c, v), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
// vmess use stream-oriented udp, so clash needs a net.UDPAddr
|
|
||||||
if !metadata.Resolved() {
|
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't resolve ip")
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
|
||||||
defer cancel()
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s connect error", v.addr)
|
|
||||||
}
|
|
||||||
tcpKeepAlive(c)
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
|
||||||
}
|
|
||||||
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
HostName: option.Server,
|
|
||||||
Port: strconv.Itoa(option.Port),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Vmess{
|
|
||||||
Base: &Base{
|
|
||||||
name: option.Name,
|
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
|
||||||
tp: C.Vmess,
|
|
||||||
udp: true,
|
|
||||||
},
|
|
||||||
client: client,
|
|
||||||
option: &option,
|
|
||||||
}, 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.DstIP.To4())
|
|
||||||
case C.AtypIPv6:
|
|
||||||
addrType = byte(vmess.AtypIPv6)
|
|
||||||
addr = make([]byte, net.IPv6len)
|
|
||||||
copy(addr[:], metadata.DstIP.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.DstPort)
|
|
||||||
return &vmess.DstAddr{
|
|
||||||
UDP: metadata.NetWork == C.UDP,
|
|
||||||
AddrType: addrType,
|
|
||||||
Addr: addr,
|
|
||||||
Port: uint(port),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type vmessPacketConn struct {
|
|
||||||
net.Conn
|
|
||||||
rAddr net.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
|
||||||
return uc.Conn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *vmessPacketConn) WriteWithMetadata(p []byte, metadata *C.Metadata) (n int, err error) {
|
|
||||||
return uc.Conn.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
|
||||||
n, err := uc.Conn.Read(b)
|
|
||||||
return n, uc.rAddr, err
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultGetProxiesDuration = time.Second * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy {
|
|
||||||
proxies := []C.Proxy{}
|
|
||||||
for _, provider := range providers {
|
|
||||||
proxies = append(proxies, provider.Proxies()...)
|
|
||||||
}
|
|
||||||
return proxies
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Fallback struct {
|
|
||||||
*outbound.Base
|
|
||||||
single *singledo.Single
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) Now() string {
|
|
||||||
proxy := f.findAliveProxy()
|
|
||||||
return proxy.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
proxy := f.findAliveProxy()
|
|
||||||
c, err := proxy.DialContext(ctx, metadata)
|
|
||||||
if err == nil {
|
|
||||||
c.AppendToChains(f)
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
proxy := f.findAliveProxy()
|
|
||||||
pc, err := proxy.DialUDP(metadata)
|
|
||||||
if err == nil {
|
|
||||||
pc.AppendToChains(f)
|
|
||||||
}
|
|
||||||
return pc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) SupportUDP() bool {
|
|
||||||
proxy := f.findAliveProxy()
|
|
||||||
return proxy.SupportUDP()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) Unwrap(metadata *C.Metadata) C.Proxy {
|
|
||||||
proxy := f.findAliveProxy()
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) proxies() []C.Proxy {
|
|
||||||
elm, _, _ := f.single.Do(func() (interface{}, error) {
|
|
||||||
return getProvidersProxies(f.providers), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fallback) findAliveProxy() C.Proxy {
|
|
||||||
proxies := f.proxies()
|
|
||||||
for _, proxy := range proxies {
|
|
||||||
if proxy.Alive() {
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.proxies()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFallback(name string, providers []provider.ProxyProvider) *Fallback {
|
|
||||||
return &Fallback{
|
|
||||||
Base: outbound.NewBase(name, "", C.Fallback, false),
|
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
|
||||||
providers: providers,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,125 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/murmur3"
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
|
|
||||||
"golang.org/x/net/publicsuffix"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoadBalance struct {
|
|
||||||
*outbound.Base
|
|
||||||
single *singledo.Single
|
|
||||||
maxRetry int
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
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.DstIP == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.DstIP.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) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
|
||||||
defer func() {
|
|
||||||
if err == nil {
|
|
||||||
c.AppendToChains(lb)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := lb.Unwrap(metadata)
|
|
||||||
|
|
||||||
c, err = proxy.DialContext(ctx, metadata)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
|
||||||
defer func() {
|
|
||||||
if err == nil {
|
|
||||||
pc.AppendToChains(lb)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
proxy := lb.Unwrap(metadata)
|
|
||||||
|
|
||||||
return proxy.DialUDP(metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *LoadBalance) SupportUDP() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
|
||||||
proxies := lb.proxies()
|
|
||||||
buckets := int32(len(proxies))
|
|
||||||
for i := 0; i < lb.maxRetry; i, key = i+1, key+1 {
|
|
||||||
idx := jumpHash(key, buckets)
|
|
||||||
proxy := proxies[idx]
|
|
||||||
if proxy.Alive() {
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lb *LoadBalance) proxies() []C.Proxy {
|
|
||||||
elm, _, _ := lb.single.Do(func() (interface{}, error) {
|
|
||||||
return getProvidersProxies(lb.providers), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLoadBalance(name string, providers []provider.ProxyProvider) *LoadBalance {
|
|
||||||
return &LoadBalance{
|
|
||||||
Base: outbound.NewBase(name, "", C.LoadBalance, false),
|
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
|
||||||
maxRetry: 3,
|
|
||||||
providers: providers,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,146 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errFormat = errors.New("format error")
|
|
||||||
errType = errors.New("unsupport type")
|
|
||||||
errMissUse = errors.New("`use` field should not be empty")
|
|
||||||
errMissProxy = errors.New("`use` or `proxies` missing")
|
|
||||||
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
|
||||||
errDuplicateProvider = errors.New("`duplicate provider name")
|
|
||||||
)
|
|
||||||
|
|
||||||
type GroupCommonOption struct {
|
|
||||||
Name string `group:"name"`
|
|
||||||
Type string `group:"type"`
|
|
||||||
Proxies []string `group:"proxies,omitempty"`
|
|
||||||
Use []string `group:"use,omitempty"`
|
|
||||||
URL string `group:"url,omitempty"`
|
|
||||||
Interval int `group:"interval,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
|
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
|
||||||
|
|
||||||
groupOption := &GroupCommonOption{}
|
|
||||||
if err := decoder.Decode(config, groupOption); err != nil {
|
|
||||||
return nil, errFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if groupOption.Type == "" || groupOption.Name == "" {
|
|
||||||
return nil, errFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
groupName := groupOption.Name
|
|
||||||
|
|
||||||
providers := []provider.ProxyProvider{}
|
|
||||||
|
|
||||||
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
|
|
||||||
return nil, errMissProxy
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(groupOption.Proxies) != 0 {
|
|
||||||
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if Use not empty, drop health check options
|
|
||||||
if len(groupOption.Use) != 0 {
|
|
||||||
hc := provider.NewHealthCheck(ps, "", 0)
|
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
providers = append(providers, pd)
|
|
||||||
} else {
|
|
||||||
// select don't need health check
|
|
||||||
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
|
||||||
hc := provider.NewHealthCheck(ps, "", 0)
|
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
providers = append(providers, pd)
|
|
||||||
providersMap[groupName] = pd
|
|
||||||
} else {
|
|
||||||
if groupOption.URL == "" || groupOption.Interval == 0 {
|
|
||||||
return nil, errMissHealthCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval))
|
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
providers = append(providers, pd)
|
|
||||||
providersMap[groupName] = pd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(groupOption.Use) != 0 {
|
|
||||||
list, err := getProviders(providersMap, groupOption.Use)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
providers = append(providers, list...)
|
|
||||||
}
|
|
||||||
|
|
||||||
var group C.ProxyAdapter
|
|
||||||
switch groupOption.Type {
|
|
||||||
case "url-test":
|
|
||||||
group = NewURLTest(groupName, providers)
|
|
||||||
case "select":
|
|
||||||
group = NewSelector(groupName, providers)
|
|
||||||
case "fallback":
|
|
||||||
group = NewFallback(groupName, providers)
|
|
||||||
case "load-balance":
|
|
||||||
group = NewLoadBalance(groupName, providers)
|
|
||||||
case "relay":
|
|
||||||
group = NewRelay(groupName, providers)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return group, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) {
|
|
||||||
var ps []provider.ProxyProvider
|
|
||||||
for _, name := range list {
|
|
||||||
p, ok := mapping[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("'%s' not found", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.VehicleType() == provider.Compatible {
|
|
||||||
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
|
|
||||||
}
|
|
||||||
ps = append(ps, p)
|
|
||||||
}
|
|
||||||
return ps, nil
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Selector struct {
|
|
||||||
*outbound.Base
|
|
||||||
single *singledo.Single
|
|
||||||
selected string
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
|
||||||
c, err := s.selectedProxy().DialContext(ctx, metadata)
|
|
||||||
if err == nil {
|
|
||||||
c.AppendToChains(s)
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
pc, err := s.selectedProxy().DialUDP(metadata)
|
|
||||||
if err == nil {
|
|
||||||
pc.AppendToChains(s)
|
|
||||||
}
|
|
||||||
return pc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) SupportUDP() bool {
|
|
||||||
return s.selectedProxy().SupportUDP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
|
||||||
var all []string
|
|
||||||
for _, proxy := range getProvidersProxies(s.providers) {
|
|
||||||
all = append(all, proxy.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
"type": s.Type().String(),
|
|
||||||
"now": s.Now(),
|
|
||||||
"all": all,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) Now() string {
|
|
||||||
return s.selectedProxy().Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) Set(name string) error {
|
|
||||||
for _, proxy := range getProvidersProxies(s.providers) {
|
|
||||||
if proxy.Name() == name {
|
|
||||||
s.selected = name
|
|
||||||
s.single.Reset()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("Proxy does not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
|
||||||
return s.selectedProxy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Selector) selectedProxy() C.Proxy {
|
|
||||||
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
|
||||||
proxies := getProvidersProxies(s.providers)
|
|
||||||
for _, proxy := range proxies {
|
|
||||||
if proxy.Name() == s.selected {
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies[0], nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return elm.(C.Proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSelector(name string, providers []provider.ProxyProvider) *Selector {
|
|
||||||
selected := providers[0].Proxies()[0].Name()
|
|
||||||
return &Selector{
|
|
||||||
Base: outbound.NewBase(name, "", C.Selector, false),
|
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
|
||||||
providers: providers,
|
|
||||||
selected: selected,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
package outboundgroup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
|
||||||
"github.com/Dreamacro/clash/adapters/provider"
|
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
type URLTest struct {
|
|
||||||
*outbound.Base
|
|
||||||
single *singledo.Single
|
|
||||||
fastSingle *singledo.Single
|
|
||||||
providers []provider.ProxyProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) Now() string {
|
|
||||||
return u.fast().Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
|
||||||
c, err = u.fast().DialContext(ctx, metadata)
|
|
||||||
if err == nil {
|
|
||||||
c.AppendToChains(u)
|
|
||||||
}
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|
||||||
pc, err := u.fast().DialUDP(metadata)
|
|
||||||
if err == nil {
|
|
||||||
pc.AppendToChains(u)
|
|
||||||
}
|
|
||||||
return pc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
|
||||||
return u.fast()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) proxies() []C.Proxy {
|
|
||||||
elm, _, _ := u.single.Do(func() (interface{}, error) {
|
|
||||||
return getProvidersProxies(u.providers), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return elm.([]C.Proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) fast() C.Proxy {
|
|
||||||
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
|
|
||||||
proxies := u.proxies()
|
|
||||||
fast := proxies[0]
|
|
||||||
min := fast.LastDelay()
|
|
||||||
for _, proxy := range proxies[1:] {
|
|
||||||
if !proxy.Alive() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
delay := proxy.LastDelay()
|
|
||||||
if delay < min {
|
|
||||||
fast = proxy
|
|
||||||
min = delay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fast, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return elm.(C.Proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) SupportUDP() bool {
|
|
||||||
return u.fast().SupportUDP()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
|
||||||
var all []string
|
|
||||||
for _, proxy := range u.proxies() {
|
|
||||||
all = append(all, proxy.Name())
|
|
||||||
}
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
"type": u.Type().String(),
|
|
||||||
"now": u.Now(),
|
|
||||||
"all": all,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewURLTest(name string, providers []provider.ProxyProvider) *URLTest {
|
|
||||||
return &URLTest{
|
|
||||||
Base: outbound.NewBase(name, "", C.URLTest, false),
|
|
||||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
|
||||||
fastSingle: singledo.NewSingle(time.Second * 10),
|
|
||||||
providers: providers,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
package provider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultURLTestTimeout = time.Second * 5
|
|
||||||
)
|
|
||||||
|
|
||||||
type HealthCheckOption struct {
|
|
||||||
URL string
|
|
||||||
Interval uint
|
|
||||||
}
|
|
||||||
|
|
||||||
type HealthCheck struct {
|
|
||||||
url string
|
|
||||||
proxies []C.Proxy
|
|
||||||
interval uint
|
|
||||||
done chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) process() {
|
|
||||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
|
||||||
|
|
||||||
go hc.check()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
hc.check()
|
|
||||||
case <-hc.done:
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
|
||||||
hc.proxies = proxies
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) auto() bool {
|
|
||||||
return hc.interval != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
|
||||||
for _, proxy := range hc.proxies {
|
|
||||||
go proxy.URLTest(ctx, hc.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hc *HealthCheck) close() {
|
|
||||||
hc.done <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck {
|
|
||||||
return &HealthCheck{
|
|
||||||
proxies: proxies,
|
|
||||||
url: url,
|
|
||||||
interval: interval,
|
|
||||||
done: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,246 +0,0 @@
|
|||||||
package provider
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapters/outbound"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ReservedName = "default"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Provider Type
|
|
||||||
const (
|
|
||||||
Proxy ProviderType = iota
|
|
||||||
Rule
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProviderType defined
|
|
||||||
type ProviderType int
|
|
||||||
|
|
||||||
func (pt ProviderType) String() string {
|
|
||||||
switch pt {
|
|
||||||
case Proxy:
|
|
||||||
return "Proxy"
|
|
||||||
case Rule:
|
|
||||||
return "Rule"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider interface
|
|
||||||
type Provider interface {
|
|
||||||
Name() string
|
|
||||||
VehicleType() VehicleType
|
|
||||||
Type() ProviderType
|
|
||||||
Initial() error
|
|
||||||
Update() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProxyProvider interface
|
|
||||||
type ProxyProvider interface {
|
|
||||||
Provider
|
|
||||||
Proxies() []C.Proxy
|
|
||||||
HealthCheck()
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxySchema struct {
|
|
||||||
Proxies []map[string]interface{} `yaml:"proxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// for auto gc
|
|
||||||
type ProxySetProvider struct {
|
|
||||||
*proxySetProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
type proxySetProvider struct {
|
|
||||||
*fetcher
|
|
||||||
proxies []C.Proxy
|
|
||||||
healthCheck *HealthCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
"name": pp.Name(),
|
|
||||||
"type": pp.Type().String(),
|
|
||||||
"vehicleType": pp.VehicleType().String(),
|
|
||||||
"proxies": pp.Proxies(),
|
|
||||||
"updatedAt": pp.updatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Name() string {
|
|
||||||
return pp.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) HealthCheck() {
|
|
||||||
pp.healthCheck.check()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Update() error {
|
|
||||||
elm, same, err := pp.fetcher.Update()
|
|
||||||
if err == nil && !same {
|
|
||||||
pp.onUpdate(elm)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Initial() error {
|
|
||||||
elm, err := pp.fetcher.Initial()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pp.onUpdate(elm)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Type() ProviderType {
|
|
||||||
return Proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) Proxies() []C.Proxy {
|
|
||||||
return pp.proxies
|
|
||||||
}
|
|
||||||
|
|
||||||
func proxiesParse(buf []byte) (interface{}, error) {
|
|
||||||
schema := &ProxySchema{}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(buf, schema); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if schema.Proxies == nil {
|
|
||||||
return nil, errors.New("File must have a `proxies` field")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies := []C.Proxy{}
|
|
||||||
for idx, mapping := range schema.Proxies {
|
|
||||||
proxy, err := outbound.ParseProxy(mapping)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Proxy %d error: %w", idx, err)
|
|
||||||
}
|
|
||||||
proxies = append(proxies, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
return nil, errors.New("File doesn't have any valid proxy")
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxies, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
|
||||||
pp.proxies = proxies
|
|
||||||
pp.healthCheck.setProxy(proxies)
|
|
||||||
go pp.healthCheck.check()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopProxyProvider(pd *ProxySetProvider) {
|
|
||||||
pd.healthCheck.close()
|
|
||||||
pd.fetcher.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
|
|
||||||
if hc.auto() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
|
|
||||||
pd := &proxySetProvider{
|
|
||||||
proxies: []C.Proxy{},
|
|
||||||
healthCheck: hc,
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate := func(elm interface{}) {
|
|
||||||
ret := elm.([]C.Proxy)
|
|
||||||
pd.setProxies(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate)
|
|
||||||
pd.fetcher = fetcher
|
|
||||||
|
|
||||||
wrapper := &ProxySetProvider{pd}
|
|
||||||
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
// for auto gc
|
|
||||||
type CompatibleProvider struct {
|
|
||||||
*compatibleProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
type compatibleProvider struct {
|
|
||||||
name string
|
|
||||||
healthCheck *HealthCheck
|
|
||||||
proxies []C.Proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]interface{}{
|
|
||||||
"name": cp.Name(),
|
|
||||||
"type": cp.Type().String(),
|
|
||||||
"vehicleType": cp.VehicleType().String(),
|
|
||||||
"proxies": cp.Proxies(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Name() string {
|
|
||||||
return cp.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) HealthCheck() {
|
|
||||||
cp.healthCheck.check()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Update() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Initial() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) VehicleType() VehicleType {
|
|
||||||
return Compatible
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Type() ProviderType {
|
|
||||||
return Proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cp *compatibleProvider) Proxies() []C.Proxy {
|
|
||||||
return cp.proxies
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
|
||||||
pd.healthCheck.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
return nil, errors.New("Provider need one proxy at least")
|
|
||||||
}
|
|
||||||
|
|
||||||
if hc.auto() {
|
|
||||||
go hc.process()
|
|
||||||
}
|
|
||||||
|
|
||||||
pd := &compatibleProvider{
|
|
||||||
name: name,
|
|
||||||
proxies: proxies,
|
|
||||||
healthCheck: hc,
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper := &CompatibleProvider{pd}
|
|
||||||
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
|
||||||
return wrapper, nil
|
|
||||||
}
|
|
105
common/batch/batch.go
Normal file
105
common/batch/batch.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package batch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option = func(b *Batch)
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Value any
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Key string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithConcurrencyNum(n int) Option {
|
||||||
|
return func(b *Batch) {
|
||||||
|
q := make(chan struct{}, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
q <- struct{}{}
|
||||||
|
}
|
||||||
|
b.queue = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch similar to errgroup, but can control the maximum number of concurrent
|
||||||
|
type Batch struct {
|
||||||
|
result map[string]Result
|
||||||
|
queue chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
mux sync.Mutex
|
||||||
|
err *Error
|
||||||
|
once sync.Once
|
||||||
|
cancel func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Go(key string, fn func() (any, error)) {
|
||||||
|
b.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
if b.queue != nil {
|
||||||
|
<-b.queue
|
||||||
|
defer func() {
|
||||||
|
b.queue <- struct{}{}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := fn()
|
||||||
|
if err != nil {
|
||||||
|
b.once.Do(func() {
|
||||||
|
b.err = &Error{key, err}
|
||||||
|
if b.cancel != nil {
|
||||||
|
b.cancel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := Result{value, err}
|
||||||
|
b.mux.Lock()
|
||||||
|
defer b.mux.Unlock()
|
||||||
|
b.result[key] = ret
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Wait() *Error {
|
||||||
|
b.wg.Wait()
|
||||||
|
if b.cancel != nil {
|
||||||
|
b.cancel()
|
||||||
|
}
|
||||||
|
return b.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
|
||||||
|
err := b.Wait()
|
||||||
|
return b.Result(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batch) Result() map[string]Result {
|
||||||
|
b.mux.Lock()
|
||||||
|
defer b.mux.Unlock()
|
||||||
|
copy := map[string]Result{}
|
||||||
|
for k, v := range b.result {
|
||||||
|
copy[k] = v
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
b := &Batch{
|
||||||
|
result: map[string]Result{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.cancel = cancel
|
||||||
|
return b, ctx
|
||||||
|
}
|
83
common/batch/batch_test.go
Normal file
83
common/batch/batch_test.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package batch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBatch(t *testing.T) {
|
||||||
|
b, _ := New(context.Background())
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
b.Go("foo", func() (any, error) {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
return "foo", nil
|
||||||
|
})
|
||||||
|
b.Go("bar", func() (any, error) {
|
||||||
|
time.Sleep(time.Millisecond * 150)
|
||||||
|
return "bar", nil
|
||||||
|
})
|
||||||
|
result, err := b.WaitAndGetResult()
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
duration := time.Since(now)
|
||||||
|
assert.Less(t, duration, time.Millisecond*200)
|
||||||
|
assert.Equal(t, 2, len(result))
|
||||||
|
|
||||||
|
for k, v := range result {
|
||||||
|
assert.NoError(t, v.Err)
|
||||||
|
assert.Equal(t, k, v.Value.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchWithConcurrencyNum(t *testing.T) {
|
||||||
|
b, _ := New(
|
||||||
|
context.Background(),
|
||||||
|
WithConcurrencyNum(3),
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
idx := i
|
||||||
|
b.Go(strconv.Itoa(idx), func() (any, error) {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
return strconv.Itoa(idx), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
result, _ := b.WaitAndGetResult()
|
||||||
|
duration := time.Since(now)
|
||||||
|
assert.Greater(t, duration, time.Millisecond*260)
|
||||||
|
assert.Equal(t, 7, len(result))
|
||||||
|
|
||||||
|
for k, v := range result {
|
||||||
|
assert.NoError(t, v.Err)
|
||||||
|
assert.Equal(t, k, v.Value.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchContext(t *testing.T) {
|
||||||
|
b, ctx := New(context.Background())
|
||||||
|
|
||||||
|
b.Go("error", func() (any, error) {
|
||||||
|
time.Sleep(time.Millisecond * 100)
|
||||||
|
return nil, errors.New("test error")
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Go("ctx", func() (any, error) {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := b.WaitAndGetResult()
|
||||||
|
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
assert.Equal(t, "error", err.Key)
|
||||||
|
|
||||||
|
assert.Equal(t, ctx.Err(), result["ctx"].Err)
|
||||||
|
}
|
106
common/cache/cache.go
vendored
106
common/cache/cache.go
vendored
@ -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
|
|
||||||
}
|
|
70
common/cache/cache_test.go
vendored
70
common/cache/cache_test.go
vendored
@ -1,70 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
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")
|
|
||||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
|
||||||
|
|
||||||
s := c.Get("string")
|
|
||||||
assert.Equal(t, s.(string), "a", "should recv 'a'")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCache_TTL(t *testing.T) {
|
|
||||||
interval := 200 * time.Millisecond
|
|
||||||
ttl := 20 * time.Millisecond
|
|
||||||
now := time.Now()
|
|
||||||
c := New(interval)
|
|
||||||
c.Put("int", 1, ttl)
|
|
||||||
c.Put("int2", 2, ttl)
|
|
||||||
|
|
||||||
i := c.Get("int")
|
|
||||||
_, expired := c.GetWithExpire("int2")
|
|
||||||
assert.Equal(t, i.(int), 1, "should recv 1")
|
|
||||||
assert.True(t, now.Before(expired))
|
|
||||||
|
|
||||||
time.Sleep(ttl * 2)
|
|
||||||
i = c.Get("int")
|
|
||||||
j, _ := c.GetWithExpire("int2")
|
|
||||||
assert.Nil(t, i, "should recv nil")
|
|
||||||
assert.Nil(t, j, "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")
|
|
||||||
j, _ := c.GetWithExpire("int")
|
|
||||||
assert.Nil(t, i, "should recv nil")
|
|
||||||
assert.Nil(t, j, "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()
|
|
||||||
}
|
|
53
common/cache/lrucache.go
vendored
53
common/cache/lrucache.go
vendored
@ -12,7 +12,7 @@ import (
|
|||||||
type Option func(*LruCache)
|
type Option func(*LruCache)
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||||
type EvictCallback = func(key interface{}, value interface{})
|
type EvictCallback = func(key any, value any)
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict(cb EvictCallback) Option {
|
func WithEvict(cb EvictCallback) Option {
|
||||||
@ -57,18 +57,18 @@ type LruCache struct {
|
|||||||
maxAge int64
|
maxAge int64
|
||||||
maxSize int
|
maxSize int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cache map[interface{}]*list.Element
|
cache map[any]*list.Element
|
||||||
lru *list.List // Front is least-recent
|
lru *list.List // Front is least-recent
|
||||||
updateAgeOnGet bool
|
updateAgeOnGet bool
|
||||||
staleReturn bool
|
staleReturn bool
|
||||||
onEvict EvictCallback
|
onEvict EvictCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLRUCache creates an LruCache
|
// New creates an LruCache
|
||||||
func NewLRUCache(options ...Option) *LruCache {
|
func New(options ...Option) *LruCache {
|
||||||
lc := &LruCache{
|
lc := &LruCache{
|
||||||
lru: list.New(),
|
lru: list.New(),
|
||||||
cache: make(map[interface{}]*list.Element),
|
cache: make(map[any]*list.Element),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@ -78,9 +78,9 @@ func NewLRUCache(options ...Option) *LruCache {
|
|||||||
return lc
|
return lc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the interface{} representation of a cached response and a bool
|
// Get returns the any representation of a cached response and a bool
|
||||||
// set to true if the key was found.
|
// set to true if the key was found.
|
||||||
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
func (c *LruCache) Get(key any) (any, bool) {
|
||||||
entry := c.get(key)
|
entry := c.get(key)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
@ -90,11 +90,11 @@ func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
|||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithExpire returns the interface{} representation of a cached response,
|
// GetWithExpire returns the any representation of a cached response,
|
||||||
// a time.Time Give expected expires,
|
// a time.Time Give expected expires,
|
||||||
// and a bool set to true if the key was found.
|
// and a bool set to true if the key was found.
|
||||||
// This method will NOT check the maxAge of element and will NOT update the expires.
|
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||||
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
|
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
|
||||||
entry := c.get(key)
|
entry := c.get(key)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, time.Time{}, false
|
return nil, time.Time{}, false
|
||||||
@ -104,7 +104,7 @@ func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||||
func (c *LruCache) Exist(key interface{}) bool {
|
func (c *LruCache) Exist(key any) bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stores the interface{} representation of a response for a given key.
|
// Set stores the any representation of a response for a given key.
|
||||||
func (c *LruCache) Set(key interface{}, value interface{}) {
|
func (c *LruCache) Set(key any, value any) {
|
||||||
expires := int64(0)
|
expires := int64(0)
|
||||||
if c.maxAge > 0 {
|
if c.maxAge > 0 {
|
||||||
expires = time.Now().Unix() + c.maxAge
|
expires = time.Now().Unix() + c.maxAge
|
||||||
@ -121,9 +121,9 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
|
|||||||
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWithExpire stores the interface{} representation of a response for a given key and given exires.
|
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||||
// The expires time will round to second.
|
// The expires time will round to second.
|
||||||
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -146,7 +146,24 @@ func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires tim
|
|||||||
c.maybeDeleteOldest()
|
c.maybeDeleteOldest()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LruCache) get(key interface{}) *entry {
|
// CloneTo clone and overwrite elements to another LruCache
|
||||||
|
func (c *LruCache) CloneTo(n *LruCache) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
n.lru = list.New()
|
||||||
|
n.cache = make(map[any]*list.Element)
|
||||||
|
|
||||||
|
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||||
|
elm := e.Value.(*entry)
|
||||||
|
n.cache[elm.key] = n.lru.PushBack(elm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LruCache) get(key any) *entry {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -171,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the value associated with a key.
|
// Delete removes the value associated with a key.
|
||||||
func (c *LruCache) Delete(key string) {
|
func (c *LruCache) Delete(key any) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
@ -200,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type entry struct {
|
type entry struct {
|
||||||
key interface{}
|
key any
|
||||||
value interface{}
|
value any
|
||||||
expires int64
|
expires int64
|
||||||
}
|
}
|
||||||
|
37
common/cache/lrucache_test.go
vendored
37
common/cache/lrucache_test.go
vendored
@ -19,7 +19,7 @@ var entries = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUCache(t *testing.T) {
|
func TestLRUCache(t *testing.T) {
|
||||||
c := NewLRUCache()
|
c := New()
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
c.Set(e.key, e.value)
|
c.Set(e.key, e.value)
|
||||||
@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUMaxAge(t *testing.T) {
|
func TestLRUMaxAge(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(86400))
|
c := New(WithAge(86400))
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expected := now + 86400
|
expected := now + 86400
|
||||||
@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUpdateOnGet(t *testing.T) {
|
func TestLRUpdateOnGet(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
c := New(WithAge(86400), WithUpdateAgeOnGet())
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expires := now + 86400/2
|
expires := now + 86400/2
|
||||||
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMaxSize(t *testing.T) {
|
func TestMaxSize(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(2))
|
c := New(WithSize(2))
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExist(t *testing.T) {
|
func TestExist(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(1))
|
c := New(WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
assert.True(t, c.Exist(1))
|
assert.True(t, c.Exist(1))
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
|
|||||||
|
|
||||||
func TestEvict(t *testing.T) {
|
func TestEvict(t *testing.T) {
|
||||||
temp := 0
|
temp := 0
|
||||||
evict := func(key interface{}, value interface{}) {
|
evict := func(key any, value any) {
|
||||||
temp = key.(int) + value.(int)
|
temp = key.(int) + value.(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := NewLRUCache(WithEvict(evict), WithSize(1))
|
c := New(WithEvict(evict), WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetWithExpire(t *testing.T) {
|
func TestSetWithExpire(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(1))
|
c := New(WithAge(1))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -149,11 +149,10 @@ func TestSetWithExpire(t *testing.T) {
|
|||||||
assert.Equal(t, nil, res)
|
assert.Equal(t, nil, res)
|
||||||
assert.Equal(t, time.Time{}, expires)
|
assert.Equal(t, time.Time{}, expires)
|
||||||
assert.Equal(t, false, exist)
|
assert.Equal(t, false, exist)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStale(t *testing.T) {
|
func TestStale(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(1), WithStale(true))
|
c := New(WithAge(1), WithStale(true))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -164,3 +163,21 @@ func TestStale(t *testing.T) {
|
|||||||
assert.Equal(t, tenSecBefore, expires)
|
assert.Equal(t, tenSecBefore, expires)
|
||||||
assert.Equal(t, true, exist)
|
assert.Equal(t, true, exist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloneTo(t *testing.T) {
|
||||||
|
o := New(WithSize(10))
|
||||||
|
o.Set("1", 1)
|
||||||
|
o.Set("2", 2)
|
||||||
|
|
||||||
|
n := New(WithSize(2))
|
||||||
|
n.Set("3", 3)
|
||||||
|
n.Set("4", 4)
|
||||||
|
|
||||||
|
o.CloneTo(n)
|
||||||
|
|
||||||
|
assert.False(t, n.Exist("3"))
|
||||||
|
assert.True(t, n.Exist("1"))
|
||||||
|
|
||||||
|
n.Set("5", 5)
|
||||||
|
assert.False(t, n.Exist("1"))
|
||||||
|
}
|
||||||
|
@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *digest32) Sum32() (h1 uint32) {
|
func (d *digest32) Sum32() (h1 uint32) {
|
||||||
|
|
||||||
h1 = d.h1
|
h1 = d.h1
|
||||||
|
|
||||||
var k1 uint32
|
var k1 uint32
|
||||||
|
44
common/net/bufconn.go
Normal file
44
common/net/bufconn.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BufferedConn struct {
|
||||||
|
r *bufio.Reader
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBufferedConn(c net.Conn) *BufferedConn {
|
||||||
|
if bc, ok := c.(*BufferedConn); ok {
|
||||||
|
return bc
|
||||||
|
}
|
||||||
|
return &BufferedConn{bufio.NewReader(c), c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader returns the internal bufio.Reader.
|
||||||
|
func (c *BufferedConn) Reader() *bufio.Reader {
|
||||||
|
return c.r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek returns the next n bytes without advancing the reader.
|
||||||
|
func (c *BufferedConn) Peek(n int) ([]byte, error) {
|
||||||
|
return c.r.Peek(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BufferedConn) Read(p []byte) (int, error) {
|
||||||
|
return c.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BufferedConn) ReadByte() (byte, error) {
|
||||||
|
return c.r.ReadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BufferedConn) UnreadByte() error {
|
||||||
|
return c.r.UnreadByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BufferedConn) Buffered() int {
|
||||||
|
return c.r.Buffered()
|
||||||
|
}
|
11
common/net/io.go
Normal file
11
common/net/io.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type ReadOnlyReader struct {
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
type WriteOnlyWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
24
common/net/relay.go
Normal file
24
common/net/relay.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relay copies between left and right bidirectionally.
|
||||||
|
func Relay(leftConn, rightConn net.Conn) {
|
||||||
|
ch := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
||||||
|
// See also https://github.com/Dreamacro/clash/pull/1209
|
||||||
|
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
|
||||||
|
leftConn.SetReadDeadline(time.Now())
|
||||||
|
ch <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
|
||||||
|
rightConn.SetReadDeadline(time.Now())
|
||||||
|
<-ch
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
package observable
|
package observable
|
||||||
|
|
||||||
type Iterable <-chan interface{}
|
type Iterable <-chan any
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
package observable
|
package observable
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func iterator(item []interface{}) chan interface{} {
|
func iterator(item []any) chan any {
|
||||||
ch := make(chan interface{})
|
ch := make(chan any)
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
for _, elm := range item {
|
for _, elm := range item {
|
||||||
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable(t *testing.T) {
|
func TestObservable(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -33,29 +33,29 @@ func TestObservable(t *testing.T) {
|
|||||||
assert.Equal(t, count, 5)
|
assert.Equal(t, count, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_MutilSubscribe(t *testing.T) {
|
func TestObservable_MultiSubscribe(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
ch1, _ := src.Subscribe()
|
ch1, _ := src.Subscribe()
|
||||||
ch2, _ := src.Subscribe()
|
ch2, _ := src.Subscribe()
|
||||||
count := 0
|
count := atomic.NewInt32(0)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
waitCh := func(ch <-chan interface{}) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
count++
|
count.Inc()
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}
|
}
|
||||||
go waitCh(ch1)
|
go waitCh(ch1)
|
||||||
go waitCh(ch2)
|
go waitCh(ch2)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Equal(t, count, 10)
|
assert.Equal(t, int32(10), count.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribe(t *testing.T) {
|
func TestObservable_UnSubscribe(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, _ := src.Subscribe()
|
data, _ := src.Subscribe()
|
||||||
<-data
|
<-data
|
||||||
@ -75,17 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
||||||
sub := Subscription(make(chan interface{}))
|
sub := Subscription(make(chan any))
|
||||||
iter := iterator([]interface{}{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
src.UnSubscribe(sub)
|
src.UnSubscribe(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||||
// waiting for other goroutine recycle
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
time.Sleep(120 * time.Millisecond)
|
|
||||||
init := runtime.NumGoroutine()
|
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
max := 100
|
max := 100
|
||||||
|
|
||||||
@ -97,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(max)
|
wg.Add(max)
|
||||||
waitCh := func(ch <-chan interface{}) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@ -107,6 +104,43 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
go waitCh(ch)
|
go waitCh(ch)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
now := runtime.NumGoroutine()
|
|
||||||
assert.Equal(t, init, now)
|
for _, sub := range list {
|
||||||
|
_, more := <-sub
|
||||||
|
assert.False(t, more)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, more := <-list[0]
|
||||||
|
assert.False(t, more)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Benchmark_Observable_1000(b *testing.B) {
|
||||||
|
ch := make(chan any)
|
||||||
|
o := NewObservable(ch)
|
||||||
|
num := 1000
|
||||||
|
|
||||||
|
subs := []Subscription{}
|
||||||
|
for i := 0; i < num; i++ {
|
||||||
|
sub, _ := o.Subscribe()
|
||||||
|
subs = append(subs, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(num)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for _, sub := range subs {
|
||||||
|
go func(s Subscription) {
|
||||||
|
for range s {
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ch <- i
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ch)
|
||||||
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
@ -2,34 +2,32 @@ package observable
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/eapache/channels.v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Subscription <-chan interface{}
|
type Subscription <-chan any
|
||||||
|
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
buffer *channels.InfiniteChannel
|
buffer chan any
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Emit(item interface{}) {
|
func (s *Subscriber) Emit(item any) {
|
||||||
s.buffer.In() <- item
|
s.buffer <- item
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Out() Subscription {
|
func (s *Subscriber) Out() Subscription {
|
||||||
return s.buffer.Out()
|
return s.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Close() {
|
func (s *Subscriber) Close() {
|
||||||
s.once.Do(func() {
|
s.once.Do(func() {
|
||||||
s.buffer.Close()
|
close(s.buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSubscriber() *Subscriber {
|
func newSubscriber() *Subscriber {
|
||||||
sub := &Subscriber{
|
sub := &Subscriber{
|
||||||
buffer: channels.NewInfiniteChannel(),
|
buffer: make(chan any, 200),
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ type Picker struct {
|
|||||||
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
errOnce sync.Once
|
errOnce sync.Once
|
||||||
result interface{}
|
result any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
|
|||||||
|
|
||||||
// Wait blocks until all function calls from the Go method have returned,
|
// Wait blocks until all function calls from the Go method have returned,
|
||||||
// then returns the first nil error result (if any) from them.
|
// then returns the first nil error result (if any) from them.
|
||||||
func (p *Picker) Wait() interface{} {
|
func (p *Picker) Wait() any {
|
||||||
p.wg.Wait()
|
p.wg.Wait()
|
||||||
if p.cancel != nil {
|
if p.cancel != nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
|
|||||||
|
|
||||||
// Go calls the given function in a new goroutine.
|
// Go calls the given function in a new goroutine.
|
||||||
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
||||||
func (p *Picker) Go(f func() (interface{}, error)) {
|
func (p *Picker) Go(f func() (any, error)) {
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
|
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
|
||||||
return func() (interface{}, error) {
|
return func() (any, error) {
|
||||||
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
|
@ -8,11 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultAllocator *Allocator
|
var defaultAllocator = NewAllocator()
|
||||||
|
|
||||||
func init() {
|
|
||||||
defaultAllocator = NewAllocator()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||||
type Allocator struct {
|
type Allocator struct {
|
||||||
@ -27,7 +23,7 @@ func NewAllocator() *Allocator {
|
|||||||
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
||||||
for k := range alloc.buffers {
|
for k := range alloc.buffers {
|
||||||
i := k
|
i := k
|
||||||
alloc.buffers[k].New = func() interface{} {
|
alloc.buffers[k].New = func() any {
|
||||||
return make([]byte, 1<<uint32(i))
|
return make([]byte, 1<<uint32(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,30 +32,42 @@ func NewAllocator() *Allocator {
|
|||||||
|
|
||||||
// Get a []byte from pool with most appropriate cap
|
// Get a []byte from pool with most appropriate cap
|
||||||
func (alloc *Allocator) Get(size int) []byte {
|
func (alloc *Allocator) Get(size int) []byte {
|
||||||
if size <= 0 || size > 65536 {
|
switch {
|
||||||
|
case size < 0:
|
||||||
|
panic("alloc.Get: len out of range")
|
||||||
|
case size == 0:
|
||||||
return nil
|
return nil
|
||||||
}
|
case size > 65536:
|
||||||
|
return make([]byte, size)
|
||||||
|
default:
|
||||||
|
bits := msb(size)
|
||||||
|
if size == 1<<bits {
|
||||||
|
return alloc.buffers[bits].Get().([]byte)[:size]
|
||||||
|
}
|
||||||
|
|
||||||
bits := msb(size)
|
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
||||||
if size == 1<<bits {
|
|
||||||
return alloc.buffers[bits].Get().([]byte)[:size]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put returns a []byte to pool for future use,
|
// Put returns a []byte to pool for future use,
|
||||||
// which the cap must be exactly 2^n
|
// which the cap must be exactly 2^n
|
||||||
func (alloc *Allocator) Put(buf []byte) error {
|
func (alloc *Allocator) Put(buf []byte) error {
|
||||||
|
if cap(buf) == 0 || cap(buf) > 65536 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
bits := msb(cap(buf))
|
bits := msb(cap(buf))
|
||||||
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
if cap(buf) != 1<<bits {
|
||||||
return errors.New("allocator Put() incorrect buffer size")
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint
|
||||||
|
//lint:ignore SA6002 ignore temporarily
|
||||||
alloc.buffers[bits].Put(buf)
|
alloc.buffers[bits].Put(buf)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// msb return the pos of most significiant bit
|
// msb return the pos of most significant bit
|
||||||
func msb(size int) uint16 {
|
func msb(size int) uint16 {
|
||||||
return uint16(bits.Len32(uint32(size)) - 1)
|
return uint16(bits.Len32(uint32(size)) - 1)
|
||||||
}
|
}
|
||||||
|
@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
|
|||||||
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
||||||
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
||||||
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
||||||
assert.Nil(t, alloc.Get(65537))
|
assert.Equal(t, 65537, len(alloc.Get(65537)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocPut(t *testing.T) {
|
func TestAllocPut(t *testing.T) {
|
||||||
alloc := NewAllocator()
|
alloc := NewAllocator()
|
||||||
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
|
assert.Nil(t, alloc.Put(nil), "put nil misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 3, 3)), "put elem:3 []bytes misbehavior")
|
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 4, 4)), "put elem:4 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 65536, 65536)), "put elem:65536 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 65537, 65537)), "put elem:65537 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocPutThenGet(t *testing.T) {
|
func TestAllocPutThenGet(t *testing.T) {
|
||||||
|
31
common/pool/buffer.go
Normal file
31
common/pool/buffer.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/protobytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
|
||||||
|
bytesBufferPool = sync.Pool{New: func() any { return &protobytes.BytesWriter{} }}
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBuffer() *bytes.Buffer {
|
||||||
|
return bufferPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBuffer(buf *bytes.Buffer) {
|
||||||
|
buf.Reset()
|
||||||
|
bufferPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBytesBuffer() *protobytes.BytesWriter {
|
||||||
|
return bytesBufferPool.Get().(*protobytes.BytesWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBytesBuffer(buf *protobytes.BytesWriter) {
|
||||||
|
buf.Reset()
|
||||||
|
bytesBufferPool.Put(buf)
|
||||||
|
}
|
@ -5,6 +5,11 @@ const (
|
|||||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
||||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
||||||
RelayBufferSize = 20 * 1024
|
RelayBufferSize = 20 * 1024
|
||||||
|
|
||||||
|
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
|
||||||
|
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
|
||||||
|
// set to 9000, so the UDP Buffer size set to 16Kib
|
||||||
|
UDPBufferSize = 16 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
func Get(size int) []byte {
|
func Get(size int) []byte {
|
||||||
|
@ -6,12 +6,12 @@ import (
|
|||||||
|
|
||||||
// Queue is a simple concurrent safe queue
|
// Queue is a simple concurrent safe queue
|
||||||
type Queue struct {
|
type Queue struct {
|
||||||
items []interface{}
|
items []any
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put add the item to the queue.
|
// Put add the item to the queue.
|
||||||
func (q *Queue) Put(items ...interface{}) {
|
func (q *Queue) Put(items ...any) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pop returns the head of items.
|
// Pop returns the head of items.
|
||||||
func (q *Queue) Pop() interface{} {
|
func (q *Queue) Pop() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ func (q *Queue) Pop() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last returns the last of item.
|
// Last returns the last of item.
|
||||||
func (q *Queue) Last() interface{} {
|
func (q *Queue) Last() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -47,8 +47,8 @@ func (q *Queue) Last() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy get the copy of queue.
|
// Copy get the copy of queue.
|
||||||
func (q *Queue) Copy() []interface{} {
|
func (q *Queue) Copy() []any {
|
||||||
items := []interface{}{}
|
items := []any{}
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
items = append(items, q.items...)
|
items = append(items, q.items...)
|
||||||
q.lock.RUnlock()
|
q.lock.RUnlock()
|
||||||
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
|
|||||||
// New is a constructor for a new concurrent safe queue.
|
// New is a constructor for a new concurrent safe queue.
|
||||||
func New(hint int64) *Queue {
|
func New(hint int64) *Queue {
|
||||||
return &Queue{
|
return &Queue{
|
||||||
items: make([]interface{}, 0, hint),
|
items: make([]any, 0, hint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type call struct {
|
type call struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
val interface{}
|
val any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,11 +20,12 @@ type Single struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Val interface{}
|
Val any
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
|
// Do single.Do likes sync.singleFlight
|
||||||
|
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.Before(s.last.Add(s.wait)) {
|
if now.Before(s.last.Add(s.wait)) {
|
||||||
@ -44,9 +45,12 @@ func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, s
|
|||||||
s.mux.Unlock()
|
s.mux.Unlock()
|
||||||
call.val, call.err = fn()
|
call.val, call.err = fn()
|
||||||
call.wg.Done()
|
call.wg.Done()
|
||||||
|
|
||||||
|
s.mux.Lock()
|
||||||
s.call = nil
|
s.call = nil
|
||||||
s.result = &Result{call.val, call.err}
|
s.result = &Result{call.val, call.err}
|
||||||
s.last = now
|
s.last = now
|
||||||
|
s.mux.Unlock()
|
||||||
return call.val, call.err, false
|
return call.val, call.err, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,13 +6,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
shardCount := 0
|
shardCount := atomic.NewInt32(0)
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -25,7 +26,7 @@ func TestBasic(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
_, _, shard := single.Do(call)
|
_, _, shard := single.Do(call)
|
||||||
if shard {
|
if shard {
|
||||||
shardCount++
|
shardCount.Inc()
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@ -33,13 +34,13 @@ func TestBasic(t *testing.T) {
|
|||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
assert.Equal(t, 1, foo)
|
assert.Equal(t, 1, foo)
|
||||||
assert.Equal(t, 4, shardCount)
|
assert.Equal(t, int32(4), shardCount.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimer(t *testing.T) {
|
func TestTimer(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -55,7 +56,7 @@ func TestTimer(t *testing.T) {
|
|||||||
func TestReset(t *testing.T) {
|
func TestReset(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// +build !linux
|
//go:build !linux
|
||||||
|
|
||||||
package sockopt
|
package sockopt
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
|
|||||||
return &Decoder{option: &option}
|
return &Decoder{option: &option}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode transform a map[string]interface{} to a struct
|
// Decode transform a map[string]any to a struct
|
||||||
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("Decode must recive a ptr struct")
|
return fmt.Errorf("Decode must recive a ptr struct")
|
||||||
}
|
}
|
||||||
@ -37,14 +37,16 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
|||||||
v := reflect.ValueOf(dst).Elem()
|
v := reflect.ValueOf(dst).Elem()
|
||||||
for idx := 0; idx < v.NumField(); idx++ {
|
for idx := 0; idx < v.NumField(); idx++ {
|
||||||
field := t.Field(idx)
|
field := t.Field(idx)
|
||||||
|
if field.Anonymous {
|
||||||
|
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
tag := field.Tag.Get(d.option.TagName)
|
tag := field.Tag.Get(d.option.TagName)
|
||||||
str := strings.SplitN(tag, ",", 2)
|
key, omitKey, found := strings.Cut(tag, ",")
|
||||||
key := str[0]
|
omitempty := found && omitKey == "omitempty"
|
||||||
omitempty := false
|
|
||||||
if len(str) > 1 {
|
|
||||||
omitempty = str[1] == "omitempty"
|
|
||||||
}
|
|
||||||
|
|
||||||
value, ok := src[key]
|
value, ok := src[key]
|
||||||
if !ok || value == nil {
|
if !ok || value == nil {
|
||||||
@ -62,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decode(name string, data any, val reflect.Value) error {
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
return d.decodeInt(name, data, val)
|
return d.decodeInt(name, data, val)
|
||||||
@ -83,12 +85,14 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
case kind == reflect.Int:
|
case kind == reflect.Int:
|
||||||
val.SetInt(dataVal.Int())
|
val.SetInt(dataVal.Int())
|
||||||
|
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
|
||||||
|
val.SetInt(int64(dataVal.Float()))
|
||||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||||
var i int64
|
var i int64
|
||||||
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
|
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
|
||||||
@ -106,7 +110,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
@ -123,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
@ -140,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
|
||||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||||
valType := val.Type()
|
valType := val.Type()
|
||||||
valElemType := valType.Elem()
|
valElemType := valType.Elem()
|
||||||
@ -155,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
|||||||
for valSlice.Len() <= i {
|
for valSlice.Len() <= i {
|
||||||
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
||||||
}
|
}
|
||||||
currentField := valSlice.Index(i)
|
|
||||||
|
|
||||||
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
||||||
|
if currentData == nil {
|
||||||
|
// in weakly type mode, null will convert to zero value
|
||||||
|
if d.option.WeaklyTypedInput {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
|
||||||
|
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("'%s' can not be null", fieldName)
|
||||||
|
}
|
||||||
|
currentField := valSlice.Index(i)
|
||||||
if err := d.decode(fieldName, currentData, currentField); err != nil {
|
if err := d.decode(fieldName, currentData, currentField); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -167,7 +181,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error {
|
||||||
valType := val.Type()
|
valType := val.Type()
|
||||||
valKeyType := valType.Key()
|
valKeyType := valType.Key()
|
||||||
valElemType := valType.Elem()
|
valElemType := valType.Elem()
|
||||||
@ -239,7 +253,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error {
|
||||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||||
|
|
||||||
// If the type of the value to write to and the data match directly,
|
// If the type of the value to write to and the data match directly,
|
||||||
@ -267,7 +281,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
dataValKeys := make(map[reflect.Value]struct{})
|
dataValKeys := make(map[reflect.Value]struct{})
|
||||||
dataValKeysUnused := make(map[interface{}]struct{})
|
dataValKeysUnused := make(map[any]struct{})
|
||||||
for _, dataValKey := range dataVal.MapKeys() {
|
for _, dataValKey := range dataVal.MapKeys() {
|
||||||
dataValKeys[dataValKey] = struct{}{}
|
dataValKeys[dataValKey] = struct{}{}
|
||||||
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
||||||
@ -392,7 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
val.Set(dataVal)
|
val.Set(dataVal)
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package structure
|
package structure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var decoder = NewDecoder(Option{TagName: "test"})
|
var (
|
||||||
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
decoder = NewDecoder(Option{TagName: "test"})
|
||||||
|
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
||||||
|
)
|
||||||
|
|
||||||
type Baz struct {
|
type Baz struct {
|
||||||
Foo int `test:"foo"`
|
Foo int `test:"foo"`
|
||||||
@ -24,7 +27,7 @@ type BazOptional struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Basic(t *testing.T) {
|
func TestStructure_Basic(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": "test",
|
"bar": "test",
|
||||||
"extra": false,
|
"extra": false,
|
||||||
@ -37,16 +40,12 @@ func TestStructure_Basic(t *testing.T) {
|
|||||||
|
|
||||||
s := &Baz{}
|
s := &Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Slice(t *testing.T) {
|
func TestStructure_Slice(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": []string{"one", "two"},
|
"bar": []string{"one", "two"},
|
||||||
}
|
}
|
||||||
@ -58,16 +57,12 @@ func TestStructure_Slice(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Optional(t *testing.T) {
|
func TestStructure_Optional(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,50 +72,40 @@ func TestStructure_Optional(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazOptional{}
|
s := &BazOptional{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_MissingKey(t *testing.T) {
|
func TestStructure_MissingKey(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Baz{}
|
s := &Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_ParamError(t *testing.T) {
|
func TestStructure_ParamError(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{}
|
rawMap := map[string]any{}
|
||||||
s := Baz{}
|
s := Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_SliceTypeError(t *testing.T) {
|
func TestStructure_SliceTypeError(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": []int{1, 2},
|
"bar": []int{1, 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_WeakType(t *testing.T) {
|
func TestStructure_WeakType(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": "1",
|
"foo": "1",
|
||||||
"bar": []int{1},
|
"bar": []int{1},
|
||||||
}
|
}
|
||||||
@ -132,10 +117,65 @@ func TestStructure_WeakType(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := weakTypeDecoder.Decode(rawMap, s)
|
err := weakTypeDecoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
func TestStructure_Nest(t *testing.T) {
|
||||||
}
|
rawMap := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
goal := BazOptional{
|
||||||
|
Foo: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
BazOptional
|
||||||
|
}{}
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, s.BazOptional, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructure_SliceNilValue(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
"bar": []any{"bar", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
goal := &BazSlice{
|
||||||
|
Foo: 1,
|
||||||
|
Bar: []string{"bar", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &BazSlice{}
|
||||||
|
err := weakTypeDecoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, goal.Bar, s.Bar)
|
||||||
|
|
||||||
|
s = &BazSlice{}
|
||||||
|
err = decoder.Decode(rawMap, s)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructure_SliceNilValueComplex(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"bar": []any{map[string]any{"bar": "foo"}, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
Bar []map[string]any `test:"bar"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Nil(t, s.Bar[1])
|
||||||
|
|
||||||
|
ss := &struct {
|
||||||
|
Bar []Baz `test:"bar"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = decoder.Decode(rawMap, ss)
|
||||||
|
assert.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
|
|||||||
au.storage.Store(user.User, user.Pass)
|
au.storage.Store(user.User, user.Pass)
|
||||||
}
|
}
|
||||||
usernames := make([]string, 0, len(users))
|
usernames := make([]string, 0, len(users))
|
||||||
au.storage.Range(func(key, value interface{}) bool {
|
au.storage.Range(func(key, value any) bool {
|
||||||
usernames = append(usernames, key.(string))
|
usernames = append(usernames, key.(string))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
28
component/dhcp/conn.go
Normal file
28
component/dhcp/conn.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) {
|
||||||
|
listenAddr := "0.0.0.0:68"
|
||||||
|
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||||
|
listenAddr = "255.255.255.255:68"
|
||||||
|
}
|
||||||
|
|
||||||
|
options := []dialer.Option{
|
||||||
|
dialer.WithInterface(ifaceName),
|
||||||
|
dialer.WithAddrReuse(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback bind on windows, because syscall bind can not receive broadcast
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
options = append(options, dialer.WithFallbackBind(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialer.ListenPacket(ctx, "udp4", listenAddr, options...)
|
||||||
|
}
|
88
component/dhcp/dhcp.go
Normal file
88
component/dhcp/dhcp.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package dhcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
|
|
||||||
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotResponding = errors.New("DHCP not responding")
|
||||||
|
ErrNotFound = errors.New("DNS option not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
|
||||||
|
conn, err := ListenDHCPClient(context, ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
result := make(chan []net.IP, 1)
|
||||||
|
|
||||||
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go receiveOffer(conn, discovery.TransactionID, result)
|
||||||
|
|
||||||
|
_, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case r, ok := <-result:
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
case <-context.Done():
|
||||||
|
return nil, ErrNotResponding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
|
||||||
|
defer close(result)
|
||||||
|
|
||||||
|
buf := make([]byte, dhcpv4.MaxMessageSize)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, _, err := conn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt, err := dhcpv4.FromBytes(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkt.MessageType() != dhcpv4.MessageTypeOffer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkt.TransactionID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dns := pkt.DNS()
|
||||||
|
if len(dns) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result <- dns
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
66
component/dialer/bind_darwin.go
Normal file
66
component/dialer/bind_darwin.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||||
|
|
||||||
|
func bindControl(ifaceIdx int, chain controlFn) controlFn {
|
||||||
|
return func(network, address string, c syscall.RawConn) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil && chain != nil {
|
||||||
|
err = chain(network, address, c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
|
if err == nil {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerErr error
|
||||||
|
err = c.Control(func(fd uintptr) {
|
||||||
|
switch network {
|
||||||
|
case "tcp4", "udp4":
|
||||||
|
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
|
||||||
|
case "tcp6", "udp6":
|
||||||
|
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
err = innerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||||
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||||
|
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.Control = bindControl(ifaceObj.Index, lc.Control)
|
||||||
|
return address, nil
|
||||||
|
}
|
51
component/dialer/bind_linux.go
Normal file
51
component/dialer/bind_linux.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||||
|
|
||||||
|
func bindControl(ifaceName string, chain controlFn) controlFn {
|
||||||
|
return func(network, address string, c syscall.RawConn) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil && chain != nil {
|
||||||
|
err = chain(network, address, c)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ipStr, _, err := net.SplitHostPort(address)
|
||||||
|
if err == nil {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip != nil && !ip.IsGlobalUnicast() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerErr error
|
||||||
|
err = c.Control(func(fd uintptr) {
|
||||||
|
innerErr = unix.BindToDevice(int(fd), ifaceName)
|
||||||
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
err = innerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||||
|
dialer.Control = bindControl(ifaceName, dialer.Control)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||||
|
lc.Control = bindControl(ifaceName, lc.Control)
|
||||||
|
|
||||||
|
return address, nil
|
||||||
|
}
|
47
component/dialer/bind_others.go
Normal file
47
component/dialer/bind_others.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//go:build !linux && !darwin && !windows
|
||||||
|
|
||||||
|
package dialer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
|
||||||
|
if !destination.IsGlobalUnicast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
local := uint64(0)
|
||||||
|
if dialer.LocalAddr != nil {
|
||||||
|
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
|
||||||
|
if err == nil {
|
||||||
|
local, _ = strconv.ParseUint(port, 10, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer.LocalAddr = addr
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
|
||||||
|
_, port, err := net.SplitHostPort(address)
|
||||||
|
if err != nil {
|
||||||
|
port = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
local, _ := strconv.ParseUint(port, 10, 16)
|
||||||
|
|
||||||
|
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr.String(), nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user