Compare commits
231 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 |
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
76
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,76 +0,0 @@
|
|||||||
name: Bug report
|
|
||||||
description: Create a report to help us improve
|
|
||||||
title: "[Bug] "
|
|
||||||
body:
|
|
||||||
- type: checkboxes
|
|
||||||
id: ensure
|
|
||||||
attributes:
|
|
||||||
label: Verify steps
|
|
||||||
description: "
|
|
||||||
在提交之前,请确认
|
|
||||||
Please verify that you've followed these steps
|
|
||||||
"
|
|
||||||
options:
|
|
||||||
- label: "
|
|
||||||
如果你可以自己 debug 并解决的话,提交 PR 吧
|
|
||||||
Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- label: "
|
|
||||||
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
|
||||||
I have searched on the [issue tracker](……/) for a related issue.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- label: "
|
|
||||||
我已经使用 dev 分支版本测试过,问题依旧存在
|
|
||||||
I have tested using the dev branch, and the issue still exists.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- label: "
|
|
||||||
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
|
||||||
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- label: "
|
|
||||||
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
|
|
||||||
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Clash version
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: os
|
|
||||||
attributes:
|
|
||||||
label: What OS are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- macOS
|
|
||||||
- Windows
|
|
||||||
- Linux
|
|
||||||
- OpenBSD/FreeBSD
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
render: yaml
|
|
||||||
label: "Clash config"
|
|
||||||
description: "
|
|
||||||
在下方附上 Clash core 脱敏后配置文件的内容
|
|
||||||
Paste the Clash core configuration below.
|
|
||||||
"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
render: shell
|
|
||||||
label: Clash log
|
|
||||||
description: "
|
|
||||||
在下方附上 Clash Core 的日志,log level 使用 DEBUG
|
|
||||||
Paste the Clash core log below with the log level set to `DEBUG`.
|
|
||||||
"
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
validations:
|
|
||||||
required: true
|
|
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
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,6 +1,9 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
|
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Get help in GitHub Discussions
|
- name: (中文)阅读 Wiki
|
||||||
url: https://github.com/Dreamacro/clash/discussions
|
url: https://dreamacro.github.io/clash/zh_CN/
|
||||||
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Clash's GitHub Discussions!
|
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
|
||||||
|
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,36 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Suggest an idea for this project
|
|
||||||
title: "[Feature] "
|
|
||||||
body:
|
|
||||||
- type: checkboxes
|
|
||||||
id: ensure
|
|
||||||
attributes:
|
|
||||||
label: Verify steps
|
|
||||||
description: "
|
|
||||||
在提交之前,请确认
|
|
||||||
Please verify that you've followed these steps
|
|
||||||
"
|
|
||||||
options:
|
|
||||||
- label: "
|
|
||||||
我已经在 [Issue Tracker](……/) 中找过我要提出的请求
|
|
||||||
I have searched on the [issue tracker](……/) for a related feature request.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- label: "
|
|
||||||
我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
|
|
||||||
I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue.
|
|
||||||
"
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Possible Solution
|
|
||||||
description: "
|
|
||||||
此项非必须,但是如果你有想法的话欢迎提出。
|
|
||||||
Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
|
|
||||||
"
|
|
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: 此项非必须,但是如果你有想法的话欢迎提出。
|
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: "CodeQL"
|
name: CodeQL
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -16,15 +16,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
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:
|
steps:
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Set up docker buildx
|
- name: Set up docker buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to Github Package
|
- name: Login to Github Package
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: Dreamacro
|
username: Dreamacro
|
||||||
@ -43,20 +43,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Build dev branch and push
|
- name: Build dev branch and push
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
|
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: Get all docker tags
|
- name: Get all docker tags
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: actions/github-script@v4
|
uses: actions/github-script@v6
|
||||||
id: tags
|
id: tags
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
|
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
|
||||||
const tags = [
|
const tags = [
|
||||||
'dreamacro/clash:latest',
|
'dreamacro/clash:latest',
|
||||||
`dreamacro/clash:${ref}`,
|
`dreamacro/clash:${ref}`,
|
||||||
@ -68,9 +70,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Build release and push
|
- name: Build release and push
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{steps.tags.outputs.result}}
|
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,40 +1,38 @@
|
|||||||
name: Go
|
name: Release
|
||||||
on: [push, pull_request]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.17
|
check-latest: true
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Get dependencies, run test and static check
|
- name: Get dependencies, run test
|
||||||
run: |
|
run: |
|
||||||
go test ./...
|
go test ./...
|
||||||
go vet ./...
|
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
|
||||||
staticcheck -- $(go list ./...)
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
env:
|
env:
|
||||||
NAME: clash
|
NAME: clash
|
||||||
BINDIR: bin
|
BINDIR: bin
|
||||||
run: make -j releases
|
run: make -j $(go run ./test/main.go) releases
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v4
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
|
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-stale: 60
|
||||||
|
16
.gitignore
vendored
16
.gitignore
vendored
@ -12,7 +12,7 @@ bin/*
|
|||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
*.out
|
*.out
|
||||||
|
|
||||||
# dep
|
# go mod vendor
|
||||||
vendor
|
vendor
|
||||||
|
|
||||||
# GoLand
|
# GoLand
|
||||||
@ -20,3 +20,17 @@ vendor
|
|||||||
|
|
||||||
# macOS file
|
# macOS file
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# test suite
|
||||||
|
test/config/cache*
|
||||||
|
|
||||||
|
# docs site generator
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# docs site cache
|
||||||
|
docs/.vitepress/cache
|
||||||
|
|
||||||
|
# docs site build files
|
||||||
|
docs/.vitepress/dist
|
||||||
|
23
.golangci.yaml
Normal file
23
.golangci.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gci:
|
||||||
|
custom-order: true
|
||||||
|
sections:
|
||||||
|
- standard
|
||||||
|
- prefix(github.com/Dreamacro/clash)
|
||||||
|
- default
|
||||||
|
staticcheck:
|
||||||
|
go: '1.20'
|
20
Dockerfile
20
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
|
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 --from=tonistiigi/xx:golang / /
|
||||||
COPY . /clash-src
|
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||||
RUN go mod download && \
|
|
||||||
make docker && \
|
RUN --mount=target=. \
|
||||||
mv ./bin/clash-docker /clash
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
|
make BINDIR= ${TARGETOS}-${TARGETARCH}${TARGETVARIANT} && \
|
||||||
|
mv /clash* /clash
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
|
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /Country.mmdb /root/.config/clash/
|
COPY --from=builder /Country.mmdb /root/.config/clash/
|
||||||
COPY --from=builder /clash /
|
COPY --from=builder /clash /
|
||||||
ENTRYPOINT ["/clash"]
|
ENTRYPOINT ["/clash"]
|
||||||
|
46
Makefile
46
Makefile
@ -8,37 +8,43 @@ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clas
|
|||||||
|
|
||||||
PLATFORM_LIST = \
|
PLATFORM_LIST = \
|
||||||
darwin-amd64 \
|
darwin-amd64 \
|
||||||
|
darwin-amd64-v3 \
|
||||||
darwin-arm64 \
|
darwin-arm64 \
|
||||||
linux-386 \
|
linux-386 \
|
||||||
linux-amd64 \
|
linux-amd64 \
|
||||||
|
linux-amd64-v3 \
|
||||||
linux-armv5 \
|
linux-armv5 \
|
||||||
linux-armv6 \
|
linux-armv6 \
|
||||||
linux-armv7 \
|
linux-armv7 \
|
||||||
linux-armv8 \
|
linux-arm64 \
|
||||||
linux-mips-softfloat \
|
linux-mips-softfloat \
|
||||||
linux-mips-hardfloat \
|
linux-mips-hardfloat \
|
||||||
linux-mipsle-softfloat \
|
linux-mipsle-softfloat \
|
||||||
linux-mipsle-hardfloat \
|
linux-mipsle-hardfloat \
|
||||||
linux-mips64 \
|
linux-mips64 \
|
||||||
linux-mips64le \
|
linux-mips64le \
|
||||||
|
linux-riscv64 \
|
||||||
|
linux-loong64 \
|
||||||
freebsd-386 \
|
freebsd-386 \
|
||||||
freebsd-amd64 \
|
freebsd-amd64 \
|
||||||
|
freebsd-amd64-v3 \
|
||||||
freebsd-arm64
|
freebsd-arm64
|
||||||
|
|
||||||
WINDOWS_ARCH_LIST = \
|
WINDOWS_ARCH_LIST = \
|
||||||
windows-386 \
|
windows-386 \
|
||||||
windows-amd64 \
|
windows-amd64 \
|
||||||
|
windows-amd64-v3 \
|
||||||
windows-arm64 \
|
windows-arm64 \
|
||||||
windows-arm32v7
|
windows-armv7
|
||||||
|
|
||||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
||||||
|
|
||||||
docker:
|
|
||||||
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
darwin-amd64:
|
darwin-amd64:
|
||||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
darwin-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
darwin-arm64:
|
darwin-arm64:
|
||||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -48,6 +54,9 @@ linux-386:
|
|||||||
linux-amd64:
|
linux-amd64:
|
||||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-armv5:
|
linux-armv5:
|
||||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -57,7 +66,7 @@ linux-armv6:
|
|||||||
linux-armv7:
|
linux-armv7:
|
||||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-armv8:
|
linux-arm64:
|
||||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
linux-mips-softfloat:
|
linux-mips-softfloat:
|
||||||
@ -78,12 +87,21 @@ linux-mips64:
|
|||||||
linux-mips64le:
|
linux-mips64le:
|
||||||
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-riscv64:
|
||||||
|
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
linux-loong64:
|
||||||
|
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-386:
|
freebsd-386:
|
||||||
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-amd64:
|
freebsd-amd64:
|
||||||
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
|
freebsd-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
freebsd-arm64:
|
freebsd-arm64:
|
||||||
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||||
|
|
||||||
@ -93,10 +111,13 @@ windows-386:
|
|||||||
windows-amd64:
|
windows-amd64:
|
||||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
|
windows-amd64-v3:
|
||||||
|
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-arm64:
|
windows-arm64:
|
||||||
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
windows-arm32v7:
|
windows-armv7:
|
||||||
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||||
|
|
||||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||||
@ -112,5 +133,16 @@ $(zip_releases): %.zip : %
|
|||||||
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
||||||
|
|
||||||
releases: $(gz_releases) $(zip_releases)
|
releases: $(gz_releases) $(zip_releases)
|
||||||
|
|
||||||
|
LINT_OS_LIST := darwin windows linux freebsd openbsd
|
||||||
|
|
||||||
|
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
|
||||||
|
%-lint:
|
||||||
|
GOOS=$* golangci-lint run ./...
|
||||||
|
|
||||||
|
lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
|
||||||
|
%-lint-fix:
|
||||||
|
GOOS=$* golangci-lint run --fix ./...
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm $(BINDIR)/*
|
rm $(BINDIR)/*
|
||||||
|
49
README.md
49
README.md
@ -7,56 +7,47 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/Dreamacro/clash/actions">
|
<a href="https://github.com/Dreamacro/clash/actions">
|
||||||
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions">
|
<img src="https://img.shields.io/github/actions/workflow/status/Dreamacro/clash/release.yml?branch=master&style=flat-square" alt="Github Actions">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
|
||||||
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
|
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
|
||||||
<a href="https://github.com/Dreamacro/clash/releases">
|
<a href="https://github.com/Dreamacro/clash/releases">
|
||||||
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
|
||||||
|
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Local HTTP/HTTPS/SOCKS server with authentication support
|
This is a general overview of the features that comes with Clash.
|
||||||
- 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
|
|
||||||
|
|
||||||
## Premium Features
|
- 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
|
||||||
|
|
||||||
- TUN mode on macOS, Linux and Windows. [Doc](https://github.com/Dreamacro/clash/wiki/premium-core-features#tun-device)
|
*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).*
|
||||||
- Match your tunnel by [Script](https://github.com/Dreamacro/clash/wiki/premium-core-features#script)
|
|
||||||
- [Rule Provider](https://github.com/Dreamacro/clash/wiki/premium-core-features#rule-providers)
|
|
||||||
|
|
||||||
## Getting Started
|
## Documentation
|
||||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
|
||||||
|
|
||||||
## Premium Release
|
You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/).
|
||||||
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
|
|
||||||
|
|
||||||
## Development
|
|
||||||
If you want to build an application that uses clash as a library, check out the the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
|
||||||
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
|
||||||
|
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is released under the GPL-3.0 license.
|
This software is released under the GPL-3.0 license.
|
||||||
|
|
||||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- [x] Complementing the necessary rule operators
|
|
||||||
- [x] Redir proxy
|
|
||||||
- [x] UDP support
|
|
||||||
- [x] Connection manager
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
@ -34,14 +35,26 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
|
||||||
if err != nil {
|
p.alive.Store(err == nil)
|
||||||
p.alive.Store(false)
|
|
||||||
}
|
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialUDP implements C.ProxyAdapter
|
||||||
|
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return p.ListenPacketContext(ctx, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
|
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
|
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
|
||||||
|
p.alive.Store(err == nil)
|
||||||
|
return pc, err
|
||||||
|
}
|
||||||
|
|
||||||
// DelayHistory implements C.Proxy
|
// DelayHistory implements C.Proxy
|
||||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||||
queue := p.history.Copy()
|
queue := p.history.Copy()
|
||||||
@ -78,9 +91,10 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
return inner, err
|
return inner, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping := map[string]interface{}{}
|
mapping := map[string]any{}
|
||||||
json.Unmarshal(inner, &mapping)
|
json.Unmarshal(inner, &mapping)
|
||||||
mapping["history"] = p.DelayHistory()
|
mapping["history"] = p.DelayHistory()
|
||||||
|
mapping["alive"] = p.Alive()
|
||||||
mapping["name"] = p.Name()
|
mapping["name"] = p.Name()
|
||||||
mapping["udp"] = p.SupportUDP()
|
mapping["udp"] = p.SupportUDP()
|
||||||
return json.Marshal(mapping)
|
return json.Marshal(mapping)
|
||||||
@ -88,12 +102,13 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
// URLTest get the delay for the specified URL
|
// URLTest get the delay for the specified URL
|
||||||
// implements C.Proxy
|
// implements C.Proxy
|
||||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
func (p *Proxy) URLTest(ctx context.Context, url string) (delay, meanDelay uint16, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.alive.Store(err == nil)
|
p.alive.Store(err == nil)
|
||||||
record := C.DelayHistory{Time: time.Now()}
|
record := C.DelayHistory{Time: time.Now()}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
record.Delay = t
|
record.Delay = delay
|
||||||
|
record.MeanDelay = meanDelay
|
||||||
}
|
}
|
||||||
p.history.Put(record)
|
p.history.Put(record)
|
||||||
if p.history.Len() > 10 {
|
if p.history.Len() > 10 {
|
||||||
@ -143,7 +158,16 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
t = uint16(time.Since(start) / time.Millisecond)
|
delay = uint16(time.Since(start) / time.Millisecond)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// ignore error because some server will hijack the connection and close immediately
|
||||||
|
return delay, 0, nil
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
meanDelay = uint16(time.Since(start) / time.Millisecond / 2)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +195,6 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addr = C.Metadata{
|
addr = C.Metadata{
|
||||||
AddrType: C.AtypDomainName,
|
|
||||||
Host: u.Hostname(),
|
Host: u.Hostname(),
|
||||||
DstIP: nil,
|
DstIP: nil,
|
||||||
DstPort: port,
|
DstPort: port,
|
||||||
|
@ -2,6 +2,7 @@ package inbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/context"
|
"github.com/Dreamacro/clash/context"
|
||||||
@ -9,13 +10,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewHTTP receive normal http request and return HTTPContext
|
// NewHTTP receive normal http request and return HTTPContext
|
||||||
func NewHTTP(target string, source net.Addr, conn net.Conn) *context.ConnContext {
|
func NewHTTP(target socks5.Addr, source net.Addr, originTarget net.Addr, conn net.Conn) *context.ConnContext {
|
||||||
metadata := parseSocksAddr(socks5.ParseAddr(target))
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.TCP
|
metadata.NetWork = C.TCP
|
||||||
metadata.Type = C.HTTP
|
metadata.Type = C.HTTP
|
||||||
if ip, port, err := parseAddr(source.String()); err == nil {
|
if ip, port, err := parseAddr(source.String()); err == nil {
|
||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
if originTarget != nil {
|
||||||
|
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
}
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package inbound
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/context"
|
"github.com/Dreamacro/clash/context"
|
||||||
@ -16,5 +17,8 @@ func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package inbound
|
package inbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
@ -17,7 +20,7 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPacket is PacketAdapter generator
|
// NewPacket is PacketAdapter generator
|
||||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
||||||
metadata := parseSocksAddr(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.UDP
|
metadata.NetWork = C.UDP
|
||||||
metadata.Type = source
|
metadata.Type = source
|
||||||
@ -25,7 +28,11 @@ func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAda
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
if originTarget != nil {
|
||||||
|
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
|
}
|
||||||
return &PacketAdapter{
|
return &PacketAdapter{
|
||||||
UDPPacket: packet,
|
UDPPacket: packet,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
@ -2,6 +2,7 @@ package inbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/context"
|
"github.com/Dreamacro/clash/context"
|
||||||
@ -17,6 +18,8 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
|
|||||||
metadata.SrcIP = ip
|
metadata.SrcIP = ip
|
||||||
metadata.SrcPort = port
|
metadata.SrcPort = port
|
||||||
}
|
}
|
||||||
|
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
|
||||||
|
metadata.OriginDst = addrPort
|
||||||
|
}
|
||||||
return context.NewConnContext(conn, metadata)
|
return context.NewConnContext(conn, metadata)
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{}
|
||||||
AddrType: int(target[0]),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch target[0] {
|
switch target[0] {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
@ -45,20 +43,12 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
|
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{
|
||||||
NetWork: C.TCP,
|
NetWork: C.TCP,
|
||||||
AddrType: C.AtypDomainName,
|
|
||||||
Host: host,
|
Host: host,
|
||||||
DstIP: nil,
|
DstIP: nil,
|
||||||
DstPort: port,
|
DstPort: port,
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
if ip != nil {
|
|
||||||
switch {
|
|
||||||
case ip.To4() == nil:
|
|
||||||
metadata.AddrType = C.AtypIPv6
|
|
||||||
default:
|
|
||||||
metadata.AddrType = C.AtypIPv4
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
metadata.DstIP = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
package outbound
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Base struct {
|
type Base struct {
|
||||||
name string
|
name string
|
||||||
addr string
|
addr string
|
||||||
|
iface string
|
||||||
tp C.AdapterType
|
tp C.AdapterType
|
||||||
udp bool
|
udp bool
|
||||||
|
rmark int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name implements C.ProxyAdapter
|
// Name implements C.ProxyAdapter
|
||||||
@ -30,8 +34,8 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
return c, errors.New("no support")
|
return c, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
return nil, errors.New("no support")
|
return nil, errors.New("no support")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,8 +61,42 @@ func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
|
// DialOptions return []dialer.Option from struct
|
||||||
return &Base{name, addr, tp, udp}
|
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 {
|
type conn struct {
|
||||||
|
@ -13,8 +13,8 @@ type Direct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
|
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -22,9 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
return NewConn(c, d), nil
|
return NewConn(c, d), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
|
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,11 @@ type Http struct {
|
|||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
Headers http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpOption struct {
|
type HttpOption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -33,13 +35,16 @@ type HttpOption struct {
|
|||||||
TLS bool `proxy:"tls,omitempty"`
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
SNI string `proxy:"sni,omitempty"`
|
SNI string `proxy:"sni,omitempty"`
|
||||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
Headers map[string]string `proxy:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if h.tlsConfig != nil {
|
if h.tlsConfig != nil {
|
||||||
cc := tls.Client(c, h.tlsConfig)
|
cc := tls.Client(c, h.tlsConfig)
|
||||||
err := cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err := cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
@ -53,14 +58,16 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
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)
|
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = h.StreamConn(c, metadata)
|
c, err = h.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -78,11 +85,11 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
Host: addr,
|
Host: addr,
|
||||||
},
|
},
|
||||||
Host: addr,
|
Host: addr,
|
||||||
Header: http.Header{
|
Header: h.Headers.Clone(),
|
||||||
"Proxy-Connection": []string{"Keep-Alive"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Proxy-Connection", "Keep-Alive")
|
||||||
|
|
||||||
if h.user != "" && h.pass != "" {
|
if h.user != "" && h.pass != "" {
|
||||||
auth := h.user + ":" + h.pass
|
auth := h.user + ":" + h.pass
|
||||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||||
@ -129,14 +136,22 @@ func NewHttp(option HttpOption) *Http {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
headers := http.Header{}
|
||||||
|
for name, value := range option.Headers {
|
||||||
|
headers.Add(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
return &Http{
|
return &Http{
|
||||||
Base: &Base{
|
Base: &Base{
|
||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Http,
|
tp: C.Http,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
Headers: headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,13 +15,13 @@ type Reject struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
return NewConn(&NopConn{}, r), nil
|
return NewConn(&nopConn{}, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
return nil, errors.New("match reject rule")
|
return newPacketConn(&nopPacketConn{}, r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReject() *Reject {
|
func NewReject() *Reject {
|
||||||
@ -34,30 +34,29 @@ func NewReject() *Reject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type NopConn struct{}
|
type nopConn struct{}
|
||||||
|
|
||||||
func (rw *NopConn) Read(b []byte) (int, error) {
|
func (rw *nopConn) Read(b []byte) (int, error) {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rw *NopConn) Write(b []byte) (int, error) {
|
func (rw *nopConn) Write(b []byte) (int, error) {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close is fake function for net.Conn
|
func (rw *nopConn) Close() error { return nil }
|
||||||
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 }
|
||||||
|
|
||||||
// LocalAddr is fake function for net.Conn
|
type nopPacketConn struct{}
|
||||||
func (rw *NopConn) LocalAddr() net.Addr { return nil }
|
|
||||||
|
|
||||||
// RemoteAddr is fake function for net.Conn
|
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
|
||||||
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
|
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
|
||||||
|
func (npc *nopPacketConn) Close() error { return nil }
|
||||||
// SetDeadline is fake function for net.Conn
|
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
|
||||||
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
|
func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
// SetReadDeadline is fake function for net.Conn
|
func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
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 }
|
|
||||||
|
@ -10,11 +10,10 @@ import (
|
|||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/transport/shadowsocks/core"
|
||||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||||
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocks struct {
|
type ShadowSocks struct {
|
||||||
@ -28,6 +27,7 @@ type ShadowSocks struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksOption struct {
|
type ShadowSocksOption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -35,7 +35,7 @@ type ShadowSocksOption struct {
|
|||||||
Cipher string `proxy:"cipher"`
|
Cipher string `proxy:"cipher"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Plugin string `proxy:"plugin,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 {
|
type simpleObfsOption struct {
|
||||||
@ -74,22 +74,24 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
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)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
return NewConn(c, ss), err
|
return NewConn(c, ss), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -158,6 +160,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
|||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Shadowsocks,
|
tp: C.Shadowsocks,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
cipher: ciph,
|
cipher: ciph,
|
||||||
|
|
||||||
|
@ -8,12 +8,11 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/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/obfs"
|
||||||
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
||||||
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
|
||||||
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ShadowSocksR struct {
|
type ShadowSocksR struct {
|
||||||
@ -24,6 +23,7 @@ type ShadowSocksR struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ShadowSocksROption struct {
|
type ShadowSocksROption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -59,22 +59,24 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
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)
|
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = ssr.StreamConn(c, metadata)
|
c, err = ssr.StreamConn(c, metadata)
|
||||||
return NewConn(c, ssr), err
|
return NewConn(c, ssr), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -91,6 +93,12 @@ func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
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))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
cipher := option.Cipher
|
cipher := option.Cipher
|
||||||
password := option.Password
|
password := option.Password
|
||||||
@ -102,13 +110,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
|||||||
ivSize int
|
ivSize int
|
||||||
key []byte
|
key []byte
|
||||||
)
|
)
|
||||||
|
|
||||||
if option.Cipher == "dummy" {
|
if option.Cipher == "dummy" {
|
||||||
ivSize = 0
|
ivSize = 0
|
||||||
key = core.Kdf(option.Password, 16)
|
key = core.Kdf(option.Password, 16)
|
||||||
} else {
|
} else {
|
||||||
ciph, ok := coreCiph.(*core.StreamCipher)
|
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
|
return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
|
||||||
}
|
}
|
||||||
ivSize = ciph.IVSize()
|
ivSize = ciph.IVSize()
|
||||||
key = ciph.Key
|
key = ciph.Key
|
||||||
@ -140,6 +149,8 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
|||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.ShadowsocksR,
|
tp: C.ShadowsocksR,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
cipher: coreCiph,
|
cipher: coreCiph,
|
||||||
obfs: obfs,
|
obfs: obfs,
|
||||||
|
@ -22,12 +22,14 @@ type Snell struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SnellOption struct {
|
type SnellOption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Psk string `proxy:"psk"`
|
Psk string `proxy:"psk"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Version int `proxy:"version,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 {
|
type streamOption struct {
|
||||||
@ -51,20 +53,20 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
|||||||
// StreamConn implements C.ProxyAdapter
|
// StreamConn implements C.ProxyAdapter
|
||||||
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
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})
|
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)
|
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
if s.version == snell.Version2 {
|
if s.version == snell.Version2 && len(opts) == 0 {
|
||||||
c, err := s.pool.Get()
|
c, err := s.pool.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
port, _ := strconv.Atoi(metadata.DstPort)
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
|
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
|
||||||
c.Close()
|
c.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -72,18 +74,38 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
|
|||||||
return NewConn(c, s), 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = s.StreamConn(c, metadata)
|
c, err = s.StreamConn(c, metadata)
|
||||||
return NewConn(c, s), err
|
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) {
|
func NewSnell(option SnellOption) (*Snell, error) {
|
||||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
psk := []byte(option.Psk)
|
psk := []byte(option.Psk)
|
||||||
@ -105,7 +127,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
if option.Version == 0 {
|
if option.Version == 0 {
|
||||||
option.Version = snell.DefaultSnellVersion
|
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)
|
return nil, fmt.Errorf("snell version error: %d", option.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +142,9 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
name: option.Name,
|
name: option.Name,
|
||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Snell,
|
tp: C.Snell,
|
||||||
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
psk: psk,
|
psk: psk,
|
||||||
obfsOption: obfsOption,
|
obfsOption: obfsOption,
|
||||||
@ -122,7 +153,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
|||||||
|
|
||||||
if option.Version == snell.Version2 {
|
if option.Version == snell.Version2 {
|
||||||
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -25,6 +24,7 @@ type Socks5 struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Socks5Option struct {
|
type Socks5Option struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -39,7 +39,9 @@ type Socks5Option struct {
|
|||||||
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||||
if ss.tls {
|
if ss.tls {
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
err := cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err := cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
@ -60,14 +62,16 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
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)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = ss.StreamConn(c, metadata)
|
c, err = ss.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -77,11 +81,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Co
|
|||||||
return NewConn(c, ss), nil
|
return NewConn(c, ss), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||||
defer cancel()
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||||
return
|
return
|
||||||
@ -89,11 +91,15 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
|
|
||||||
if ss.tls {
|
if ss.tls {
|
||||||
cc := tls.Client(c, ss.tlsConfig)
|
cc := tls.Client(c, ss.tlsConfig)
|
||||||
err = cc.Handshake()
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||||
|
defer cancel()
|
||||||
|
err = cc.HandshakeContext(ctx)
|
||||||
c = cc
|
c = cc
|
||||||
}
|
}
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
var user *socks5.User
|
var user *socks5.User
|
||||||
@ -110,13 +116,13 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pc, err := dialer.ListenPacket(context.Background(), "udp", "")
|
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(ioutil.Discard, c)
|
io.Copy(io.Discard, c)
|
||||||
c.Close()
|
c.Close()
|
||||||
// A UDP association terminates when the TCP connection that the UDP
|
// A UDP association terminates when the TCP connection that the UDP
|
||||||
// ASSOCIATE request arrived on terminates. RFC1928
|
// ASSOCIATE request arrived on terminates. RFC1928
|
||||||
@ -155,6 +161,8 @@ func NewSocks5(option Socks5Option) *Socks5 {
|
|||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Socks5,
|
tp: C.Socks5,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
@ -18,6 +19,7 @@ import (
|
|||||||
type Trojan struct {
|
type Trojan struct {
|
||||||
*Base
|
*Base
|
||||||
instance *trojan.Trojan
|
instance *trojan.Trojan
|
||||||
|
option *TrojanOption
|
||||||
|
|
||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
gunTLSConfig *tls.Config
|
||||||
@ -26,6 +28,7 @@ type Trojan struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TrojanOption struct {
|
type TrojanOption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
@ -36,6 +39,34 @@ type TrojanOption struct {
|
|||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,omitempty"`
|
Network string `proxy:"network,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,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
|
// StreamConn implements C.ProxyAdapter
|
||||||
@ -44,7 +75,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
if t.transport != nil {
|
if t.transport != nil {
|
||||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
||||||
} else {
|
} else {
|
||||||
c, err = t.instance.StreamConn(c)
|
c, err = t.plainStream(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -56,9 +87,9 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
// gun transport
|
// gun transport
|
||||||
if t.transport != nil {
|
if t.transport != nil && len(opts) == 0 {
|
||||||
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -72,13 +103,15 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
|||||||
return NewConn(c, t), nil
|
return NewConn(c, t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
|
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = t.StreamConn(c, metadata)
|
c, err = t.StreamConn(c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -88,27 +121,29 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
|||||||
return NewConn(c, t), err
|
return NewConn(c, t), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
|
|
||||||
// grpc transport
|
// grpc transport
|
||||||
if t.transport != nil {
|
if t.transport != nil && len(opts) == 0 {
|
||||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
} else {
|
} else {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
|
||||||
defer cancel()
|
|
||||||
c, err = dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
c, err = t.instance.StreamConn(c)
|
c, err = t.plainStream(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
@ -143,13 +178,16 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
addr: addr,
|
addr: addr,
|
||||||
tp: C.Trojan,
|
tp: C.Trojan,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
instance: trojan.New(tOption),
|
instance: trojan.New(tOption),
|
||||||
|
option: &option,
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.Network == "grpc" {
|
if option.Network == "grpc" {
|
||||||
dialFn := func(network, addr string) (net.Conn, error) {
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
|
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package outbound
|
package outbound
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -9,6 +8,8 @@ import (
|
|||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/socks5"
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/protobytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func tcpKeepAlive(c net.Conn) {
|
func tcpKeepAlive(c net.Conn) {
|
||||||
@ -19,23 +20,24 @@ func tcpKeepAlive(c net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||||
var buf [][]byte
|
buf := protobytes.BytesWriter{}
|
||||||
aType := uint8(metadata.AddrType)
|
|
||||||
p, _ := strconv.Atoi(metadata.DstPort)
|
addrType := metadata.AddrType()
|
||||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
buf.PutUint8(uint8(addrType))
|
||||||
switch metadata.AddrType {
|
|
||||||
|
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
|
switch addrType {
|
||||||
case socks5.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
len := uint8(len(metadata.Host))
|
buf.PutUint8(uint8(len(metadata.Host)))
|
||||||
host := []byte(metadata.Host)
|
buf.PutString(metadata.Host)
|
||||||
buf = [][]byte{{aType, len}, host, port}
|
|
||||||
case socks5.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
host := metadata.DstIP.To4()
|
buf.PutSlice(metadata.DstIP.To4())
|
||||||
buf = [][]byte{{aType}, host, port}
|
|
||||||
case socks5.AtypIPv6:
|
case socks5.AtypIPv6:
|
||||||
host := metadata.DstIP.To16()
|
buf.PutSlice(metadata.DstIP.To16())
|
||||||
buf = [][]byte{{aType}, host, port}
|
|
||||||
}
|
}
|
||||||
return bytes.Join(buf, nil)
|
|
||||||
|
buf.PutUint16be(uint16(p))
|
||||||
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
||||||
|
@ -14,11 +14,14 @@ import (
|
|||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/transport/gun"
|
"github.com/Dreamacro/clash/transport/gun"
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
"github.com/Dreamacro/clash/transport/vmess"
|
"github.com/Dreamacro/clash/transport/vmess"
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
|
||||||
|
|
||||||
type Vmess struct {
|
type Vmess struct {
|
||||||
*Base
|
*Base
|
||||||
client *vmess.Client
|
client *vmess.Client
|
||||||
@ -31,23 +34,22 @@ type Vmess struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VmessOption struct {
|
type VmessOption struct {
|
||||||
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
UUID string `proxy:"uuid"`
|
UUID string `proxy:"uuid"`
|
||||||
AlterID int `proxy:"alterId"`
|
AlterID int `proxy:"alterId"`
|
||||||
Cipher string `proxy:"cipher"`
|
Cipher string `proxy:"cipher"`
|
||||||
TLS bool `proxy:"tls,omitempty"`
|
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
Network string `proxy:"network,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"`
|
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||||
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
|
WSOpts WSOptions `proxy:"ws-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 {
|
type HTTPOptions struct {
|
||||||
@ -77,13 +79,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
var err error
|
var err error
|
||||||
switch v.option.Network {
|
switch v.option.Network {
|
||||||
case "ws":
|
case "ws":
|
||||||
if v.option.WSOpts.Path == "" {
|
|
||||||
v.option.WSOpts.Path = v.option.WSPath
|
|
||||||
}
|
|
||||||
if len(v.option.WSOpts.Headers) == 0 {
|
|
||||||
v.option.WSOpts.Headers = v.option.WSHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
host, port, _ := net.SplitHostPort(v.addr)
|
host, port, _ := net.SplitHostPort(v.addr)
|
||||||
wsOpts := &vmess.WebsocketConfig{
|
wsOpts := &vmess.WebsocketConfig{
|
||||||
Host: host,
|
Host: host,
|
||||||
@ -95,7 +90,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
|
|
||||||
if len(v.option.WSOpts.Headers) != 0 {
|
if len(v.option.WSOpts.Headers) != 0 {
|
||||||
header := http.Header{}
|
header := http.Header{}
|
||||||
for key, value := range v.option.WSHeaders {
|
for key, value := range v.option.WSOpts.Headers {
|
||||||
header.Add(key, value)
|
header.Add(key, value)
|
||||||
}
|
}
|
||||||
wsOpts.Headers = header
|
wsOpts.Headers = header
|
||||||
@ -103,8 +98,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
|
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
wsOpts.TLS = true
|
wsOpts.TLS = true
|
||||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
wsOpts.TLSConfig = &tls.Config{
|
||||||
wsOpts.ServerName = v.option.ServerName
|
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)
|
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||||
case "http":
|
case "http":
|
||||||
@ -185,14 +188,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.transport != nil && len(opts) == 0 {
|
||||||
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -202,19 +207,21 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
|
|||||||
return NewConn(c, v), nil
|
return NewConn(c, v), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
c, err = v.StreamConn(c, metadata)
|
||||||
return NewConn(c, v), err
|
return NewConn(c, v), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
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
|
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
|
||||||
if !metadata.Resolved() {
|
if !metadata.Resolved() {
|
||||||
ip, err := resolver.ResolveIP(metadata.Host)
|
ip, err := resolver.ResolveIP(metadata.Host)
|
||||||
@ -226,23 +233,25 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
|||||||
|
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.transport != nil && len(opts) == 0 {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||||
} else {
|
} else {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
|
||||||
defer cancel()
|
|
||||||
c, err = dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
|
|
||||||
c, err = v.StreamConn(c, metadata)
|
c, err = v.StreamConn(c, metadata)
|
||||||
}
|
}
|
||||||
@ -281,6 +290,8 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||||
tp: C.Vmess,
|
tp: C.Vmess,
|
||||||
udp: option.UDP,
|
udp: option.UDP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
},
|
},
|
||||||
client: client,
|
client: client,
|
||||||
option: &option,
|
option: &option,
|
||||||
@ -293,7 +304,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
}
|
}
|
||||||
case "grpc":
|
case "grpc":
|
||||||
dialFn := func(network, addr string) (net.Conn, error) {
|
dialFn := func(network, addr string) (net.Conn, error) {
|
||||||
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
|
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
@ -327,23 +338,23 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||||
var addrType byte
|
var addrType byte
|
||||||
var addr []byte
|
var addr []byte
|
||||||
switch metadata.AddrType {
|
switch metadata.AddrType() {
|
||||||
case C.AtypIPv4:
|
case socks5.AtypIPv4:
|
||||||
addrType = byte(vmess.AtypIPv4)
|
addrType = vmess.AtypIPv4
|
||||||
addr = make([]byte, net.IPv4len)
|
addr = make([]byte, net.IPv4len)
|
||||||
copy(addr[:], metadata.DstIP.To4())
|
copy(addr[:], metadata.DstIP.To4())
|
||||||
case C.AtypIPv6:
|
case socks5.AtypIPv6:
|
||||||
addrType = byte(vmess.AtypIPv6)
|
addrType = vmess.AtypIPv6
|
||||||
addr = make([]byte, net.IPv6len)
|
addr = make([]byte, net.IPv6len)
|
||||||
copy(addr[:], metadata.DstIP.To16())
|
copy(addr[:], metadata.DstIP.To16())
|
||||||
case C.AtypDomainName:
|
case socks5.AtypDomainName:
|
||||||
addrType = byte(vmess.AtypDomainName)
|
addrType = vmess.AtypDomainName
|
||||||
addr = make([]byte, len(metadata.Host)+1)
|
addr = make([]byte, len(metadata.Host)+1)
|
||||||
addr[0] = byte(len(metadata.Host))
|
addr[0] = byte(len(metadata.Host))
|
||||||
copy(addr[1:], []byte(metadata.Host))
|
copy(addr[1:], []byte(metadata.Host))
|
||||||
}
|
}
|
||||||
|
|
||||||
port, _ := strconv.Atoi(metadata.DstPort)
|
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
return &vmess.DstAddr{
|
return &vmess.DstAddr{
|
||||||
UDP: metadata.NetWork == C.UDP,
|
UDP: metadata.NetWork == C.UDP,
|
||||||
AddrType: addrType,
|
AddrType: addrType,
|
||||||
@ -357,7 +368,14 @@ type vmessPacketConn struct {
|
|||||||
rAddr net.Addr
|
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) {
|
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)
|
return uc.Conn.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,14 +11,19 @@ const (
|
|||||||
defaultGetProxiesDuration = time.Second * 5
|
defaultGetProxiesDuration = time.Second * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func touchProviders(providers []provider.ProxyProvider) {
|
||||||
|
for _, provider := range providers {
|
||||||
|
provider.Touch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
||||||
proxies := []C.Proxy{}
|
proxies := []C.Proxy{}
|
||||||
for _, provider := range providers {
|
for _, provider := range providers {
|
||||||
if touch {
|
if touch {
|
||||||
proxies = append(proxies, provider.ProxiesWithTouch()...)
|
provider.Touch()
|
||||||
} else {
|
|
||||||
proxies = append(proxies, provider.Proxies()...)
|
|
||||||
}
|
}
|
||||||
|
proxies = append(proxies, provider.Proxies()...)
|
||||||
}
|
}
|
||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
@ -23,19 +24,19 @@ func (f *Fallback) Now() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
proxy := f.findAliveProxy(true)
|
proxy := f.findAliveProxy(true)
|
||||||
c, err := proxy.DialContext(ctx, metadata)
|
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(f)
|
c.AppendToChains(f)
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
proxy := f.findAliveProxy(true)
|
proxy := f.findAliveProxy(true)
|
||||||
pc, err := proxy.DialUDP(metadata)
|
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(f)
|
pc.AppendToChains(f)
|
||||||
}
|
}
|
||||||
@ -58,7 +59,7 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
|||||||
for _, proxy := range f.proxies(false) {
|
for _, proxy := range f.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": f.Type().String(),
|
"type": f.Type().String(),
|
||||||
"now": f.Now(),
|
"now": f.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
@ -72,7 +73,7 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) proxies(touch bool) []C.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
|
return getProvidersProxies(f.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -90,11 +91,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
|||||||
return proxies[0]
|
return proxies[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||||
return &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),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
disableUDP: options.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/murmur3"
|
"github.com/Dreamacro/clash/common/murmur3"
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
@ -28,12 +29,10 @@ type LoadBalance struct {
|
|||||||
|
|
||||||
var errStrategy = errors.New("unsupported strategy")
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
|
||||||
func parseStrategy(config map[string]interface{}) string {
|
func parseStrategy(config map[string]any) string {
|
||||||
if elm, ok := config["strategy"]; ok {
|
if strategy, ok := config["strategy"].(string); ok {
|
||||||
if strategy, ok := elm.(string); ok {
|
|
||||||
return strategy
|
return strategy
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return "consistent-hashing"
|
return "consistent-hashing"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +68,7 @@ func jumpHash(key uint64, buckets int32) int32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(lb)
|
c.AppendToChains(lb)
|
||||||
@ -78,12 +77,12 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
|
|||||||
|
|
||||||
proxy := lb.Unwrap(metadata)
|
proxy := lb.Unwrap(metadata)
|
||||||
|
|
||||||
c, err = proxy.DialContext(ctx, metadata)
|
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(lb)
|
pc.AppendToChains(lb)
|
||||||
@ -91,8 +90,7 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
proxy := lb.Unwrap(metadata)
|
proxy := lb.Unwrap(metadata)
|
||||||
|
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
|
||||||
return proxy.DialUDP(metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportUDP implements C.ProxyAdapter
|
// SupportUDP implements C.ProxyAdapter
|
||||||
@ -129,6 +127,13 @@ 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]
|
return proxies[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +145,7 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
|
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 getProvidersProxies(lb.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -153,13 +158,13 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
|||||||
for _, proxy := range lb.proxies(false) {
|
for _, proxy := range lb.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": lb.Type().String(),
|
"type": lb.Type().String(),
|
||||||
"all": all,
|
"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
|
var strategyFn strategyFn
|
||||||
switch strategy {
|
switch strategy {
|
||||||
case "consistent-hashing":
|
case "consistent-hashing":
|
||||||
@ -170,10 +175,15 @@ func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvid
|
|||||||
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||||
}
|
}
|
||||||
return &LoadBalance{
|
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),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
strategyFn: strategyFn,
|
strategyFn: strategyFn,
|
||||||
disableUDP: options.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,13 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/adapter/provider"
|
"github.com/Dreamacro/clash/adapter/provider"
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
|
regexp "github.com/dlclark/regexp2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -15,10 +18,11 @@ var (
|
|||||||
errType = errors.New("unsupport type")
|
errType = errors.New("unsupport type")
|
||||||
errMissProxy = errors.New("`use` or `proxies` missing")
|
errMissProxy = errors.New("`use` or `proxies` missing")
|
||||||
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
||||||
errDuplicateProvider = errors.New("`duplicate provider name")
|
errDuplicateProvider = errors.New("duplicate provider name")
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupCommonOption struct {
|
type GroupCommonOption struct {
|
||||||
|
outbound.BasicOption
|
||||||
Name string `group:"name"`
|
Name string `group:"name"`
|
||||||
Type string `group:"type"`
|
Type string `group:"type"`
|
||||||
Proxies []string `group:"proxies,omitempty"`
|
Proxies []string `group:"proxies,omitempty"`
|
||||||
@ -27,9 +31,10 @@ type GroupCommonOption struct {
|
|||||||
Interval int `group:"interval,omitempty"`
|
Interval int `group:"interval,omitempty"`
|
||||||
Lazy bool `group:"lazy,omitempty"`
|
Lazy bool `group:"lazy,omitempty"`
|
||||||
DisableUDP bool `group:"disable-udp,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]types.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})
|
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
||||||
|
|
||||||
groupOption := &GroupCommonOption{
|
groupOption := &GroupCommonOption{
|
||||||
@ -43,32 +48,33 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
return nil, errFormat
|
return nil, errFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
groupName := groupOption.Name
|
var (
|
||||||
|
groupName = groupOption.Name
|
||||||
|
filterReg *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
providers := []types.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 {
|
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 {
|
if len(groupOption.Proxies) != 0 {
|
||||||
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
||||||
if err != nil {
|
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 {
|
if _, ok := providersMap[groupName]; ok {
|
||||||
return nil, errDuplicateProvider
|
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
// select don't need health check
|
// select don't need health check
|
||||||
@ -76,35 +82,39 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
providers = append(providers, pd)
|
providers = append(providers, pd)
|
||||||
providersMap[groupName] = pd
|
providersMap[groupName] = pd
|
||||||
} else {
|
} else {
|
||||||
if groupOption.URL == "" || groupOption.Interval == 0 {
|
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)
|
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("%s: %w", groupName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
providers = append(providers, pd)
|
providers = append(providers, pd)
|
||||||
providersMap[groupName] = pd
|
providersMap[groupName] = pd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(groupOption.Use) != 0 {
|
if len(groupOption.Use) != 0 {
|
||||||
list, err := getProviders(providersMap, groupOption.Use)
|
list, err := getProviders(providersMap, groupOption.Use)
|
||||||
if err != nil {
|
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...)
|
providers = append(providers, list...)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var group C.ProxyAdapter
|
var group C.ProxyAdapter
|
||||||
switch groupOption.Type {
|
switch groupOption.Type {
|
||||||
@ -121,7 +131,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
|
|||||||
case "relay":
|
case "relay":
|
||||||
group = NewRelay(groupOption, providers)
|
group = NewRelay(groupOption, providers)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return group, nil
|
return group, nil
|
||||||
|
@ -19,7 +19,7 @@ type Relay struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
var proxies []C.Proxy
|
var proxies []C.Proxy
|
||||||
for _, proxy := range r.proxies(metadata, true) {
|
for _, proxy := range r.proxies(metadata, true) {
|
||||||
if proxy.Type() != C.Direct {
|
if proxy.Type() != C.Direct {
|
||||||
@ -29,15 +29,15 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
|
|||||||
|
|
||||||
switch len(proxies) {
|
switch len(proxies) {
|
||||||
case 0:
|
case 0:
|
||||||
return outbound.NewDirect().DialContext(ctx, metadata)
|
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
||||||
case 1:
|
case 1:
|
||||||
return proxies[0].DialContext(ctx, metadata)
|
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
first := proxies[0]
|
first := proxies[0]
|
||||||
last := proxies[len(proxies)-1]
|
last := proxies[len(proxies)-1]
|
||||||
|
|
||||||
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
|
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||||
}
|
}
|
||||||
@ -72,14 +72,14 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
|
|||||||
for _, proxy := range r.rawProxies(false) {
|
for _, proxy := range r.rawProxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": r.Type().String(),
|
"type": r.Type().String(),
|
||||||
"all": all,
|
"all": all,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||||
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
elm, _, _ := r.single.Do(func() (any, error) {
|
||||||
return getProvidersProxies(r.providers, touch), nil
|
return getProvidersProxies(r.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -100,9 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
|||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||||
return &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),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
@ -20,17 +21,17 @@ type Selector struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||||
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(s)
|
c.AppendToChains(s)
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := s.selectedProxy(true).DialUDP(metadata)
|
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(s)
|
pc.AppendToChains(s)
|
||||||
}
|
}
|
||||||
@ -53,7 +54,7 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
|
|||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": s.Type().String(),
|
"type": s.Type().String(),
|
||||||
"now": s.Now(),
|
"now": s.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
@ -82,7 +83,7 @@ func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
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)
|
proxies := getProvidersProxies(s.providers, touch)
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if proxy.Name() == s.selected {
|
if proxy.Name() == s.selected {
|
||||||
@ -96,13 +97,18 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
|||||||
return elm.(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()
|
selected := providers[0].Proxies()[0].Name()
|
||||||
return &Selector{
|
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),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
disableUDP: options.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/singledo"
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
@ -34,17 +35,17 @@ func (u *URLTest) Now() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
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)
|
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.AppendToChains(u)
|
c.AppendToChains(u)
|
||||||
}
|
}
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialUDP implements C.ProxyAdapter
|
// ListenPacketContext implements C.ProxyAdapter
|
||||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||||
pc, err := u.fast(true).DialUDP(metadata)
|
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pc.AppendToChains(u)
|
pc.AppendToChains(u)
|
||||||
}
|
}
|
||||||
@ -57,7 +58,7 @@ func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) proxies(touch bool) []C.Proxy {
|
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
|
return getProvidersProxies(u.providers, touch), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ func (u *URLTest) proxies(touch bool) []C.Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *URLTest) fast(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)
|
proxies := u.proxies(touch)
|
||||||
fast := proxies[0]
|
fast := proxies[0]
|
||||||
min := fast.LastDelay()
|
min := fast.LastDelay()
|
||||||
@ -94,6 +95,9 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
|||||||
|
|
||||||
return u.fastNode, nil
|
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)
|
return elm.(C.Proxy)
|
||||||
}
|
}
|
||||||
@ -113,33 +117,36 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
|
|||||||
for _, proxy := range u.proxies(false) {
|
for _, proxy := range u.proxies(false) {
|
||||||
all = append(all, proxy.Name())
|
all = append(all, proxy.Name())
|
||||||
}
|
}
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"type": u.Type().String(),
|
"type": u.Type().String(),
|
||||||
"now": u.Now(),
|
"now": u.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
|
func parseURLTestOption(config map[string]any) []urlTestOption {
|
||||||
opts := []urlTestOption{}
|
opts := []urlTestOption{}
|
||||||
|
|
||||||
// tolerance
|
// tolerance
|
||||||
if elm, ok := config["tolerance"]; ok {
|
if tolerance, ok := config["tolerance"].(int); ok {
|
||||||
if tolerance, ok := elm.(int); ok {
|
|
||||||
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||||
urlTest := &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),
|
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||||
fastSingle: singledo.NewSingle(time.Second * 10),
|
fastSingle: singledo.NewSingle(time.Second * 10),
|
||||||
providers: providers,
|
providers: providers,
|
||||||
disableUDP: commonOptions.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
|
@ -18,7 +18,6 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypDomainName,
|
|
||||||
Host: host,
|
Host: host,
|
||||||
DstIP: nil,
|
DstIP: nil,
|
||||||
DstPort: port,
|
DstPort: port,
|
||||||
@ -26,7 +25,6 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
return
|
return
|
||||||
} else if ip4 := ip.To4(); ip4 != nil {
|
} else if ip4 := ip.To4(); ip4 != nil {
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypIPv4,
|
|
||||||
Host: "",
|
Host: "",
|
||||||
DstIP: ip4,
|
DstIP: ip4,
|
||||||
DstPort: port,
|
DstPort: port,
|
||||||
@ -35,7 +33,6 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypIPv6,
|
|
||||||
Host: "",
|
Host: "",
|
||||||
DstIP: ip,
|
DstIP: ip,
|
||||||
DstPort: port,
|
DstPort: port,
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
C "github.com/Dreamacro/clash/constant"
|
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})
|
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||||
proxyType, existType := mapping["type"].(string)
|
proxyType, existType := mapping["type"].(string)
|
||||||
if !existType {
|
if !existType {
|
||||||
|
@ -3,7 +3,6 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@ -13,21 +12,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileMode os.FileMode = 0666
|
fileMode os.FileMode = 0o666
|
||||||
dirMode os.FileMode = 0755
|
dirMode os.FileMode = 0o755
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser = func([]byte) (interface{}, error)
|
type parser = func([]byte) (any, error)
|
||||||
|
|
||||||
type fetcher struct {
|
type fetcher struct {
|
||||||
name string
|
name string
|
||||||
vehicle types.Vehicle
|
vehicle types.Vehicle
|
||||||
|
interval time.Duration
|
||||||
updatedAt *time.Time
|
updatedAt *time.Time
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
hash [16]byte
|
hash [16]byte
|
||||||
parser parser
|
parser parser
|
||||||
onUpdate func(interface{})
|
onUpdate func(any)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Name() string {
|
func (f *fetcher) Name() string {
|
||||||
@ -38,17 +38,19 @@ func (f *fetcher) VehicleType() types.VehicleType {
|
|||||||
return f.vehicle.Type()
|
return f.vehicle.Type()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Initial() (interface{}, error) {
|
func (f *fetcher) Initial() (any, error) {
|
||||||
var (
|
var (
|
||||||
buf []byte
|
buf []byte
|
||||||
err error
|
err error
|
||||||
isLocal bool
|
isLocal bool
|
||||||
|
immediatelyUpdate bool
|
||||||
)
|
)
|
||||||
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
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()
|
modTime := stat.ModTime()
|
||||||
f.updatedAt = &modTime
|
f.updatedAt = &modTime
|
||||||
isLocal = true
|
isLocal = true
|
||||||
|
immediatelyUpdate = time.Since(modTime) > f.interval
|
||||||
} else {
|
} else {
|
||||||
buf, err = f.vehicle.Read()
|
buf, err = f.vehicle.Read()
|
||||||
}
|
}
|
||||||
@ -87,13 +89,13 @@ func (f *fetcher) Initial() (interface{}, error) {
|
|||||||
|
|
||||||
// pull proxies automatically
|
// pull proxies automatically
|
||||||
if f.ticker != nil {
|
if f.ticker != nil {
|
||||||
go f.pullLoop()
|
go f.pullLoop(immediatelyUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxies, nil
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) Update() (interface{}, bool, error) {
|
func (f *fetcher) Update() (any, bool, error) {
|
||||||
buf, err := f.vehicle.Read()
|
buf, err := f.vehicle.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@ -103,6 +105,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
|
|||||||
hash := md5.Sum(buf)
|
hash := md5.Sum(buf)
|
||||||
if bytes.Equal(f.hash[:], hash[:]) {
|
if bytes.Equal(f.hash[:], hash[:]) {
|
||||||
f.updatedAt = &now
|
f.updatedAt = &now
|
||||||
|
os.Chtimes(f.vehicle.Path(), now, now)
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,25 +133,33 @@ func (f *fetcher) Destroy() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) pullLoop() {
|
func (f *fetcher) pullLoop(immediately bool) {
|
||||||
for {
|
update := func() {
|
||||||
select {
|
|
||||||
case <-f.ticker.C:
|
|
||||||
elm, same, err := f.Update()
|
elm, same, err := f.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if same {
|
if same {
|
||||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
if f.onUpdate != nil {
|
if f.onUpdate != nil {
|
||||||
f.onUpdate(elm)
|
f.onUpdate(elm)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if immediately {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.ticker.C:
|
||||||
|
update()
|
||||||
case <-f.done:
|
case <-f.done:
|
||||||
f.ticker.Stop()
|
f.ticker.Stop()
|
||||||
return
|
return
|
||||||
@ -165,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 types.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
|
var ticker *time.Ticker
|
||||||
if interval != 0 {
|
if interval != 0 {
|
||||||
ticker = time.NewTicker(interval)
|
ticker = time.NewTicker(interval)
|
||||||
@ -178,6 +189,7 @@ func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, pars
|
|||||||
name: name,
|
name: name,
|
||||||
ticker: ticker,
|
ticker: ticker,
|
||||||
vehicle: vehicle,
|
vehicle: vehicle,
|
||||||
|
interval: interval,
|
||||||
parser: parser,
|
parser: parser,
|
||||||
done: make(chan struct{}, 1),
|
done: make(chan struct{}, 1),
|
||||||
onUpdate: onUpdate,
|
onUpdate: onUpdate,
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/Dreamacro/clash/common/batch"
|
"github.com/Dreamacro/clash/common/batch"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
|
"github.com/samber/lo"
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,13 +32,20 @@ type HealthCheck struct {
|
|||||||
func (hc *HealthCheck) process() {
|
func (hc *HealthCheck) process() {
|
||||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||||
|
|
||||||
go hc.check()
|
go hc.checkAll()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
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:
|
case <-hc.done:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
@ -58,11 +66,15 @@ func (hc *HealthCheck) touch() {
|
|||||||
hc.lastTouch.Store(time.Now().Unix())
|
hc.lastTouch.Store(time.Now().Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
func (hc *HealthCheck) checkAll() {
|
||||||
|
hc.check(hc.proxies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) check(proxies []C.Proxy) {
|
||||||
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
|
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
|
||||||
for _, proxy := range hc.proxies {
|
for _, proxy := range proxies {
|
||||||
p := proxy
|
p := proxy
|
||||||
b.Go(p.Name(), func() (interface{}, error) {
|
b.Go(p.Name(), func() (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
p.URLTest(ctx, hc.url)
|
p.URLTest(ctx, hc.url)
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
errVehicleType = errors.New("unsupport vehicle type")
|
errVehicleType = errors.New("unsupport vehicle type")
|
||||||
|
errSubPath = errors.New("path is not subpath of home directory")
|
||||||
)
|
)
|
||||||
|
|
||||||
type healthCheckSchema struct {
|
type healthCheckSchema struct {
|
||||||
@ -26,10 +27,11 @@ type proxyProviderSchema struct {
|
|||||||
Path string `provider:"path"`
|
Path string `provider:"path"`
|
||||||
URL string `provider:"url,omitempty"`
|
URL string `provider:"url,omitempty"`
|
||||||
Interval int `provider:"interval,omitempty"`
|
Interval int `provider:"interval,omitempty"`
|
||||||
|
Filter string `provider:"filter,omitempty"`
|
||||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyProvider(name string, mapping map[string]interface{}) (types.ProxyProvider, error) {
|
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
|
||||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||||
|
|
||||||
schema := &proxyProviderSchema{
|
schema := &proxyProviderSchema{
|
||||||
@ -54,11 +56,15 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (types.Prox
|
|||||||
case "file":
|
case "file":
|
||||||
vehicle = NewFileVehicle(path)
|
vehicle = NewFileVehicle(path)
|
||||||
case "http":
|
case "http":
|
||||||
|
if !C.Path.IsSubPath(path) {
|
||||||
|
return nil, fmt.Errorf("%w: %s", errSubPath, path)
|
||||||
|
}
|
||||||
vehicle = NewHTTPVehicle(schema.URL, path)
|
vehicle = NewHTTPVehicle(schema.URL, path)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||||
return NewProxySetProvider(name, interval, vehicle, hc), nil
|
filter := schema.Filter
|
||||||
|
return NewProxySetProvider(name, interval, filter, vehicle, hc)
|
||||||
}
|
}
|
||||||
|
@ -8,18 +8,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter"
|
"github.com/Dreamacro/clash/adapter"
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/common/singledo"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
regexp "github.com/dlclark/regexp2"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var reject = adapter.NewProxy(outbound.NewReject())
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ReservedName = "default"
|
ReservedName = "default"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProxySchema struct {
|
type ProxySchema struct {
|
||||||
Proxies []map[string]interface{} `yaml:"proxies"`
|
Proxies []map[string]any `yaml:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// for auto gc
|
// for auto gc
|
||||||
@ -34,7 +40,7 @@ type proxySetProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"name": pp.Name(),
|
"name": pp.Name(),
|
||||||
"type": pp.Type().String(),
|
"type": pp.Type().String(),
|
||||||
"vehicleType": pp.VehicleType().String(),
|
"vehicleType": pp.VehicleType().String(),
|
||||||
@ -48,7 +54,7 @@ func (pp *proxySetProvider) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) HealthCheck() {
|
func (pp *proxySetProvider) HealthCheck() {
|
||||||
pp.healthCheck.check()
|
pp.healthCheck.checkAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) Update() error {
|
func (pp *proxySetProvider) Update() error {
|
||||||
@ -77,12 +83,44 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
|
|||||||
return pp.proxies
|
return pp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
func (pp *proxySetProvider) Touch() {
|
||||||
pp.healthCheck.touch()
|
pp.healthCheck.touch()
|
||||||
return pp.Proxies()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxiesParse(buf []byte) (interface{}, error) {
|
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{}
|
schema := &ProxySchema{}
|
||||||
|
|
||||||
if err := yaml.Unmarshal(buf, schema); err != nil {
|
if err := yaml.Unmarshal(buf, schema); err != nil {
|
||||||
@ -95,6 +133,15 @@ func proxiesParse(buf []byte) (interface{}, error) {
|
|||||||
|
|
||||||
proxies := []C.Proxy{}
|
proxies := []C.Proxy{}
|
||||||
for idx, mapping := range schema.Proxies {
|
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)
|
proxy, err := adapter.ParseProxy(mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
|
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
|
||||||
@ -103,46 +150,21 @@ func proxiesParse(buf []byte) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(proxies) == 0 {
|
if len(proxies) == 0 {
|
||||||
return nil, errors.New("file doesn't have any valid proxy")
|
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
|
return proxies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
|
||||||
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 types.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
|
pd.fetcher = fetcher
|
||||||
|
|
||||||
wrapper := &ProxySetProvider{pd}
|
wrapper := &ProxySetProvider{pd}
|
||||||
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
||||||
return wrapper
|
return wrapper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// for auto gc
|
// for auto gc
|
||||||
@ -157,7 +179,7 @@ type compatibleProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]any{
|
||||||
"name": cp.Name(),
|
"name": cp.Name(),
|
||||||
"type": cp.Type().String(),
|
"type": cp.Type().String(),
|
||||||
"vehicleType": cp.VehicleType().String(),
|
"vehicleType": cp.VehicleType().String(),
|
||||||
@ -170,7 +192,7 @@ func (cp *compatibleProvider) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) HealthCheck() {
|
func (cp *compatibleProvider) HealthCheck() {
|
||||||
cp.healthCheck.check()
|
cp.healthCheck.checkAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) Update() error {
|
func (cp *compatibleProvider) Update() error {
|
||||||
@ -193,9 +215,8 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
|
|||||||
return cp.proxies
|
return cp.proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
func (cp *compatibleProvider) Touch() {
|
||||||
cp.healthCheck.touch()
|
cp.healthCheck.touch()
|
||||||
return cp.Proxies()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
@ -221,3 +242,81 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
|
|||||||
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
||||||
return wrapper, nil
|
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,10 +2,11 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
@ -25,7 +26,7 @@ func (f *FileVehicle) Path() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileVehicle) Read() ([]byte, error) {
|
func (f *FileVehicle) Read() ([]byte, error) {
|
||||||
return ioutil.ReadFile(f.path)
|
return os.ReadFile(f.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileVehicle(path string) *FileVehicle {
|
func NewFileVehicle(path string) *FileVehicle {
|
||||||
@ -84,7 +85,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(resp.Body)
|
buf, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import (
|
|||||||
type Option = func(b *Batch)
|
type Option = func(b *Batch)
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Value interface{}
|
Value any
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ type Batch struct {
|
|||||||
cancel func()
|
cancel func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Batch) Go(key string, fn func() (interface{}, error)) {
|
func (b *Batch) Go(key string, fn func() (any, error)) {
|
||||||
b.wg.Add(1)
|
b.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer b.wg.Done()
|
defer b.wg.Done()
|
||||||
|
@ -14,11 +14,11 @@ func TestBatch(t *testing.T) {
|
|||||||
b, _ := New(context.Background())
|
b, _ := New(context.Background())
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
b.Go("foo", func() (interface{}, error) {
|
b.Go("foo", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return "foo", nil
|
return "foo", nil
|
||||||
})
|
})
|
||||||
b.Go("bar", func() (interface{}, error) {
|
b.Go("bar", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 150)
|
time.Sleep(time.Millisecond * 150)
|
||||||
return "bar", nil
|
return "bar", nil
|
||||||
})
|
})
|
||||||
@ -45,7 +45,7 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
for i := 0; i < 7; i++ {
|
for i := 0; i < 7; i++ {
|
||||||
idx := i
|
idx := i
|
||||||
b.Go(strconv.Itoa(idx), func() (interface{}, error) {
|
b.Go(strconv.Itoa(idx), func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return strconv.Itoa(idx), nil
|
return strconv.Itoa(idx), nil
|
||||||
})
|
})
|
||||||
@ -64,12 +64,12 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
|
|||||||
func TestBatchContext(t *testing.T) {
|
func TestBatchContext(t *testing.T) {
|
||||||
b, ctx := New(context.Background())
|
b, ctx := New(context.Background())
|
||||||
|
|
||||||
b.Go("error", func() (interface{}, error) {
|
b.Go("error", func() (any, error) {
|
||||||
time.Sleep(time.Millisecond * 100)
|
time.Sleep(time.Millisecond * 100)
|
||||||
return nil, errors.New("test error")
|
return nil, errors.New("test error")
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Go("ctx", func() (interface{}, error) {
|
b.Go("ctx", func() (any, error) {
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
return nil, ctx.Err()
|
return nil, 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)
|
type Option func(*LruCache)
|
||||||
|
|
||||||
// EvictCallback is used to get a callback when a cache entry is evicted
|
// EvictCallback is used to get a callback when a cache entry is evicted
|
||||||
type EvictCallback = func(key interface{}, value interface{})
|
type EvictCallback = func(key any, value any)
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict(cb EvictCallback) Option {
|
func WithEvict(cb EvictCallback) Option {
|
||||||
@ -57,18 +57,18 @@ type LruCache struct {
|
|||||||
maxAge int64
|
maxAge int64
|
||||||
maxSize int
|
maxSize int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cache map[interface{}]*list.Element
|
cache map[any]*list.Element
|
||||||
lru *list.List // Front is least-recent
|
lru *list.List // Front is least-recent
|
||||||
updateAgeOnGet bool
|
updateAgeOnGet bool
|
||||||
staleReturn bool
|
staleReturn bool
|
||||||
onEvict EvictCallback
|
onEvict EvictCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLRUCache creates an LruCache
|
// New creates an LruCache
|
||||||
func NewLRUCache(options ...Option) *LruCache {
|
func New(options ...Option) *LruCache {
|
||||||
lc := &LruCache{
|
lc := &LruCache{
|
||||||
lru: list.New(),
|
lru: list.New(),
|
||||||
cache: make(map[interface{}]*list.Element),
|
cache: make(map[any]*list.Element),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
@ -78,9 +78,9 @@ func NewLRUCache(options ...Option) *LruCache {
|
|||||||
return lc
|
return lc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the interface{} representation of a cached response and a bool
|
// Get returns the any representation of a cached response and a bool
|
||||||
// set to true if the key was found.
|
// set to true if the key was found.
|
||||||
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
func (c *LruCache) Get(key any) (any, bool) {
|
||||||
entry := c.get(key)
|
entry := c.get(key)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
@ -90,11 +90,11 @@ func (c *LruCache) Get(key interface{}) (interface{}, bool) {
|
|||||||
return value, true
|
return value, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWithExpire returns the interface{} representation of a cached response,
|
// GetWithExpire returns the any representation of a cached response,
|
||||||
// a time.Time Give expected expires,
|
// a time.Time Give expected expires,
|
||||||
// and a bool set to true if the key was found.
|
// and a bool set to true if the key was found.
|
||||||
// This method will NOT check the maxAge of element and will NOT update the expires.
|
// This method will NOT check the maxAge of element and will NOT update the expires.
|
||||||
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
|
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
|
||||||
entry := c.get(key)
|
entry := c.get(key)
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, time.Time{}, false
|
return nil, time.Time{}, false
|
||||||
@ -104,7 +104,7 @@ func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exist returns if key exist in cache but not put item to the head of linked list
|
// Exist returns if key exist in cache but not put item to the head of linked list
|
||||||
func (c *LruCache) Exist(key interface{}) bool {
|
func (c *LruCache) Exist(key any) bool {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stores the interface{} representation of a response for a given key.
|
// Set stores the any representation of a response for a given key.
|
||||||
func (c *LruCache) Set(key interface{}, value interface{}) {
|
func (c *LruCache) Set(key any, value any) {
|
||||||
expires := int64(0)
|
expires := int64(0)
|
||||||
if c.maxAge > 0 {
|
if c.maxAge > 0 {
|
||||||
expires = time.Now().Unix() + c.maxAge
|
expires = time.Now().Unix() + c.maxAge
|
||||||
@ -121,9 +121,9 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
|
|||||||
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
c.SetWithExpire(key, value, time.Unix(expires, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
|
// SetWithExpire stores the any representation of a response for a given key and given expires.
|
||||||
// The expires time will round to second.
|
// The expires time will round to second.
|
||||||
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
|
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
|
|||||||
defer n.mu.Unlock()
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
n.lru = list.New()
|
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() {
|
for e := c.lru.Front(); e != nil; e = e.Next() {
|
||||||
elm := e.Value.(*entry)
|
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()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@ -188,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the value associated with a key.
|
// Delete removes the value associated with a key.
|
||||||
func (c *LruCache) Delete(key interface{}) {
|
func (c *LruCache) Delete(key any) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
|
|
||||||
if le, ok := c.cache[key]; ok {
|
if le, ok := c.cache[key]; ok {
|
||||||
@ -217,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type entry struct {
|
type entry struct {
|
||||||
key interface{}
|
key any
|
||||||
value interface{}
|
value any
|
||||||
expires int64
|
expires int64
|
||||||
}
|
}
|
||||||
|
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) {
|
func TestLRUCache(t *testing.T) {
|
||||||
c := NewLRUCache()
|
c := New()
|
||||||
|
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
c.Set(e.key, e.value)
|
c.Set(e.key, e.value)
|
||||||
@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUMaxAge(t *testing.T) {
|
func TestLRUMaxAge(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(86400))
|
c := New(WithAge(86400))
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expected := now + 86400
|
expected := now + 86400
|
||||||
@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLRUpdateOnGet(t *testing.T) {
|
func TestLRUpdateOnGet(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
|
c := New(WithAge(86400), WithUpdateAgeOnGet())
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
expires := now + 86400/2
|
expires := now + 86400/2
|
||||||
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMaxSize(t *testing.T) {
|
func TestMaxSize(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(2))
|
c := New(WithSize(2))
|
||||||
// Add one expired entry
|
// Add one expired entry
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
_, ok := c.Get("foo")
|
_, ok := c.Get("foo")
|
||||||
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExist(t *testing.T) {
|
func TestExist(t *testing.T) {
|
||||||
c := NewLRUCache(WithSize(1))
|
c := New(WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
assert.True(t, c.Exist(1))
|
assert.True(t, c.Exist(1))
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
|
|||||||
|
|
||||||
func TestEvict(t *testing.T) {
|
func TestEvict(t *testing.T) {
|
||||||
temp := 0
|
temp := 0
|
||||||
evict := func(key interface{}, value interface{}) {
|
evict := func(key any, value any) {
|
||||||
temp = key.(int) + value.(int)
|
temp = key.(int) + value.(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
c := NewLRUCache(WithEvict(evict), WithSize(1))
|
c := New(WithEvict(evict), WithSize(1))
|
||||||
c.Set(1, 2)
|
c.Set(1, 2)
|
||||||
c.Set(2, 3)
|
c.Set(2, 3)
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetWithExpire(t *testing.T) {
|
func TestSetWithExpire(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(1))
|
c := New(WithAge(1))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -149,11 +149,10 @@ func TestSetWithExpire(t *testing.T) {
|
|||||||
assert.Equal(t, nil, res)
|
assert.Equal(t, nil, res)
|
||||||
assert.Equal(t, time.Time{}, expires)
|
assert.Equal(t, time.Time{}, expires)
|
||||||
assert.Equal(t, false, exist)
|
assert.Equal(t, false, exist)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStale(t *testing.T) {
|
func TestStale(t *testing.T) {
|
||||||
c := NewLRUCache(WithAge(1), WithStale(true))
|
c := New(WithAge(1), WithStale(true))
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
tenSecBefore := time.Unix(now-10, 0)
|
tenSecBefore := time.Unix(now-10, 0)
|
||||||
@ -166,11 +165,11 @@ func TestStale(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCloneTo(t *testing.T) {
|
func TestCloneTo(t *testing.T) {
|
||||||
o := NewLRUCache(WithSize(10))
|
o := New(WithSize(10))
|
||||||
o.Set("1", 1)
|
o.Set("1", 1)
|
||||||
o.Set("2", 2)
|
o.Set("2", 2)
|
||||||
|
|
||||||
n := NewLRUCache(WithSize(2))
|
n := New(WithSize(2))
|
||||||
n.Set("3", 3)
|
n.Set("3", 3)
|
||||||
n.Set("4", 4)
|
n.Set("4", 4)
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *digest32) Sum32() (h1 uint32) {
|
func (d *digest32) Sum32() (h1 uint32) {
|
||||||
|
|
||||||
h1 = d.h1
|
h1 = d.h1
|
||||||
|
|
||||||
var k1 uint32
|
var k1 uint32
|
||||||
|
@ -11,6 +11,9 @@ type BufferedConn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewBufferedConn(c net.Conn) *BufferedConn {
|
func NewBufferedConn(c net.Conn) *BufferedConn {
|
||||||
|
if bc, ok := c.(*BufferedConn); ok {
|
||||||
|
return bc
|
||||||
|
}
|
||||||
return &BufferedConn{bufio.NewReader(c), c}
|
return &BufferedConn{bufio.NewReader(c), c}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
common/net/relay.go
Normal file
24
common/net/relay.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relay copies between left and right bidirectionally.
|
||||||
|
func Relay(leftConn, rightConn net.Conn) {
|
||||||
|
ch := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
||||||
|
// See also https://github.com/Dreamacro/clash/pull/1209
|
||||||
|
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
|
||||||
|
leftConn.SetReadDeadline(time.Now())
|
||||||
|
ch <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
|
||||||
|
rightConn.SetReadDeadline(time.Now())
|
||||||
|
<-ch
|
||||||
|
}
|
@ -1,3 +1,3 @@
|
|||||||
package observable
|
package observable
|
||||||
|
|
||||||
type Iterable <-chan interface{}
|
type Iterable <-chan any
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func iterator(item []interface{}) chan interface{} {
|
func iterator(item []any) chan any {
|
||||||
ch := make(chan interface{})
|
ch := make(chan any)
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
for _, elm := range item {
|
for _, elm := range item {
|
||||||
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable(t *testing.T) {
|
func TestObservable(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -34,15 +34,15 @@ func TestObservable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_MultiSubscribe(t *testing.T) {
|
func TestObservable_MultiSubscribe(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
ch1, _ := src.Subscribe()
|
ch1, _ := src.Subscribe()
|
||||||
ch2, _ := src.Subscribe()
|
ch2, _ := src.Subscribe()
|
||||||
var count = atomic.NewInt32(0)
|
count := atomic.NewInt32(0)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
waitCh := func(ch <-chan interface{}) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
count.Inc()
|
count.Inc()
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribe(t *testing.T) {
|
func TestObservable_UnSubscribe(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, err := src.Subscribe()
|
data, err := src.Subscribe()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
func TestObservable_SubscribeClosedSource(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
data, _ := src.Subscribe()
|
data, _ := src.Subscribe()
|
||||||
<-data
|
<-data
|
||||||
@ -75,14 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
|
||||||
sub := Subscription(make(chan interface{}))
|
sub := Subscription(make(chan any))
|
||||||
iter := iterator([]interface{}{1})
|
iter := iterator([]any{1})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
src.UnSubscribe(sub)
|
src.UnSubscribe(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
||||||
iter := iterator([]interface{}{1, 2, 3, 4, 5})
|
iter := iterator([]any{1, 2, 3, 4, 5})
|
||||||
src := NewObservable(iter)
|
src := NewObservable(iter)
|
||||||
max := 100
|
max := 100
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(max)
|
wg.Add(max)
|
||||||
waitCh := func(ch <-chan interface{}) {
|
waitCh := func(ch <-chan any) {
|
||||||
for range ch {
|
for range ch {
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
@ -115,7 +115,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Benchmark_Observable_1000(b *testing.B) {
|
func Benchmark_Observable_1000(b *testing.B) {
|
||||||
ch := make(chan interface{})
|
ch := make(chan any)
|
||||||
o := NewObservable(ch)
|
o := NewObservable(ch)
|
||||||
num := 1000
|
num := 1000
|
||||||
|
|
||||||
|
@ -4,14 +4,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Subscription <-chan interface{}
|
type Subscription <-chan any
|
||||||
|
|
||||||
type Subscriber struct {
|
type Subscriber struct {
|
||||||
buffer chan interface{}
|
buffer chan any
|
||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Emit(item interface{}) {
|
func (s *Subscriber) Emit(item any) {
|
||||||
s.buffer <- item
|
s.buffer <- item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ func (s *Subscriber) Close() {
|
|||||||
|
|
||||||
func newSubscriber() *Subscriber {
|
func newSubscriber() *Subscriber {
|
||||||
sub := &Subscriber{
|
sub := &Subscriber{
|
||||||
buffer: make(chan interface{}, 200),
|
buffer: make(chan any, 200),
|
||||||
}
|
}
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ type Picker struct {
|
|||||||
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
errOnce sync.Once
|
errOnce sync.Once
|
||||||
result interface{}
|
result any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
|
|||||||
|
|
||||||
// Wait blocks until all function calls from the Go method have returned,
|
// Wait blocks until all function calls from the Go method have returned,
|
||||||
// then returns the first nil error result (if any) from them.
|
// then returns the first nil error result (if any) from them.
|
||||||
func (p *Picker) Wait() interface{} {
|
func (p *Picker) Wait() any {
|
||||||
p.wg.Wait()
|
p.wg.Wait()
|
||||||
if p.cancel != nil {
|
if p.cancel != nil {
|
||||||
p.cancel()
|
p.cancel()
|
||||||
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
|
|||||||
|
|
||||||
// Go calls the given function in a new goroutine.
|
// Go calls the given function in a new goroutine.
|
||||||
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
// The first call to return a nil error cancels the group; its result will be returned by Wait.
|
||||||
func (p *Picker) Go(f func() (interface{}, error)) {
|
func (p *Picker) Go(f func() (any, error)) {
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
|
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
|
||||||
return func() (interface{}, error) {
|
return func() (any, error) {
|
||||||
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
|
@ -23,7 +23,7 @@ func NewAllocator() *Allocator {
|
|||||||
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
|
||||||
for k := range alloc.buffers {
|
for k := range alloc.buffers {
|
||||||
i := k
|
i := k
|
||||||
alloc.buffers[k].New = func() interface{} {
|
alloc.buffers[k].New = func() any {
|
||||||
return make([]byte, 1<<uint32(i))
|
return make([]byte, 1<<uint32(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,10 +32,14 @@ func NewAllocator() *Allocator {
|
|||||||
|
|
||||||
// Get a []byte from pool with most appropriate cap
|
// Get a []byte from pool with most appropriate cap
|
||||||
func (alloc *Allocator) Get(size int) []byte {
|
func (alloc *Allocator) Get(size int) []byte {
|
||||||
if size <= 0 || size > 65536 {
|
switch {
|
||||||
|
case size < 0:
|
||||||
|
panic("alloc.Get: len out of range")
|
||||||
|
case size == 0:
|
||||||
return nil
|
return nil
|
||||||
}
|
case size > 65536:
|
||||||
|
return make([]byte, size)
|
||||||
|
default:
|
||||||
bits := msb(size)
|
bits := msb(size)
|
||||||
if size == 1<<bits {
|
if size == 1<<bits {
|
||||||
return alloc.buffers[bits].Get().([]byte)[:size]
|
return alloc.buffers[bits].Get().([]byte)[:size]
|
||||||
@ -43,15 +47,21 @@ func (alloc *Allocator) Get(size int) []byte {
|
|||||||
|
|
||||||
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Put returns a []byte to pool for future use,
|
// Put returns a []byte to pool for future use,
|
||||||
// which the cap must be exactly 2^n
|
// which the cap must be exactly 2^n
|
||||||
func (alloc *Allocator) Put(buf []byte) error {
|
func (alloc *Allocator) Put(buf []byte) error {
|
||||||
|
if cap(buf) == 0 || cap(buf) > 65536 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
bits := msb(cap(buf))
|
bits := msb(cap(buf))
|
||||||
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
if cap(buf) != 1<<bits {
|
||||||
return errors.New("allocator Put() incorrect buffer size")
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint
|
||||||
//lint:ignore SA6002 ignore temporarily
|
//lint:ignore SA6002 ignore temporarily
|
||||||
alloc.buffers[bits].Put(buf)
|
alloc.buffers[bits].Put(buf)
|
||||||
return nil
|
return nil
|
||||||
|
@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
|
|||||||
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
assert.Equal(t, 1024, cap(alloc.Get(1023)))
|
||||||
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
assert.Equal(t, 1024, len(alloc.Get(1024)))
|
||||||
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
assert.Equal(t, 65536, len(alloc.Get(65536)))
|
||||||
assert.Nil(t, alloc.Get(65537))
|
assert.Equal(t, 65537, len(alloc.Get(65537)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocPut(t *testing.T) {
|
func TestAllocPut(t *testing.T) {
|
||||||
alloc := NewAllocator()
|
alloc := NewAllocator()
|
||||||
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
|
assert.Nil(t, alloc.Put(nil), "put nil misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
|
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
|
||||||
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
|
||||||
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocPutThenGet(t *testing.T) {
|
func TestAllocPutThenGet(t *testing.T) {
|
||||||
|
31
common/pool/buffer.go
Normal file
31
common/pool/buffer.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package pool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/protobytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
|
||||||
|
bytesBufferPool = sync.Pool{New: func() any { return &protobytes.BytesWriter{} }}
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetBuffer() *bytes.Buffer {
|
||||||
|
return bufferPool.Get().(*bytes.Buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBuffer(buf *bytes.Buffer) {
|
||||||
|
buf.Reset()
|
||||||
|
bufferPool.Put(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBytesBuffer() *protobytes.BytesWriter {
|
||||||
|
return bytesBufferPool.Get().(*protobytes.BytesWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBytesBuffer(buf *protobytes.BytesWriter) {
|
||||||
|
buf.Reset()
|
||||||
|
bytesBufferPool.Put(buf)
|
||||||
|
}
|
@ -5,6 +5,11 @@ const (
|
|||||||
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
|
||||||
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
|
||||||
RelayBufferSize = 20 * 1024
|
RelayBufferSize = 20 * 1024
|
||||||
|
|
||||||
|
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
|
||||||
|
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
|
||||||
|
// set to 9000, so the UDP Buffer size set to 16Kib
|
||||||
|
UDPBufferSize = 16 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
func Get(size int) []byte {
|
func Get(size int) []byte {
|
||||||
|
@ -6,12 +6,12 @@ import (
|
|||||||
|
|
||||||
// Queue is a simple concurrent safe queue
|
// Queue is a simple concurrent safe queue
|
||||||
type Queue struct {
|
type Queue struct {
|
||||||
items []interface{}
|
items []any
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put add the item to the queue.
|
// Put add the item to the queue.
|
||||||
func (q *Queue) Put(items ...interface{}) {
|
func (q *Queue) Put(items ...any) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pop returns the head of items.
|
// Pop returns the head of items.
|
||||||
func (q *Queue) Pop() interface{} {
|
func (q *Queue) Pop() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ func (q *Queue) Pop() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last returns the last of item.
|
// Last returns the last of item.
|
||||||
func (q *Queue) Last() interface{} {
|
func (q *Queue) Last() any {
|
||||||
if len(q.items) == 0 {
|
if len(q.items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -47,8 +47,8 @@ func (q *Queue) Last() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy get the copy of queue.
|
// Copy get the copy of queue.
|
||||||
func (q *Queue) Copy() []interface{} {
|
func (q *Queue) Copy() []any {
|
||||||
items := []interface{}{}
|
items := []any{}
|
||||||
q.lock.RLock()
|
q.lock.RLock()
|
||||||
items = append(items, q.items...)
|
items = append(items, q.items...)
|
||||||
q.lock.RUnlock()
|
q.lock.RUnlock()
|
||||||
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
|
|||||||
// New is a constructor for a new concurrent safe queue.
|
// New is a constructor for a new concurrent safe queue.
|
||||||
func New(hint int64) *Queue {
|
func New(hint int64) *Queue {
|
||||||
return &Queue{
|
return &Queue{
|
||||||
items: make([]interface{}, 0, hint),
|
items: make([]any, 0, hint),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type call struct {
|
type call struct {
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
val interface{}
|
val any
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,13 +20,12 @@ type Single struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Val interface{}
|
Val any
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do single.Do likes sync.singleFlight
|
// Do single.Do likes sync.singleFlight
|
||||||
//lint:ignore ST1008 it likes sync.singleFlight
|
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
|
||||||
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
|
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.Before(s.last.Add(s.wait)) {
|
if now.Before(s.last.Add(s.wait)) {
|
||||||
|
@ -12,8 +12,8 @@ import (
|
|||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
var shardCount = atomic.NewInt32(0)
|
shardCount := atomic.NewInt32(0)
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
time.Sleep(time.Millisecond * 5)
|
time.Sleep(time.Millisecond * 5)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
|
|||||||
func TestTimer(t *testing.T) {
|
func TestTimer(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
|
|||||||
func TestReset(t *testing.T) {
|
func TestReset(t *testing.T) {
|
||||||
single := NewSingle(time.Millisecond * 30)
|
single := NewSingle(time.Millisecond * 30)
|
||||||
foo := 0
|
foo := 0
|
||||||
call := func() (interface{}, error) {
|
call := func() (any, error) {
|
||||||
foo++
|
foo++
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux
|
||||||
// +build !linux
|
|
||||||
|
|
||||||
package sockopt
|
package sockopt
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
|
|||||||
return &Decoder{option: &option}
|
return &Decoder{option: &option}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode transform a map[string]interface{} to a struct
|
// Decode transform a map[string]any to a struct
|
||||||
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("Decode must recive a ptr struct")
|
return fmt.Errorf("Decode must recive a ptr struct")
|
||||||
}
|
}
|
||||||
@ -37,14 +37,16 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
|||||||
v := reflect.ValueOf(dst).Elem()
|
v := reflect.ValueOf(dst).Elem()
|
||||||
for idx := 0; idx < v.NumField(); idx++ {
|
for idx := 0; idx < v.NumField(); idx++ {
|
||||||
field := t.Field(idx)
|
field := t.Field(idx)
|
||||||
|
if field.Anonymous {
|
||||||
|
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
tag := field.Tag.Get(d.option.TagName)
|
tag := field.Tag.Get(d.option.TagName)
|
||||||
str := strings.SplitN(tag, ",", 2)
|
key, omitKey, found := strings.Cut(tag, ",")
|
||||||
key := str[0]
|
omitempty := found && omitKey == "omitempty"
|
||||||
omitempty := false
|
|
||||||
if len(str) > 1 {
|
|
||||||
omitempty = str[1] == "omitempty"
|
|
||||||
}
|
|
||||||
|
|
||||||
value, ok := src[key]
|
value, ok := src[key]
|
||||||
if !ok || value == nil {
|
if !ok || value == nil {
|
||||||
@ -62,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decode(name string, data any, val reflect.Value) error {
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case reflect.Int:
|
case reflect.Int:
|
||||||
return d.decodeInt(name, data, val)
|
return d.decodeInt(name, data, val)
|
||||||
@ -83,12 +85,14 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
case kind == reflect.Int:
|
case kind == reflect.Int:
|
||||||
val.SetInt(dataVal.Int())
|
val.SetInt(dataVal.Int())
|
||||||
|
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
|
||||||
|
val.SetInt(int64(dataVal.Float()))
|
||||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||||
var i int64
|
var i int64
|
||||||
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
|
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
|
||||||
@ -106,7 +110,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
@ -123,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
kind := dataVal.Kind()
|
kind := dataVal.Kind()
|
||||||
switch {
|
switch {
|
||||||
@ -140,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
|
||||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||||
valType := val.Type()
|
valType := val.Type()
|
||||||
valElemType := valType.Elem()
|
valElemType := valType.Elem()
|
||||||
@ -155,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
|||||||
for valSlice.Len() <= i {
|
for valSlice.Len() <= i {
|
||||||
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
|
||||||
}
|
}
|
||||||
currentField := valSlice.Index(i)
|
|
||||||
|
|
||||||
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
fieldName := fmt.Sprintf("%s[%d]", name, i)
|
||||||
|
if currentData == nil {
|
||||||
|
// in weakly type mode, null will convert to zero value
|
||||||
|
if d.option.WeaklyTypedInput {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
|
||||||
|
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("'%s' can not be null", fieldName)
|
||||||
|
}
|
||||||
|
currentField := valSlice.Index(i)
|
||||||
if err := d.decode(fieldName, currentData, currentField); err != nil {
|
if err := d.decode(fieldName, currentData, currentField); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -167,7 +181,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error {
|
||||||
valType := val.Type()
|
valType := val.Type()
|
||||||
valKeyType := valType.Key()
|
valKeyType := valType.Key()
|
||||||
valElemType := valType.Elem()
|
valElemType := valType.Elem()
|
||||||
@ -239,7 +253,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error {
|
func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error {
|
||||||
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
dataVal := reflect.Indirect(reflect.ValueOf(data))
|
||||||
|
|
||||||
// If the type of the value to write to and the data match directly,
|
// If the type of the value to write to and the data match directly,
|
||||||
@ -267,7 +281,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
dataValKeys := make(map[reflect.Value]struct{})
|
dataValKeys := make(map[reflect.Value]struct{})
|
||||||
dataValKeysUnused := make(map[interface{}]struct{})
|
dataValKeysUnused := make(map[any]struct{})
|
||||||
for _, dataValKey := range dataVal.MapKeys() {
|
for _, dataValKey := range dataVal.MapKeys() {
|
||||||
dataValKeys[dataValKey] = struct{}{}
|
dataValKeys[dataValKey] = struct{}{}
|
||||||
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
||||||
@ -392,7 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
|
func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) {
|
||||||
dataVal := reflect.ValueOf(data)
|
dataVal := reflect.ValueOf(data)
|
||||||
val.Set(dataVal)
|
val.Set(dataVal)
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
package structure
|
package structure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var decoder = NewDecoder(Option{TagName: "test"})
|
var (
|
||||||
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
decoder = NewDecoder(Option{TagName: "test"})
|
||||||
|
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
|
||||||
|
)
|
||||||
|
|
||||||
type Baz struct {
|
type Baz struct {
|
||||||
Foo int `test:"foo"`
|
Foo int `test:"foo"`
|
||||||
@ -24,7 +27,7 @@ type BazOptional struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Basic(t *testing.T) {
|
func TestStructure_Basic(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": "test",
|
"bar": "test",
|
||||||
"extra": false,
|
"extra": false,
|
||||||
@ -37,16 +40,12 @@ func TestStructure_Basic(t *testing.T) {
|
|||||||
|
|
||||||
s := &Baz{}
|
s := &Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Slice(t *testing.T) {
|
func TestStructure_Slice(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": []string{"one", "two"},
|
"bar": []string{"one", "two"},
|
||||||
}
|
}
|
||||||
@ -58,16 +57,12 @@ func TestStructure_Slice(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_Optional(t *testing.T) {
|
func TestStructure_Optional(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,50 +72,40 @@ func TestStructure_Optional(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazOptional{}
|
s := &BazOptional{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_MissingKey(t *testing.T) {
|
func TestStructure_MissingKey(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Baz{}
|
s := &Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_ParamError(t *testing.T) {
|
func TestStructure_ParamError(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{}
|
rawMap := map[string]any{}
|
||||||
s := Baz{}
|
s := Baz{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_SliceTypeError(t *testing.T) {
|
func TestStructure_SliceTypeError(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
"bar": []int{1, 2},
|
"bar": []int{1, 2},
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := decoder.Decode(rawMap, s)
|
err := decoder.Decode(rawMap, s)
|
||||||
if err == nil {
|
assert.NotNilf(t, err, "should throw error: %#v", s)
|
||||||
t.Fatalf("should throw error: %#v", s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStructure_WeakType(t *testing.T) {
|
func TestStructure_WeakType(t *testing.T) {
|
||||||
rawMap := map[string]interface{}{
|
rawMap := map[string]any{
|
||||||
"foo": "1",
|
"foo": "1",
|
||||||
"bar": []int{1},
|
"bar": []int{1},
|
||||||
}
|
}
|
||||||
@ -132,10 +117,65 @@ func TestStructure_WeakType(t *testing.T) {
|
|||||||
|
|
||||||
s := &BazSlice{}
|
s := &BazSlice{}
|
||||||
err := weakTypeDecoder.Decode(rawMap, s)
|
err := weakTypeDecoder.Decode(rawMap, s)
|
||||||
if err != nil {
|
assert.Nil(t, err)
|
||||||
t.Fatal(err.Error())
|
assert.Equal(t, goal, s)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(s, goal) {
|
|
||||||
t.Fatalf("bad: %#v", s)
|
func TestStructure_Nest(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goal := BazOptional{
|
||||||
|
Foo: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
BazOptional
|
||||||
|
}{}
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, s.BazOptional, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructure_SliceNilValue(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
"bar": []any{"bar", nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
goal := &BazSlice{
|
||||||
|
Foo: 1,
|
||||||
|
Bar: []string{"bar", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &BazSlice{}
|
||||||
|
err := weakTypeDecoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, goal.Bar, s.Bar)
|
||||||
|
|
||||||
|
s = &BazSlice{}
|
||||||
|
err = decoder.Decode(rawMap, s)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructure_SliceNilValueComplex(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"bar": []any{map[string]any{"bar": "foo"}, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
Bar []map[string]any `test:"bar"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Nil(t, s.Bar[1])
|
||||||
|
|
||||||
|
ss := &struct {
|
||||||
|
Bar []Baz `test:"bar"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err = decoder.Decode(rawMap, ss)
|
||||||
|
assert.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
|
|||||||
au.storage.Store(user.User, user.Pass)
|
au.storage.Store(user.User, user.Pass)
|
||||||
}
|
}
|
||||||
usernames := make([]string, 0, len(users))
|
usernames := make([]string, 0, len(users))
|
||||||
au.storage.Range(func(key, value interface{}) bool {
|
au.storage.Range(func(key, value any) bool {
|
||||||
usernames = append(usernames, key.(string))
|
usernames = append(usernames, key.(string))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
@ -14,5 +14,15 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er
|
|||||||
listenAddr = "255.255.255.255:68"
|
listenAddr = "255.255.255.255:68"
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true))
|
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...)
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,10 @@ package dhcp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,7 +24,12 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, er
|
|||||||
|
|
||||||
result := make(chan []net.IP, 1)
|
result := make(chan []net.IP, 1)
|
||||||
|
|
||||||
discovery, err := dhcpv4.NewDiscovery(randomHardware(), dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -80,15 +86,3 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomHardware() net.HardwareAddr {
|
|
||||||
addr := make(net.HardwareAddr, 6)
|
|
||||||
|
|
||||||
addr[0] = 0xff
|
|
||||||
|
|
||||||
for i := 1; i < len(addr); i++ {
|
|
||||||
addr[i] = byte(rand.Intn(254) + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
@ -4,9 +4,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/iface"
|
"github.com/Dreamacro/clash/component/iface"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
type controlFn = func(network, address string, c syscall.RawConn) error
|
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||||
@ -27,14 +27,21 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Control(func(fd uintptr) {
|
var innerErr error
|
||||||
|
err = c.Control(func(fd uintptr) {
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp4", "udp4":
|
case "tcp4", "udp4":
|
||||||
unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
|
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
|
||||||
case "tcp6", "udp6":
|
case "tcp6", "udp6":
|
||||||
unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
|
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
err = innerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,9 +25,16 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Control(func(fd uintptr) {
|
var innerErr error
|
||||||
unix.BindToDevice(int(fd), ifaceName)
|
err = c.Control(func(fd uintptr) {
|
||||||
|
innerErr = unix.BindToDevice(int(fd), ifaceName)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
err = innerErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,72 +1,26 @@
|
|||||||
//go:build !linux && !darwin
|
//go:build !linux && !darwin && !windows
|
||||||
// +build !linux,!darwin
|
|
||||||
|
|
||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"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 bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
|
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
|
||||||
if !destination.IsGlobalUnicast() {
|
if !destination.IsGlobalUnicast() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
local := 0
|
local := uint64(0)
|
||||||
if dialer.LocalAddr != nil {
|
if dialer.LocalAddr != nil {
|
||||||
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
|
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
local, _ = strconv.Atoi(port)
|
local, _ = strconv.ParseUint(port, 10, 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := lookupLocalAddr(ifaceName, network, destination, local)
|
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -82,9 +36,9 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
|
|||||||
port = "0"
|
port = "0"
|
||||||
}
|
}
|
||||||
|
|
||||||
local, _ := strconv.Atoi(port)
|
local, _ := strconv.ParseUint(port, 10, 16)
|
||||||
|
|
||||||
addr, err := lookupLocalAddr(ifaceName, network, nil, local)
|
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
@ -36,13 +36,14 @@ func DialContext(ctx context.Context, network, address string, options ...Option
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
|
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
|
||||||
cfg := &config{}
|
cfg := &option{
|
||||||
|
interfaceName: DefaultInterface.Load(),
|
||||||
|
routingMark: int(DefaultRoutingMark.Load()),
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.skipDefault {
|
|
||||||
for _, o := range DefaultOptions {
|
for _, o := range DefaultOptions {
|
||||||
o(cfg)
|
o(cfg)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range options {
|
for _, o := range options {
|
||||||
o(cfg)
|
o(cfg)
|
||||||
@ -50,7 +51,15 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
|
|||||||
|
|
||||||
lc := &net.ListenConfig{}
|
lc := &net.ListenConfig{}
|
||||||
if cfg.interfaceName != "" {
|
if cfg.interfaceName != "" {
|
||||||
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -59,18 +68,22 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
|
|||||||
if cfg.addrReuse {
|
if cfg.addrReuse {
|
||||||
addrReuseToListenConfig(lc)
|
addrReuseToListenConfig(lc)
|
||||||
}
|
}
|
||||||
|
if cfg.routingMark != 0 {
|
||||||
|
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
|
||||||
|
}
|
||||||
|
|
||||||
return lc.ListenPacket(ctx, network, address)
|
return lc.ListenPacket(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
|
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
|
||||||
opt := &config{}
|
opt := &option{
|
||||||
|
interfaceName: DefaultInterface.Load(),
|
||||||
|
routingMark: int(DefaultRoutingMark.Load()),
|
||||||
|
}
|
||||||
|
|
||||||
if !opt.skipDefault {
|
|
||||||
for _, o := range DefaultOptions {
|
for _, o := range DefaultOptions {
|
||||||
o(opt)
|
o(opt)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range options {
|
for _, o := range options {
|
||||||
o(opt)
|
o(opt)
|
||||||
@ -78,10 +91,19 @@ func dialContext(ctx context.Context, network string, destination net.IP, port s
|
|||||||
|
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
if opt.interfaceName != "" {
|
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 {
|
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if opt.routingMark != 0 {
|
||||||
|
bindMarkToDialer(opt.routingMark, dialer, network, destination)
|
||||||
|
}
|
||||||
|
|
||||||
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
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()
|
||||||
|
}
|
@ -1,31 +1,42 @@
|
|||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
|
import "go.uber.org/atomic"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultOptions []Option
|
DefaultOptions []Option
|
||||||
|
DefaultInterface = atomic.NewString("")
|
||||||
|
DefaultRoutingMark = atomic.NewInt32(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type option struct {
|
||||||
skipDefault bool
|
|
||||||
interfaceName string
|
interfaceName string
|
||||||
|
fallbackBind bool
|
||||||
addrReuse bool
|
addrReuse bool
|
||||||
|
routingMark int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(opt *config)
|
type Option func(opt *option)
|
||||||
|
|
||||||
func WithInterface(name string) Option {
|
func WithInterface(name string) Option {
|
||||||
return func(opt *config) {
|
return func(opt *option) {
|
||||||
opt.interfaceName = name
|
opt.interfaceName = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithFallbackBind(fallback bool) Option {
|
||||||
|
return func(opt *option) {
|
||||||
|
opt.fallbackBind = fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithAddrReuse(reuse bool) Option {
|
func WithAddrReuse(reuse bool) Option {
|
||||||
return func(opt *config) {
|
return func(opt *option) {
|
||||||
opt.addrReuse = reuse
|
opt.addrReuse = reuse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithSkipDefault(skip bool) Option {
|
func WithRoutingMark(mark int) Option {
|
||||||
return func(opt *config) {
|
return func(opt *option) {
|
||||||
opt.skipDefault = skip
|
opt.routingMark = mark
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
|
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
|
||||||
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows
|
|
||||||
|
|
||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
|
|
||||||
|
|
||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
|
55
component/fakeip/cachefile.go
Normal file
55
component/fakeip/cachefile.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package fakeip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cachefileStore struct {
|
||||||
|
cache *cachefile.CacheFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByHost implements store.GetByHost
|
||||||
|
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
|
||||||
|
elm := c.cache.GetFakeip([]byte(host))
|
||||||
|
if elm == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return net.IP(elm), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByHost implements store.PutByHost
|
||||||
|
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
|
||||||
|
c.cache.PutFakeip([]byte(host), ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIP implements store.GetByIP
|
||||||
|
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
|
||||||
|
elm := c.cache.GetFakeip(ip.To4())
|
||||||
|
if elm == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return string(elm), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByIP implements store.PutByIP
|
||||||
|
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
|
||||||
|
c.cache.PutFakeip(ip.To4(), []byte(host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelByIP implements store.DelByIP
|
||||||
|
func (c *cachefileStore) DelByIP(ip net.IP) {
|
||||||
|
ip = ip.To4()
|
||||||
|
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exist implements store.Exist
|
||||||
|
func (c *cachefileStore) Exist(ip net.IP) bool {
|
||||||
|
_, exist := c.GetByIP(ip)
|
||||||
|
return exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneTo implements store.CloneTo
|
||||||
|
// already persistence
|
||||||
|
func (c *cachefileStore) CloneTo(store store) {}
|
69
component/fakeip/memory.go
Normal file
69
component/fakeip/memory.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package fakeip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type memoryStore struct {
|
||||||
|
cache *cache.LruCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByHost implements store.GetByHost
|
||||||
|
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
|
||||||
|
if elm, exist := m.cache.Get(host); exist {
|
||||||
|
ip := elm.(net.IP)
|
||||||
|
|
||||||
|
// ensure ip --> host on head of linked list
|
||||||
|
m.cache.Get(ipToUint(ip.To4()))
|
||||||
|
return ip, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByHost implements store.PutByHost
|
||||||
|
func (m *memoryStore) PutByHost(host string, ip net.IP) {
|
||||||
|
m.cache.Set(host, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIP implements store.GetByIP
|
||||||
|
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
|
||||||
|
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
|
||||||
|
host := elm.(string)
|
||||||
|
|
||||||
|
// ensure host --> ip on head of linked list
|
||||||
|
m.cache.Get(host)
|
||||||
|
return host, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutByIP implements store.PutByIP
|
||||||
|
func (m *memoryStore) PutByIP(ip net.IP, host string) {
|
||||||
|
m.cache.Set(ipToUint(ip.To4()), host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelByIP implements store.DelByIP
|
||||||
|
func (m *memoryStore) DelByIP(ip net.IP) {
|
||||||
|
ipNum := ipToUint(ip.To4())
|
||||||
|
if elm, exist := m.cache.Get(ipNum); exist {
|
||||||
|
m.cache.Delete(elm.(string))
|
||||||
|
}
|
||||||
|
m.cache.Delete(ipNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exist implements store.Exist
|
||||||
|
func (m *memoryStore) Exist(ip net.IP) bool {
|
||||||
|
return m.cache.Exist(ipToUint(ip.To4()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneTo implements store.CloneTo
|
||||||
|
// only for memoryStore to memoryStore
|
||||||
|
func (m *memoryStore) CloneTo(store store) {
|
||||||
|
if ms, ok := store.(*memoryStore); ok {
|
||||||
|
m.cache.CloneTo(ms.cache)
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,25 @@ package fakeip
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||||
"github.com/Dreamacro/clash/component/trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Pool is a implementation about fake ip generator without storage
|
type store interface {
|
||||||
|
GetByHost(host string) (net.IP, bool)
|
||||||
|
PutByHost(host string, ip net.IP)
|
||||||
|
GetByIP(ip net.IP) (string, bool)
|
||||||
|
PutByIP(ip net.IP, host string)
|
||||||
|
DelByIP(ip net.IP)
|
||||||
|
Exist(ip net.IP) bool
|
||||||
|
CloneTo(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool is an implementation about fake ip generator without storage
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
max uint32
|
max uint32
|
||||||
min uint32
|
min uint32
|
||||||
@ -18,25 +30,22 @@ type Pool struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
host *trie.DomainTrie
|
host *trie.DomainTrie
|
||||||
ipnet *net.IPNet
|
ipnet *net.IPNet
|
||||||
cache *cache.LruCache
|
store store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup return a fake ip with host
|
// Lookup return a fake ip with host
|
||||||
func (p *Pool) Lookup(host string) net.IP {
|
func (p *Pool) Lookup(host string) net.IP {
|
||||||
p.mux.Lock()
|
p.mux.Lock()
|
||||||
defer p.mux.Unlock()
|
defer p.mux.Unlock()
|
||||||
if elm, exist := p.cache.Get(host); exist {
|
|
||||||
ip := elm.(net.IP)
|
|
||||||
|
|
||||||
// ensure ip --> host on head of linked list
|
// RFC4343: DNS Case Insensitive, we SHOULD return result with all cases.
|
||||||
n := ipToUint(ip.To4())
|
host = strings.ToLower(host)
|
||||||
offset := n - p.min + 1
|
if ip, exist := p.store.GetByHost(host); exist {
|
||||||
p.cache.Get(offset)
|
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := p.get(host)
|
ip := p.get(host)
|
||||||
p.cache.Set(host, ip)
|
p.store.PutByHost(host, ip)
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,22 +58,11 @@ func (p *Pool) LookBack(ip net.IP) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
n := ipToUint(ip.To4())
|
return p.store.GetByIP(ip)
|
||||||
offset := n - p.min + 1
|
|
||||||
|
|
||||||
if elm, exist := p.cache.Get(offset); exist {
|
|
||||||
host := elm.(string)
|
|
||||||
|
|
||||||
// ensure host --> ip on head of linked list
|
|
||||||
p.cache.Get(host)
|
|
||||||
return host, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", false
|
// ShouldSkipped return if domain should be skipped
|
||||||
}
|
func (p *Pool) ShouldSkipped(domain string) bool {
|
||||||
|
|
||||||
// LookupHost return if domain in host
|
|
||||||
func (p *Pool) LookupHost(domain string) bool {
|
|
||||||
if p.host == nil {
|
if p.host == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -80,9 +78,7 @@ func (p *Pool) Exist(ip net.IP) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
n := ipToUint(ip.To4())
|
return p.store.Exist(ip)
|
||||||
offset := n - p.min + 1
|
|
||||||
return p.cache.Exist(offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gateway return gateway ip
|
// Gateway return gateway ip
|
||||||
@ -95,26 +91,30 @@ func (p *Pool) IPNet() *net.IPNet {
|
|||||||
return p.ipnet
|
return p.ipnet
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchFrom clone cache from old pool
|
// CloneFrom clone cache from old pool
|
||||||
func (p *Pool) PatchFrom(o *Pool) {
|
func (p *Pool) CloneFrom(o *Pool) {
|
||||||
o.cache.CloneTo(p.cache)
|
o.store.CloneTo(p.store)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pool) get(host string) net.IP {
|
func (p *Pool) get(host string) net.IP {
|
||||||
current := p.offset
|
current := p.offset
|
||||||
for {
|
for {
|
||||||
p.offset = (p.offset + 1) % (p.max - p.min)
|
ip := uintToIP(p.min + p.offset)
|
||||||
// Avoid infinite loops
|
if !p.store.Exist(ip) {
|
||||||
if p.offset == current {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if !p.cache.Exist(p.offset) {
|
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||||
|
// Avoid infinite loops
|
||||||
|
if p.offset == current {
|
||||||
|
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||||
|
ip := uintToIP(p.min + p.offset)
|
||||||
|
p.store.DelByIP(ip)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ip := uintToIP(p.min + p.offset - 1)
|
ip := uintToIP(p.min + p.offset)
|
||||||
p.cache.Set(p.offset, host)
|
p.store.PutByIP(ip, host)
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,11 +130,24 @@ func uintToIP(v uint32) net.IP {
|
|||||||
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
|
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New return Pool instance
|
type Options struct {
|
||||||
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
|
IPNet *net.IPNet
|
||||||
min := ipToUint(ipnet.IP) + 2
|
Host *trie.DomainTrie
|
||||||
|
|
||||||
ones, bits := ipnet.Mask.Size()
|
// Size sets the maximum number of entries in memory
|
||||||
|
// and does not work if Persistence is true
|
||||||
|
Size int
|
||||||
|
|
||||||
|
// Persistence will save the data to disk.
|
||||||
|
// Size will not work and record will be fully stored.
|
||||||
|
Persistence bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New return Pool instance
|
||||||
|
func New(options Options) (*Pool, error) {
|
||||||
|
min := ipToUint(options.IPNet.IP) + 2
|
||||||
|
|
||||||
|
ones, bits := options.IPNet.Mask.Size()
|
||||||
total := 1<<uint(bits-ones) - 2
|
total := 1<<uint(bits-ones) - 2
|
||||||
|
|
||||||
if total <= 0 {
|
if total <= 0 {
|
||||||
@ -142,12 +155,22 @@ func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
max := min + uint32(total) - 1
|
max := min + uint32(total) - 1
|
||||||
return &Pool{
|
pool := &Pool{
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
gateway: min - 1,
|
gateway: min - 1,
|
||||||
host: host,
|
host: options.Host,
|
||||||
ipnet: ipnet,
|
ipnet: options.IPNet,
|
||||||
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
|
}
|
||||||
}, nil
|
if options.Persistence {
|
||||||
|
pool.store = &cachefileStore{
|
||||||
|
cache: cachefile.Cache(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pool.store = &memoryStore{
|
||||||
|
cache: cache.New(cache.WithSize(options.Size * 2)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
}
|
}
|
||||||
|
@ -2,38 +2,143 @@ package fakeip
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||||
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func createPools(options Options) ([]*Pool, string, error) {
|
||||||
|
pool, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
filePool, tempfile, err := createCachefileStore(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*Pool{pool, filePool}, tempfile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createCachefileStore(options Options) (*Pool, string, error) {
|
||||||
|
pool, err := New(options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
f, err := os.CreateTemp("", "clash")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.store = &cachefileStore{
|
||||||
|
cache: &cachefile.CacheFile{DB: db},
|
||||||
|
}
|
||||||
|
return pool, f.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestPool_Basic(t *testing.T) {
|
func TestPool_Basic(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||||
pool, _ := New(ipnet, 10, nil)
|
pools, tempfile, err := createPools(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(tempfile)
|
||||||
|
|
||||||
|
for _, pool := range pools {
|
||||||
first := pool.Lookup("foo.com")
|
first := pool.Lookup("foo.com")
|
||||||
last := pool.Lookup("bar.com")
|
last := pool.Lookup("bar.com")
|
||||||
bar, exist := pool.LookBack(last)
|
bar, exist := pool.LookBack(last)
|
||||||
|
|
||||||
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
|
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
|
||||||
|
assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
|
||||||
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
|
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
|
||||||
assert.True(t, exist)
|
assert.True(t, exist)
|
||||||
assert.Equal(t, bar, "bar.com")
|
assert.Equal(t, bar, "bar.com")
|
||||||
|
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
|
||||||
|
assert.Equal(t, pool.IPNet().String(), ipnet.String())
|
||||||
|
assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
|
||||||
|
assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
|
||||||
|
assert.False(t, pool.Exist(net.ParseIP("::1")))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPool_Cycle(t *testing.T) {
|
func TestPool_Case_Insensitive(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||||
pool, _ := New(ipnet, 10, nil)
|
pools, tempfile, err := createPools(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(tempfile)
|
||||||
|
|
||||||
|
for _, pool := range pools {
|
||||||
first := pool.Lookup("foo.com")
|
first := pool.Lookup("foo.com")
|
||||||
same := pool.Lookup("baz.com")
|
last := pool.Lookup("Foo.Com")
|
||||||
|
foo, exist := pool.LookBack(last)
|
||||||
|
|
||||||
assert.True(t, first.Equal(same))
|
assert.True(t, first.Equal(pool.Lookup("Foo.Com")))
|
||||||
|
assert.Equal(t, pool.Lookup("fOo.cOM"), first)
|
||||||
|
assert.True(t, exist)
|
||||||
|
assert.Equal(t, foo, "foo.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPool_CycleUsed(t *testing.T) {
|
||||||
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||||
|
pools, tempfile, err := createPools(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 10,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(tempfile)
|
||||||
|
|
||||||
|
for _, pool := range pools {
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("2.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 4}, pool.Lookup("4.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 5}, pool.Lookup("5.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 6}, pool.Lookup("6.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("12.com"))
|
||||||
|
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPool_Skip(t *testing.T) {
|
||||||
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
|
||||||
|
tree := trie.New()
|
||||||
|
tree.Insert("example.com", tree)
|
||||||
|
pools, tempfile, err := createPools(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 10,
|
||||||
|
Host: tree,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer os.Remove(tempfile)
|
||||||
|
|
||||||
|
for _, pool := range pools {
|
||||||
|
assert.True(t, pool.ShouldSkipped("example.com"))
|
||||||
|
assert.False(t, pool.ShouldSkipped("foo.com"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPool_MaxCacheSize(t *testing.T) {
|
func TestPool_MaxCacheSize(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
||||||
pool, _ := New(ipnet, 2, nil)
|
pool, _ := New(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 2,
|
||||||
|
})
|
||||||
|
|
||||||
first := pool.Lookup("foo.com")
|
first := pool.Lookup("foo.com")
|
||||||
pool.Lookup("bar.com")
|
pool.Lookup("bar.com")
|
||||||
@ -45,7 +150,10 @@ func TestPool_MaxCacheSize(t *testing.T) {
|
|||||||
|
|
||||||
func TestPool_DoubleMapping(t *testing.T) {
|
func TestPool_DoubleMapping(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
||||||
pool, _ := New(ipnet, 2, nil)
|
pool, _ := New(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 2,
|
||||||
|
})
|
||||||
|
|
||||||
// fill cache
|
// fill cache
|
||||||
fooIP := pool.Lookup("foo.com")
|
fooIP := pool.Lookup("foo.com")
|
||||||
@ -70,9 +178,35 @@ func TestPool_DoubleMapping(t *testing.T) {
|
|||||||
assert.False(t, bazIP.Equal(newBazIP))
|
assert.False(t, bazIP.Equal(newBazIP))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPool_Clone(t *testing.T) {
|
||||||
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
|
||||||
|
pool, _ := New(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
first := pool.Lookup("foo.com")
|
||||||
|
last := pool.Lookup("bar.com")
|
||||||
|
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
|
||||||
|
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
|
||||||
|
|
||||||
|
newPool, _ := New(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 2,
|
||||||
|
})
|
||||||
|
newPool.CloneFrom(pool)
|
||||||
|
_, firstExist := newPool.LookBack(first)
|
||||||
|
_, lastExist := newPool.LookBack(last)
|
||||||
|
assert.True(t, firstExist)
|
||||||
|
assert.True(t, lastExist)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPool_Error(t *testing.T) {
|
func TestPool_Error(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
|
||||||
_, err := New(ipnet, 10, nil)
|
_, err := New(Options{
|
||||||
|
IPNet: ipnet,
|
||||||
|
Size: 10,
|
||||||
|
})
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,15 @@ type Interface struct {
|
|||||||
HardwareAddr net.HardwareAddr
|
HardwareAddr net.HardwareAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrIfaceNotFound = errors.New("interface not found")
|
var (
|
||||||
var ErrAddrNotFound = errors.New("addr not found")
|
ErrIfaceNotFound = errors.New("interface not found")
|
||||||
|
ErrAddrNotFound = errors.New("addr not found")
|
||||||
|
)
|
||||||
|
|
||||||
var interfaces = singledo.NewSingle(time.Second * 20)
|
var interfaces = singledo.NewSingle(time.Second * 20)
|
||||||
|
|
||||||
func ResolveInterface(name string) (*Interface, error) {
|
func ResolveInterface(name string) (*Interface, error) {
|
||||||
value, err, _ := interfaces.Do(func() (interface{}, error) {
|
value, err, _ := interfaces.Do(func() (any, error) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
22
component/ipset/ipset_linux.go
Normal file
22
component/ipset/ipset_linux.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package ipset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test whether the ip is in the set or not
|
||||||
|
func Test(setName string, ip net.IP) (bool, error) {
|
||||||
|
return netlink.IpsetTest(setName, &netlink.IPSetEntry{
|
||||||
|
IP: ip,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dumps a specific ipset to check if we can use the set normally
|
||||||
|
func Verify(setName string) error {
|
||||||
|
_, err := netlink.IpsetList(setName)
|
||||||
|
return err
|
||||||
|
}
|
17
component/ipset/ipset_others.go
Normal file
17
component/ipset/ipset_others.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package ipset
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Always return false in non-linux
|
||||||
|
func Test(setName string, ip net.IP) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always pass in non-linux
|
||||||
|
func Verify(setName string) error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -9,8 +9,10 @@ import (
|
|||||||
"github.com/oschwald/geoip2-golang"
|
"github.com/oschwald/geoip2-golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mmdb *geoip2.Reader
|
var (
|
||||||
var once sync.Once
|
mmdb *geoip2.Reader
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
func LoadFromBytes(buffer []byte) {
|
func LoadFromBytes(buffer []byte) {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
|
@ -6,17 +6,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Factory = func(context.Context) (interface{}, error)
|
type Factory = func(context.Context) (any, error)
|
||||||
|
|
||||||
type entry struct {
|
type entry struct {
|
||||||
elm interface{}
|
elm any
|
||||||
time time.Time
|
time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option func(*pool)
|
type Option func(*pool)
|
||||||
|
|
||||||
// WithEvict set the evict callback
|
// WithEvict set the evict callback
|
||||||
func WithEvict(cb func(interface{})) Option {
|
func WithEvict(cb func(any)) Option {
|
||||||
return func(p *pool) {
|
return func(p *pool) {
|
||||||
p.evict = cb
|
p.evict = cb
|
||||||
}
|
}
|
||||||
@ -32,7 +32,7 @@ func WithAge(maxAge int64) Option {
|
|||||||
// WithSize defined max size of Pool
|
// WithSize defined max size of Pool
|
||||||
func WithSize(maxSize int) Option {
|
func WithSize(maxSize int) Option {
|
||||||
return func(p *pool) {
|
return func(p *pool) {
|
||||||
p.ch = make(chan interface{}, maxSize)
|
p.ch = make(chan any, maxSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,13 +42,13 @@ type Pool struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type pool struct {
|
type pool struct {
|
||||||
ch chan interface{}
|
ch chan any
|
||||||
factory Factory
|
factory Factory
|
||||||
evict func(interface{})
|
evict func(any)
|
||||||
maxAge int64
|
maxAge int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
|
func (p *pool) GetContext(ctx context.Context) (any, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -68,11 +68,11 @@ func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) Get() (interface{}, error) {
|
func (p *pool) Get() (any, error) {
|
||||||
return p.GetContext(context.Background())
|
return p.GetContext(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pool) Put(item interface{}) {
|
func (p *pool) Put(item any) {
|
||||||
e := &entry{
|
e := &entry{
|
||||||
elm: item,
|
elm: item,
|
||||||
time: time.Now(),
|
time: time.Now(),
|
||||||
@ -100,7 +100,7 @@ func recycle(p *Pool) {
|
|||||||
|
|
||||||
func New(factory Factory, options ...Option) *Pool {
|
func New(factory Factory, options ...Option) *Pool {
|
||||||
p := &pool{
|
p := &pool{
|
||||||
ch: make(chan interface{}, 10),
|
ch: make(chan any, 10),
|
||||||
factory: factory,
|
factory: factory,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
func lg() Factory {
|
func lg() Factory {
|
||||||
initial := -1
|
initial := -1
|
||||||
return func(context.Context) (interface{}, error) {
|
return func(context.Context) (any, error) {
|
||||||
initial++
|
initial++
|
||||||
return initial, nil
|
return initial, nil
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ func TestPool_MaxSize(t *testing.T) {
|
|||||||
size := 5
|
size := 5
|
||||||
pool := New(g, WithSize(size))
|
pool := New(g, WithSize(size))
|
||||||
|
|
||||||
items := []interface{}{}
|
items := []any{}
|
||||||
|
|
||||||
for i := 0; i < size; i++ {
|
for i := 0; i < size; i++ {
|
||||||
item, _ := pool.Get()
|
item, _ := pool.Get()
|
||||||
|
@ -2,7 +2,7 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -16,6 +16,6 @@ const (
|
|||||||
UDP = "udp"
|
UDP = "udp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
|
func FindProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
|
||||||
return findProcessName(network, srcIP, srcPort)
|
return findProcessPath(network, from, to)
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net"
|
"net/netip"
|
||||||
"path/filepath"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -16,7 +17,23 @@ const (
|
|||||||
proccallnumpidinfo = 0x2
|
proccallnumpidinfo = 0x2
|
||||||
)
|
)
|
||||||
|
|
||||||
func findProcessName(network string, ip net.IP, port int) (string, error) {
|
var structSize = func() int {
|
||||||
|
value, _ := syscall.Sysctl("kern.osrelease")
|
||||||
|
major, _, _ := strings.Cut(value, ".")
|
||||||
|
n, _ := strconv.ParseInt(major, 10, 64)
|
||||||
|
switch true {
|
||||||
|
case n >= 22:
|
||||||
|
return 408
|
||||||
|
default:
|
||||||
|
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||||
|
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||||
|
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||||
|
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||||
|
return 384
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
func findProcessPath(network string, from netip.AddrPort, _ netip.AddrPort) (string, error) {
|
||||||
var spath string
|
var spath string
|
||||||
switch network {
|
switch network {
|
||||||
case TCP:
|
case TCP:
|
||||||
@ -27,7 +44,7 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
|
|||||||
return "", ErrInvalidNetwork
|
return "", ErrInvalidNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
isIPv4 := ip.To4() != nil
|
isIPv4 := from.Addr().Is4()
|
||||||
|
|
||||||
value, err := syscall.Sysctl(spath)
|
value, err := syscall.Sysctl(spath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,50 +52,64 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := []byte(value)
|
buf := []byte(value)
|
||||||
|
itemSize := structSize
|
||||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
|
||||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
|
||||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
|
||||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
|
||||||
itemSize := 384
|
|
||||||
if network == TCP {
|
if network == TCP {
|
||||||
// rup8(sizeof(xtcpcb_n))
|
// rup8(sizeof(xtcpcb_n))
|
||||||
itemSize += 208
|
itemSize += 208
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fallbackUDPProcess string
|
||||||
// skip the first xinpgen(24 bytes) block
|
// skip the first xinpgen(24 bytes) block
|
||||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||||
// offset of xinpcb_n and xsocket_n
|
// offset of xinpcb_n and xsocket_n
|
||||||
inp, so := i, i+104
|
inp, so := i, i+104
|
||||||
|
|
||||||
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
||||||
if uint16(port) != srcPort {
|
if from.Port() != srcPort {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: add dstPort check
|
||||||
|
|
||||||
// xinpcb_n.inp_vflag
|
// xinpcb_n.inp_vflag
|
||||||
flag := buf[inp+44]
|
flag := buf[inp+44]
|
||||||
|
|
||||||
var srcIP net.IP
|
var (
|
||||||
|
srcIP netip.Addr
|
||||||
|
srcIPOk bool
|
||||||
|
srcIsIPv4 bool
|
||||||
|
)
|
||||||
switch {
|
switch {
|
||||||
case flag&0x1 > 0 && isIPv4:
|
case flag&0x1 > 0 && isIPv4:
|
||||||
// ipv4
|
// ipv4
|
||||||
srcIP = net.IP(buf[inp+76 : inp+80])
|
srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+76 : inp+80])
|
||||||
|
srcIsIPv4 = true
|
||||||
case flag&0x2 > 0 && !isIPv4:
|
case flag&0x2 > 0 && !isIPv4:
|
||||||
// ipv6
|
// ipv6
|
||||||
srcIP = net.IP(buf[inp+64 : inp+80])
|
srcIP, srcIPOk = netip.AddrFromSlice(buf[inp+64 : inp+80])
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !srcIPOk {
|
||||||
if !ip.Equal(srcIP) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if from.Addr() == srcIP { // FIXME: add dstIP check
|
||||||
// xsocket_n.so_last_pid
|
// xsocket_n.so_last_pid
|
||||||
pid := readNativeUint32(buf[so+68 : so+72])
|
pid := readNativeUint32(buf[so+68 : so+72])
|
||||||
return getExecPathFromPID(pid)
|
return getExecPathFromPID(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// udp packet connection may be not equal with srcIP
|
||||||
|
if network == UDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
||||||
|
fallbackUDPProcess, _ = getExecPathFromPID(readNativeUint32(buf[so+68 : so+72]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if network == UDP && fallbackUDPProcess != "" {
|
||||||
|
return fallbackUDPProcess, nil
|
||||||
|
}
|
||||||
|
|
||||||
return "", ErrNotFound
|
return "", ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +127,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
|
|||||||
return "", errno
|
return "", errno
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Base(unix.ByteSliceToString(buf)), nil
|
return unix.ByteSliceToString(buf), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readNativeUint32(b []byte) uint32 {
|
func readNativeUint32(b []byte) uint32 {
|
||||||
|
217
component/process/process_freebsd.go
Normal file
217
component/process/process_freebsd.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Xinpgen12 [64]byte // size 64
|
||||||
|
|
||||||
|
type InEndpoints12 struct {
|
||||||
|
FPort [2]byte
|
||||||
|
LPort [2]byte
|
||||||
|
FAddr [16]byte
|
||||||
|
LAddr [16]byte
|
||||||
|
ZoneID uint32
|
||||||
|
} // size 40
|
||||||
|
|
||||||
|
type XTcpcb12 struct {
|
||||||
|
Len uint32 // offset 0
|
||||||
|
_ [20]byte // offset 4
|
||||||
|
SocketAddr uint64 // offset 24
|
||||||
|
_ [84]byte // offset 32
|
||||||
|
Family uint32 // offset 116
|
||||||
|
_ [140]byte // offset 120
|
||||||
|
InEndpoints InEndpoints12 // offset 260
|
||||||
|
_ [444]byte // offset 300
|
||||||
|
} // size 744
|
||||||
|
|
||||||
|
type XInpcb12 struct {
|
||||||
|
Len uint32 // offset 0
|
||||||
|
_ [12]byte // offset 4
|
||||||
|
SocketAddr uint64 // offset 16
|
||||||
|
_ [84]byte // offset 24
|
||||||
|
Family uint32 // offset 108
|
||||||
|
_ [140]byte // offset 112
|
||||||
|
InEndpoints InEndpoints12 // offset 252
|
||||||
|
_ [108]byte // offset 292
|
||||||
|
} // size 400
|
||||||
|
|
||||||
|
type XFile12 struct {
|
||||||
|
Size uint64 // offset 0
|
||||||
|
Pid uint32 // offset 8
|
||||||
|
_ [44]byte // offset 12
|
||||||
|
DataAddr uint64 // offset 56
|
||||||
|
_ [64]byte // offset 64
|
||||||
|
} // size 128
|
||||||
|
|
||||||
|
var majorVersion = func() int {
|
||||||
|
releaseVersion, err := unix.Sysctl("kern.osrelease")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
majorVersionText, _, _ := strings.Cut(releaseVersion, ".")
|
||||||
|
|
||||||
|
majorVersion, err := strconv.Atoi(majorVersionText)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return majorVersion
|
||||||
|
}()
|
||||||
|
|
||||||
|
func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
|
||||||
|
switch majorVersion {
|
||||||
|
case 12, 13:
|
||||||
|
return findProcessPath12(network, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrPlatformNotSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProcessPath12(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
|
||||||
|
switch network {
|
||||||
|
case TCP:
|
||||||
|
data, err := unix.SysctlRaw("net.inet.tcp.pcblist")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < int(unsafe.Sizeof(Xinpgen12{})) {
|
||||||
|
return "", fmt.Errorf("invalid sysctl data len: %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data[unsafe.Sizeof(Xinpgen12{}):]
|
||||||
|
|
||||||
|
for len(data) > int(unsafe.Sizeof(XTcpcb12{}.Len)) {
|
||||||
|
tcb := (*XTcpcb12)(unsafe.Pointer(&data[0]))
|
||||||
|
if tcb.Len < uint32(unsafe.Sizeof(XTcpcb12{})) || uint32(len(data)) < tcb.Len {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data[tcb.Len:]
|
||||||
|
|
||||||
|
var (
|
||||||
|
connFromAddr netip.Addr
|
||||||
|
connToAddr netip.Addr
|
||||||
|
)
|
||||||
|
if tcb.Family == unix.AF_INET {
|
||||||
|
connFromAddr = netip.AddrFrom4([4]byte(tcb.InEndpoints.LAddr[12:16]))
|
||||||
|
connToAddr = netip.AddrFrom4([4]byte(tcb.InEndpoints.FAddr[12:16]))
|
||||||
|
} else if tcb.Family == unix.AF_INET6 {
|
||||||
|
connFromAddr = netip.AddrFrom16(tcb.InEndpoints.LAddr)
|
||||||
|
connToAddr = netip.AddrFrom16(tcb.InEndpoints.FAddr)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
connFrom := netip.AddrPortFrom(connFromAddr, binary.BigEndian.Uint16(tcb.InEndpoints.LPort[:]))
|
||||||
|
connTo := netip.AddrPortFrom(connToAddr, binary.BigEndian.Uint16(tcb.InEndpoints.FPort[:]))
|
||||||
|
|
||||||
|
if connFrom == from && connTo == to {
|
||||||
|
pid, err := findPidBySocketAddr12(tcb.SocketAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return findExecutableByPid(pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case UDP:
|
||||||
|
data, err := unix.SysctlRaw("net.inet.udp.pcblist")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < int(unsafe.Sizeof(Xinpgen12{})) {
|
||||||
|
return "", fmt.Errorf("invalid sysctl data len: %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data[unsafe.Sizeof(Xinpgen12{}):]
|
||||||
|
|
||||||
|
for len(data) > int(unsafe.Sizeof(XInpcb12{}.Len)) {
|
||||||
|
icb := (*XInpcb12)(unsafe.Pointer(&data[0]))
|
||||||
|
if icb.Len < uint32(unsafe.Sizeof(XInpcb12{})) || uint32(len(data)) < icb.Len {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data = data[icb.Len:]
|
||||||
|
|
||||||
|
var connFromAddr netip.Addr
|
||||||
|
if icb.Family == unix.AF_INET {
|
||||||
|
connFromAddr = netip.AddrFrom4([4]byte(icb.InEndpoints.LAddr[12:16]))
|
||||||
|
} else if icb.Family == unix.AF_INET6 {
|
||||||
|
connFromAddr = netip.AddrFrom16(icb.InEndpoints.LAddr)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
connFromPort := binary.BigEndian.Uint16(icb.InEndpoints.LPort[:])
|
||||||
|
|
||||||
|
if (connFromAddr == from.Addr() || connFromAddr.IsUnspecified()) && connFromPort == from.Port() {
|
||||||
|
pid, err := findPidBySocketAddr12(icb.SocketAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return findExecutableByPid(pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPidBySocketAddr12(socketAddr uint64) (uint32, error) {
|
||||||
|
buf, err := unix.SysctlRaw("kern.file")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filesLen := len(buf) / int(unsafe.Sizeof(XFile12{}))
|
||||||
|
files := unsafe.Slice((*XFile12)(unsafe.Pointer(&buf[0])), filesLen)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Size != uint64(unsafe.Sizeof(XFile12{})) {
|
||||||
|
return 0, fmt.Errorf("invalid xfile size: %d", file.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.DataAddr == socketAddr {
|
||||||
|
return file.Pid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func findExecutableByPid(pid uint32) (string, error) {
|
||||||
|
buf := make([]byte, unix.PathMax)
|
||||||
|
size := uint64(len(buf))
|
||||||
|
mib := [4]uint32{
|
||||||
|
unix.CTL_KERN,
|
||||||
|
14, // KERN_PROC
|
||||||
|
12, // KERN_PROC_PATHNAME
|
||||||
|
pid,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, errno := unix.Syscall6(
|
||||||
|
unix.SYS___SYSCTL,
|
||||||
|
uintptr(unsafe.Pointer(&mib[0])),
|
||||||
|
uintptr(len(mib)),
|
||||||
|
uintptr(unsafe.Pointer(&buf[0])),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 || size == 0 {
|
||||||
|
return "", fmt.Errorf("sysctl: get proc name: %w", errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf[:size-1]), nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user