Compare commits
361 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 |
100
.github/ISSUE_TEMPLATE/bug_report.md
vendored
100
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,100 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
感谢你向 Clash Core 提交 issue!
|
||||
在提交之前,请确认:
|
||||
|
||||
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
|
||||
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
||||
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
|
||||
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
||||
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
|
||||
|
||||
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
|
||||
|
||||
Thanks for opening an issue towards the Clash core!
|
||||
But before so, please do the following checklist:
|
||||
|
||||
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
||||
- [ ] I have searched on the [issue tracker](……/) for a related issue.
|
||||
- [ ] I have tested using the dev branch, and the issue still exists.
|
||||
- [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
|
||||
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
|
||||
|
||||
Please understand that we close issues that fail to follow this issue template.
|
||||
-->
|
||||
|
||||
------------------------------------------------------------------
|
||||
|
||||
<!--
|
||||
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
|
||||
Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
|
||||
-->
|
||||
|
||||
### Clash config
|
||||
<!--
|
||||
在下方附上 Clash core 脱敏后配置文件的内容
|
||||
Paste the Clash core configuration below.
|
||||
-->
|
||||
<details>
|
||||
<summary>config.yaml</summary>
|
||||
|
||||
```yaml
|
||||
……
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Clash log
|
||||
<!--
|
||||
在下方附上 Clash Core 的日志,log level 使用 DEBUG
|
||||
Paste the Clash core log below with the log level set to `DEBUG`.
|
||||
-->
|
||||
```
|
||||
……
|
||||
```
|
||||
|
||||
### 环境 Environment
|
||||
|
||||
* 操作系统 (the OS that the Clash core is running on)
|
||||
……
|
||||
* 网路环境或拓扑 (network conditions/topology)
|
||||
……
|
||||
* iptables,如果适用 (if applicable)
|
||||
……
|
||||
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
|
||||
……
|
||||
* 其他 (any other information that would be useful)
|
||||
……
|
||||
|
||||
### 说明 Description
|
||||
|
||||
<!--
|
||||
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
|
||||
-->
|
||||
|
||||
### 重现问题的具体布骤 Steps to Reproduce
|
||||
|
||||
1. [First Step]
|
||||
2. [Second Step]
|
||||
3. ……
|
||||
|
||||
**我预期会发生……?**
|
||||
<!-- **Expected behavior:** [What you expected to happen] -->
|
||||
|
||||
**实际上发生了什么?**
|
||||
<!-- **Actual behavior:** [What actually happened] -->
|
||||
|
||||
### 可能的解决方案 Possible Solution
|
||||
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
|
||||
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!-- or ideas how to implement the addition or change -->
|
||||
|
||||
### 更多信息 More Information
|
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
|
26
.github/workflows/docker.yml
vendored
26
.github/workflows/docker.yml
vendored
@ -13,29 +13,29 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up docker buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Github Package
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: Dreamacro
|
||||
@ -43,20 +43,22 @@ jobs:
|
||||
|
||||
- name: Build dev branch and push
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
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@v3
|
||||
uses: actions/github-script@v6
|
||||
id: tags
|
||||
with:
|
||||
script: |
|
||||
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
|
||||
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
|
||||
const tags = [
|
||||
'dreamacro/clash:latest',
|
||||
`dreamacro/clash:${ref}`,
|
||||
@ -68,9 +70,11 @@ jobs:
|
||||
|
||||
- name: Build release and push
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
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,47 +1,42 @@
|
||||
name: Go
|
||||
on: [push, pull_request]
|
||||
name: Release
|
||||
on: [push]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
check-latest: true
|
||||
go-version: '1.20'
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Get dependencies, run test and static check
|
||||
- name: Get dependencies, run test
|
||||
run: |
|
||||
go test ./...
|
||||
go vet ./...
|
||||
go get -u honnef.co/go/tools/cmd/staticcheck
|
||||
staticcheck -- $(go list ./...)
|
||||
|
||||
- name: Build
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
NAME: clash
|
||||
BINDIR: bin
|
||||
run: make -j releases
|
||||
run: make -j $(go run ./test/main.go) releases
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: bin/*
|
||||
draft: true
|
||||
prerelease: true
|
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@ -11,9 +11,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
||||
days-before-stale: 60
|
||||
days-before-close: 5
|
||||
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -12,7 +12,7 @@ bin/*
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# dep
|
||||
# go mod vendor
|
||||
vendor
|
||||
|
||||
# GoLand
|
||||
@ -20,3 +20,17 @@ vendor
|
||||
|
||||
# macOS file
|
||||
.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,18 +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
|
||||
WORKDIR /clash-src
|
||||
WORKDIR /workdir
|
||||
COPY --from=tonistiigi/xx:golang / /
|
||||
COPY . /clash-src
|
||||
RUN go mod download && \
|
||||
make docker && \
|
||||
mv ./bin/clash-docker /clash
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
|
||||
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
|
||||
LABEL org.opencontainers.image.source https://github.com/Dreamacro/clash
|
||||
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 /clash /
|
||||
ENTRYPOINT ["/clash"]
|
||||
|
60
Makefile
60
Makefile
@ -8,40 +8,55 @@ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clas
|
||||
|
||||
PLATFORM_LIST = \
|
||||
darwin-amd64 \
|
||||
darwin-amd64-v3 \
|
||||
darwin-arm64 \
|
||||
linux-386 \
|
||||
linux-amd64 \
|
||||
linux-amd64-v3 \
|
||||
linux-armv5 \
|
||||
linux-armv6 \
|
||||
linux-armv7 \
|
||||
linux-armv8 \
|
||||
linux-arm64 \
|
||||
linux-mips-softfloat \
|
||||
linux-mips-hardfloat \
|
||||
linux-mipsle-softfloat \
|
||||
linux-mipsle-hardfloat \
|
||||
linux-mips64 \
|
||||
linux-mips64le \
|
||||
linux-riscv64 \
|
||||
linux-loong64 \
|
||||
freebsd-386 \
|
||||
freebsd-amd64
|
||||
freebsd-amd64 \
|
||||
freebsd-amd64-v3 \
|
||||
freebsd-arm64
|
||||
|
||||
WINDOWS_ARCH_LIST = \
|
||||
windows-386 \
|
||||
windows-amd64 \
|
||||
windows-arm32v7
|
||||
windows-amd64-v3 \
|
||||
windows-arm64 \
|
||||
windows-armv7
|
||||
|
||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||
|
||||
docker:
|
||||
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64:
|
||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-v3:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-arm64:
|
||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-386:
|
||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64:
|
||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-v3:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv5:
|
||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
@ -51,7 +66,7 @@ linux-armv6:
|
||||
linux-armv7:
|
||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-armv8:
|
||||
linux-arm64:
|
||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-mips-softfloat:
|
||||
@ -72,19 +87,37 @@ linux-mips64:
|
||||
linux-mips64le:
|
||||
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:
|
||||
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
freebsd-amd64:
|
||||
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:
|
||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64:
|
||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-arm32v7:
|
||||
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))
|
||||
@ -100,5 +133,16 @@ $(zip_releases): %.zip : %
|
||||
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
||||
|
||||
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:
|
||||
rm $(BINDIR)/*
|
||||
|
44
README.md
44
README.md
@ -7,45 +7,47 @@
|
||||
|
||||
<p align="center">
|
||||
<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 href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
||||
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
||||
</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">
|
||||
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
||||
</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>
|
||||
|
||||
## Features
|
||||
|
||||
- Local HTTP/HTTPS/SOCKS server with authentication support
|
||||
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
|
||||
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
|
||||
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
|
||||
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
|
||||
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
|
||||
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||
- Comprehensive HTTP RESTful API controller
|
||||
This is a general overview of the features that comes with Clash.
|
||||
|
||||
## Getting Started
|
||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||
- 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
|
||||
|
||||
*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).*
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/).
|
||||
|
||||
## Credits
|
||||
|
||||
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
||||
|
||||
## License
|
||||
|
||||
This software is released under the GPL-3.0 license.
|
||||
|
||||
[](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,122 +1,61 @@
|
||||
package outbound
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
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 struct {
|
||||
net.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 net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||
return &packetConn{pc, []string{a.Name()}}
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue
|
||||
alive *atomic.Bool
|
||||
}
|
||||
|
||||
// Alive implements C.Proxy
|
||||
func (p *Proxy) Alive() bool {
|
||||
return p.alive.Load()
|
||||
}
|
||||
|
||||
// Dial implements C.Proxy
|
||||
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()
|
||||
return p.DialContext(ctx, metadata)
|
||||
}
|
||||
|
||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||
if err != nil {
|
||||
p.alive.Store(false)
|
||||
}
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
||||
p.alive.Store(err == nil)
|
||||
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 {
|
||||
queue := p.history.Copy()
|
||||
histories := []C.DelayHistory{}
|
||||
@ -127,6 +66,7 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var max uint16 = 0xffff
|
||||
if !p.alive.Load() {
|
||||
@ -144,26 +84,31 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
||||
return history.Delay
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||
if err != nil {
|
||||
return inner, err
|
||||
}
|
||||
|
||||
mapping := map[string]interface{}{}
|
||||
mapping := map[string]any{}
|
||||
json.Unmarshal(inner, &mapping)
|
||||
mapping["history"] = p.DelayHistory()
|
||||
mapping["alive"] = p.Alive()
|
||||
mapping["name"] = p.Name()
|
||||
mapping["udp"] = p.SupportUDP()
|
||||
return json.Marshal(mapping)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
p.alive.Store(err == nil)
|
||||
record := C.DelayHistory{Time: time.Now()}
|
||||
if err == nil {
|
||||
record.Delay = t
|
||||
record.Delay = delay
|
||||
record.MeanDelay = meanDelay
|
||||
}
|
||||
p.history.Put(record)
|
||||
if p.history.Len() > 10 {
|
||||
@ -206,15 +151,53 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
|
||||
@ -17,7 +20,7 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
|
||||
}
|
||||
|
||||
// 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.NetWork = C.UDP
|
||||
metadata.Type = source
|
||||
@ -25,7 +28,11 @@ func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAda
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
|
||||
if originTarget != nil {
|
||||
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
|
||||
metadata.OriginDst = addrPort
|
||||
}
|
||||
}
|
||||
return &PacketAdapter{
|
||||
UDPPacket: packet,
|
||||
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)
|
||||
}
|
@ -6,14 +6,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||
metadata := &C.Metadata{
|
||||
AddrType: int(target[0]),
|
||||
}
|
||||
metadata := &C.Metadata{}
|
||||
|
||||
switch target[0] {
|
||||
case socks5.AtypDomainName:
|
||||
@ -45,20 +43,12 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
||||
|
||||
metadata := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
DstIP: nil,
|
||||
DstPort: port,
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
switch {
|
||||
case ip.To4() == nil:
|
||||
metadata.AddrType = C.AtypIPv6
|
||||
default:
|
||||
metadata.AddrType = C.AtypIPv4
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
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()}}
|
||||
}
|
@ -12,10 +12,9 @@ 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)
|
||||
// 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
|
||||
}
|
||||
@ -23,8 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
||||
return NewConn(c, d), nil
|
||||
}
|
||||
|
||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
// 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
|
||||
}
|
@ -22,9 +22,11 @@ type Http struct {
|
||||
user string
|
||||
pass string
|
||||
tlsConfig *tls.Config
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
type HttpOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
@ -33,12 +35,16 @@ type HttpOption struct {
|
||||
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) {
|
||||
if h.tlsConfig != nil {
|
||||
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
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
@ -51,13 +57,18 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", h.addr)
|
||||
// DialContext implements C.ProxyAdapter
|
||||
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 {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = h.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -74,11 +85,11 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
Host: addr,
|
||||
},
|
||||
Host: addr,
|
||||
Header: http.Header{
|
||||
"Proxy-Connection": []string{"Keep-Alive"},
|
||||
},
|
||||
Header: h.Headers.Clone(),
|
||||
}
|
||||
|
||||
req.Header.Add("Proxy-Connection", "Keep-Alive")
|
||||
|
||||
if h.user != "" && h.pass != "" {
|
||||
auth := h.user + ":" + h.pass
|
||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||
@ -121,19 +132,26 @@ func NewHttp(option HttpOption) *Http {
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: sni,
|
||||
}
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
for name, value := range option.Headers {
|
||||
headers.Add(name, value)
|
||||
}
|
||||
|
||||
return &Http{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Http,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
},
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@ -10,12 +9,11 @@ import (
|
||||
|
||||
"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/socks5"
|
||||
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||
"github.com/Dreamacro/clash/transport/shadowsocks/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 {
|
||||
@ -29,6 +27,7 @@ type ShadowSocks struct {
|
||||
}
|
||||
|
||||
type ShadowSocksOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
@ -36,11 +35,11 @@ type ShadowSocksOption struct {
|
||||
Cipher string `proxy:"cipher"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Plugin string `proxy:"plugin,omitempty"`
|
||||
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
||||
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
|
||||
}
|
||||
|
||||
type simpleObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Mode string `obfs:"mode,omitempty"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
}
|
||||
|
||||
@ -54,6 +53,7 @@ type v2rayObfsOption struct {
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
switch ss.obfsMode {
|
||||
case "tls":
|
||||
@ -73,25 +73,32 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
// DialContext implements C.ProxyAdapter
|
||||
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 {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = ss.StreamConn(c, metadata)
|
||||
return NewConn(c, ss), err
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||
if err != nil {
|
||||
pc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -99,12 +106,6 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
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) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
cipher := option.Cipher
|
||||
@ -150,7 +151,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
if opts.TLS {
|
||||
v2rayOption.TLS = true
|
||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||
v2rayOption.SessionCache = getClientSessionCache()
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,6 +160,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
addr: addr,
|
||||
tp: C.Shadowsocks,
|
||||
udp: option.UDP,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
},
|
||||
cipher: ciph,
|
||||
|
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
|
||||
}
|
@ -8,9 +8,9 @@ import (
|
||||
|
||||
"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"
|
||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||
"github.com/Dreamacro/clash/transport/snell"
|
||||
)
|
||||
|
||||
type Snell struct {
|
||||
@ -22,12 +22,14 @@ type Snell struct {
|
||||
}
|
||||
|
||||
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]interface{} `proxy:"obfs-opts,omitempty"`
|
||||
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
|
||||
}
|
||||
|
||||
type streamOption struct {
|
||||
@ -48,35 +50,62 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
||||
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.Atoi(metadata.DstPort)
|
||||
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
if s.version == snell.Version2 {
|
||||
// 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.Atoi(metadata.DstPort)
|
||||
err = snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||
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)
|
||||
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)
|
||||
@ -87,7 +116,10 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
||||
}
|
||||
|
||||
if obfsOption.Mode != "tls" && obfsOption.Mode != "http" {
|
||||
switch obfsOption.Mode {
|
||||
case "tls", "http", "":
|
||||
break
|
||||
default:
|
||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
||||
}
|
||||
|
||||
@ -95,7 +127,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
if option.Version == 0 {
|
||||
option.Version = snell.DefaultSnellVersion
|
||||
}
|
||||
if option.Version != snell.Version1 && option.Version != snell.Version2 {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -104,6 +142,9 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Snell,
|
||||
udp: option.UDP,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
},
|
||||
psk: psk,
|
||||
obfsOption: obfsOption,
|
||||
@ -112,7 +153,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
|
||||
if option.Version == snell.Version2 {
|
||||
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
@ -6,13 +6,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/socks5"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
type Socks5 struct {
|
||||
@ -25,6 +24,7 @@ type Socks5 struct {
|
||||
}
|
||||
|
||||
type Socks5Option struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
@ -35,10 +35,13 @@ type Socks5Option struct {
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
if ss.tls {
|
||||
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
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
// DialContext implements C.ProxyAdapter
|
||||
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 {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = ss.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -73,10 +81,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn
|
||||
return NewConn(c, ss), nil
|
||||
}
|
||||
|
||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
|
||||
defer cancel()
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
return
|
||||
@ -84,15 +91,15 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
|
||||
if ss.tls {
|
||||
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
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
}
|
||||
}()
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
tcpKeepAlive(c)
|
||||
var user *socks5.User
|
||||
@ -109,20 +116,34 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, c)
|
||||
io.Copy(io.Discard, c)
|
||||
c.Close()
|
||||
// A UDP association terminates when the TCP connection that the UDP
|
||||
// ASSOCIATE request arrived on terminates. RFC1928
|
||||
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 {
|
||||
@ -130,7 +151,6 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
||||
if option.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: option.Server,
|
||||
}
|
||||
}
|
||||
@ -141,6 +161,8 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Socks5,
|
||||
udp: option.UDP,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
},
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
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
|
||||
}
|
@ -3,22 +3,27 @@ package outboundgroup
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
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 {
|
||||
proxies = append(proxies, provider.ProxiesWithTouch()...)
|
||||
} else {
|
||||
proxies = append(proxies, provider.Proxies()...)
|
||||
provider.Touch()
|
||||
}
|
||||
proxies = append(proxies, provider.Proxies()...)
|
||||
}
|
||||
return proxies
|
||||
}
|
@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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 {
|
||||
@ -22,24 +23,27 @@ func (f *Fallback) Now() string {
|
||||
return proxy.Name()
|
||||
}
|
||||
|
||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
// 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)
|
||||
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||
if err == nil {
|
||||
c.AppendToChains(f)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
// 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.DialUDP(metadata)
|
||||
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
|
||||
@ -49,25 +53,27 @@ func (f *Fallback) SupportUDP() bool {
|
||||
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]interface{}{
|
||||
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() (interface{}, error) {
|
||||
elm, _, _ := f.single.Do(func() (any, error) {
|
||||
return getProvidersProxies(f.providers, touch), nil
|
||||
})
|
||||
|
||||
@ -85,11 +91,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||
return proxies[0]
|
||||
}
|
||||
|
||||
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||
return &Fallback{
|
||||
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
|
||||
Base: outbound.NewBase(outbound.BaseOption{
|
||||
Name: option.Name,
|
||||
Type: C.Fallback,
|
||||
Interface: option.Interface,
|
||||
RoutingMark: option.RoutingMark,
|
||||
}),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
providers: providers,
|
||||
disableUDP: options.DisableUDP,
|
||||
disableUDP: option.DisableUDP,
|
||||
}
|
||||
}
|
@ -7,11 +7,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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"
|
||||
)
|
||||
@ -28,12 +29,10 @@ type LoadBalance struct {
|
||||
|
||||
var errStrategy = errors.New("unsupported strategy")
|
||||
|
||||
func parseStrategy(config map[string]interface{}) string {
|
||||
if elm, ok := config["strategy"]; ok {
|
||||
if strategy, ok := elm.(string); ok {
|
||||
func parseStrategy(config map[string]any) string {
|
||||
if strategy, ok := config["strategy"].(string); ok {
|
||||
return strategy
|
||||
}
|
||||
}
|
||||
return "consistent-hashing"
|
||||
}
|
||||
|
||||
@ -68,7 +67,8 @@ func jumpHash(key uint64, buckets int32) int32 {
|
||||
return int32(b)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
// 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)
|
||||
@ -77,11 +77,12 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
|
||||
|
||||
proxy := lb.Unwrap(metadata)
|
||||
|
||||
c, err = proxy.DialContext(ctx, metadata)
|
||||
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||
return
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||
// 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)
|
||||
@ -89,10 +90,10 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
|
||||
}()
|
||||
|
||||
proxy := lb.Unwrap(metadata)
|
||||
|
||||
return proxy.DialUDP(metadata)
|
||||
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) SupportUDP() bool {
|
||||
return !lb.disableUDP
|
||||
}
|
||||
@ -126,35 +127,44 @@ func strategyConsistentHashing() strategyFn {
|
||||
}
|
||||
}
|
||||
|
||||
// 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() (interface{}, error) {
|
||||
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]interface{}{
|
||||
return json.Marshal(map[string]any{
|
||||
"type": lb.Type().String(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||
func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||
var strategyFn strategyFn
|
||||
switch strategy {
|
||||
case "consistent-hashing":
|
||||
@ -165,10 +175,15 @@ func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvid
|
||||
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||
}
|
||||
return &LoadBalance{
|
||||
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
|
||||
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: options.DisableUDP,
|
||||
disableUDP: option.DisableUDP,
|
||||
}, nil
|
||||
}
|
@ -4,9 +4,13 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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 (
|
||||
@ -14,10 +18,11 @@ var (
|
||||
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")
|
||||
errDuplicateProvider = errors.New("duplicate provider name")
|
||||
)
|
||||
|
||||
type GroupCommonOption struct {
|
||||
outbound.BasicOption
|
||||
Name string `group:"name"`
|
||||
Type string `group:"type"`
|
||||
Proxies []string `group:"proxies,omitempty"`
|
||||
@ -26,9 +31,10 @@ type GroupCommonOption struct {
|
||||
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]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
|
||||
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{
|
||||
@ -42,32 +48,33 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
||||
return nil, errFormat
|
||||
}
|
||||
|
||||
groupName := groupOption.Name
|
||||
var (
|
||||
groupName = groupOption.Name
|
||||
filterReg *regexp.Regexp
|
||||
)
|
||||
|
||||
providers := []provider.ProxyProvider{}
|
||||
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, errMissProxy
|
||||
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, err
|
||||
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||
}
|
||||
|
||||
// if Use not empty, drop health check options
|
||||
if len(groupOption.Use) != 0 {
|
||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers = append(providers, pd)
|
||||
} else {
|
||||
if _, ok := providersMap[groupName]; ok {
|
||||
return nil, errDuplicateProvider
|
||||
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
|
||||
}
|
||||
|
||||
// select don't need health check
|
||||
@ -75,35 +82,39 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||
}
|
||||
|
||||
providers = append(providers, pd)
|
||||
providersMap[groupName] = pd
|
||||
} else {
|
||||
if groupOption.URL == "" || groupOption.Interval == 0 {
|
||||
return nil, errMissHealthCheck
|
||||
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, err
|
||||
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, err
|
||||
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 {
|
||||
@ -120,7 +131,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
||||
case "relay":
|
||||
group = NewRelay(groupOption, providers)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
||||
return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type)
|
||||
}
|
||||
|
||||
return group, nil
|
||||
@ -138,15 +149,15 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) {
|
||||
var ps []provider.ProxyProvider
|
||||
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() == provider.Compatible {
|
||||
if p.VehicleType() == types.Compatible {
|
||||
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
|
||||
}
|
||||
ps = append(ps, p)
|
@ -3,14 +3,13 @@ package outboundgroup
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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 Relay struct {
|
||||
@ -19,15 +18,26 @@ type Relay struct {
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
proxies := r.proxies(metadata, true)
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("proxy does not exist")
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
var proxies []C.Proxy
|
||||
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]
|
||||
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 {
|
||||
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||
}
|
||||
@ -56,19 +66,20 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
||||
return outbound.NewConn(c, r), nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range r.rawProxies(false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
return json.Marshal(map[string]any{
|
||||
"type": r.Type().String(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
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, touch), nil
|
||||
})
|
||||
|
||||
@ -89,9 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||
return proxies
|
||||
}
|
||||
|
||||
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||
return &Relay{
|
||||
Base: outbound.NewBase(options.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),
|
||||
providers: providers,
|
||||
}
|
@ -5,10 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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 {
|
||||
@ -19,22 +20,25 @@ type Selector struct {
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
||||
// 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
|
||||
}
|
||||
|
||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := s.selectedProxy(true).DialUDP(metadata)
|
||||
// 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
|
||||
@ -43,13 +47,14 @@ func (s *Selector) SupportUDP() bool {
|
||||
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]interface{}{
|
||||
return json.Marshal(map[string]any{
|
||||
"type": s.Type().String(),
|
||||
"now": s.Now(),
|
||||
"all": all,
|
||||
@ -72,12 +77,13 @@ func (s *Selector) Set(name string) error {
|
||||
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() (interface{}, error) {
|
||||
elm, _, _ := s.single.Do(func() (any, error) {
|
||||
proxies := getProvidersProxies(s.providers, touch)
|
||||
for _, proxy := range proxies {
|
||||
if proxy.Name() == s.selected {
|
||||
@ -91,13 +97,18 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||
return elm.(C.Proxy)
|
||||
}
|
||||
|
||||
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||
selected := providers[0].Proxies()[0].Name()
|
||||
return &Selector{
|
||||
Base: outbound.NewBase(options.Name, "", C.Selector, false),
|
||||
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: options.DisableUDP,
|
||||
disableUDP: option.DisableUDP,
|
||||
}
|
||||
}
|
@ -5,10 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapters/outbound"
|
||||
"github.com/Dreamacro/clash/adapters/provider"
|
||||
"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)
|
||||
@ -33,28 +34,31 @@ func (u *URLTest) Now() string {
|
||||
return u.fast(false).Name()
|
||||
}
|
||||
|
||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
c, err = u.fast(true).DialContext(ctx, metadata)
|
||||
// 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
|
||||
}
|
||||
|
||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := u.fast(true).DialUDP(metadata)
|
||||
// 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() (interface{}, error) {
|
||||
elm, _, _ := u.single.Do(func() (any, error) {
|
||||
return getProvidersProxies(u.providers, touch), nil
|
||||
})
|
||||
|
||||
@ -62,11 +66,17 @@ func (u *URLTest) proxies(touch bool) []C.Proxy {
|
||||
}
|
||||
|
||||
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
|
||||
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
|
||||
}
|
||||
@ -79,16 +89,20 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
||||
}
|
||||
|
||||
// tolerance
|
||||
if u.fastNode == nil || u.fastNode.LastDelay() > fast.LastDelay()+u.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
|
||||
@ -97,38 +111,42 @@ func (u *URLTest) SupportUDP() bool {
|
||||
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]interface{}{
|
||||
return json.Marshal(map[string]any{
|
||||
"type": u.Type().String(),
|
||||
"now": u.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
|
||||
func parseURLTestOption(config map[string]any) []urlTestOption {
|
||||
opts := []urlTestOption{}
|
||||
|
||||
// tolerance
|
||||
if elm, ok := config["tolerance"]; ok {
|
||||
if tolerance, ok := elm.(int); ok {
|
||||
if tolerance, ok := config["tolerance"].(int); ok {
|
||||
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||
urlTest := &URLTest{
|
||||
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
|
||||
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: commonOptions.DisableUDP,
|
||||
disableUDP: option.DisableUDP,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
@ -16,33 +16,28 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
if ip.To4() != nil {
|
||||
if ip == nil {
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypIPv4,
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
} else {
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypIPv6,
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
DstIP: nil,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
} else if ip4 := ip.To4(); ip4 != nil {
|
||||
addr = &C.Metadata{
|
||||
Host: "",
|
||||
DstIP: ip4,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
addr = &C.Metadata{
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
@ -1,13 +1,14 @@
|
||||
package outbound
|
||||
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]interface{}) (C.Proxy, error) {
|
||||
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 {
|
||||
@ -20,36 +21,36 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
||||
)
|
||||
switch proxyType {
|
||||
case "ss":
|
||||
ssOption := &ShadowSocksOption{}
|
||||
ssOption := &outbound.ShadowSocksOption{}
|
||||
err = decoder.Decode(mapping, ssOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = NewShadowSocks(*ssOption)
|
||||
proxy, err = outbound.NewShadowSocks(*ssOption)
|
||||
case "ssr":
|
||||
ssrOption := &ShadowSocksROption{}
|
||||
ssrOption := &outbound.ShadowSocksROption{}
|
||||
err = decoder.Decode(mapping, ssrOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = NewShadowSocksR(*ssrOption)
|
||||
proxy, err = outbound.NewShadowSocksR(*ssrOption)
|
||||
case "socks5":
|
||||
socksOption := &Socks5Option{}
|
||||
socksOption := &outbound.Socks5Option{}
|
||||
err = decoder.Decode(mapping, socksOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = NewSocks5(*socksOption)
|
||||
proxy = outbound.NewSocks5(*socksOption)
|
||||
case "http":
|
||||
httpOption := &HttpOption{}
|
||||
httpOption := &outbound.HttpOption{}
|
||||
err = decoder.Decode(mapping, httpOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = NewHttp(*httpOption)
|
||||
proxy = outbound.NewHttp(*httpOption)
|
||||
case "vmess":
|
||||
vmessOption := &VmessOption{
|
||||
HTTPOpts: HTTPOptions{
|
||||
vmessOption := &outbound.VmessOption{
|
||||
HTTPOpts: outbound.HTTPOptions{
|
||||
Method: "GET",
|
||||
Path: []string{"/"},
|
||||
},
|
||||
@ -58,21 +59,21 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = NewVmess(*vmessOption)
|
||||
proxy, err = outbound.NewVmess(*vmessOption)
|
||||
case "snell":
|
||||
snellOption := &SnellOption{}
|
||||
snellOption := &outbound.SnellOption{}
|
||||
err = decoder.Decode(mapping, snellOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = NewSnell(*snellOption)
|
||||
proxy, err = outbound.NewSnell(*snellOption)
|
||||
case "trojan":
|
||||
trojanOption := &TrojanOption{}
|
||||
trojanOption := &outbound.TrojanOption{}
|
||||
err = decoder.Decode(mapping, trojanOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = NewTrojan(*trojanOption)
|
||||
proxy, err = outbound.NewTrojan(*trojanOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
@ -3,51 +3,54 @@ package provider
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
)
|
||||
|
||||
var (
|
||||
fileMode os.FileMode = 0666
|
||||
dirMode os.FileMode = 0755
|
||||
fileMode os.FileMode = 0o666
|
||||
dirMode os.FileMode = 0o755
|
||||
)
|
||||
|
||||
type parser = func([]byte) (interface{}, error)
|
||||
type parser = func([]byte) (any, error)
|
||||
|
||||
type fetcher struct {
|
||||
name string
|
||||
vehicle Vehicle
|
||||
vehicle types.Vehicle
|
||||
interval time.Duration
|
||||
updatedAt *time.Time
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
hash [16]byte
|
||||
parser parser
|
||||
onUpdate func(interface{})
|
||||
onUpdate func(any)
|
||||
}
|
||||
|
||||
func (f *fetcher) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *fetcher) VehicleType() VehicleType {
|
||||
func (f *fetcher) VehicleType() types.VehicleType {
|
||||
return f.vehicle.Type()
|
||||
}
|
||||
|
||||
func (f *fetcher) Initial() (interface{}, error) {
|
||||
func (f *fetcher) Initial() (any, error) {
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
isLocal bool
|
||||
immediatelyUpdate bool
|
||||
)
|
||||
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
||||
buf, err = os.ReadFile(f.vehicle.Path())
|
||||
modTime := stat.ModTime()
|
||||
f.updatedAt = &modTime
|
||||
isLocal = true
|
||||
immediatelyUpdate = time.Since(modTime) > f.interval
|
||||
} else {
|
||||
buf, err = f.vehicle.Read()
|
||||
}
|
||||
@ -72,9 +75,11 @@ func (f *fetcher) Initial() (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isLocal = false
|
||||
}
|
||||
|
||||
if f.vehicle.Type() != File {
|
||||
if f.vehicle.Type() != types.File && !isLocal {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -84,13 +89,13 @@ func (f *fetcher) Initial() (interface{}, error) {
|
||||
|
||||
// pull proxies automatically
|
||||
if f.ticker != nil {
|
||||
go f.pullLoop()
|
||||
go f.pullLoop(immediatelyUpdate)
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) Update() (interface{}, bool, error) {
|
||||
func (f *fetcher) Update() (any, bool, error) {
|
||||
buf, err := f.vehicle.Read()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@ -100,6 +105,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
||||
hash := md5.Sum(buf)
|
||||
if bytes.Equal(f.hash[:], hash[:]) {
|
||||
f.updatedAt = &now
|
||||
os.Chtimes(f.vehicle.Path(), now, now)
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
@ -108,9 +114,11 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if f.vehicle.Type() != types.File {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
f.updatedAt = &now
|
||||
f.hash = hash
|
||||
@ -125,25 +133,33 @@ func (f *fetcher) Destroy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fetcher) pullLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-f.ticker.C:
|
||||
func (f *fetcher) pullLoop(immediately bool) {
|
||||
update := func() {
|
||||
elm, same, err := f.Update()
|
||||
if err != nil {
|
||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
if same {
|
||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||
if f.onUpdate != nil {
|
||||
f.onUpdate(elm)
|
||||
}
|
||||
}
|
||||
|
||||
if immediately {
|
||||
update()
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-f.ticker.C:
|
||||
update()
|
||||
case <-f.done:
|
||||
f.ticker.Stop()
|
||||
return
|
||||
@ -160,10 +176,10 @@ func safeWrite(path string, buf []byte) error {
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, buf, fileMode)
|
||||
return os.WriteFile(path, buf, fileMode)
|
||||
}
|
||||
|
||||
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
|
||||
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
|
||||
var ticker *time.Ticker
|
||||
if interval != 0 {
|
||||
ticker = time.NewTicker(interval)
|
||||
@ -173,6 +189,7 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
|
||||
name: name,
|
||||
ticker: ticker,
|
||||
vehicle: vehicle,
|
||||
interval: interval,
|
||||
parser: parser,
|
||||
done: make(chan struct{}, 1),
|
||||
onUpdate: onUpdate,
|
@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/batch"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
@ -30,13 +32,20 @@ type HealthCheck struct {
|
||||
func (hc *HealthCheck) process() {
|
||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||
|
||||
go hc.check()
|
||||
go hc.checkAll()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
now := time.Now().Unix()
|
||||
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||
hc.check()
|
||||
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()
|
||||
@ -57,14 +66,22 @@ func (hc *HealthCheck) touch() {
|
||||
hc.lastTouch.Store(time.Now().Unix())
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) check() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||
for _, proxy := range hc.proxies {
|
||||
go proxy.URLTest(ctx, hc.url)
|
||||
}
|
||||
func (hc *HealthCheck) checkAll() {
|
||||
hc.check(hc.proxies)
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
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() {
|
@ -7,10 +7,12 @@ import (
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
)
|
||||
|
||||
var (
|
||||
errVehicleType = errors.New("unsupport vehicle type")
|
||||
errSubPath = errors.New("path is not subpath of home directory")
|
||||
)
|
||||
|
||||
type healthCheckSchema struct {
|
||||
@ -25,10 +27,11 @@ type proxyProviderSchema struct {
|
||||
Path string `provider:"path"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
Filter string `provider:"filter,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})
|
||||
|
||||
schema := &proxyProviderSchema{
|
||||
@ -40,7 +43,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hcInterval uint = 0
|
||||
var hcInterval uint
|
||||
if schema.HealthCheck.Enable {
|
||||
hcInterval = uint(schema.HealthCheck.Interval)
|
||||
}
|
||||
@ -48,16 +51,20 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
|
||||
|
||||
path := C.Path.Resolve(schema.Path)
|
||||
|
||||
var vehicle Vehicle
|
||||
var vehicle types.Vehicle
|
||||
switch schema.Type {
|
||||
case "file":
|
||||
vehicle = NewFileVehicle(path)
|
||||
case "http":
|
||||
if !C.Path.IsSubPath(path) {
|
||||
return nil, fmt.Errorf("%w: %s", errSubPath, path)
|
||||
}
|
||||
vehicle = NewHTTPVehicle(schema.URL, path)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||
}
|
||||
|
||||
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 (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Type() VehicleType {
|
||||
return File
|
||||
func (f *FileVehicle) Type() types.VehicleType {
|
||||
return types.File
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Path() string {
|
||||
@ -52,7 +26,7 @@ func (f *FileVehicle) Path() string {
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Read() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
return os.ReadFile(f.path)
|
||||
}
|
||||
|
||||
func NewFileVehicle(path string) *FileVehicle {
|
||||
@ -64,8 +38,8 @@ type HTTPVehicle struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *HTTPVehicle) Type() VehicleType {
|
||||
return HTTP
|
||||
func (h *HTTPVehicle) Type() types.VehicleType {
|
||||
return types.HTTP
|
||||
}
|
||||
|
||||
func (h *HTTPVehicle) Path() string {
|
||||
@ -99,7 +73,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * 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}
|
||||
@ -107,8 +83,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
buf, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
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,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,134 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/ssr/obfs"
|
||||
"github.com/Dreamacro/clash/component/ssr/protocol"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
|
||||
)
|
||||
|
||||
type ShadowSocksR struct {
|
||||
*Base
|
||||
cipher *core.StreamCipher
|
||||
obfs obfs.Obfs
|
||||
protocol protocol.Protocol
|
||||
}
|
||||
|
||||
type ShadowSocksROption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
Obfs string `proxy:"obfs"`
|
||||
ObfsParam string `proxy:"obfs-param,omitempty"`
|
||||
Protocol string `proxy:"protocol"`
|
||||
ProtocolParam string `proxy:"protocol-param,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
}
|
||||
|
||||
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
c = obfs.NewConn(c, ssr.obfs)
|
||||
c = ssr.cipher.StreamConn(c)
|
||||
conn, ok := c.(*shadowstream.Conn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid connection type")
|
||||
}
|
||||
iv, err := conn.ObtainWriteIV()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c = protocol.NewConn(c, ssr.protocol, iv)
|
||||
_, err = c.Write(serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
c, err = ssr.StreamConn(c, metadata)
|
||||
return NewConn(c, ssr), err
|
||||
}
|
||||
|
||||
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := resolveUDPAddr("udp", ssr.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc = ssr.cipher.PacketConn(pc)
|
||||
pc = protocol.NewPacketConn(pc, ssr.protocol)
|
||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||
}
|
||||
|
||||
func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"type": ssr.Type().String(),
|
||||
})
|
||||
}
|
||||
|
||||
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
cipher := option.Cipher
|
||||
password := option.Password
|
||||
coreCiph, err := core.PickCipher(cipher, nil, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize cipher error: %w", addr, err)
|
||||
}
|
||||
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not a supported stream cipher in ssr", cipher)
|
||||
}
|
||||
|
||||
obfs, err := obfs.PickObfs(option.Obfs, &obfs.Base{
|
||||
IVSize: ciph.IVSize(),
|
||||
Key: ciph.Key,
|
||||
HeadLen: 30,
|
||||
Host: option.Server,
|
||||
Port: option.Port,
|
||||
Param: option.ObfsParam,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
|
||||
}
|
||||
|
||||
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
|
||||
IV: nil,
|
||||
Key: ciph.Key,
|
||||
TCPMss: 1460,
|
||||
Param: option.ProtocolParam,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
|
||||
}
|
||||
protocol.SetOverhead(obfs.GetObfsOverhead() + protocol.GetProtocolOverhead())
|
||||
|
||||
return &ShadowSocksR{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.ShadowsocksR,
|
||||
udp: option.UDP,
|
||||
},
|
||||
cipher: ciph,
|
||||
obfs: obfs,
|
||||
protocol: protocol,
|
||||
}, nil
|
||||
}
|
@ -1,107 +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(pc, t), err
|
||||
}
|
||||
|
||||
func (t *Trojan) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"type": t.Type().String(),
|
||||
})
|
||||
}
|
||||
|
||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
|
||||
tOption := &trojan.Option{
|
||||
Password: option.Password,
|
||||
ALPN: option.ALPN,
|
||||
ServerName: option.Server,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if option.SNI != "" {
|
||||
tOption.ServerName = option.SNI
|
||||
}
|
||||
|
||||
return &Trojan{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Trojan,
|
||||
udp: option.UDP,
|
||||
},
|
||||
instance: trojan.New(tOption),
|
||||
}, nil
|
||||
}
|
@ -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,260 +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"`
|
||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||
WSPath string `proxy:"ws-path,omitempty"`
|
||||
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
ServerName string `proxy:"servername,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPOptions struct {
|
||||
Method string `proxy:"method,omitempty"`
|
||||
Path []string `proxy:"path,omitempty"`
|
||||
Headers map[string][]string `proxy:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type HTTP2Options struct {
|
||||
Host []string `proxy:"host,omitempty"`
|
||||
Path string `proxy:"path,omitempty"`
|
||||
}
|
||||
|
||||
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
var err error
|
||||
switch v.option.Network {
|
||||
case "ws":
|
||||
host, port, _ := net.SplitHostPort(v.addr)
|
||||
wsOpts := &vmess.WebsocketConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: v.option.WSPath,
|
||||
}
|
||||
|
||||
if len(v.option.WSHeaders) != 0 {
|
||||
header := http.Header{}
|
||||
for key, value := range v.option.WSHeaders {
|
||||
header.Add(key, value)
|
||||
}
|
||||
wsOpts.Headers = header
|
||||
}
|
||||
|
||||
if v.option.TLS {
|
||||
wsOpts.TLS = true
|
||||
wsOpts.SessionCache = getClientSessionCache()
|
||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
||||
wsOpts.ServerName = v.option.ServerName
|
||||
}
|
||||
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||
case "http":
|
||||
// readability first, so just copy default TLS logic
|
||||
if v.option.TLS {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
httpOpts := &vmess.HTTPConfig{
|
||||
Host: host,
|
||||
Method: v.option.HTTPOpts.Method,
|
||||
Path: v.option.HTTPOpts.Path,
|
||||
Headers: v.option.HTTPOpts.Headers,
|
||||
}
|
||||
|
||||
c = vmess.StreamHTTPConn(c, httpOpts)
|
||||
case "h2":
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, &tlsOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h2Opts := &vmess.H2Config{
|
||||
Hosts: v.option.HTTP2Opts.Host,
|
||||
Path: v.option.HTTP2Opts.Path,
|
||||
}
|
||||
|
||||
c, err = vmess.StreamH2Conn(c, h2Opts)
|
||||
default:
|
||||
// handle TLS
|
||||
if v.option.TLS {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||
}
|
||||
|
||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
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: %s", v.addr, err.Error())
|
||||
}
|
||||
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
|
||||
}
|
||||
if option.Network == "h2" && !option.TLS {
|
||||
return nil, fmt.Errorf("TLS must be true with h2 network")
|
||||
}
|
||||
|
||||
return &Vmess{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Vmess,
|
||||
udp: option.UDP,
|
||||
},
|
||||
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) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, err := uc.Conn.Read(b)
|
||||
return n, uc.rAddr, err
|
||||
}
|
@ -1,261 +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
|
||||
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
||||
// Commonly used in Dial and DialUDP
|
||||
ProxiesWithTouch() []C.Proxy
|
||||
HealthCheck()
|
||||
}
|
||||
|
||||
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 (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
||||
pp.healthCheck.touch()
|
||||
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)
|
||||
if pp.healthCheck.auto() {
|
||||
go pp.healthCheck.check()
|
||||
}
|
||||
}
|
||||
|
||||
func stopProxyProvider(pd *ProxySetProvider) {
|
||||
pd.healthCheck.close()
|
||||
pd.fetcher.Destroy()
|
||||
}
|
||||
|
||||
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
|
||||
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 (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
||||
cp.healthCheck.touch()
|
||||
return cp.Proxies()
|
||||
}
|
||||
|
||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||
pd.healthCheck.close()
|
||||
}
|
||||
|
||||
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
||||
if len(proxies) == 0 {
|
||||
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()
|
||||
}
|
38
common/cache/lrucache.go
vendored
38
common/cache/lrucache.go
vendored
@ -12,7 +12,7 @@ import (
|
||||
type Option func(*LruCache)
|
||||
|
||||
// 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
|
||||
func WithEvict(cb EvictCallback) Option {
|
||||
@ -57,18 +57,18 @@ type LruCache struct {
|
||||
maxAge int64
|
||||
maxSize int
|
||||
mu sync.Mutex
|
||||
cache map[interface{}]*list.Element
|
||||
cache map[any]*list.Element
|
||||
lru *list.List // Front is least-recent
|
||||
updateAgeOnGet bool
|
||||
staleReturn bool
|
||||
onEvict EvictCallback
|
||||
}
|
||||
|
||||
// NewLRUCache creates an LruCache
|
||||
func NewLRUCache(options ...Option) *LruCache {
|
||||
// New creates an LruCache
|
||||
func New(options ...Option) *LruCache {
|
||||
lc := &LruCache{
|
||||
lru: list.New(),
|
||||
cache: make(map[interface{}]*list.Element),
|
||||
cache: make(map[any]*list.Element),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
@ -78,9 +78,9 @@ func NewLRUCache(options ...Option) *LruCache {
|
||||
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.
|
||||
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
||||
func (c *LruCache) Get(key any) (any, bool) {
|
||||
entry := c.get(key)
|
||||
if entry == nil {
|
||||
return nil, false
|
||||
@ -90,11 +90,11 @@ func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
||||
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,
|
||||
// and a bool set to true if the key was found.
|
||||
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
|
||||
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
|
||||
entry := c.get(key)
|
||||
if entry == nil {
|
||||
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
|
||||
func (c *LruCache) Exist(key interface{}) bool {
|
||||
func (c *LruCache) Exist(key any) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// Set stores the interface{} representation of a response for a given key.
|
||||
func (c *LruCache) Set(key interface{}, value interface{}) {
|
||||
// Set stores the any representation of a response for a given key.
|
||||
func (c *LruCache) Set(key any, value any) {
|
||||
expires := int64(0)
|
||||
if c.maxAge > 0 {
|
||||
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))
|
||||
}
|
||||
|
||||
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
|
||||
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||
// The expires time will round to second.
|
||||
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
||||
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -155,7 +155,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
|
||||
defer n.mu.Unlock()
|
||||
|
||||
n.lru = list.New()
|
||||
n.cache = make(map[interface{}]*list.Element)
|
||||
n.cache = make(map[any]*list.Element)
|
||||
|
||||
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||
elm := e.Value.(*entry)
|
||||
@ -163,7 +163,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LruCache) get(key interface{}) *entry {
|
||||
func (c *LruCache) get(key any) *entry {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@ -188,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
|
||||
}
|
||||
|
||||
// Delete removes the value associated with a key.
|
||||
func (c *LruCache) Delete(key interface{}) {
|
||||
func (c *LruCache) Delete(key any) {
|
||||
c.mu.Lock()
|
||||
|
||||
if le, ok := c.cache[key]; ok {
|
||||
@ -217,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
key interface{}
|
||||
value interface{}
|
||||
key any
|
||||
value any
|
||||
expires int64
|
||||
}
|
||||
|
23
common/cache/lrucache_test.go
vendored
23
common/cache/lrucache_test.go
vendored
@ -19,7 +19,7 @@ var entries = []struct {
|
||||
}
|
||||
|
||||
func TestLRUCache(t *testing.T) {
|
||||
c := NewLRUCache()
|
||||
c := New()
|
||||
|
||||
for _, e := range entries {
|
||||
c.Set(e.key, e.value)
|
||||
@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUMaxAge(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400))
|
||||
c := New(WithAge(86400))
|
||||
|
||||
now := time.Now().Unix()
|
||||
expected := now + 86400
|
||||
@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUpdateOnGet(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
||||
c := New(WithAge(86400), WithUpdateAgeOnGet())
|
||||
|
||||
now := time.Now().Unix()
|
||||
expires := now + 86400/2
|
||||
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMaxSize(t *testing.T) {
|
||||
c := NewLRUCache(WithSize(2))
|
||||
c := New(WithSize(2))
|
||||
// Add one expired entry
|
||||
c.Set("foo", "bar")
|
||||
_, ok := c.Get("foo")
|
||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExist(t *testing.T) {
|
||||
c := NewLRUCache(WithSize(1))
|
||||
c := New(WithSize(1))
|
||||
c.Set(1, 2)
|
||||
assert.True(t, c.Exist(1))
|
||||
c.Set(2, 3)
|
||||
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
|
||||
|
||||
func TestEvict(t *testing.T) {
|
||||
temp := 0
|
||||
evict := func(key interface{}, value interface{}) {
|
||||
evict := func(key any, value any) {
|
||||
temp = key.(int) + value.(int)
|
||||
}
|
||||
|
||||
c := NewLRUCache(WithEvict(evict), WithSize(1))
|
||||
c := New(WithEvict(evict), WithSize(1))
|
||||
c.Set(1, 2)
|
||||
c.Set(2, 3)
|
||||
|
||||
@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetWithExpire(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(1))
|
||||
c := New(WithAge(1))
|
||||
now := time.Now().Unix()
|
||||
|
||||
tenSecBefore := time.Unix(now-10, 0)
|
||||
@ -149,11 +149,10 @@ func TestSetWithExpire(t *testing.T) {
|
||||
assert.Equal(t, nil, res)
|
||||
assert.Equal(t, time.Time{}, expires)
|
||||
assert.Equal(t, false, exist)
|
||||
|
||||
}
|
||||
|
||||
func TestStale(t *testing.T) {
|
||||
c := NewLRUCache(WithAge(1), WithStale(true))
|
||||
c := New(WithAge(1), WithStale(true))
|
||||
now := time.Now().Unix()
|
||||
|
||||
tenSecBefore := time.Unix(now-10, 0)
|
||||
@ -166,11 +165,11 @@ func TestStale(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCloneTo(t *testing.T) {
|
||||
o := NewLRUCache(WithSize(10))
|
||||
o := New(WithSize(10))
|
||||
o.Set("1", 1)
|
||||
o.Set("2", 2)
|
||||
|
||||
n := NewLRUCache(WithSize(2))
|
||||
n := New(WithSize(2))
|
||||
n.Set("3", 3)
|
||||
n.Set("4", 4)
|
||||
|
||||
|
@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
|
||||
}
|
||||
|
||||
func (d *digest32) Sum32() (h1 uint32) {
|
||||
|
||||
h1 = d.h1
|
||||
|
||||
var k1 uint32
|
||||
|
@ -1,4 +1,4 @@
|
||||
package mixed
|
||||
package net
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -11,6 +11,9 @@ type BufferedConn struct {
|
||||
}
|
||||
|
||||
func NewBufferedConn(c net.Conn) *BufferedConn {
|
||||
if bc, ok := c.(*BufferedConn); ok {
|
||||
return bc
|
||||
}
|
||||
return &BufferedConn{bufio.NewReader(c), c}
|
||||
}
|
||||
|
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
|
||||
|
||||
type Iterable <-chan interface{}
|
||||
type Iterable <-chan any
|
||||
|
@ -9,8 +9,8 @@ import (
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func iterator(item []interface{}) chan interface{} {
|
||||
ch := make(chan interface{})
|
||||
func iterator(item []any) chan any {
|
||||
ch := make(chan any)
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
for _, elm := range item {
|
||||
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
|
||||
}
|
||||
|
||||
func TestObservable(t *testing.T) {
|
||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||
src := NewObservable(iter)
|
||||
data, err := src.Subscribe()
|
||||
assert.Nil(t, err)
|
||||
@ -34,15 +34,15 @@ func TestObservable(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)
|
||||
ch1, _ := src.Subscribe()
|
||||
ch2, _ := src.Subscribe()
|
||||
var count = atomic.NewInt32(0)
|
||||
count := atomic.NewInt32(0)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
waitCh := func(ch <-chan interface{}) {
|
||||
waitCh := func(ch <-chan any) {
|
||||
for range ch {
|
||||
count.Inc()
|
||||
}
|
||||
@ -55,7 +55,7 @@ func TestObservable_MultiSubscribe(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)
|
||||
data, err := src.Subscribe()
|
||||
assert.Nil(t, err)
|
||||
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
||||
iter := iterator([]interface{}{1})
|
||||
iter := iterator([]any{1})
|
||||
src := NewObservable(iter)
|
||||
data, _ := src.Subscribe()
|
||||
<-data
|
||||
@ -75,14 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
||||
sub := Subscription(make(chan interface{}))
|
||||
iter := iterator([]interface{}{1})
|
||||
sub := Subscription(make(chan any))
|
||||
iter := iterator([]any{1})
|
||||
src := NewObservable(iter)
|
||||
src.UnSubscribe(sub)
|
||||
}
|
||||
|
||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
||||
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||
src := NewObservable(iter)
|
||||
max := 100
|
||||
|
||||
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(max)
|
||||
waitCh := func(ch <-chan interface{}) {
|
||||
waitCh := func(ch <-chan any) {
|
||||
for range ch {
|
||||
}
|
||||
wg.Done()
|
||||
@ -115,7 +115,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||
}
|
||||
|
||||
func Benchmark_Observable_1000(b *testing.B) {
|
||||
ch := make(chan interface{})
|
||||
ch := make(chan any)
|
||||
o := NewObservable(ch)
|
||||
num := 1000
|
||||
|
||||
|
@ -4,14 +4,14 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Subscription <-chan interface{}
|
||||
type Subscription <-chan any
|
||||
|
||||
type Subscriber struct {
|
||||
buffer chan interface{}
|
||||
buffer chan any
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (s *Subscriber) Emit(item interface{}) {
|
||||
func (s *Subscriber) Emit(item any) {
|
||||
s.buffer <- item
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func (s *Subscriber) Close() {
|
||||
|
||||
func newSubscriber() *Subscriber {
|
||||
sub := &Subscriber{
|
||||
buffer: make(chan interface{}, 200),
|
||||
buffer: make(chan any, 200),
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ type Picker struct {
|
||||
|
||||
once sync.Once
|
||||
errOnce sync.Once
|
||||
result interface{}
|
||||
result any
|
||||
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,
|
||||
// then returns the first nil error result (if any) from them.
|
||||
func (p *Picker) Wait() interface{} {
|
||||
func (p *Picker) Wait() any {
|
||||
p.wg.Wait()
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
|
||||
|
||||
// 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.
|
||||
func (p *Picker) Go(f func() (interface{}, error)) {
|
||||
func (p *Picker) Go(f func() (any, error)) {
|
||||
p.wg.Add(1)
|
||||
|
||||
go func() {
|
||||
|
@ -8,8 +8,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
|
||||
return func() (interface{}, error) {
|
||||
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
|
||||
return func() (any, error) {
|
||||
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
||||
select {
|
||||
case <-timer.C:
|
||||
|
@ -8,11 +8,7 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var defaultAllocator *Allocator
|
||||
|
||||
func init() {
|
||||
defaultAllocator = NewAllocator()
|
||||
}
|
||||
var defaultAllocator = NewAllocator()
|
||||
|
||||
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||
type Allocator struct {
|
||||
@ -27,7 +23,7 @@ func NewAllocator() *Allocator {
|
||||
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
||||
for k := range alloc.buffers {
|
||||
i := k
|
||||
alloc.buffers[k].New = func() interface{} {
|
||||
alloc.buffers[k].New = func() any {
|
||||
return make([]byte, 1<<uint32(i))
|
||||
}
|
||||
}
|
||||
@ -36,26 +32,36 @@ func NewAllocator() *Allocator {
|
||||
|
||||
// Get a []byte from pool with most appropriate cap
|
||||
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
|
||||
}
|
||||
|
||||
case size > 65536:
|
||||
return make([]byte, size)
|
||||
default:
|
||||
bits := msb(size)
|
||||
if size == 1<<bits {
|
||||
return alloc.buffers[bits].Get().([]byte)[:size]
|
||||
}
|
||||
|
||||
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
||||
}
|
||||
}
|
||||
|
||||
// Put returns a []byte to pool for future use,
|
||||
// which the cap must be exactly 2^n
|
||||
func (alloc *Allocator) Put(buf []byte) error {
|
||||
if cap(buf) == 0 || cap(buf) > 65536 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
//nolint
|
||||
//lint:ignore SA6002 ignore temporarily
|
||||
alloc.buffers[bits].Put(buf)
|
||||
return nil
|
||||
|
@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
|
||||
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
||||
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
||||
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
||||
assert.Nil(t, alloc.Get(65537))
|
||||
assert.Equal(t, 65537, len(alloc.Get(65537)))
|
||||
}
|
||||
|
||||
func TestAllocPut(t *testing.T) {
|
||||
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)), "put elem:3 []bytes misbehavior")
|
||||
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
|
||||
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
||||
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
|
||||
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||
}
|
||||
|
||||
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
|
||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
||||
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 {
|
||||
|
@ -6,12 +6,12 @@ import (
|
||||
|
||||
// Queue is a simple concurrent safe queue
|
||||
type Queue struct {
|
||||
items []interface{}
|
||||
items []any
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Put add the item to the queue.
|
||||
func (q *Queue) Put(items ...interface{}) {
|
||||
func (q *Queue) Put(items ...any) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...interface{}) {
|
||||
}
|
||||
|
||||
// Pop returns the head of items.
|
||||
func (q *Queue) Pop() interface{} {
|
||||
func (q *Queue) Pop() any {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -35,7 +35,7 @@ func (q *Queue) Pop() interface{} {
|
||||
}
|
||||
|
||||
// Last returns the last of item.
|
||||
func (q *Queue) Last() interface{} {
|
||||
func (q *Queue) Last() any {
|
||||
if len(q.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -47,8 +47,8 @@ func (q *Queue) Last() interface{} {
|
||||
}
|
||||
|
||||
// Copy get the copy of queue.
|
||||
func (q *Queue) Copy() []interface{} {
|
||||
items := []interface{}{}
|
||||
func (q *Queue) Copy() []any {
|
||||
items := []any{}
|
||||
q.lock.RLock()
|
||||
items = append(items, q.items...)
|
||||
q.lock.RUnlock()
|
||||
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
|
||||
// New is a constructor for a new concurrent safe queue.
|
||||
func New(hint int64) *Queue {
|
||||
return &Queue{
|
||||
items: make([]interface{}, 0, hint),
|
||||
items: make([]any, 0, hint),
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
type call struct {
|
||||
wg sync.WaitGroup
|
||||
val interface{}
|
||||
val any
|
||||
err error
|
||||
}
|
||||
|
||||
@ -20,13 +20,12 @@ type Single struct {
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Val interface{}
|
||||
Val any
|
||||
Err error
|
||||
}
|
||||
|
||||
// Do single.Do likes sync.singleFlight
|
||||
//lint:ignore ST1008 it likes sync.singleFlight
|
||||
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
|
||||
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
|
||||
s.mux.Lock()
|
||||
now := time.Now()
|
||||
if now.Before(s.last.Add(s.wait)) {
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
func TestBasic(t *testing.T) {
|
||||
single := NewSingle(time.Millisecond * 30)
|
||||
foo := 0
|
||||
var shardCount = atomic.NewInt32(0)
|
||||
call := func() (interface{}, error) {
|
||||
shardCount := atomic.NewInt32(0)
|
||||
call := func() (any, error) {
|
||||
foo++
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
return nil, nil
|
||||
@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
|
||||
func TestTimer(t *testing.T) {
|
||||
single := NewSingle(time.Millisecond * 30)
|
||||
foo := 0
|
||||
call := func() (interface{}, error) {
|
||||
call := func() (any, error) {
|
||||
foo++
|
||||
return nil, nil
|
||||
}
|
||||
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
|
||||
func TestReset(t *testing.T) {
|
||||
single := NewSingle(time.Millisecond * 30)
|
||||
foo := 0
|
||||
call := func() (interface{}, error) {
|
||||
call := func() (any, error) {
|
||||
foo++
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// +build !linux
|
||||
//go:build !linux
|
||||
|
||||
package sockopt
|
||||
|
||||
|
@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
|
||||
return &Decoder{option: &option}
|
||||
}
|
||||
|
||||
// Decode transform a map[string]interface{} to a struct
|
||||
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
||||
// Decode transform a map[string]any to a struct
|
||||
func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||
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()
|
||||
for idx := 0; idx < v.NumField(); 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)
|
||||
str := strings.SplitN(tag, ",", 2)
|
||||
key := str[0]
|
||||
omitempty := false
|
||||
if len(str) > 1 {
|
||||
omitempty = str[1] == "omitempty"
|
||||
}
|
||||
key, omitKey, found := strings.Cut(tag, ",")
|
||||
omitempty := found && omitKey == "omitempty"
|
||||
|
||||
value, ok := src[key]
|
||||
if !ok || value == nil {
|
||||
@ -62,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
||||
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() {
|
||||
case reflect.Int:
|
||||
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)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case kind == reflect.Int:
|
||||
val.SetInt(dataVal.Int())
|
||||
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
|
||||
val.SetInt(int64(dataVal.Float()))
|
||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||
var i int64
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
@ -123,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
|
||||
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)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
@ -140,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
|
||||
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))
|
||||
valType := val.Type()
|
||||
valElemType := valType.Elem()
|
||||
@ -155,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
||||
for valSlice.Len() <= i {
|
||||
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
||||
}
|
||||
currentField := valSlice.Index(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 {
|
||||
return err
|
||||
}
|
||||
@ -167,7 +181,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
||||
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()
|
||||
valKeyType := valType.Key()
|
||||
valElemType := valType.Elem()
|
||||
@ -239,7 +253,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
|
||||
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))
|
||||
|
||||
// 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{})
|
||||
dataValKeysUnused := make(map[interface{}]struct{})
|
||||
dataValKeysUnused := make(map[any]struct{})
|
||||
for _, dataValKey := range dataVal.MapKeys() {
|
||||
dataValKeys[dataValKey] = struct{}{}
|
||||
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
||||
@ -392,7 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
||||
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)
|
||||
val.Set(dataVal)
|
||||
return nil
|
||||
|
@ -1,12 +1,15 @@
|
||||
package structure
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var decoder = NewDecoder(Option{TagName: "test"})
|
||||
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
||||
var (
|
||||
decoder = NewDecoder(Option{TagName: "test"})
|
||||
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
||||
)
|
||||
|
||||
type Baz struct {
|
||||
Foo int `test:"foo"`
|
||||
@ -24,7 +27,7 @@ type BazOptional struct {
|
||||
}
|
||||
|
||||
func TestStructure_Basic(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
"bar": "test",
|
||||
"extra": false,
|
||||
@ -37,16 +40,12 @@ func TestStructure_Basic(t *testing.T) {
|
||||
|
||||
s := &Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, goal, s)
|
||||
}
|
||||
|
||||
func TestStructure_Slice(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
"bar": []string{"one", "two"},
|
||||
}
|
||||
@ -58,16 +57,12 @@ func TestStructure_Slice(t *testing.T) {
|
||||
|
||||
s := &BazSlice{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, goal, s)
|
||||
}
|
||||
|
||||
func TestStructure_Optional(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
}
|
||||
|
||||
@ -77,50 +72,40 @@ func TestStructure_Optional(t *testing.T) {
|
||||
|
||||
s := &BazOptional{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, goal, s)
|
||||
}
|
||||
|
||||
func TestStructure_MissingKey(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
}
|
||||
|
||||
s := &Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||
}
|
||||
|
||||
func TestStructure_ParamError(t *testing.T) {
|
||||
rawMap := map[string]interface{}{}
|
||||
rawMap := map[string]any{}
|
||||
s := Baz{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||
}
|
||||
|
||||
func TestStructure_SliceTypeError(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
"bar": []int{1, 2},
|
||||
}
|
||||
|
||||
s := &BazSlice{}
|
||||
err := decoder.Decode(rawMap, s)
|
||||
if err == nil {
|
||||
t.Fatalf("should throw error: %#v", s)
|
||||
}
|
||||
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||
}
|
||||
|
||||
func TestStructure_WeakType(t *testing.T) {
|
||||
rawMap := map[string]interface{}{
|
||||
rawMap := map[string]any{
|
||||
"foo": "1",
|
||||
"bar": []int{1},
|
||||
}
|
||||
@ -132,10 +117,65 @@ func TestStructure_WeakType(t *testing.T) {
|
||||
|
||||
s := &BazSlice{}
|
||||
err := weakTypeDecoder.Decode(rawMap, s)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(s, goal) {
|
||||
t.Fatalf("bad: %#v", s)
|
||||
}
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, goal, 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)
|
||||
}
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
var (
|
||||
errPlatformNotSupport = errors.New("unsupport platform")
|
||||
)
|
||||
|
||||
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
|
||||
ipv4 := ip.To4() != nil
|
||||
|
||||
for _, elm := range addrs {
|
||||
addr, ok := elm.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
addrV4 := addr.IP.To4() != nil
|
||||
|
||||
if addrV4 && ipv4 {
|
||||
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
||||
} else if !addrV4 && !ipv4 {
|
||||
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrAddrNotFound
|
||||
}
|
||||
|
||||
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
|
||||
ipv4 := ip.To4() != nil
|
||||
|
||||
for _, elm := range addrs {
|
||||
addr, ok := elm.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
addrV4 := addr.IP.To4() != nil
|
||||
|
||||
if addrV4 && ipv4 {
|
||||
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
||||
} else if !addrV4 && !ipv4 {
|
||||
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrAddrNotFound
|
||||
}
|
||||
|
||||
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch network {
|
||||
case "tcp", "tcp4", "tcp6":
|
||||
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
|
||||
dialer.LocalAddr = addr
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
case "udp", "udp4", "udp6":
|
||||
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
|
||||
dialer.LocalAddr = addr
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fallbackBindToListenConfig(name string) (string, error) {
|
||||
iface, err := net.InterfaceByName(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, elm := range addrs {
|
||||
addr, ok := elm.(*net.IPNet)
|
||||
if !ok || addr.IP.To4() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
return net.JoinHostPort(addr.IP.String(), "0"), nil
|
||||
}
|
||||
|
||||
return "", ErrAddrNotFound
|
||||
}
|
@ -3,49 +3,64 @@ 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) controlFn {
|
||||
return 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 nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
var innerErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx)
|
||||
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
|
||||
case "tcp6", "udp6":
|
||||
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx)
|
||||
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
|
||||
}
|
||||
})
|
||||
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer.Control = bindControl(iface.Index)
|
||||
|
||||
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||
iface, err := net.InterfaceByName(ifaceName)
|
||||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
lc.Control = bindControl(iface.Index)
|
||||
|
||||
return nil
|
||||
lc.Control = bindControl(ifaceObj.Index, lc.Control)
|
||||
return address, nil
|
||||
}
|
||||
|
@ -3,34 +3,49 @@ package dialer
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||
|
||||
func bindControl(ifaceName string) controlFn {
|
||||
return 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 nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
syscall.BindToDevice(int(fd), ifaceName)
|
||||
var innerErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
innerErr = unix.BindToDevice(int(fd), ifaceName)
|
||||
})
|
||||
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||
dialer.Control = bindControl(ifaceName)
|
||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
|
||||
dialer.Control = bindControl(ifaceName, dialer.Control)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
|
||||
lc.Control = bindControl(ifaceName)
|
||||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||
lc.Control = bindControl(ifaceName, lc.Control)
|
||||
|
||||
return nil
|
||||
return address, nil
|
||||
}
|
||||
|
@ -1,13 +1,47 @@
|
||||
// +build !linux,!darwin
|
||||
//go:build !linux && !darwin && !windows
|
||||
|
||||
package dialer
|
||||
|
||||
import "net"
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
|
||||
return errPlatformNotSupport
|
||||
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(lc *net.ListenConfig, ifaceName string) error {
|
||||
return errPlatformNotSupport
|
||||
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
|
||||
}
|
||||
|
98
component/dialer/bind_windows.go
Normal file
98
component/dialer/bind_windows.go
Normal file
@ -0,0 +1,98 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
IP_UNICAST_IF = 31
|
||||
IPV6_UNICAST_IF = 31
|
||||
)
|
||||
|
||||
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) {
|
||||
if ipStr == "" && strings.HasPrefix(network, "udp") {
|
||||
// When listening udp ":0", we should bind socket to interface4 and interface6 at the same time
|
||||
// and ignore the error of bind6
|
||||
_ = bindSocketToInterface6(windows.Handle(fd), ifaceIdx)
|
||||
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx)
|
||||
return
|
||||
}
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
innerErr = bindSocketToInterface4(windows.Handle(fd), ifaceIdx)
|
||||
case "tcp6", "udp6":
|
||||
innerErr = bindSocketToInterface6(windows.Handle(fd), ifaceIdx)
|
||||
}
|
||||
})
|
||||
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func bindSocketToInterface4(handle windows.Handle, ifaceIdx int) error {
|
||||
// MSDN says for IPv4 this needs to be in net byte order, so that it's like an IP address with leading zeros.
|
||||
// Ref: https://learn.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
|
||||
var bytes [4]byte
|
||||
binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx))
|
||||
index := *(*uint32)(unsafe.Pointer(&bytes[0]))
|
||||
err := windows.SetsockoptInt(handle, windows.IPPROTO_IP, IP_UNICAST_IF, int(index))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindSocketToInterface6(handle windows.Handle, ifaceIdx int) error {
|
||||
return windows.SetsockoptInt(handle, windows.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -8,22 +8,7 @@ import (
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
)
|
||||
|
||||
func Dialer() (*net.Dialer, error) {
|
||||
dialer := &net.Dialer{}
|
||||
if DialerHook != nil {
|
||||
if err := DialerHook(dialer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func Dial(network, address string) (net.Conn, error) {
|
||||
return DialContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp4", "tcp6", "udp4", "udp6":
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
@ -31,11 +16,6 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer, err := Dialer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ip net.IP
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
@ -43,38 +23,92 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
default:
|
||||
ip, err = resolver.ResolveIPv6(host)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if DialHook != nil {
|
||||
if err := DialHook(dialer, network, ip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
return dialContext(ctx, network, ip, port, options)
|
||||
case "tcp", "udp":
|
||||
return dualStackDialContext(ctx, network, address)
|
||||
return dualStackDialContext(ctx, network, address, options)
|
||||
default:
|
||||
return nil, errors.New("network invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func ListenPacket(network, address string) (net.PacketConn, error) {
|
||||
cfg := &net.ListenConfig{}
|
||||
if ListenPacketHook != nil {
|
||||
var err error
|
||||
address, err = ListenPacketHook(cfg, address)
|
||||
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
|
||||
cfg := &option{
|
||||
interfaceName: DefaultInterface.Load(),
|
||||
routingMark: int(DefaultRoutingMark.Load()),
|
||||
}
|
||||
|
||||
for _, o := range DefaultOptions {
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
lc := &net.ListenConfig{}
|
||||
if cfg.interfaceName != "" {
|
||||
var (
|
||||
addr string
|
||||
err error
|
||||
)
|
||||
if cfg.fallbackBind {
|
||||
addr, err = fallbackBindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
|
||||
} else {
|
||||
addr, err = bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
address = addr
|
||||
}
|
||||
if cfg.addrReuse {
|
||||
addrReuseToListenConfig(lc)
|
||||
}
|
||||
if cfg.routingMark != 0 {
|
||||
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
|
||||
}
|
||||
|
||||
return cfg.ListenPacket(context.Background(), network, address)
|
||||
return lc.ListenPacket(ctx, network, address)
|
||||
}
|
||||
|
||||
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
|
||||
opt := &option{
|
||||
interfaceName: DefaultInterface.Load(),
|
||||
routingMark: int(DefaultRoutingMark.Load()),
|
||||
}
|
||||
|
||||
for _, o := range DefaultOptions {
|
||||
o(opt)
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(opt)
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
if opt.interfaceName != "" {
|
||||
if opt.fallbackBind {
|
||||
if err := fallbackBindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if opt.routingMark != 0 {
|
||||
bindMarkToDialer(opt.routingMark, dialer, network, destination)
|
||||
}
|
||||
|
||||
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
||||
}
|
||||
|
||||
func dualStackDialContext(ctx context.Context, network, address string, options []Option) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -105,12 +139,6 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
|
||||
}
|
||||
}()
|
||||
|
||||
dialer, err := Dialer()
|
||||
if err != nil {
|
||||
result.error = err
|
||||
return
|
||||
}
|
||||
|
||||
var ip net.IP
|
||||
if ipv6 {
|
||||
ip, result.error = resolver.ResolveIPv6(host)
|
||||
@ -122,12 +150,7 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
|
||||
}
|
||||
result.resolved = true
|
||||
|
||||
if DialHook != nil {
|
||||
if result.error = DialHook(dialer, network, ip); result.error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
result.Conn, result.error = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
result.Conn, result.error = dialContext(ctx, network, ip, port, options)
|
||||
}
|
||||
|
||||
go startRacer(ctx, network+"4", host, false)
|
||||
|
90
component/dialer/fallbackbind.go
Normal file
90
component/dialer/fallbackbind.go
Normal file
@ -0,0 +1,90 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
)
|
||||
|
||||
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
|
||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var addr *net.IPNet
|
||||
switch network {
|
||||
case "udp4", "tcp4":
|
||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||
case "tcp6", "udp6":
|
||||
addr, err = ifaceObj.PickIPv6Addr(destination)
|
||||
default:
|
||||
if destination != nil {
|
||||
if destination.To4() != nil {
|
||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||
} else {
|
||||
addr, err = ifaceObj.PickIPv6Addr(destination)
|
||||
}
|
||||
} else {
|
||||
addr, err = ifaceObj.PickIPv4Addr(destination)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(network, "tcp") {
|
||||
return &net.TCPAddr{
|
||||
IP: addr.IP,
|
||||
Port: port,
|
||||
}, nil
|
||||
} else if strings.HasPrefix(network, "udp") {
|
||||
return &net.UDPAddr{
|
||||
IP: addr.IP,
|
||||
Port: port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, iface.ErrAddrNotFound
|
||||
}
|
||||
|
||||
func fallbackBindIfaceToDialer(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 fallbackBindIfaceToListenConfig(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
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
type DialerHookFunc = func(dialer *net.Dialer) error
|
||||
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
|
||||
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
|
||||
|
||||
var (
|
||||
DialerHook DialerHookFunc
|
||||
DialHook DialHookFunc
|
||||
ListenPacketHook ListenPacketHookFunc
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAddrNotFound = errors.New("addr not found")
|
||||
ErrNetworkNotSupport = errors.New("network not support")
|
||||
)
|
||||
|
||||
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
|
||||
return func(lc *net.ListenConfig, address string) (string, error) {
|
||||
err := bindIfaceToListenConfig(lc, name)
|
||||
if err == errPlatformNotSupport {
|
||||
address, err = fallbackBindToListenConfig(name)
|
||||
}
|
||||
|
||||
return address, err
|
||||
}
|
||||
}
|
||||
|
||||
func DialerWithInterface(name string) DialHookFunc {
|
||||
return func(dialer *net.Dialer, network string, ip net.IP) error {
|
||||
err := bindIfaceToDialer(dialer, name)
|
||||
if err == errPlatformNotSupport {
|
||||
err = fallbackBindToDialer(dialer, network, ip, name)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
43
component/dialer/mark_linux.go
Normal file
43
component/dialer/mark_linux.go
Normal file
@ -0,0 +1,43 @@
|
||||
//go:build linux
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
|
||||
dialer.Control = bindMarkToControl(mark, dialer.Control)
|
||||
}
|
||||
|
||||
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
|
||||
lc.Control = bindMarkToControl(mark, lc.Control)
|
||||
}
|
||||
|
||||
func bindMarkToControl(mark 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) {
|
||||
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||
})
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
26
component/dialer/mark_nonlinux.go
Normal file
26
component/dialer/mark_nonlinux.go
Normal file
@ -0,0 +1,26 @@
|
||||
//go:build !linux
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
)
|
||||
|
||||
var printMarkWarnOnce sync.Once
|
||||
|
||||
func printMarkWarn() {
|
||||
printMarkWarnOnce.Do(func() {
|
||||
log.Warnln("Routing mark on socket is not supported on current platform")
|
||||
})
|
||||
}
|
||||
|
||||
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
|
||||
printMarkWarn()
|
||||
}
|
||||
|
||||
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
|
||||
printMarkWarn()
|
||||
}
|
42
component/dialer/options.go
Normal file
42
component/dialer/options.go
Normal file
@ -0,0 +1,42 @@
|
||||
package dialer
|
||||
|
||||
import "go.uber.org/atomic"
|
||||
|
||||
var (
|
||||
DefaultOptions []Option
|
||||
DefaultInterface = atomic.NewString("")
|
||||
DefaultRoutingMark = atomic.NewInt32(0)
|
||||
)
|
||||
|
||||
type option struct {
|
||||
interfaceName string
|
||||
fallbackBind bool
|
||||
addrReuse bool
|
||||
routingMark int
|
||||
}
|
||||
|
||||
type Option func(opt *option)
|
||||
|
||||
func WithInterface(name string) Option {
|
||||
return func(opt *option) {
|
||||
opt.interfaceName = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithFallbackBind(fallback bool) Option {
|
||||
return func(opt *option) {
|
||||
opt.fallbackBind = fallback
|
||||
}
|
||||
}
|
||||
|
||||
func WithAddrReuse(reuse bool) Option {
|
||||
return func(opt *option) {
|
||||
opt.addrReuse = reuse
|
||||
}
|
||||
}
|
||||
|
||||
func WithRoutingMark(mark int) Option {
|
||||
return func(opt *option) {
|
||||
opt.routingMark = mark
|
||||
}
|
||||
}
|
9
component/dialer/reuse_others.go
Normal file
9
component/dialer/reuse_others.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func addrReuseToListenConfig(*net.ListenConfig) {}
|
27
component/dialer/reuse_unix.go
Normal file
27
component/dialer/reuse_unix.go
Normal file
@ -0,0 +1,27 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func addrReuseToListenConfig(lc *net.ListenConfig) {
|
||||
chain := lc.Control
|
||||
|
||||
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
})
|
||||
}
|
||||
}
|
24
component/dialer/reuse_windows.go
Normal file
24
component/dialer/reuse_windows.go
Normal file
@ -0,0 +1,24 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func addrReuseToListenConfig(lc *net.ListenConfig) {
|
||||
chain := lc.Control
|
||||
|
||||
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
|
||||
})
|
||||
}
|
||||
}
|
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