Compare commits
144 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 |
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: 此项非必须,但是如果你有想法的话欢迎提出。
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -19,12 +19,12 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
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
|
12
.github/workflows/docker.yml
vendored
12
.github/workflows/docker.yml
vendored
@ -18,24 +18,24 @@ jobs:
|
|||||||
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,7 +43,7 @@ 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/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
@ -70,7 +70,7 @@ 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/v6,linux/arm/v7,linux/arm64
|
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
|
||||||
|
10
.github/workflows/linter.yml
vendored
10
.github/workflows/linter.yml
vendored
@ -6,15 +6,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get latest go version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
check-latest: true
|
||||||
|
go-version: '1.20'
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -4,21 +4,17 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Get latest go version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ steps.version.outputs.go_version }}
|
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@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache go module
|
- name: Cache go module
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
@ -36,7 +32,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NAME: clash
|
NAME: clash
|
||||||
BINDIR: bin
|
BINDIR: bin
|
||||||
run: make -j releases
|
run: make -j $(go run ./test/main.go) releases
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
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@v5
|
- 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
|
||||||
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -23,3 +23,14 @@ vendor
|
|||||||
|
|
||||||
# test suite
|
# test suite
|
||||||
test/config/cache*
|
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
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- gofumpt
|
|
||||||
- staticcheck
|
|
||||||
- govet
|
|
||||||
- gci
|
- gci
|
||||||
|
- gofumpt
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- misspell
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gci:
|
gci:
|
||||||
|
custom-order: true
|
||||||
sections:
|
sections:
|
||||||
- standard
|
- standard
|
||||||
- prefix(github.com/Dreamacro/clash)
|
- prefix(github.com/Dreamacro/clash)
|
||||||
- default
|
- default
|
||||||
staticcheck:
|
staticcheck:
|
||||||
go: '1.18'
|
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 tzdata
|
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"]
|
||||||
|
34
Makefile
34
Makefile
@ -16,13 +16,15 @@ PLATFORM_LIST = \
|
|||||||
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-amd64-v3 \
|
||||||
@ -33,13 +35,10 @@ WINDOWS_ARCH_LIST = \
|
|||||||
windows-amd64 \
|
windows-amd64 \
|
||||||
windows-amd64-v3 \
|
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)-$@
|
||||||
|
|
||||||
@ -67,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:
|
||||||
@ -88,6 +87,12 @@ 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)-$@
|
||||||
|
|
||||||
@ -112,7 +117,7 @@ windows-amd64-v3:
|
|||||||
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))
|
||||||
@ -129,12 +134,15 @@ all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
|
|||||||
|
|
||||||
releases: $(gz_releases) $(zip_releases)
|
releases: $(gz_releases) $(zip_releases)
|
||||||
|
|
||||||
lint:
|
LINT_OS_LIST := darwin windows linux freebsd openbsd
|
||||||
GOOS=darwin golangci-lint run ./...
|
|
||||||
GOOS=windows golangci-lint run ./...
|
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
|
||||||
GOOS=linux golangci-lint run ./...
|
%-lint:
|
||||||
GOOS=freebsd golangci-lint run ./...
|
GOOS=$* golangci-lint run ./...
|
||||||
GOOS=openbsd 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)/*
|
||||||
|
39
README.md
39
README.md
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<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">
|
||||||
@ -23,35 +23,28 @@
|
|||||||
|
|
||||||
## 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, IPCIDR or Process 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)
|
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
|||||||
mapping := map[string]any{}
|
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)
|
||||||
@ -101,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 {
|
||||||
@ -156,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,10 +195,9 @@ 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,
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewHTTP receive normal http request and return HTTPContext
|
// NewHTTP receive normal http request and return HTTPContext
|
||||||
func NewHTTP(target socks5.Addr, 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(target)
|
metadata := parseSocksAddr(target)
|
||||||
metadata.NetWork = C.TCP
|
metadata.NetWork = C.TCP
|
||||||
metadata.Type = C.HTTP
|
metadata.Type = C.HTTP
|
||||||
@ -17,5 +18,10 @@ func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnCo
|
|||||||
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:
|
||||||
@ -44,21 +42,13 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
|||||||
host = strings.TrimRight(host, ".")
|
host = strings.TrimRight(host, ".")
|
||||||
|
|
||||||
metadata := &C.Metadata{
|
metadata := &C.Metadata{
|
||||||
NetWork: C.TCP,
|
NetWork: C.TCP,
|
||||||
AddrType: C.AtypDomainName,
|
Host: host,
|
||||||
Host: host,
|
DstIP: nil,
|
||||||
DstIP: nil,
|
DstPort: port,
|
||||||
DstPort: port,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
if ip != nil {
|
|
||||||
switch {
|
|
||||||
case ip.To4() == nil:
|
|
||||||
metadata.AddrType = C.AtypIPv6
|
|
||||||
default:
|
|
||||||
metadata.AddrType = C.AtypIPv4
|
|
||||||
}
|
|
||||||
metadata.DstIP = ip
|
metadata.DstIP = ip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,25 +22,29 @@ type Http struct {
|
|||||||
user string
|
user string
|
||||||
pass string
|
pass string
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
Headers http.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpOption struct {
|
type HttpOption struct {
|
||||||
BasicOption
|
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"`
|
||||||
UserName string `proxy:"username,omitempty"`
|
UserName string `proxy:"username,omitempty"`
|
||||||
Password string `proxy:"password,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
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)
|
||||||
@ -61,7 +65,9 @@ func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di
|
|||||||
}
|
}
|
||||||
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,12 +84,12 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
|||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Host: addr,
|
Host: addr,
|
||||||
},
|
},
|
||||||
Host: addr,
|
Host: addr,
|
||||||
Header: http.Header{
|
Header: h.Headers.Clone(),
|
||||||
"Proxy-Connection": []string{"Keep-Alive"},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Proxy-Connection", "Keep-Alive")
|
||||||
|
|
||||||
if h.user != "" && h.pass != "" {
|
if h.user != "" && h.pass != "" {
|
||||||
auth := h.user + ":" + h.pass
|
auth := h.user + ":" + h.pass
|
||||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||||
@ -130,6 +136,11 @@ 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,
|
||||||
@ -141,5 +152,6 @@ func NewHttp(option HttpOption) *Http {
|
|||||||
user: option.UserName,
|
user: option.UserName,
|
||||||
pass: option.Password,
|
pass: option.Password,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
|
Headers: headers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
@ -82,7 +81,9 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op
|
|||||||
}
|
}
|
||||||
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
|
||||||
|
@ -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 {
|
||||||
@ -67,7 +66,9 @@ func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
}
|
}
|
||||||
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
|
||||||
|
@ -80,7 +80,9 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
}
|
}
|
||||||
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
|
||||||
|
@ -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)
|
||||||
@ -67,7 +69,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
|
|||||||
}
|
}
|
||||||
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 {
|
||||||
@ -87,11 +91,15 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
|
|
||||||
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
|
||||||
|
@ -109,7 +109,9 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
|||||||
}
|
}
|
||||||
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 {
|
||||||
@ -129,13 +131,17 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
|
|||||||
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 {
|
||||||
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
|
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)
|
||||||
}
|
}
|
||||||
defer safeConnClose(c, err)
|
defer func(c net.Conn) {
|
||||||
|
safeConnClose(c, err)
|
||||||
|
}(c)
|
||||||
tcpKeepAlive(c)
|
tcpKeepAlive(c)
|
||||||
c, err = t.plainStream(c)
|
c, err = t.plainStream(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -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)
|
|
||||||
|
addrType := metadata.AddrType()
|
||||||
|
buf.PutUint8(uint8(addrType))
|
||||||
|
|
||||||
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
|
||||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
switch addrType {
|
||||||
switch metadata.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
|
||||||
@ -192,7 +195,9 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
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 {
|
||||||
@ -207,7 +212,9 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
|||||||
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
|
||||||
@ -231,7 +238,9 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
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 {
|
||||||
@ -240,7 +249,9 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
|
|||||||
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)
|
||||||
}
|
}
|
||||||
@ -327,17 +338,17 @@ 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))
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,8 @@ type LoadBalance struct {
|
|||||||
var errStrategy = errors.New("unsupported strategy")
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
|
||||||
func parseStrategy(config map[string]any) 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"
|
||||||
}
|
}
|
||||||
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
|
||||||
|
regexp "github.com/dlclark/regexp2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -29,6 +31,7 @@ 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]any, 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) {
|
||||||
@ -45,22 +48,33 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
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 _, 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
|
||||||
@ -68,20 +82,20 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
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)
|
||||||
@ -92,9 +106,14 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
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
|
||||||
@ -112,7 +131,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
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
|
||||||
|
@ -66,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() (any, 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()
|
||||||
@ -95,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)
|
||||||
}
|
}
|
||||||
@ -125,10 +128,8 @@ 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
|
||||||
|
@ -18,27 +18,24 @@ 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,
|
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = &C.Metadata{
|
addr = &C.Metadata{
|
||||||
AddrType: C.AtypIPv6,
|
Host: "",
|
||||||
Host: "",
|
DstIP: ip,
|
||||||
DstIP: ip,
|
DstPort: port,
|
||||||
DstPort: port,
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ 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{}
|
||||||
@ -39,15 +40,17 @@ func (f *fetcher) VehicleType() types.VehicleType {
|
|||||||
|
|
||||||
func (f *fetcher) Initial() (any, 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 = os.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()
|
||||||
}
|
}
|
||||||
@ -86,7 +89,7 @@ func (f *fetcher) Initial() (any, 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
|
||||||
@ -130,25 +133,33 @@ func (f *fetcher) Destroy() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fetcher) pullLoop() {
|
func (f *fetcher) pullLoop(immediately bool) {
|
||||||
|
update := func() {
|
||||||
|
elm, same, err := f.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if same {
|
||||||
|
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||||
|
if f.onUpdate != nil {
|
||||||
|
f.onUpdate(elm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if immediately {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-f.ticker.C:
|
case <-f.ticker.C:
|
||||||
elm, same, err := f.Update()
|
update()
|
||||||
if err != nil {
|
|
||||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if same {
|
|
||||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
|
||||||
if f.onUpdate != nil {
|
|
||||||
f.onUpdate(elm)
|
|
||||||
}
|
|
||||||
case <-f.done:
|
case <-f.done:
|
||||||
f.ticker.Stop()
|
f.ticker.Stop()
|
||||||
return
|
return
|
||||||
@ -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,9 +66,13 @@ 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() (any, error) {
|
b.Go(p.Name(), func() (any, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
|
@ -10,7 +10,10 @@ import (
|
|||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errVehicleType = errors.New("unsupport vehicle type")
|
var (
|
||||||
|
errVehicleType = errors.New("unsupport vehicle type")
|
||||||
|
errSubPath = errors.New("path is not subpath of home directory")
|
||||||
|
)
|
||||||
|
|
||||||
type healthCheckSchema struct {
|
type healthCheckSchema struct {
|
||||||
Enable bool `provider:"enable"`
|
Enable bool `provider:"enable"`
|
||||||
@ -53,6 +56,9 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
|
|||||||
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)
|
||||||
|
@ -4,17 +4,22 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@ -49,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 {
|
||||||
@ -78,16 +83,15 @@ 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 (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||||
pp.proxies = proxies
|
pp.proxies = proxies
|
||||||
pp.healthCheck.setProxy(proxies)
|
pp.healthCheck.setProxy(proxies)
|
||||||
if pp.healthCheck.auto() {
|
if pp.healthCheck.auto() {
|
||||||
go pp.healthCheck.check()
|
go pp.healthCheck.checkAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +101,7 @@ func stopProxyProvider(pd *ProxySetProvider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
||||||
filterReg, err := regexp.Compile(filter)
|
filterReg, err := regexp.Compile(filter, regexp.None)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid filter regex: %w", err)
|
return nil, fmt.Errorf("invalid filter regex: %w", err)
|
||||||
}
|
}
|
||||||
@ -129,8 +133,14 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
|
|||||||
|
|
||||||
proxies := []C.Proxy{}
|
proxies := []C.Proxy{}
|
||||||
for idx, mapping := range schema.Proxies {
|
for idx, mapping := range schema.Proxies {
|
||||||
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) {
|
if name, ok := mapping["name"].(string); ok && len(filter) > 0 {
|
||||||
continue
|
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 {
|
||||||
@ -182,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 {
|
||||||
@ -205,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) {
|
||||||
@ -233,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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 any
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put element in Cache with its ttl
|
|
||||||
func (c *cache) Put(key any, payload any, 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 any) any {
|
|
||||||
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 any) (payload any, 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 any) 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()
|
|
||||||
}
|
|
4
common/cache/lrucache.go
vendored
4
common/cache/lrucache.go
vendored
@ -64,8 +64,8 @@ type LruCache struct {
|
|||||||
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[any]*list.Element),
|
cache: make(map[any]*list.Element),
|
||||||
|
20
common/cache/lrucache_test.go
vendored
20
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)
|
||||||
@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
|
|||||||
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)
|
||||||
@ -152,7 +152,7 @@ func TestSetWithExpire(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -165,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)
|
||||||
|
|
||||||
|
@ -4,8 +4,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/pool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Relay copies between left and right bidirectionally.
|
// Relay copies between left and right bidirectionally.
|
||||||
@ -13,18 +11,14 @@ func Relay(leftConn, rightConn net.Conn) {
|
|||||||
ch := make(chan error)
|
ch := make(chan error)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
buf := pool.Get(pool.RelayBufferSize)
|
|
||||||
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
|
||||||
// See also https://github.com/Dreamacro/clash/pull/1209
|
// See also https://github.com/Dreamacro/clash/pull/1209
|
||||||
_, err := io.CopyBuffer(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn}, buf)
|
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
|
||||||
pool.Put(buf)
|
|
||||||
leftConn.SetReadDeadline(time.Now())
|
leftConn.SetReadDeadline(time.Now())
|
||||||
ch <- err
|
ch <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buf := pool.Get(pool.RelayBufferSize)
|
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
|
||||||
io.CopyBuffer(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn}, buf)
|
|
||||||
pool.Put(buf)
|
|
||||||
rightConn.SetReadDeadline(time.Now())
|
rightConn.SetReadDeadline(time.Now())
|
||||||
<-ch
|
<-ch
|
||||||
}
|
}
|
||||||
|
@ -32,28 +32,37 @@ func NewAllocator() *Allocator {
|
|||||||
|
|
||||||
// Get a []byte from pool with most appropriate cap
|
// Get a []byte from pool with most appropriate cap
|
||||||
func (alloc *Allocator) Get(size int) []byte {
|
func (alloc *Allocator) Get(size int) []byte {
|
||||||
if size <= 0 || size > 65536 {
|
switch {
|
||||||
|
case size < 0:
|
||||||
|
panic("alloc.Get: len out of range")
|
||||||
|
case size == 0:
|
||||||
return nil
|
return nil
|
||||||
}
|
case size > 65536:
|
||||||
|
return make([]byte, size)
|
||||||
|
default:
|
||||||
|
bits := msb(size)
|
||||||
|
if size == 1<<bits {
|
||||||
|
return alloc.buffers[bits].Get().([]byte)[:size]
|
||||||
|
}
|
||||||
|
|
||||||
bits := msb(size)
|
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
||||||
if size == 1<<bits {
|
|
||||||
return alloc.buffers[bits].Get().([]byte)[:size]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return alloc.buffers[bits+1].Get().([]byte)[:size]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put returns a []byte to pool for future use,
|
// Put returns a []byte to pool for future use,
|
||||||
// which the cap must be exactly 2^n
|
// which the cap must be exactly 2^n
|
||||||
func (alloc *Allocator) Put(buf []byte) error {
|
func (alloc *Allocator) Put(buf []byte) error {
|
||||||
|
if cap(buf) == 0 || cap(buf) > 65536 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
bits := msb(cap(buf))
|
bits := msb(cap(buf))
|
||||||
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
|
if cap(buf) != 1<<bits {
|
||||||
return errors.New("allocator Put() incorrect buffer size")
|
return errors.New("allocator Put() incorrect buffer size")
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore SA6002 ignore temporarily
|
|
||||||
//nolint
|
//nolint
|
||||||
|
//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) {
|
||||||
|
@ -3,9 +3,14 @@ package pool
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/protobytes"
|
||||||
)
|
)
|
||||||
|
|
||||||
var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
|
var (
|
||||||
|
bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
|
||||||
|
bytesBufferPool = sync.Pool{New: func() any { return &protobytes.BytesWriter{} }}
|
||||||
|
)
|
||||||
|
|
||||||
func GetBuffer() *bytes.Buffer {
|
func GetBuffer() *bytes.Buffer {
|
||||||
return bufferPool.Get().(*bytes.Buffer)
|
return bufferPool.Get().(*bytes.Buffer)
|
||||||
@ -15,3 +20,12 @@ func PutBuffer(buf *bytes.Buffer) {
|
|||||||
buf.Reset()
|
buf.Reset()
|
||||||
bufferPool.Put(buf)
|
bufferPool.Put(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetBytesBuffer() *protobytes.BytesWriter {
|
||||||
|
return bytesBufferPool.Get().(*protobytes.BytesWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutBytesBuffer(buf *protobytes.BytesWriter) {
|
||||||
|
buf.Reset()
|
||||||
|
bytesBufferPool.Put(buf)
|
||||||
|
}
|
||||||
|
@ -25,7 +25,6 @@ type Result struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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() (any, error)) (v any, err error, shared bool) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
@ -1,57 +1,12 @@
|
|||||||
//go:build !linux && !darwin
|
//go:build !linux && !darwin && !windows
|
||||||
|
|
||||||
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
|
||||||
|
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
|
||||||
|
}
|
@ -51,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
|
||||||
}
|
}
|
||||||
@ -83,8 +91,14 @@ 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 err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
if opt.fallbackBind {
|
||||||
return nil, err
|
if err := fallbackBindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opt.routingMark != 0 {
|
if opt.routingMark != 0 {
|
||||||
|
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
|
||||||
|
}
|
@ -31,13 +31,13 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Control(func(fd uintptr) {
|
var innerErr error
|
||||||
switch network {
|
err = c.Control(func(fd uintptr) {
|
||||||
case "tcp4", "udp4":
|
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||||
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
|
||||||
case "tcp6", "udp6":
|
|
||||||
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if innerErr != nil {
|
||||||
|
err = innerErr
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ var (
|
|||||||
|
|
||||||
type option struct {
|
type option struct {
|
||||||
interfaceName string
|
interfaceName string
|
||||||
|
fallbackBind bool
|
||||||
addrReuse bool
|
addrReuse bool
|
||||||
routingMark int
|
routingMark int
|
||||||
}
|
}
|
||||||
@ -22,6 +23,12 @@ func WithInterface(name string) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 *option) {
|
return func(opt *option) {
|
||||||
opt.addrReuse = reuse
|
opt.addrReuse = reuse
|
||||||
|
@ -3,6 +3,7 @@ package fakeip
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
@ -20,7 +21,7 @@ type store interface {
|
|||||||
CloneTo(store)
|
CloneTo(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pool is a implementation about fake ip generator without storage
|
// Pool is an implementation about fake ip generator without storage
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
max uint32
|
max uint32
|
||||||
min uint32
|
min uint32
|
||||||
@ -36,6 +37,9 @@ type Pool struct {
|
|||||||
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()
|
||||||
|
|
||||||
|
// RFC4343: DNS Case Insensitive, we SHOULD return result with all cases.
|
||||||
|
host = strings.ToLower(host)
|
||||||
if ip, exist := p.store.GetByHost(host); exist {
|
if ip, exist := p.store.GetByHost(host); exist {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
@ -95,21 +99,21 @@ func (p *Pool) CloneFrom(o *Pool) {
|
|||||||
func (p *Pool) get(host string) net.IP {
|
func (p *Pool) get(host string) net.IP {
|
||||||
current := p.offset
|
current := p.offset
|
||||||
for {
|
for {
|
||||||
|
ip := uintToIP(p.min + p.offset)
|
||||||
|
if !p.store.Exist(ip) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
p.offset = (p.offset + 1) % (p.max - p.min)
|
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||||
// Avoid infinite loops
|
// Avoid infinite loops
|
||||||
if p.offset == current {
|
if p.offset == current {
|
||||||
p.offset = (p.offset + 1) % (p.max - p.min)
|
p.offset = (p.offset + 1) % (p.max - p.min)
|
||||||
ip := uintToIP(p.min + p.offset - 1)
|
ip := uintToIP(p.min + p.offset)
|
||||||
p.store.DelByIP(ip)
|
p.store.DelByIP(ip)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := uintToIP(p.min + p.offset - 1)
|
|
||||||
if !p.store.Exist(ip) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ip := uintToIP(p.min + p.offset - 1)
|
ip := uintToIP(p.min + p.offset)
|
||||||
p.store.PutByIP(ip, host)
|
p.store.PutByIP(ip, host)
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
@ -164,7 +168,7 @@ func New(options Options) (*Pool, error) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pool.store = &memoryStore{
|
pool.store = &memoryStore{
|
||||||
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
|
cache: cache.New(cache.WithSize(options.Size * 2)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package fakeip
|
package fakeip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@ -75,6 +74,27 @@ func TestPool_Basic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPool_Case_Insensitive(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 {
|
||||||
|
first := pool.Lookup("foo.com")
|
||||||
|
last := pool.Lookup("Foo.Com")
|
||||||
|
foo, exist := pool.LookBack(last)
|
||||||
|
|
||||||
|
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) {
|
func TestPool_CycleUsed(t *testing.T) {
|
||||||
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
|
||||||
pools, tempfile, err := createPools(Options{
|
pools, tempfile, err := createPools(Options{
|
||||||
@ -85,15 +105,13 @@ func TestPool_CycleUsed(t *testing.T) {
|
|||||||
defer os.Remove(tempfile)
|
defer os.Remove(tempfile)
|
||||||
|
|
||||||
for _, pool := range pools {
|
for _, pool := range pools {
|
||||||
foo := pool.Lookup("foo.com")
|
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("2.com"))
|
||||||
bar := pool.Lookup("bar.com")
|
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
|
||||||
for i := 0; i < 3; i++ {
|
assert.Equal(t, net.IP{192, 168, 0, 4}, pool.Lookup("4.com"))
|
||||||
pool.Lookup(fmt.Sprintf("%d.com", i))
|
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"))
|
||||||
baz := pool.Lookup("baz.com")
|
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("12.com"))
|
||||||
next := pool.Lookup("foo.com")
|
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
|
||||||
assert.True(t, foo.Equal(baz))
|
|
||||||
assert.True(t, next.Equal(bar))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -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,7 +2,9 @@ package process
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net"
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -15,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:
|
||||||
@ -26,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 {
|
||||||
@ -34,48 +52,62 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// xsocket_n.so_last_pid
|
if from.Addr() == srcIP { // FIXME: add dstIP check
|
||||||
pid := readNativeUint32(buf[so+68 : so+72])
|
// xsocket_n.so_last_pid
|
||||||
return getExecPathFromPID(pid)
|
pid := readNativeUint32(buf[so+68 : so+72])
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
@ -1,233 +0,0 @@
|
|||||||
package process
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// store process name for when dealing with multiple PROCESS-NAME rules
|
|
||||||
var (
|
|
||||||
defaultSearcher *searcher
|
|
||||||
|
|
||||||
once sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
|
||||||
once.Do(func() {
|
|
||||||
if err := initSearcher(); err != nil {
|
|
||||||
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
|
|
||||||
log.Warnln("All PROCESS-NAME rules will be skipped")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if defaultSearcher == nil {
|
|
||||||
return "", ErrPlatformNotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
var spath string
|
|
||||||
isTCP := network == TCP
|
|
||||||
switch network {
|
|
||||||
case TCP:
|
|
||||||
spath = "net.inet.tcp.pcblist"
|
|
||||||
case UDP:
|
|
||||||
spath = "net.inet.udp.pcblist"
|
|
||||||
default:
|
|
||||||
return "", ErrInvalidNetwork
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := syscall.Sysctl(spath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := []byte(value)
|
|
||||||
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return getExecPathFromPID(pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExecPathFromPID(pid uint32) (string, error) {
|
|
||||||
buf := make([]byte, 2048)
|
|
||||||
size := uint64(len(buf))
|
|
||||||
// CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid
|
|
||||||
mib := [4]uint32{1, 14, 12, pid}
|
|
||||||
|
|
||||||
_, _, errno := syscall.Syscall6(
|
|
||||||
syscall.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 "", errno
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(buf[:size-1]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNativeUint32(b []byte) uint32 {
|
|
||||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
type searcher struct {
|
|
||||||
// sizeof(struct xinpgen)
|
|
||||||
headSize int
|
|
||||||
// sizeof(struct xtcpcb)
|
|
||||||
tcpItemSize int
|
|
||||||
// sizeof(struct xinpcb)
|
|
||||||
udpItemSize int
|
|
||||||
udpInpOffset int
|
|
||||||
port int
|
|
||||||
ip int
|
|
||||||
vflag int
|
|
||||||
socket int
|
|
||||||
|
|
||||||
// sizeof(struct xfile)
|
|
||||||
fileItemSize int
|
|
||||||
data int
|
|
||||||
pid int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
|
|
||||||
var itemSize int
|
|
||||||
var inpOffset int
|
|
||||||
|
|
||||||
if isTCP {
|
|
||||||
// struct xtcpcb
|
|
||||||
itemSize = s.tcpItemSize
|
|
||||||
inpOffset = 8
|
|
||||||
} else {
|
|
||||||
// struct xinpcb
|
|
||||||
itemSize = s.udpItemSize
|
|
||||||
inpOffset = s.udpInpOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
isIPv4 := ip.To4() != nil
|
|
||||||
// skip the first xinpgen block
|
|
||||||
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
|
|
||||||
inp := i + inpOffset
|
|
||||||
|
|
||||||
srcPort := binary.BigEndian.Uint16(buf[inp+s.port : inp+s.port+2])
|
|
||||||
|
|
||||||
if port != srcPort {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// xinpcb.inp_vflag
|
|
||||||
flag := buf[inp+s.vflag]
|
|
||||||
|
|
||||||
var srcIP net.IP
|
|
||||||
switch {
|
|
||||||
case flag&0x1 > 0 && isIPv4:
|
|
||||||
// ipv4
|
|
||||||
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
|
|
||||||
case flag&0x2 > 0 && !isIPv4:
|
|
||||||
// ipv6
|
|
||||||
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ip.Equal(srcIP) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// xsocket.xso_so, interpreted as big endian anyway since it's only used for comparison
|
|
||||||
socket := binary.BigEndian.Uint64(buf[inp+s.socket : inp+s.socket+8])
|
|
||||||
return s.searchSocketPid(socket)
|
|
||||||
}
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searcher) searchSocketPid(socket uint64) (uint32, error) {
|
|
||||||
value, err := syscall.Sysctl("kern.file")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := []byte(value)
|
|
||||||
|
|
||||||
// struct xfile
|
|
||||||
itemSize := s.fileItemSize
|
|
||||||
for i := 0; i+itemSize <= len(buf); i += itemSize {
|
|
||||||
// xfile.xf_data
|
|
||||||
data := binary.BigEndian.Uint64(buf[i+s.data : i+s.data+8])
|
|
||||||
if data == socket {
|
|
||||||
// xfile.xf_pid
|
|
||||||
pid := readNativeUint32(buf[i+s.pid : i+s.pid+4])
|
|
||||||
return pid, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSearcher(major int) *searcher {
|
|
||||||
var s *searcher
|
|
||||||
switch major {
|
|
||||||
case 11:
|
|
||||||
s = &searcher{
|
|
||||||
headSize: 32,
|
|
||||||
tcpItemSize: 1304,
|
|
||||||
udpItemSize: 632,
|
|
||||||
port: 198,
|
|
||||||
ip: 228,
|
|
||||||
vflag: 116,
|
|
||||||
socket: 88,
|
|
||||||
fileItemSize: 80,
|
|
||||||
data: 56,
|
|
||||||
pid: 8,
|
|
||||||
udpInpOffset: 8,
|
|
||||||
}
|
|
||||||
case 12:
|
|
||||||
fallthrough
|
|
||||||
case 13:
|
|
||||||
s = &searcher{
|
|
||||||
headSize: 64,
|
|
||||||
tcpItemSize: 744,
|
|
||||||
udpItemSize: 400,
|
|
||||||
port: 254,
|
|
||||||
ip: 284,
|
|
||||||
vflag: 392,
|
|
||||||
socket: 16,
|
|
||||||
fileItemSize: 128,
|
|
||||||
data: 56,
|
|
||||||
pid: 8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSearcher() error {
|
|
||||||
osRelease, err := syscall.Sysctl("kern.osrelease")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dot := strings.Index(osRelease, ".")
|
|
||||||
if dot != -1 {
|
|
||||||
osRelease = osRelease[:dot]
|
|
||||||
}
|
|
||||||
major, err := strconv.Atoi(osRelease)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defaultSearcher = newSearcher(major)
|
|
||||||
if defaultSearcher == nil {
|
|
||||||
return fmt.Errorf("unsupported freebsd version %d", major)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
35
component/process/process_freebsd_test.go
Normal file
35
component/process/process_freebsd_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//go:build freebsd
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnforceStructValid12(t *testing.T) {
|
||||||
|
if majorVersion != 12 && majorVersion != 13 {
|
||||||
|
t.Skipf("Unsupported freebsd version: %d", majorVersion)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 0, int(unsafe.Offsetof(XTcpcb12{}.Len)))
|
||||||
|
assert.Equal(t, 24, int(unsafe.Offsetof(XTcpcb12{}.SocketAddr)))
|
||||||
|
assert.Equal(t, 116, int(unsafe.Offsetof(XTcpcb12{}.Family)))
|
||||||
|
assert.Equal(t, 260, int(unsafe.Offsetof(XTcpcb12{}.InEndpoints)))
|
||||||
|
assert.Equal(t, 0, int(unsafe.Offsetof(XInpcb12{}.Len)))
|
||||||
|
assert.Equal(t, 16, int(unsafe.Offsetof(XInpcb12{}.SocketAddr)))
|
||||||
|
assert.Equal(t, 108, int(unsafe.Offsetof(XInpcb12{}.Family)))
|
||||||
|
assert.Equal(t, 252, int(unsafe.Offsetof(XInpcb12{}.InEndpoints)))
|
||||||
|
assert.Equal(t, 0, int(unsafe.Offsetof(XFile12{}.Size)))
|
||||||
|
assert.Equal(t, 8, int(unsafe.Offsetof(XFile12{}.Pid)))
|
||||||
|
assert.Equal(t, 56, int(unsafe.Offsetof(XFile12{}.DataAddr)))
|
||||||
|
assert.Equal(t, 64, int(unsafe.Sizeof(Xinpgen12{})))
|
||||||
|
assert.Equal(t, 744, int(unsafe.Sizeof(XTcpcb12{})))
|
||||||
|
assert.Equal(t, 400, int(unsafe.Sizeof(XInpcb12{})))
|
||||||
|
assert.Equal(t, 40, int(unsafe.Sizeof(InEndpoints12{})))
|
||||||
|
assert.Equal(t, 128, int(unsafe.Sizeof(XFile12{})))
|
||||||
|
}
|
@ -5,207 +5,228 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"unicode"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/pool"
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
|
|
||||||
|
"github.com/mdlayher/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
|
type inetDiagRequest struct {
|
||||||
var nativeEndian = func() binary.ByteOrder {
|
Family byte
|
||||||
var x uint32 = 0x01020304
|
Protocol byte
|
||||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
Ext byte
|
||||||
return binary.BigEndian
|
Pad byte
|
||||||
}
|
States uint32
|
||||||
|
|
||||||
return binary.LittleEndian
|
SrcPort [2]byte
|
||||||
}()
|
DstPort [2]byte
|
||||||
|
Src [16]byte
|
||||||
|
Dst [16]byte
|
||||||
|
If uint32
|
||||||
|
Cookie [2]uint32
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
type inetDiagResponse struct {
|
||||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
Family byte
|
||||||
socketDiagByFamily = 20
|
State byte
|
||||||
pathProc = "/proc"
|
Timer byte
|
||||||
)
|
ReTrans byte
|
||||||
|
|
||||||
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
SrcPort [2]byte
|
||||||
inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
|
DstPort [2]byte
|
||||||
|
Src [16]byte
|
||||||
|
Dst [16]byte
|
||||||
|
If uint32
|
||||||
|
Cookie [2]uint32
|
||||||
|
|
||||||
|
Expires uint32
|
||||||
|
RQueue uint32
|
||||||
|
WQueue uint32
|
||||||
|
UID uint32
|
||||||
|
INode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
|
||||||
|
inode, uid, err := resolveSocketByNetlink(network, from, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveProcessNameByProcSearch(inode, uid)
|
return resolveProcessPathByProcSearch(inode, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int32, error) {
|
func resolveSocketByNetlink(network string, from netip.AddrPort, to netip.AddrPort) (inode uint32, uid uint32, err error) {
|
||||||
var family byte
|
var families []byte
|
||||||
var protocol byte
|
if from.Addr().Unmap().Is4() {
|
||||||
|
families = []byte{unix.AF_INET, unix.AF_INET6}
|
||||||
|
} else {
|
||||||
|
families = []byte{unix.AF_INET6, unix.AF_INET}
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol byte
|
||||||
switch network {
|
switch network {
|
||||||
case TCP:
|
case TCP:
|
||||||
protocol = syscall.IPPROTO_TCP
|
protocol = unix.IPPROTO_TCP
|
||||||
case UDP:
|
case UDP:
|
||||||
protocol = syscall.IPPROTO_UDP
|
protocol = unix.IPPROTO_UDP
|
||||||
default:
|
default:
|
||||||
return 0, 0, ErrInvalidNetwork
|
return 0, 0, ErrInvalidNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip.To4() != nil {
|
if protocol == unix.IPPROTO_UDP {
|
||||||
family = syscall.AF_INET
|
// Swap from & to for udp
|
||||||
|
// See also https://www.mail-archive.com/netdev@vger.kernel.org/msg248638.html
|
||||||
|
from, to = to, from
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, family := range families {
|
||||||
|
inode, uid, err = resolveSocketByNetlinkExact(family, protocol, from, to, netlink.Request)
|
||||||
|
if err == nil {
|
||||||
|
return inode, uid, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, 0, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveSocketByNetlinkExact(family byte, protocol byte, from netip.AddrPort, to netip.AddrPort, flags netlink.HeaderFlags) (inode uint32, uid uint32, err error) {
|
||||||
|
request := &inetDiagRequest{
|
||||||
|
Family: family,
|
||||||
|
Protocol: protocol,
|
||||||
|
States: 0xffffffff,
|
||||||
|
Cookie: [2]uint32{0xffffffff, 0xffffffff},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fromAddr []byte
|
||||||
|
toAddr []byte
|
||||||
|
)
|
||||||
|
if family == unix.AF_INET {
|
||||||
|
fromAddr = net.IP(from.Addr().AsSlice()).To4()
|
||||||
|
toAddr = net.IP(to.Addr().AsSlice()).To4()
|
||||||
} else {
|
} else {
|
||||||
family = syscall.AF_INET6
|
fromAddr = net.IP(from.Addr().AsSlice()).To16()
|
||||||
|
toAddr = net.IP(to.Addr().AsSlice()).To16()
|
||||||
}
|
}
|
||||||
|
|
||||||
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
|
copy(request.Src[:], fromAddr)
|
||||||
|
copy(request.Dst[:], toAddr)
|
||||||
|
|
||||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
binary.BigEndian.PutUint16(request.SrcPort[:], from.Port())
|
||||||
|
binary.BigEndian.PutUint16(request.DstPort[:], to.Port())
|
||||||
|
|
||||||
|
conn, err := netlink.Dial(unix.NETLINK_INET_DIAG, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("dial netlink: %w", err)
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
defer syscall.Close(socket)
|
defer conn.Close()
|
||||||
|
|
||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
message := netlink.Message{
|
||||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
Header: netlink.Header{
|
||||||
|
Type: 20, // SOCK_DIAG_BY_FAMILY
|
||||||
|
Flags: flags,
|
||||||
|
},
|
||||||
|
Data: (*(*[unsafe.Sizeof(*request)]byte)(unsafe.Pointer(request)))[:],
|
||||||
|
}
|
||||||
|
|
||||||
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
|
messages, err := conn.Execute(message)
|
||||||
Family: syscall.AF_NETLINK,
|
if err != nil {
|
||||||
Pad: 0,
|
|
||||||
Pid: 0,
|
|
||||||
Groups: 0,
|
|
||||||
}); err != nil {
|
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := syscall.Write(socket, req); err != nil {
|
for _, msg := range messages {
|
||||||
return 0, 0, fmt.Errorf("write request: %w", err)
|
if len(msg.Data) < int(unsafe.Sizeof(inetDiagResponse{})) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
response := (*inetDiagResponse)(unsafe.Pointer(&msg.Data[0]))
|
||||||
|
|
||||||
|
return response.INode, response.UID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rb := pool.Get(pool.RelayBufferSize)
|
return 0, 0, ErrNotFound
|
||||||
defer pool.Put(rb)
|
}
|
||||||
|
|
||||||
n, err := syscall.Read(socket, rb)
|
func resolveProcessPathByProcSearch(inode, uid uint32) (string, error) {
|
||||||
|
procDir, err := os.Open("/proc")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("read response: %w", err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer procDir.Close()
|
||||||
|
|
||||||
messages, err := syscall.ParseNetlinkMessage(rb[:n])
|
pids, err := procDir.Readdirnames(-1)
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("parse netlink message: %w", err)
|
|
||||||
} else if len(messages) == 0 {
|
|
||||||
return 0, 0, fmt.Errorf("unexcepted netlink response")
|
|
||||||
}
|
|
||||||
|
|
||||||
message := messages[0]
|
|
||||||
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
|
|
||||||
return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR")
|
|
||||||
}
|
|
||||||
|
|
||||||
inode, uid := unpackSocketDiagResponse(&messages[0])
|
|
||||||
if inode < 0 || uid < 0 {
|
|
||||||
return 0, 0, fmt.Errorf("invalid inode(%d) or uid(%d)", inode, uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
return inode, uid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
|
|
||||||
s := make([]byte, 16)
|
|
||||||
|
|
||||||
if v4 := source.To4(); v4 != nil {
|
|
||||||
copy(s, v4)
|
|
||||||
} else {
|
|
||||||
copy(s, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
|
||||||
|
|
||||||
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
|
||||||
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
|
||||||
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
|
||||||
nativeEndian.PutUint32(buf[8:12], 0)
|
|
||||||
nativeEndian.PutUint32(buf[12:16], 0)
|
|
||||||
|
|
||||||
buf[16] = family
|
|
||||||
buf[17] = protocol
|
|
||||||
buf[18] = 0
|
|
||||||
buf[19] = 0
|
|
||||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
|
|
||||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
|
||||||
|
|
||||||
copy(buf[28:44], s)
|
|
||||||
copy(buf[44:60], net.IPv6zero)
|
|
||||||
|
|
||||||
nativeEndian.PutUint32(buf[60:64], 0)
|
|
||||||
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) {
|
|
||||||
if len(msg.Data) < 72 {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
data := msg.Data
|
|
||||||
|
|
||||||
uid = int32(nativeEndian.Uint32(data[64:68]))
|
|
||||||
inode = int32(nativeEndian.Uint32(data[68:72]))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
|
|
||||||
files, err := os.ReadDir(pathProc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := make([]byte, syscall.PathMax)
|
expectedSocketName := fmt.Appendf(nil, "socket:[%d]", inode)
|
||||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
|
||||||
|
|
||||||
for _, f := range files {
|
pathBuffer := pool.Get(64)
|
||||||
if !f.IsDir() || !isPid(f.Name()) {
|
defer pool.Put(pathBuffer)
|
||||||
|
|
||||||
|
readlinkBuffer := pool.Get(32)
|
||||||
|
defer pool.Put(readlinkBuffer)
|
||||||
|
|
||||||
|
copy(pathBuffer, "/proc/")
|
||||||
|
|
||||||
|
for _, pid := range pids {
|
||||||
|
if !isPid(pid) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := f.Info()
|
pathBuffer = append(pathBuffer[:len("/proc/")], pid...)
|
||||||
|
|
||||||
|
stat := &unix.Stat_t{}
|
||||||
|
err = unix.Stat(string(pathBuffer), stat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
continue
|
||||||
}
|
} else if stat.Uid != uid {
|
||||||
if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
processPath := path.Join(pathProc, f.Name())
|
pathBuffer = append(pathBuffer, "/fd/"...)
|
||||||
fdPath := path.Join(processPath, "fd")
|
fdsPrefixLength := len(pathBuffer)
|
||||||
|
|
||||||
fds, err := os.ReadDir(fdPath)
|
fdDir, err := os.Open(string(pathBuffer))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fds, err := fdDir.Readdirnames(-1)
|
||||||
|
fdDir.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fd := range fds {
|
for _, fd := range fds {
|
||||||
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
pathBuffer = pathBuffer[:fdsPrefixLength]
|
||||||
|
|
||||||
|
pathBuffer = append(pathBuffer, fd...)
|
||||||
|
|
||||||
|
n, err := unix.Readlink(string(pathBuffer), readlinkBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(buffer[:n], socket) {
|
if bytes.Equal(readlinkBuffer[:n], expectedSocketName) {
|
||||||
return os.Readlink(path.Join(processPath, "exe"))
|
return os.Readlink("/proc/" + pid + "/exe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
return "", fmt.Errorf("inode %d of uid %d not found", inode, uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPid(s string) bool {
|
func isPid(name string) bool {
|
||||||
return strings.IndexFunc(s, func(r rune) bool {
|
for _, c := range name {
|
||||||
return !unicode.IsDigit(r)
|
if c < '0' || c > '9' {
|
||||||
}) == -1
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
//go:build !darwin && !linux && !windows && (!freebsd || !amd64)
|
//go:build !darwin && !linux && !windows && !freebsd
|
||||||
|
|
||||||
package process
|
package process
|
||||||
|
|
||||||
import "net"
|
import (
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
func findProcessPath(_ string, _, _ netip.AddrPort) (string, error) {
|
||||||
return "", ErrPlatformNotSupport
|
return "", ErrPlatformNotSupport
|
||||||
}
|
}
|
||||||
|
112
component/process/process_test.go
Normal file
112
component/process/process_test.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConn(t *testing.T, network, address string) {
|
||||||
|
l, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Listen failed", err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Dial failed", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
rConn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Accept conn failed", err)
|
||||||
|
}
|
||||||
|
defer rConn.Close()
|
||||||
|
|
||||||
|
path, err := FindProcessPath(TCP, conn.LocalAddr().(*net.TCPAddr).AddrPort(), conn.RemoteAddr().(*net.TCPAddr).AddrPort())
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Find process path failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Get executable failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, exePath, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindProcessPathTCP(t *testing.T) {
|
||||||
|
t.Run("v4", func(t *testing.T) {
|
||||||
|
testConn(t, "tcp4", "127.0.0.1:0")
|
||||||
|
})
|
||||||
|
t.Run("v6", func(t *testing.T) {
|
||||||
|
testConn(t, "tcp6", "[::1]:0")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPacketConn(t *testing.T, network, lAddress, rAddress string) {
|
||||||
|
lConn, err := net.ListenPacket(network, lAddress)
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "ListenPacket failed", err)
|
||||||
|
}
|
||||||
|
defer lConn.Close()
|
||||||
|
|
||||||
|
rConn, err := net.ListenPacket(network, rAddress)
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "ListenPacket failed", err)
|
||||||
|
}
|
||||||
|
defer rConn.Close()
|
||||||
|
|
||||||
|
_, err = lConn.WriteTo([]byte{0}, rConn.LocalAddr())
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Send message failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, lAddr, err := rConn.ReadFrom([]byte{0})
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Receive message failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := FindProcessPath(UDP, lAddr.(*net.UDPAddr).AddrPort(), rConn.LocalAddr().(*net.UDPAddr).AddrPort())
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Find process path", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(t, "Find executable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, exePath, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindProcessPathUDP(t *testing.T) {
|
||||||
|
t.Run("v4", func(t *testing.T) {
|
||||||
|
testPacketConn(t, "udp4", "127.0.0.1:0", "127.0.0.1:0")
|
||||||
|
})
|
||||||
|
t.Run("v6", func(t *testing.T) {
|
||||||
|
testPacketConn(t, "udp6", "[::1]:0", "[::1]:0")
|
||||||
|
})
|
||||||
|
t.Run("v4AnyLocal", func(t *testing.T) {
|
||||||
|
testPacketConn(t, "udp4", "0.0.0.0:0", "127.0.0.1:0")
|
||||||
|
})
|
||||||
|
t.Run("v6AnyLocal", func(t *testing.T) {
|
||||||
|
testPacketConn(t, "udp6", "[::]:0", "[::1]:0")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkFindProcessName(b *testing.B) {
|
||||||
|
from := netip.MustParseAddrPort("127.0.0.1:11447")
|
||||||
|
to := netip.MustParseAddrPort("127.0.0.1:33669")
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
FindProcessPath(TCP, from, to)
|
||||||
|
}
|
||||||
|
}
|
@ -1,196 +1,206 @@
|
|||||||
package process
|
package process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/log"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
"github.com/Dreamacro/clash/common/pool"
|
||||||
tcpTableFunc = "GetExtendedTcpTable"
|
|
||||||
tcpTablePidConn = 4
|
|
||||||
udpTableFunc = "GetExtendedUdpTable"
|
|
||||||
udpTablePid = 1
|
|
||||||
queryProcNameFunc = "QueryFullProcessImageNameW"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
getExTCPTable uintptr
|
modIphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
|
||||||
getExUDPTable uintptr
|
|
||||||
queryProcName uintptr
|
|
||||||
|
|
||||||
once sync.Once
|
procGetExtendedTcpTable = modIphlpapi.NewProc("GetExtendedTcpTable")
|
||||||
|
procGetExtendedUdpTable = modIphlpapi.NewProc("GetExtendedUdpTable")
|
||||||
)
|
)
|
||||||
|
|
||||||
func initWin32API() error {
|
func findProcessPath(network string, from netip.AddrPort, to netip.AddrPort) (string, error) {
|
||||||
h, err := windows.LoadLibrary("iphlpapi.dll")
|
family := uint32(windows.AF_INET)
|
||||||
if err != nil {
|
if from.Addr().Is6() {
|
||||||
return fmt.Errorf("LoadLibrary iphlpapi.dll failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
getExTCPTable, err = windows.GetProcAddress(h, tcpTableFunc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GetProcAddress of %s failed: %s", tcpTableFunc, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
getExUDPTable, err = windows.GetProcAddress(h, udpTableFunc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GetProcAddress of %s failed: %s", udpTableFunc, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err = windows.LoadLibrary("kernel32.dll")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("LoadLibrary kernel32.dll failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
queryProcName, err = windows.GetProcAddress(h, queryProcNameFunc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GetProcAddress of %s failed: %s", queryProcNameFunc, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
|
|
||||||
once.Do(func() {
|
|
||||||
err := initWin32API()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
|
|
||||||
log.Warnln("All PROCESS-NAMES rules will be skiped")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
family := windows.AF_INET
|
|
||||||
if ip.To4() == nil {
|
|
||||||
family = windows.AF_INET6
|
family = windows.AF_INET6
|
||||||
}
|
}
|
||||||
|
|
||||||
var class int
|
var protocol uint32
|
||||||
var fn uintptr
|
|
||||||
switch network {
|
switch network {
|
||||||
case TCP:
|
case TCP:
|
||||||
fn = getExTCPTable
|
protocol = windows.IPPROTO_TCP
|
||||||
class = tcpTablePidConn
|
|
||||||
case UDP:
|
case UDP:
|
||||||
fn = getExUDPTable
|
protocol = windows.IPPROTO_UDP
|
||||||
class = udpTablePid
|
|
||||||
default:
|
default:
|
||||||
return "", ErrInvalidNetwork
|
return "", ErrInvalidNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := getTransportTable(fn, family, class)
|
pid, err := findPidByConnectionEndpoint(family, protocol, from, to)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := newSearcher(family == windows.AF_INET, network == TCP)
|
|
||||||
|
|
||||||
pid, err := s.Search(buf, ip, uint16(srcPort))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return getExecPathFromPID(pid)
|
return getExecPathFromPID(pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
type searcher struct {
|
func findPidByConnectionEndpoint(family uint32, protocol uint32, from netip.AddrPort, to netip.AddrPort) (uint32, error) {
|
||||||
itemSize int
|
buf := pool.Get(0)
|
||||||
port int
|
defer pool.Put(buf)
|
||||||
ip int
|
|
||||||
ipSize int
|
|
||||||
pid int
|
|
||||||
tcpState int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
|
bufSize := uint32(len(buf))
|
||||||
n := int(readNativeUint32(b[:4]))
|
|
||||||
itemSize := s.itemSize
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
row := b[4+itemSize*i : 4+itemSize*(i+1)]
|
|
||||||
|
|
||||||
if s.tcpState >= 0 {
|
loop:
|
||||||
tcpState := readNativeUint32(row[s.tcpState : s.tcpState+4])
|
for {
|
||||||
// MIB_TCP_STATE_ESTAB, only check established connections for TCP
|
var ret uintptr
|
||||||
if tcpState != 5 {
|
|
||||||
continue
|
switch protocol {
|
||||||
|
case windows.IPPROTO_TCP:
|
||||||
|
ret, _, _ = procGetExtendedTcpTable.Call(
|
||||||
|
uintptr(unsafe.Pointer(unsafe.SliceData(buf))),
|
||||||
|
uintptr(unsafe.Pointer(&bufSize)),
|
||||||
|
0,
|
||||||
|
uintptr(family),
|
||||||
|
4, // TCP_TABLE_OWNER_PID_CONNECTIONS
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
case windows.IPPROTO_UDP:
|
||||||
|
ret, _, _ = procGetExtendedUdpTable.Call(
|
||||||
|
uintptr(unsafe.Pointer(unsafe.SliceData(buf))),
|
||||||
|
uintptr(unsafe.Pointer(&bufSize)),
|
||||||
|
0,
|
||||||
|
uintptr(family),
|
||||||
|
1, // UDP_TABLE_OWNER_PID
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return 0, errors.New("unsupported network")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ret {
|
||||||
|
case 0:
|
||||||
|
buf = buf[:bufSize]
|
||||||
|
|
||||||
|
break loop
|
||||||
|
case uintptr(windows.ERROR_INSUFFICIENT_BUFFER):
|
||||||
|
pool.Put(buf)
|
||||||
|
buf = pool.Get(int(bufSize))
|
||||||
|
|
||||||
|
continue loop
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("syscall error: %d", ret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buf) < int(unsafe.Sizeof(uint32(0))) {
|
||||||
|
return 0, fmt.Errorf("invalid table size: %d", len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesSize := *(*uint32)(unsafe.Pointer(&buf[0]))
|
||||||
|
|
||||||
|
switch protocol {
|
||||||
|
case windows.IPPROTO_TCP:
|
||||||
|
if family == windows.AF_INET {
|
||||||
|
type MibTcpRowOwnerPid struct {
|
||||||
|
State uint32
|
||||||
|
LocalAddr [4]byte
|
||||||
|
LocalPort uint32
|
||||||
|
RemoteAddr [4]byte
|
||||||
|
RemotePort uint32
|
||||||
|
OwningPid uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcpRowOwnerPid{})) {
|
||||||
|
return 0, fmt.Errorf("invalid tables size: %d", len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := unsafe.Slice((*MibTcpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
|
||||||
|
for _, entry := range entries {
|
||||||
|
localAddr := netip.AddrFrom4(entry.LocalAddr)
|
||||||
|
localPort := windows.Ntohs(uint16(entry.LocalPort))
|
||||||
|
remoteAddr := netip.AddrFrom4(entry.RemoteAddr)
|
||||||
|
remotePort := windows.Ntohs(uint16(entry.RemotePort))
|
||||||
|
|
||||||
|
if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() {
|
||||||
|
return entry.OwningPid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
type MibTcp6RowOwnerPid struct {
|
||||||
|
LocalAddr [16]byte
|
||||||
|
LocalScopeID uint32
|
||||||
|
LocalPort uint32
|
||||||
|
RemoteAddr [16]byte
|
||||||
|
RemoteScopeID uint32
|
||||||
|
RemotePort uint32
|
||||||
|
State uint32
|
||||||
|
OwningPid uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibTcp6RowOwnerPid{})) {
|
||||||
|
return 0, fmt.Errorf("invalid tables size: %d", len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := unsafe.Slice((*MibTcp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
|
||||||
|
for _, entry := range entries {
|
||||||
|
localAddr := netip.AddrFrom16(entry.LocalAddr)
|
||||||
|
localPort := windows.Ntohs(uint16(entry.LocalPort))
|
||||||
|
remoteAddr := netip.AddrFrom16(entry.RemoteAddr)
|
||||||
|
remotePort := windows.Ntohs(uint16(entry.RemotePort))
|
||||||
|
|
||||||
|
if localAddr == from.Addr() && remoteAddr == to.Addr() && localPort == from.Port() && remotePort == to.Port() {
|
||||||
|
return entry.OwningPid, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case windows.IPPROTO_UDP:
|
||||||
|
if family == windows.AF_INET {
|
||||||
|
type MibUdpRowOwnerPid struct {
|
||||||
|
LocalAddr [4]byte
|
||||||
|
LocalPort uint32
|
||||||
|
OwningPid uint32
|
||||||
|
}
|
||||||
|
|
||||||
// according to MSDN, only the lower 16 bits of dwLocalPort are used and the port number is in network endian.
|
if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdpRowOwnerPid{})) {
|
||||||
// this field can be illustrated as follows depends on different machine endianess:
|
return 0, fmt.Errorf("invalid tables size: %d", len(buf))
|
||||||
// little endian: [ MSB LSB 0 0 ] interpret as native uint32 is ((LSB<<8)|MSB)
|
}
|
||||||
// big endian: [ 0 0 MSB LSB ] interpret as native uint32 is ((MSB<<8)|LSB)
|
|
||||||
// so we need an syscall.Ntohs on the lower 16 bits after read the port as native uint32
|
entries := unsafe.Slice((*MibUdpRowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
|
||||||
srcPort := syscall.Ntohs(uint16(readNativeUint32(row[s.port : s.port+4])))
|
for _, entry := range entries {
|
||||||
if srcPort != port {
|
localAddr := netip.AddrFrom4(entry.LocalAddr)
|
||||||
continue
|
localPort := windows.Ntohs(uint16(entry.LocalPort))
|
||||||
|
|
||||||
|
if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() {
|
||||||
|
return entry.OwningPid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
type MibUdp6RowOwnerPid struct {
|
||||||
|
LocalAddr [16]byte
|
||||||
|
LocalScopeId uint32
|
||||||
|
LocalPort uint32
|
||||||
|
OwningPid uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint32(len(buf))-4 < entriesSize*uint32(unsafe.Sizeof(MibUdp6RowOwnerPid{})) {
|
||||||
|
return 0, fmt.Errorf("invalid tables size: %d", len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := unsafe.Slice((*MibUdp6RowOwnerPid)(unsafe.Pointer(&buf[4])), entriesSize)
|
||||||
|
for _, entry := range entries {
|
||||||
|
localAddr := netip.AddrFrom16(entry.LocalAddr)
|
||||||
|
localPort := windows.Ntohs(uint16(entry.LocalPort))
|
||||||
|
|
||||||
|
if (localAddr == from.Addr() || localAddr.IsUnspecified()) && localPort == from.Port() {
|
||||||
|
return entry.OwningPid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
|
return 0, ErrInvalidNetwork
|
||||||
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
|
|
||||||
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid := readNativeUint32(row[s.pid : s.pid+4])
|
|
||||||
return pid, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, ErrNotFound
|
return 0, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSearcher(isV4, isTCP bool) *searcher {
|
|
||||||
var itemSize, port, ip, ipSize, pid int
|
|
||||||
tcpState := -1
|
|
||||||
switch {
|
|
||||||
case isV4 && isTCP:
|
|
||||||
// struct MIB_TCPROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid, tcpState = 24, 8, 4, 4, 20, 0
|
|
||||||
case isV4 && !isTCP:
|
|
||||||
// struct MIB_UDPROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid = 12, 4, 0, 4, 8
|
|
||||||
case !isV4 && isTCP:
|
|
||||||
// struct MIB_TCP6ROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid, tcpState = 56, 20, 0, 16, 52, 48
|
|
||||||
case !isV4 && !isTCP:
|
|
||||||
// struct MIB_UDP6ROW_OWNER_PID
|
|
||||||
itemSize, port, ip, ipSize, pid = 28, 20, 0, 16, 24
|
|
||||||
}
|
|
||||||
|
|
||||||
return &searcher{
|
|
||||||
itemSize: itemSize,
|
|
||||||
port: port,
|
|
||||||
ip: ip,
|
|
||||||
ipSize: ipSize,
|
|
||||||
pid: pid,
|
|
||||||
tcpState: tcpState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
|
|
||||||
for size, buf := uint32(8), make([]byte, 8); ; {
|
|
||||||
ptr := unsafe.Pointer(&buf[0])
|
|
||||||
err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
|
|
||||||
|
|
||||||
switch err {
|
|
||||||
case 0:
|
|
||||||
return buf, nil
|
|
||||||
case uintptr(syscall.ERROR_INSUFFICIENT_BUFFER):
|
|
||||||
buf = make([]byte, size)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("syscall error: %d", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNativeUint32(b []byte) uint32 {
|
|
||||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExecPathFromPID(pid uint32) (string, error) {
|
func getExecPathFromPID(pid uint32) (string, error) {
|
||||||
// kernel process starts with a colon in order to distinguish with normal processes
|
// kernel process starts with a colon in order to distinguish with normal processes
|
||||||
switch pid {
|
switch pid {
|
||||||
@ -207,17 +217,13 @@ func getExecPathFromPID(pid uint32) (string, error) {
|
|||||||
}
|
}
|
||||||
defer windows.CloseHandle(h)
|
defer windows.CloseHandle(h)
|
||||||
|
|
||||||
buf := make([]uint16, syscall.MAX_LONG_PATH)
|
buf := make([]uint16, windows.MAX_LONG_PATH)
|
||||||
size := uint32(len(buf))
|
size := uint32(len(buf))
|
||||||
r1, _, err := syscall.SyscallN(
|
|
||||||
queryProcName,
|
err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size)
|
||||||
uintptr(h),
|
if err != nil {
|
||||||
uintptr(1),
|
|
||||||
uintptr(unsafe.Pointer(&buf[0])),
|
|
||||||
uintptr(unsafe.Pointer(&size)),
|
|
||||||
)
|
|
||||||
if r1 == 0 {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return syscall.UTF16ToString(buf[:size]), nil
|
|
||||||
|
return windows.UTF16ToString(buf[:size]), nil
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,15 @@ package resolver
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/component/trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -33,29 +36,33 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Resolver interface {
|
type Resolver interface {
|
||||||
|
LookupIP(ctx context.Context, host string) ([]net.IP, error)
|
||||||
|
LookupIPv4(ctx context.Context, host string) ([]net.IP, error)
|
||||||
|
LookupIPv6(ctx context.Context, host string) ([]net.IP, error)
|
||||||
ResolveIP(host string) (ip net.IP, err error)
|
ResolveIP(host string) (ip net.IP, err error)
|
||||||
ResolveIPv4(host string) (ip net.IP, err error)
|
ResolveIPv4(host string) (ip net.IP, err error)
|
||||||
ResolveIPv6(host string) (ip net.IP, err error)
|
ResolveIPv6(host string) (ip net.IP, err error)
|
||||||
|
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIPv4 with a host, return ipv4
|
// LookupIPv4 with a host, return ipv4 list
|
||||||
func ResolveIPv4(host string) (net.IP, error) {
|
func LookupIPv4(ctx context.Context, host string) ([]net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
if ip := node.Data.(net.IP).To4(); ip != nil {
|
if ip := node.Data.(net.IP).To4(); ip != nil {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
if !strings.Contains(host, ":") {
|
if !strings.Contains(host, ":") {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
}
|
}
|
||||||
return nil, ErrIPVersion
|
return nil, ErrIPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
return DefaultResolver.ResolveIPv4(host)
|
return DefaultResolver.LookupIPv4(ctx, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
||||||
@ -67,31 +74,42 @@ func ResolveIPv4(host string) (net.IP, error) {
|
|||||||
return nil, ErrIPNotFound
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipAddrs[rand.Intn(len(ipAddrs))], nil
|
return ipAddrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIPv6 with a host, return ipv6
|
// ResolveIPv4 with a host, return ipv4
|
||||||
func ResolveIPv6(host string) (net.IP, error) {
|
func ResolveIPv4(host string) (net.IP, error) {
|
||||||
|
ips, err := LookupIPv4(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIPv6 with a host, return ipv6 list
|
||||||
|
func LookupIPv6(ctx context.Context, host string) ([]net.IP, error) {
|
||||||
if DisableIPv6 {
|
if DisableIPv6 {
|
||||||
return nil, ErrIPv6Disabled
|
return nil, ErrIPv6Disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
if ip := node.Data.(net.IP).To16(); ip != nil {
|
if ip := node.Data.(net.IP).To16(); ip != nil {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
if strings.Contains(host, ":") {
|
if strings.Contains(host, ":") {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
}
|
}
|
||||||
return nil, ErrIPVersion
|
return nil, ErrIPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
if DefaultResolver != nil {
|
if DefaultResolver != nil {
|
||||||
return DefaultResolver.ResolveIPv6(host)
|
return DefaultResolver.LookupIPv6(ctx, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
|
||||||
@ -103,38 +121,62 @@ func ResolveIPv6(host string) (net.IP, error) {
|
|||||||
return nil, ErrIPNotFound
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipAddrs[rand.Intn(len(ipAddrs))], nil
|
return ipAddrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIPWithResolver same as ResolveIP, but with a resolver
|
// ResolveIPv6 with a host, return ipv6
|
||||||
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
|
func ResolveIPv6(host string) (net.IP, error) {
|
||||||
|
ips, err := LookupIPv6(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIPWithResolver same as ResolveIP, but with a resolver
|
||||||
|
func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]net.IP, error) {
|
||||||
if node := DefaultHosts.Search(host); node != nil {
|
if node := DefaultHosts.Search(host); node != nil {
|
||||||
return node.Data.(net.IP), nil
|
return []net.IP{node.Data.(net.IP)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if r != nil {
|
if r != nil {
|
||||||
if DisableIPv6 {
|
if DisableIPv6 {
|
||||||
return r.ResolveIPv4(host)
|
return r.LookupIPv4(ctx, host)
|
||||||
}
|
}
|
||||||
return r.ResolveIP(host)
|
return r.LookupIP(ctx, host)
|
||||||
} else if DisableIPv6 {
|
} else if DisableIPv6 {
|
||||||
return ResolveIPv4(host)
|
return LookupIPv4(ctx, host)
|
||||||
}
|
}
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddr, err := net.ResolveIPAddr("ip", host)
|
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipAddr.IP, nil
|
return ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveIP with a host, return ip
|
||||||
|
func LookupIP(ctx context.Context, host string) ([]net.IP, error) {
|
||||||
|
return LookupIPWithResolver(ctx, host, DefaultResolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIP with a host, return ip
|
// ResolveIP with a host, return ip
|
||||||
func ResolveIP(host string) (net.IP, error) {
|
func ResolveIP(host string) (net.IP, error) {
|
||||||
return ResolveIPWithResolver(host, DefaultResolver)
|
ips, err := LookupIP(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
}
|
}
|
||||||
|
100
config/config.go
100
config/config.go
@ -22,7 +22,8 @@ import (
|
|||||||
R "github.com/Dreamacro/clash/rule"
|
R "github.com/Dreamacro/clash/rule"
|
||||||
T "github.com/Dreamacro/clash/tunnel"
|
T "github.com/Dreamacro/clash/tunnel"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"github.com/samber/lo"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// General config
|
// General config
|
||||||
@ -68,6 +69,7 @@ type DNS struct {
|
|||||||
FakeIPRange *fakeip.Pool
|
FakeIPRange *fakeip.Pool
|
||||||
Hosts *trie.DomainTrie
|
Hosts *trie.DomainTrie
|
||||||
NameServerPolicy map[string]dns.NameServer
|
NameServerPolicy map[string]dns.NameServer
|
||||||
|
SearchDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FallbackFilter config
|
// FallbackFilter config
|
||||||
@ -85,7 +87,9 @@ type Profile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Experimental config
|
// Experimental config
|
||||||
type Experimental struct{}
|
type Experimental struct {
|
||||||
|
UDPFallbackMatch bool `yaml:"udp-fallback-match"`
|
||||||
|
}
|
||||||
|
|
||||||
// Config is clash config manager
|
// Config is clash config manager
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -98,6 +102,7 @@ type Config struct {
|
|||||||
Users []auth.AuthUser
|
Users []auth.AuthUser
|
||||||
Proxies map[string]C.Proxy
|
Proxies map[string]C.Proxy
|
||||||
Providers map[string]providerTypes.ProxyProvider
|
Providers map[string]providerTypes.ProxyProvider
|
||||||
|
Tunnels []Tunnel
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawDNS struct {
|
type RawDNS struct {
|
||||||
@ -113,6 +118,7 @@ type RawDNS struct {
|
|||||||
FakeIPFilter []string `yaml:"fake-ip-filter"`
|
FakeIPFilter []string `yaml:"fake-ip-filter"`
|
||||||
DefaultNameserver []string `yaml:"default-nameserver"`
|
DefaultNameserver []string `yaml:"default-nameserver"`
|
||||||
NameServerPolicy map[string]string `yaml:"nameserver-policy"`
|
NameServerPolicy map[string]string `yaml:"nameserver-policy"`
|
||||||
|
SearchDomains []string `yaml:"search-domains"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RawFallbackFilter struct {
|
type RawFallbackFilter struct {
|
||||||
@ -122,6 +128,64 @@ type RawFallbackFilter struct {
|
|||||||
Domain []string `yaml:"domain"`
|
Domain []string `yaml:"domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tunnel struct {
|
||||||
|
Network []string `yaml:"network"`
|
||||||
|
Address string `yaml:"address"`
|
||||||
|
Target string `yaml:"target"`
|
||||||
|
Proxy string `yaml:"proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tunnel tunnel
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler
|
||||||
|
func (t *Tunnel) UnmarshalYAML(unmarshal func(any) error) error {
|
||||||
|
var tp string
|
||||||
|
if err := unmarshal(&tp); err != nil {
|
||||||
|
var inner tunnel
|
||||||
|
if err := unmarshal(&inner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = Tunnel(inner)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse udp/tcp,address,target,proxy
|
||||||
|
parts := lo.Map(strings.Split(tp, ","), func(s string, _ int) string {
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
})
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return fmt.Errorf("invalid tunnel config %s", tp)
|
||||||
|
}
|
||||||
|
network := strings.Split(parts[0], "/")
|
||||||
|
|
||||||
|
// validate network
|
||||||
|
for _, n := range network {
|
||||||
|
switch n {
|
||||||
|
case "tcp", "udp":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid tunnel network %s", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate address and target
|
||||||
|
address := parts[1]
|
||||||
|
target := parts[2]
|
||||||
|
for _, addr := range []string{address, target} {
|
||||||
|
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||||
|
return fmt.Errorf("invalid tunnel target or address %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = Tunnel(tunnel{
|
||||||
|
Network: network,
|
||||||
|
Address: address,
|
||||||
|
Target: target,
|
||||||
|
Proxy: parts[3],
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type RawConfig struct {
|
type RawConfig struct {
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
SocksPort int `yaml:"socks-port"`
|
SocksPort int `yaml:"socks-port"`
|
||||||
@ -139,6 +203,7 @@ type RawConfig struct {
|
|||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
Interface string `yaml:"interface-name"`
|
Interface string `yaml:"interface-name"`
|
||||||
RoutingMark int `yaml:"routing-mark"`
|
RoutingMark int `yaml:"routing-mark"`
|
||||||
|
Tunnels []Tunnel `yaml:"tunnels"`
|
||||||
|
|
||||||
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
|
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
|
||||||
Hosts map[string]string `yaml:"hosts"`
|
Hosts map[string]string `yaml:"hosts"`
|
||||||
@ -237,6 +302,14 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
|||||||
|
|
||||||
config.Users = parseAuthentication(rawCfg.Authentication)
|
config.Users = parseAuthentication(rawCfg.Authentication)
|
||||||
|
|
||||||
|
config.Tunnels = rawCfg.Tunnels
|
||||||
|
// verify tunnels
|
||||||
|
for _, t := range config.Tunnels {
|
||||||
|
if _, ok := config.Proxies[t.Proxy]; !ok {
|
||||||
|
return nil, fmt.Errorf("tunnel proxy %s not found", t.Proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,6 +550,10 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
|||||||
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse with specific interface
|
||||||
|
// .e.g 10.0.0.1#en0
|
||||||
|
interfaceName := u.Fragment
|
||||||
|
|
||||||
var addr, dnsNetType string
|
var addr, dnsNetType string
|
||||||
switch u.Scheme {
|
switch u.Scheme {
|
||||||
case "udp":
|
case "udp":
|
||||||
@ -489,7 +566,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
|||||||
addr, err = hostWithDefaultPort(u.Host, "853")
|
addr, err = hostWithDefaultPort(u.Host, "853")
|
||||||
dnsNetType = "tcp-tls" // DNS over TLS
|
dnsNetType = "tcp-tls" // DNS over TLS
|
||||||
case "https":
|
case "https":
|
||||||
clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path}
|
clearURL := url.URL{Scheme: "https", Host: u.Host, Path: u.Path, User: u.User}
|
||||||
addr = clearURL.String()
|
addr = clearURL.String()
|
||||||
dnsNetType = "https" // DNS over HTTPS
|
dnsNetType = "https" // DNS over HTTPS
|
||||||
case "dhcp":
|
case "dhcp":
|
||||||
@ -506,8 +583,9 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
|
|||||||
nameservers = append(
|
nameservers = append(
|
||||||
nameservers,
|
nameservers,
|
||||||
dns.NameServer{
|
dns.NameServer{
|
||||||
Net: dnsNetType,
|
Net: dnsNetType,
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
Interface: interfaceName,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -626,6 +704,18 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie) (*DNS, error) {
|
|||||||
dnsCfg.Hosts = hosts
|
dnsCfg.Hosts = hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(cfg.SearchDomains) != 0 {
|
||||||
|
for _, domain := range cfg.SearchDomains {
|
||||||
|
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
|
||||||
|
return nil, errors.New("search domains should not start or end with '.'")
|
||||||
|
}
|
||||||
|
if strings.Contains(domain, ":") {
|
||||||
|
return nil, errors.New("search domains are for ipv4 only and should not contain ports")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dnsCfg.SearchDomains = cfg.SearchDomains
|
||||||
|
}
|
||||||
|
|
||||||
return dnsCfg, nil
|
return dnsCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +101,9 @@ type ProxyAdapter interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DelayHistory struct {
|
type DelayHistory struct {
|
||||||
Time time.Time `json:"time"`
|
Time time.Time `json:"time"`
|
||||||
Delay uint16 `json:"delay"`
|
Delay uint16 `json:"delay"`
|
||||||
|
MeanDelay uint16 `json:"meanDelay"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Proxy interface {
|
type Proxy interface {
|
||||||
@ -110,7 +111,7 @@ type Proxy interface {
|
|||||||
Alive() bool
|
Alive() bool
|
||||||
DelayHistory() []DelayHistory
|
DelayHistory() []DelayHistory
|
||||||
LastDelay() uint16
|
LastDelay() uint16
|
||||||
URLTest(ctx context.Context, url string) (uint16, error)
|
URLTest(ctx context.Context, url string) (uint16, uint16, error)
|
||||||
|
|
||||||
// Deprecated: use DialContext instead.
|
// Deprecated: use DialContext instead.
|
||||||
Dial(metadata *Metadata) (Conn, error)
|
Dial(metadata *Metadata) (Conn, error)
|
||||||
|
@ -3,7 +3,7 @@ package constant
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlainContext interface {
|
type PlainContext interface {
|
||||||
|
@ -3,13 +3,13 @@ package constant
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNSModeMapping is a mapping for EnhancedMode enum
|
// DNSModeMapping is a mapping for EnhancedMode enum
|
||||||
var DNSModeMapping = map[string]DNSMode{
|
var DNSModeMapping = map[string]DNSMode{
|
||||||
DNSNormal.String(): DNSNormal,
|
DNSNormal.String(): DNSNormal,
|
||||||
DNSFakeIP.String(): DNSFakeIP,
|
DNSFakeIP.String(): DNSFakeIP,
|
||||||
DNSMapping.String(): DNSMapping,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -28,7 +28,7 @@ func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error {
|
|||||||
}
|
}
|
||||||
mode, exist := DNSModeMapping[tp]
|
mode, exist := DNSModeMapping[tp]
|
||||||
if !exist {
|
if !exist {
|
||||||
return errors.New("invalid mode")
|
return fmt.Errorf("invalid mode: %s", tp)
|
||||||
}
|
}
|
||||||
*e = mode
|
*e = mode
|
||||||
return nil
|
return nil
|
||||||
|
@ -3,15 +3,14 @@ package constant
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/transport/socks5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socks addr type
|
// Socks addr type
|
||||||
const (
|
const (
|
||||||
AtypIPv4 = 1
|
|
||||||
AtypDomainName = 3
|
|
||||||
AtypIPv6 = 4
|
|
||||||
|
|
||||||
TCP NetWork = iota
|
TCP NetWork = iota
|
||||||
UDP
|
UDP
|
||||||
|
|
||||||
@ -21,6 +20,7 @@ const (
|
|||||||
SOCKS5
|
SOCKS5
|
||||||
REDIR
|
REDIR
|
||||||
TPROXY
|
TPROXY
|
||||||
|
TUNNEL
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetWork int
|
type NetWork int
|
||||||
@ -63,16 +63,18 @@ func (t Type) MarshalJSON() ([]byte, error) {
|
|||||||
|
|
||||||
// Metadata is used to store connection address
|
// Metadata is used to store connection address
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
NetWork NetWork `json:"network"`
|
NetWork NetWork `json:"network"`
|
||||||
Type Type `json:"type"`
|
Type Type `json:"type"`
|
||||||
SrcIP net.IP `json:"sourceIP"`
|
SrcIP net.IP `json:"sourceIP"`
|
||||||
DstIP net.IP `json:"destinationIP"`
|
DstIP net.IP `json:"destinationIP"`
|
||||||
SrcPort string `json:"sourcePort"`
|
SrcPort string `json:"sourcePort"`
|
||||||
DstPort string `json:"destinationPort"`
|
DstPort string `json:"destinationPort"`
|
||||||
AddrType int `json:"-"`
|
Host string `json:"host"`
|
||||||
Host string `json:"host"`
|
DNSMode DNSMode `json:"dnsMode"`
|
||||||
DNSMode DNSMode `json:"dnsMode"`
|
ProcessPath string `json:"processPath"`
|
||||||
ProcessPath string `json:"processPath"`
|
SpecialProxy string `json:"specialProxy"`
|
||||||
|
|
||||||
|
OriginDst netip.AddrPort `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metadata) RemoteAddress() string {
|
func (m *Metadata) RemoteAddress() string {
|
||||||
@ -83,6 +85,17 @@ func (m *Metadata) SourceAddress() string {
|
|||||||
return net.JoinHostPort(m.SrcIP.String(), m.SrcPort)
|
return net.JoinHostPort(m.SrcIP.String(), m.SrcPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Metadata) AddrType() int {
|
||||||
|
switch true {
|
||||||
|
case m.Host != "" || m.DstIP == nil:
|
||||||
|
return socks5.AtypDomainName
|
||||||
|
case m.DstIP.To4() != nil:
|
||||||
|
return socks5.AtypIPv4
|
||||||
|
default:
|
||||||
|
return socks5.AtypIPv6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Metadata) Resolved() bool {
|
func (m *Metadata) Resolved() bool {
|
||||||
return m.DstIP != nil
|
return m.DstIP != nil
|
||||||
}
|
}
|
||||||
@ -93,11 +106,6 @@ func (m *Metadata) Pure() *Metadata {
|
|||||||
if m.DNSMode == DNSMapping && m.DstIP != nil {
|
if m.DNSMode == DNSMapping && m.DstIP != nil {
|
||||||
copy := *m
|
copy := *m
|
||||||
copy.Host = ""
|
copy.Host = ""
|
||||||
if copy.DstIP.To4() != nil {
|
|
||||||
copy.AddrType = AtypIPv4
|
|
||||||
} else {
|
|
||||||
copy.AddrType = AtypIPv6
|
|
||||||
}
|
|
||||||
return ©
|
return ©
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package mime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"mime"
|
|
||||||
)
|
|
||||||
|
|
||||||
var consensusMimes = map[string]string{
|
|
||||||
// rfc4329: text/javascript is obsolete, so we need to overwrite mime's builtin
|
|
||||||
".js": "application/javascript; charset=utf-8",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for ext, typ := range consensusMimes {
|
|
||||||
mime.AddExtensionType(ext, typ)
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
P "path"
|
P "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Name = "clash"
|
const Name = "clash"
|
||||||
@ -51,6 +52,18 @@ func (p *path) Resolve(path string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSubPath return true if path is a subpath of homedir
|
||||||
|
func (p *path) IsSubPath(path string) bool {
|
||||||
|
homedir := p.HomeDir()
|
||||||
|
path = p.Resolve(path)
|
||||||
|
rel, err := filepath.Rel(homedir, path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !strings.Contains(rel, "..")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *path) MMDB() string {
|
func (p *path) MMDB() string {
|
||||||
return P.Join(p.homeDir, "Country.mmdb")
|
return P.Join(p.homeDir, "Country.mmdb")
|
||||||
}
|
}
|
||||||
|
@ -66,9 +66,9 @@ type Provider interface {
|
|||||||
type ProxyProvider interface {
|
type ProxyProvider interface {
|
||||||
Provider
|
Provider
|
||||||
Proxies() []constant.Proxy
|
Proxies() []constant.Proxy
|
||||||
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
// Touch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
||||||
// Commonly used in DialContext and DialPacketConn
|
// Commonly used in DialContext and DialPacketConn
|
||||||
ProxiesWithTouch() []constant.Proxy
|
Touch()
|
||||||
HealthCheck()
|
HealthCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ const (
|
|||||||
DstPort
|
DstPort
|
||||||
Process
|
Process
|
||||||
ProcessPath
|
ProcessPath
|
||||||
|
IPSet
|
||||||
MATCH
|
MATCH
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,6 +40,8 @@ func (rt RuleType) String() string {
|
|||||||
return "Process"
|
return "Process"
|
||||||
case ProcessPath:
|
case ProcessPath:
|
||||||
return "ProcessPath"
|
return "ProcessPath"
|
||||||
|
case IPSet:
|
||||||
|
return "IPSet"
|
||||||
case MATCH:
|
case MATCH:
|
||||||
return "Match"
|
return "Match"
|
||||||
default:
|
default:
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnContext struct {
|
type ConnContext struct {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PacketConnContext struct {
|
type PacketConnContext struct {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -36,9 +37,13 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
|
|||||||
return nil, fmt.Errorf("dns %s not a valid ip", c.host)
|
return nil, fmt.Errorf("dns %s not a valid ip", c.host)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ip, err = resolver.ResolveIPWithResolver(c.host, c.r); err != nil {
|
ips, err := resolver.LookupIPWithResolver(ctx, c.host, c.r)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("use default dns resolve failed: %w", err)
|
return nil, fmt.Errorf("use default dns resolve failed: %w", err)
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, c.host)
|
||||||
}
|
}
|
||||||
|
ip = ips[rand.Intn(len(ips))]
|
||||||
}
|
}
|
||||||
|
|
||||||
network := "udp"
|
network := "udp"
|
||||||
|
19
dns/dhcp.go
19
dns/dhcp.go
@ -29,7 +29,7 @@ type dhcpClient struct {
|
|||||||
|
|
||||||
ifaceAddr *net.IPNet
|
ifaceAddr *net.IPNet
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
resolver *Resolver
|
clients []dnsClient
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,15 +41,15 @@ func (d *dhcpClient) Exchange(m *D.Msg) (msg *D.Msg, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||||
res, err := d.resolve(ctx)
|
clients, err := d.resolve(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.ExchangeContext(ctx, m)
|
return batchExchange(ctx, clients, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
|
func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
|
|
||||||
invalidated, err := d.invalidate()
|
invalidated, err := d.invalidate()
|
||||||
@ -64,8 +64,9 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), DHCPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
var res *Resolver
|
var res []dnsClient
|
||||||
dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName)
|
dns, err := dhcp.ResolveDNSFromDHCP(ctx, d.ifaceName)
|
||||||
|
// dns never empty if err is nil
|
||||||
if err == nil {
|
if err == nil {
|
||||||
nameserver := make([]NameServer, 0, len(dns))
|
nameserver := make([]NameServer, 0, len(dns))
|
||||||
for _, item := range dns {
|
for _, item := range dns {
|
||||||
@ -75,9 +76,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
res = NewResolver(Config{
|
res = transform(nameserver, nil)
|
||||||
Main: nameserver,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
@ -86,7 +85,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
|
|||||||
close(done)
|
close(done)
|
||||||
|
|
||||||
d.done = nil
|
d.done = nil
|
||||||
d.resolver = res
|
d.clients = res
|
||||||
d.err = err
|
d.err = err
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -96,7 +95,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
|
|||||||
for {
|
for {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
|
|
||||||
res, err, done := d.resolver, d.err, d.done
|
res, err, done := d.clients, d.err, d.done
|
||||||
|
|
||||||
d.lock.Unlock()
|
d.lock.Unlock()
|
||||||
|
|
||||||
|
21
dns/doh.go
21
dns/doh.go
@ -3,7 +3,10 @@ package dns
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -79,7 +82,7 @@ func (dc *dohClient) doRequest(req *http.Request) (msg *D.Msg, err error) {
|
|||||||
return msg, err
|
return msg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDoHClient(url string, r *Resolver) *dohClient {
|
func newDoHClient(url, iface string, r *Resolver) *dohClient {
|
||||||
return &dohClient{
|
return &dohClient{
|
||||||
url: url,
|
url: url,
|
||||||
transport: &http.Transport{
|
transport: &http.Transport{
|
||||||
@ -90,12 +93,24 @@ func newDoHClient(url string, r *Resolver) *dohClient {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ip, err := resolver.ResolveIPWithResolver(host, r)
|
ips, err := resolver.LookupIPWithResolver(ctx, host, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
ip := ips[rand.Intn(len(ips))]
|
||||||
|
|
||||||
|
options := []dialer.Option{}
|
||||||
|
if iface != "" {
|
||||||
|
options = append(options, dialer.WithInterface(iface))
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port))
|
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), port), options...)
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
|
||||||
|
NextProtos: []string{"dns"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ func NewEnhancer(cfg Config) *ResolverEnhancer {
|
|||||||
|
|
||||||
if cfg.EnhancedMode != C.DNSNormal {
|
if cfg.EnhancedMode != C.DNSNormal {
|
||||||
fakePool = cfg.Pool
|
fakePool = cfg.Pool
|
||||||
mapping = cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true))
|
mapping = cache.New(cache.WithSize(4096), cache.WithStale(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ResolverEnhancer{
|
return &ResolverEnhancer{
|
||||||
|
@ -87,9 +87,15 @@ func withMapping(mapping *cache.LruCache) middleware {
|
|||||||
case *D.A:
|
case *D.A:
|
||||||
ip = a.A
|
ip = a.A
|
||||||
ttl = a.Hdr.Ttl
|
ttl = a.Hdr.Ttl
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
case *D.AAAA:
|
case *D.AAAA:
|
||||||
ip = a.AAAA
|
ip = a.AAAA
|
||||||
ttl = a.Hdr.Ttl
|
ttl = a.Hdr.Ttl
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -181,9 +187,6 @@ func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
|
|||||||
|
|
||||||
if mapper.mode == C.DNSFakeIP {
|
if mapper.mode == C.DNSFakeIP {
|
||||||
middlewares = append(middlewares, withFakeIP(mapper.fakePool))
|
middlewares = append(middlewares, withFakeIP(mapper.fakePool))
|
||||||
}
|
|
||||||
|
|
||||||
if mapper.mode != C.DNSNormal {
|
|
||||||
middlewares = append(middlewares, withMapping(mapper.mapping))
|
middlewares = append(middlewares, withMapping(mapper.mapping))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
138
dns/resolver.go
138
dns/resolver.go
@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
"github.com/Dreamacro/clash/common/picker"
|
|
||||||
"github.com/Dreamacro/clash/component/fakeip"
|
"github.com/Dreamacro/clash/component/fakeip"
|
||||||
"github.com/Dreamacro/clash/component/resolver"
|
"github.com/Dreamacro/clash/component/resolver"
|
||||||
"github.com/Dreamacro/clash/component/trie"
|
"github.com/Dreamacro/clash/component/trie"
|
||||||
@ -40,21 +39,26 @@ type Resolver struct {
|
|||||||
group singleflight.Group
|
group singleflight.Group
|
||||||
lruCache *cache.LruCache
|
lruCache *cache.LruCache
|
||||||
policy *trie.DomainTrie
|
policy *trie.DomainTrie
|
||||||
|
searchDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIP request with TypeA and TypeAAAA, priority return TypeA
|
// LookupIP request with TypeA and TypeAAAA, priority return TypeA
|
||||||
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip []net.IP, err error) {
|
||||||
ch := make(chan net.IP, 1)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ch := make(chan []net.IP, 1)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
ip, err := r.resolveIP(host, D.TypeAAAA)
|
ip, err := r.lookupIP(ctx, host, D.TypeAAAA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ch <- ip
|
ch <- ip
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ip, err = r.resolveIP(host, D.TypeA)
|
ip, err = r.lookupIP(ctx, host, D.TypeA)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -67,14 +71,47 @@ func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
|||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveIP request with TypeA and TypeAAAA, priority return TypeA
|
||||||
|
func (r *Resolver) ResolveIP(host string) (ip net.IP, err error) {
|
||||||
|
ips, err := r.LookupIP(context.Background(), host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIPv4 request with TypeA
|
||||||
|
func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]net.IP, error) {
|
||||||
|
return r.lookupIP(ctx, host, D.TypeA)
|
||||||
|
}
|
||||||
|
|
||||||
// ResolveIPv4 request with TypeA
|
// ResolveIPv4 request with TypeA
|
||||||
func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) {
|
func (r *Resolver) ResolveIPv4(host string) (ip net.IP, err error) {
|
||||||
return r.resolveIP(host, D.TypeA)
|
ips, err := r.lookupIP(context.Background(), host, D.TypeA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupIPv6 request with TypeAAAA
|
||||||
|
func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]net.IP, error) {
|
||||||
|
return r.lookupIP(ctx, host, D.TypeAAAA)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveIPv6 request with TypeAAAA
|
// ResolveIPv6 request with TypeAAAA
|
||||||
func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) {
|
func (r *Resolver) ResolveIPv6(host string) (ip net.IP, err error) {
|
||||||
return r.resolveIP(host, D.TypeAAAA)
|
ips, err := r.lookupIP(context.Background(), host, D.TypeAAAA)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(ips) == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: %s", resolver.ErrIPNotFound, host)
|
||||||
|
}
|
||||||
|
return ips[rand.Intn(len(ips))], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) shouldIPFallback(ip net.IP) bool {
|
func (r *Resolver) shouldIPFallback(ip net.IP) bool {
|
||||||
@ -104,9 +141,14 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e
|
|||||||
msg = cache.(*D.Msg).Copy()
|
msg = cache.(*D.Msg).Copy()
|
||||||
if expireTime.Before(now) {
|
if expireTime.Before(now) {
|
||||||
setMsgTTL(msg, uint32(1)) // Continue fetch
|
setMsgTTL(msg, uint32(1)) // Continue fetch
|
||||||
go r.exchangeWithoutCache(ctx, m)
|
go func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout)
|
||||||
|
r.exchangeWithoutCache(ctx, m)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
setMsgTTL(msg, uint32(time.Until(expireTime).Seconds()))
|
// updating TTL by subtracting common delta time from each DNS record
|
||||||
|
updateMsgTTL(msg, uint32(time.Until(expireTime).Seconds()))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -125,7 +167,7 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M
|
|||||||
|
|
||||||
msg := result.(*D.Msg)
|
msg := result.(*D.Msg)
|
||||||
|
|
||||||
putMsgToCache(r.lruCache, q.String(), msg)
|
putMsgToCache(r.lruCache, q.String(), q, msg)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
isIPReq := isIPRequest(q)
|
isIPReq := isIPRequest(q)
|
||||||
@ -150,31 +192,10 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) {
|
func (r *Resolver) batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) {
|
||||||
fast, ctx := picker.WithTimeout(ctx, resolver.DefaultDNSTimeout)
|
ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDNSTimeout)
|
||||||
for _, client := range clients {
|
defer cancel()
|
||||||
r := client
|
|
||||||
fast.Go(func() (any, error) {
|
|
||||||
m, err := r.ExchangeContext(ctx, m)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused {
|
|
||||||
return nil, errors.New("server failure")
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
elm := fast.Wait()
|
return batchExchange(ctx, clients, m)
|
||||||
if elm == nil {
|
|
||||||
err := errors.New("all DNS requests failed")
|
|
||||||
if fErr := fast.Error(); fErr != nil {
|
|
||||||
err = fmt.Errorf("%w, first error: %s", err, fErr.Error())
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = elm.(*D.Msg)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
|
func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
|
||||||
@ -253,14 +274,15 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error) {
|
func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) ([]net.IP, error) {
|
||||||
ip = net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
if ip != nil {
|
if ip != nil {
|
||||||
isIPv4 := ip.To4() != nil
|
ip4 := ip.To4()
|
||||||
|
isIPv4 := ip4 != nil
|
||||||
if dnsType == D.TypeAAAA && !isIPv4 {
|
if dnsType == D.TypeAAAA && !isIPv4 {
|
||||||
return ip, nil
|
return []net.IP{ip}, nil
|
||||||
} else if dnsType == D.TypeA && isIPv4 {
|
} else if dnsType == D.TypeA && isIPv4 {
|
||||||
return ip, nil
|
return []net.IP{ip4}, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, resolver.ErrIPVersion
|
return nil, resolver.ErrIPVersion
|
||||||
}
|
}
|
||||||
@ -269,19 +291,33 @@ func (r *Resolver) resolveIP(host string, dnsType uint16) (ip net.IP, err error)
|
|||||||
query := &D.Msg{}
|
query := &D.Msg{}
|
||||||
query.SetQuestion(D.Fqdn(host), dnsType)
|
query.SetQuestion(D.Fqdn(host), dnsType)
|
||||||
|
|
||||||
msg, err := r.Exchange(query)
|
msg, err := r.ExchangeContext(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ips := msgToIP(msg)
|
ips := msgToIP(msg)
|
||||||
ipLength := len(ips)
|
if len(ips) != 0 {
|
||||||
if ipLength == 0 {
|
return ips, nil
|
||||||
|
} else if len(r.searchDomains) == 0 {
|
||||||
return nil, resolver.ErrIPNotFound
|
return nil, resolver.ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
ip = ips[rand.Intn(ipLength)]
|
// query provided search domains serially
|
||||||
return
|
for _, domain := range r.searchDomains {
|
||||||
|
q := &D.Msg{}
|
||||||
|
q.SetQuestion(D.Fqdn(fmt.Sprintf("%s.%s", host, domain)), dnsType)
|
||||||
|
msg, err := r.ExchangeContext(ctx, q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ips := msgToIP(msg)
|
||||||
|
if len(ips) != 0 {
|
||||||
|
return ips, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, resolver.ErrIPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) msgToDomain(msg *D.Msg) string {
|
func (r *Resolver) msgToDomain(msg *D.Msg) string {
|
||||||
@ -323,19 +359,21 @@ type Config struct {
|
|||||||
Pool *fakeip.Pool
|
Pool *fakeip.Pool
|
||||||
Hosts *trie.DomainTrie
|
Hosts *trie.DomainTrie
|
||||||
Policy map[string]NameServer
|
Policy map[string]NameServer
|
||||||
|
SearchDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResolver(config Config) *Resolver {
|
func NewResolver(config Config) *Resolver {
|
||||||
defaultResolver := &Resolver{
|
defaultResolver := &Resolver{
|
||||||
main: transform(config.Default, nil),
|
main: transform(config.Default, nil),
|
||||||
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
|
lruCache: cache.New(cache.WithSize(4096), cache.WithStale(true)),
|
||||||
}
|
}
|
||||||
|
|
||||||
r := &Resolver{
|
r := &Resolver{
|
||||||
ipv6: config.IPv6,
|
ipv6: config.IPv6,
|
||||||
main: transform(config.Main, defaultResolver),
|
main: transform(config.Main, defaultResolver),
|
||||||
lruCache: cache.NewLRUCache(cache.WithSize(4096), cache.WithStale(true)),
|
lruCache: cache.New(cache.WithSize(4096), cache.WithStale(true)),
|
||||||
hosts: config.Hosts,
|
hosts: config.Hosts,
|
||||||
|
searchDomains: config.SearchDomains,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Fallback) != 0 {
|
if len(config.Fallback) != 0 {
|
||||||
|
74
dns/util.go
74
dns/util.go
@ -1,25 +1,53 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/cache"
|
"github.com/Dreamacro/clash/common/cache"
|
||||||
|
"github.com/Dreamacro/clash/common/picker"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
D "github.com/miekg/dns"
|
D "github.com/miekg/dns"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func putMsgToCache(c *cache.LruCache, key string, msg *D.Msg) {
|
func minimalTTL(records []D.RR) uint32 {
|
||||||
|
return lo.MinBy(records, func(r1 D.RR, r2 D.RR) bool {
|
||||||
|
return r1.Header().Ttl < r2.Header().Ttl
|
||||||
|
}).Header().Ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTTL(records []D.RR, ttl uint32) {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delta := minimalTTL(records) - ttl
|
||||||
|
for i := range records {
|
||||||
|
records[i].Header().Ttl = lo.Clamp(records[i].Header().Ttl-delta, 1, records[i].Header().Ttl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putMsgToCache(c *cache.LruCache, key string, q D.Question, msg *D.Msg) {
|
||||||
|
// skip dns cache for acme challenge
|
||||||
|
if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") {
|
||||||
|
log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var ttl uint32
|
var ttl uint32
|
||||||
switch {
|
switch {
|
||||||
case len(msg.Answer) != 0:
|
case len(msg.Answer) != 0:
|
||||||
ttl = msg.Answer[0].Header().Ttl
|
ttl = minimalTTL(msg.Answer)
|
||||||
case len(msg.Ns) != 0:
|
case len(msg.Ns) != 0:
|
||||||
ttl = msg.Ns[0].Header().Ttl
|
ttl = minimalTTL(msg.Ns)
|
||||||
case len(msg.Extra) != 0:
|
case len(msg.Extra) != 0:
|
||||||
ttl = msg.Extra[0].Header().Ttl
|
ttl = minimalTTL(msg.Extra)
|
||||||
default:
|
default:
|
||||||
log.Debugln("[DNS] response msg empty: %#v", msg)
|
log.Debugln("[DNS] response msg empty: %#v", msg)
|
||||||
return
|
return
|
||||||
@ -42,6 +70,12 @@ func setMsgTTL(msg *D.Msg, ttl uint32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMsgTTL(msg *D.Msg, ttl uint32) {
|
||||||
|
updateTTL(msg.Answer, ttl)
|
||||||
|
updateTTL(msg.Ns, ttl)
|
||||||
|
updateTTL(msg.Extra, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
func isIPRequest(q D.Question) bool {
|
func isIPRequest(q D.Question) bool {
|
||||||
return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA)
|
return q.Qclass == D.ClassINET && (q.Qtype == D.TypeA || q.Qtype == D.TypeAAAA)
|
||||||
}
|
}
|
||||||
@ -51,7 +85,7 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
|||||||
for _, s := range servers {
|
for _, s := range servers {
|
||||||
switch s.Net {
|
switch s.Net {
|
||||||
case "https":
|
case "https":
|
||||||
ret = append(ret, newDoHClient(s.Addr, resolver))
|
ret = append(ret, newDoHClient(s.Addr, s.Interface, resolver))
|
||||||
continue
|
continue
|
||||||
case "dhcp":
|
case "dhcp":
|
||||||
ret = append(ret, newDHCPClient(s.Addr))
|
ret = append(ret, newDHCPClient(s.Addr))
|
||||||
@ -63,8 +97,6 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
|||||||
Client: &D.Client{
|
Client: &D.Client{
|
||||||
Net: s.Net,
|
Net: s.Net,
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
// alpn identifier, see https://tools.ietf.org/html/draft-hoffman-dprive-dns-tls-alpn-00#page-6
|
|
||||||
NextProtos: []string{"dns"},
|
|
||||||
ServerName: host,
|
ServerName: host,
|
||||||
},
|
},
|
||||||
UDPSize: 4096,
|
UDPSize: 4096,
|
||||||
@ -104,3 +136,31 @@ func msgToIP(msg *D.Msg) []net.IP {
|
|||||||
|
|
||||||
return ips
|
return ips
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) {
|
||||||
|
fast, ctx := picker.WithContext(ctx)
|
||||||
|
for _, client := range clients {
|
||||||
|
r := client
|
||||||
|
fast.Go(func() (any, error) {
|
||||||
|
m, err := r.ExchangeContext(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if m.Rcode == D.RcodeServerFailure || m.Rcode == D.RcodeRefused {
|
||||||
|
return nil, errors.New("server failure")
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
elm := fast.Wait()
|
||||||
|
if elm == nil {
|
||||||
|
err := errors.New("all DNS requests failed")
|
||||||
|
if fErr := fast.Error(); fErr != nil {
|
||||||
|
err = fmt.Errorf("%w, first error: %s", err, fErr.Error())
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = elm.(*D.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
47
docs/.vitepress/config.ts
Normal file
47
docs/.vitepress/config.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import locales from './locales'
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
title: 'Clash',
|
||||||
|
|
||||||
|
base: '/clash/',
|
||||||
|
|
||||||
|
head: [
|
||||||
|
[
|
||||||
|
'link',
|
||||||
|
{ rel: 'icon', type: "image/x-icon", href: '/clash/logo.png' }
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
locales: locales.locales,
|
||||||
|
|
||||||
|
lastUpdated: true,
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
options: {
|
||||||
|
locales: {
|
||||||
|
zh_CN: {
|
||||||
|
translations: {
|
||||||
|
button: {
|
||||||
|
buttonText: '搜索文档',
|
||||||
|
buttonAriaLabel: '搜索文档'
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
noResultsText: '无法找到相关结果',
|
||||||
|
resetButtonTitle: '清除查询条件',
|
||||||
|
footer: {
|
||||||
|
selectText: '选择',
|
||||||
|
navigateText: '切换'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
60
docs/.vitepress/locales/en_US.ts
Normal file
60
docs/.vitepress/locales/en_US.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { createRequire } from 'module'
|
||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import { generateSidebarChapter } from './side_bar.js'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
|
const chapters = generateSidebarChapter('en_US', new Map([
|
||||||
|
['introduction', 'Introduction'],
|
||||||
|
['configuration', 'Configuration'],
|
||||||
|
['premium', 'Premium'],
|
||||||
|
['runtime', 'Runtime'],
|
||||||
|
['advanced-usages', 'Advanced Usages'],
|
||||||
|
]))
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
lang: 'en-US',
|
||||||
|
|
||||||
|
description: 'A rule-based tunnel in Go.',
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
nav: nav(),
|
||||||
|
|
||||||
|
logo: '/logo.png',
|
||||||
|
|
||||||
|
lastUpdatedText: 'Last updated at',
|
||||||
|
|
||||||
|
sidebar: chapters,
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/Dreamacro/clash' },
|
||||||
|
],
|
||||||
|
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/Dreamacro/clash/edit/master/docs/:path',
|
||||||
|
text: 'Edit this page on GitHub'
|
||||||
|
},
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
level: 'deep',
|
||||||
|
label: 'On this page',
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function nav() {
|
||||||
|
return [
|
||||||
|
{ text: 'Home', link: '/' },
|
||||||
|
{ text: 'Configuration', link: '/configuration/configuration-reference' },
|
||||||
|
{
|
||||||
|
text: 'Download',
|
||||||
|
items: [
|
||||||
|
{ text: 'Open-source Edition', link: 'https://github.com/Dreamacro/clash/releases/' },
|
||||||
|
{ text: 'Premium Edition', link: 'https://github.com/Dreamacro/clash/releases/tag/premium' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
20
docs/.vitepress/locales/index.ts
Normal file
20
docs/.vitepress/locales/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import en_US from './en_US'
|
||||||
|
import zh_CN from './zh_CN'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: 'English',
|
||||||
|
lang: en_US.lang,
|
||||||
|
themeConfig: en_US.themeConfig,
|
||||||
|
description: en_US.description
|
||||||
|
},
|
||||||
|
zh_CN: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: zh_CN.lang,
|
||||||
|
themeConfig: zh_CN.themeConfig,
|
||||||
|
description: zh_CN.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
78
docs/.vitepress/locales/side_bar.ts
Normal file
78
docs/.vitepress/locales/side_bar.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import directoryTree from 'directory-tree'
|
||||||
|
import fs from 'fs'
|
||||||
|
import metadataParser from 'markdown-yaml-metadata-parser'
|
||||||
|
|
||||||
|
function getMetadataFromDoc(path: string): { sidebarTitle?: string, sidebarOrder?: number } {
|
||||||
|
const fileContents = fs.readFileSync(path, 'utf8')
|
||||||
|
|
||||||
|
return metadataParser(fileContents).metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateSidebarChapter(locale:string, chapterDirName: Map<string,string>): any[] {
|
||||||
|
if (chapterDirName.size < 1) {
|
||||||
|
console.error(chapterDirName)
|
||||||
|
throw new Error(`Could not genereate sidebar: chapterDirName is empty`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var chapterPath = ''
|
||||||
|
var sidebar: any[] = []
|
||||||
|
|
||||||
|
for (const chapterDirKey of chapterDirName.keys()) {
|
||||||
|
if (locale !== 'en_US') {
|
||||||
|
chapterPath = `./${locale}/${chapterDirKey}`
|
||||||
|
} else {
|
||||||
|
chapterPath = `./${chapterDirKey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = directoryTree(chapterPath)
|
||||||
|
|
||||||
|
if (!tree || !tree.children) {
|
||||||
|
console.error(tree)
|
||||||
|
throw new Error(`Could not genereate sidebar: invalid chapter at ${chapterPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let items: { sidebarOrder: number, text: string, link: string }[] = []
|
||||||
|
|
||||||
|
// Look into files in the chapter
|
||||||
|
for (const doc of tree.children) {
|
||||||
|
// make sure it's a .md file
|
||||||
|
if (doc.children || !doc.name.endsWith('.md'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
const { sidebarOrder, sidebarTitle } = getMetadataFromDoc(doc.path)
|
||||||
|
|
||||||
|
if (!sidebarOrder)
|
||||||
|
throw new Error('Cannot find sidebarOrder in doc metadata: ' + doc.path)
|
||||||
|
|
||||||
|
if (!sidebarTitle)
|
||||||
|
throw new Error('Cannot find sidebarTitle in doc metadata: ' + doc.path)
|
||||||
|
|
||||||
|
if (chapterDirKey === 'introduction' && doc.name === '_dummy-index.md') {
|
||||||
|
// Override index page link
|
||||||
|
items.push({
|
||||||
|
sidebarOrder,
|
||||||
|
text: sidebarTitle,
|
||||||
|
link: '/' + (locale === 'en_US' ? '' : locale + '/')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
sidebarOrder,
|
||||||
|
text: sidebarTitle,
|
||||||
|
link: "/" + doc.path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items = items.sort((a, b) => a.sidebarOrder - b.sidebarOrder)
|
||||||
|
|
||||||
|
// remove dash and capitalize first character of each word as chapter title
|
||||||
|
const text = chapterDirName.get(chapterDirKey) || chapterDirKey.split('-').join(' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||||
|
sidebar.push({
|
||||||
|
text,
|
||||||
|
collapsed: false,
|
||||||
|
items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sidebar
|
||||||
|
}
|
60
docs/.vitepress/locales/zh_CN.ts
Normal file
60
docs/.vitepress/locales/zh_CN.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { createRequire } from 'module'
|
||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
import { generateSidebarChapter } from './side_bar.js'
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
|
const chapters = generateSidebarChapter('zh_CN', new Map([
|
||||||
|
['introduction', '简介'],
|
||||||
|
['configuration', '配置'],
|
||||||
|
['premium', 'Premium 版本'],
|
||||||
|
['runtime', '运行时'],
|
||||||
|
['advanced-usages', '高级用法'],
|
||||||
|
]))
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
lang: 'zh-CN',
|
||||||
|
|
||||||
|
description: '基于规则的 Go 网络隧道. ',
|
||||||
|
|
||||||
|
themeConfig: {
|
||||||
|
nav: nav(),
|
||||||
|
|
||||||
|
logo: '/logo.png',
|
||||||
|
|
||||||
|
lastUpdatedText: '最后更新于',
|
||||||
|
|
||||||
|
sidebar: chapters,
|
||||||
|
|
||||||
|
socialLinks: [
|
||||||
|
{ icon: 'github', link: 'https://github.com/Dreamacro/clash' },
|
||||||
|
],
|
||||||
|
|
||||||
|
editLink: {
|
||||||
|
pattern: 'https://github.com/Dreamacro/clash/edit/master/docs/:path',
|
||||||
|
text: '在 GitHub 中编辑此页面'
|
||||||
|
},
|
||||||
|
|
||||||
|
docFooter: { prev: '上一篇', next: '下一篇' },
|
||||||
|
|
||||||
|
outline: {
|
||||||
|
level: 'deep',
|
||||||
|
label: '页面导航',
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function nav() {
|
||||||
|
return [
|
||||||
|
{ text: '主页', link: '/zh_CN/' },
|
||||||
|
{ text: '配置', link: '/zh_CN/configuration/configuration-reference' },
|
||||||
|
{
|
||||||
|
text: '下载',
|
||||||
|
items: [
|
||||||
|
{ text: 'GitHub 开源版', link: 'https://github.com/Dreamacro/clash/releases/' },
|
||||||
|
{ text: 'Premium 版本', link: 'https://github.com/Dreamacro/clash/releases/tag/premium' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
59
docs/advanced-usages/golang-api.md
Normal file
59
docs/advanced-usages/golang-api.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
sidebarTitle: Integrating Clash in Golang Programs
|
||||||
|
sidebarOrder: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Integrating Clash in Golang Programs
|
||||||
|
|
||||||
|
If clash does not fit your own usage, you can use Clash in your own Golang code.
|
||||||
|
|
||||||
|
There is already basic support:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
|
"github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/listener/socks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
in := make(chan constant.ConnContext, 100)
|
||||||
|
defer close(in)
|
||||||
|
|
||||||
|
l, err := socks.New("127.0.0.1:10000", in)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
|
||||||
|
println("listen at:", l.Address())
|
||||||
|
|
||||||
|
direct := outbound.NewDirect()
|
||||||
|
|
||||||
|
for c := range in {
|
||||||
|
conn := c
|
||||||
|
metadata := conn.Metadata()
|
||||||
|
fmt.Printf("request incoming from %s to %s\n", metadata.SourceAddress(), metadata.RemoteAddress())
|
||||||
|
go func () {
|
||||||
|
remote, err := direct.DialContext(context.Background(), metadata)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("dial error: %s\n", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
relay(remote, conn.Conn())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relay(l, r net.Conn) {
|
||||||
|
go io.Copy(l, r)
|
||||||
|
io.Copy(r, l)
|
||||||
|
}
|
||||||
|
```
|
93
docs/advanced-usages/openconnect.md
Normal file
93
docs/advanced-usages/openconnect.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
sidebarTitle: Rule-based OpenConnect
|
||||||
|
sidebarOrder: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rule-based OpenConnect
|
||||||
|
|
||||||
|
OpenConnect supports Cisco AnyConnect SSL VPN, Juniper Network Connect, Palo Alto Networks (PAN) GlobalProtect SSL VPN, Pulse Connect Secure SSL VPN, F5 BIG-IP SSL VPN, FortiGate SSL VPN and Array Networks SSL VPN.
|
||||||
|
|
||||||
|
For example, there would be a use case where your company uses Cisco AnyConnect for internal network access. Here I'll show you how you can use OpenConnect with policy routing powered by Clash.
|
||||||
|
|
||||||
|
First, [install vpn-slice](https://github.com/dlenski/vpn-slice#requirements). This tool overrides default routing table behaviour of OpenConnect. Simply saying, it stops the VPN from overriding your default routes.
|
||||||
|
|
||||||
|
Next you would have a script (let's say `tun0.sh`) similar to this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/bin/bash
|
||||||
|
ANYCONNECT_HOST="vpn.example.com"
|
||||||
|
ANYCONNECT_USER="john"
|
||||||
|
ANYCONNECT_PASSWORD="foobar"
|
||||||
|
ROUTING_TABLE_ID="6667"
|
||||||
|
TUN_INTERFACE="tun0"
|
||||||
|
|
||||||
|
# Add --no-dtls if the server is in mainland China. UDP in China is choppy.
|
||||||
|
echo "$ANYCONNECT_PASSWORD" | \
|
||||||
|
openconnect \
|
||||||
|
--non-inter \
|
||||||
|
--passwd-on-stdin \
|
||||||
|
--protocol=anyconnect \
|
||||||
|
--interface $TUN_INTERFACE \
|
||||||
|
--script "vpn-slice
|
||||||
|
if [ \"\$reason\" = 'connect' ]; then
|
||||||
|
ip rule add from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID
|
||||||
|
ip route add default dev \$TUNDEV scope link table $ROUTING_TABLE_ID
|
||||||
|
elif [ \"\$reason\" = 'disconnect' ]; then
|
||||||
|
ip rule del from \$INTERNAL_IP4_ADDRESS table $ROUTING_TABLE_ID
|
||||||
|
ip route del default dev \$TUNDEV scope link table $ROUTING_TABLE_ID
|
||||||
|
fi" \
|
||||||
|
--user $ANYCONNECT_USER \
|
||||||
|
https://$ANYCONNECT_HOST
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, we configure it as a systemd service. Create `/etc/systemd/system/tun0.service`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Cisco AnyConnect VPN
|
||||||
|
After=network-online.target
|
||||||
|
Conflicts=shutdown.target sleep.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/path/to/tun0.sh
|
||||||
|
KillSignal=SIGINT
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we enable & start the service.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
chmod +x /path/to/tun0.sh
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable tun0
|
||||||
|
systemctl start tun0
|
||||||
|
```
|
||||||
|
|
||||||
|
From here you can look at the logs to see if it's running properly. Simple way is to look at if `tun0` interface has been created.
|
||||||
|
|
||||||
|
Similar to the Wireguard one, having an outbound to a TUN device is simple as adding a proxy group:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
proxy-groups:
|
||||||
|
- name: Cisco AnyConnect VPN
|
||||||
|
type: select
|
||||||
|
interface-name: tun0
|
||||||
|
proxies:
|
||||||
|
- DIRECT
|
||||||
|
```
|
||||||
|
|
||||||
|
... and it's ready to use! Add the desired rules:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
rules:
|
||||||
|
- DOMAIN-SUFFIX,internal.company.com,Cisco AnyConnect VPN
|
||||||
|
```
|
||||||
|
|
||||||
|
You should look at the debug level logs when something does not seem right.
|
||||||
|
|
40
docs/advanced-usages/wireguard.md
Normal file
40
docs/advanced-usages/wireguard.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
sidebarTitle: Rule-based Wireguard
|
||||||
|
sidebarOrder: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rule-based Wireguard
|
||||||
|
|
||||||
|
Suppose your kernel supports Wireguard and you have it enabled. The `Table` option stops _wg-quick_ from overriding default routes.
|
||||||
|
|
||||||
|
Example `wg0.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = ...
|
||||||
|
Address = 172.16.0.1/32
|
||||||
|
MTU = ...
|
||||||
|
Table = off
|
||||||
|
PostUp = ip rule add from 172.16.0.1/32 table 6666
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
AllowedIPs = 0.0.0.0/0
|
||||||
|
AllowedIPs = ::/0
|
||||||
|
PublicKey = ...
|
||||||
|
Endpoint = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in Clash you would only need to have a DIRECT proxy group that has a specific outbound interface:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
proxy-groups:
|
||||||
|
- name: Wireguard
|
||||||
|
type: select
|
||||||
|
interface-name: wg0
|
||||||
|
proxies:
|
||||||
|
- DIRECT
|
||||||
|
rules:
|
||||||
|
- DOMAIN,google.com,Wireguard
|
||||||
|
```
|
||||||
|
|
||||||
|
This should perform better than whereas if Clash implemented its own userspace Wireguard client. Wireguard is supported in the kernel.
|
BIN
docs/assets/connection-flow.png
Normal file
BIN
docs/assets/connection-flow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
480
docs/configuration/configuration-reference.md
Normal file
480
docs/configuration/configuration-reference.md
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
---
|
||||||
|
sidebarTitle: Configuration Reference
|
||||||
|
sidebarOrder: 7
|
||||||
|
---
|
||||||
|
|
||||||
|
# Configuration Reference
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Port of HTTP(S) proxy server on the local end
|
||||||
|
port: 7890
|
||||||
|
|
||||||
|
# Port of SOCKS5 proxy server on the local end
|
||||||
|
socks-port: 7891
|
||||||
|
|
||||||
|
# Transparent proxy server port for Linux and macOS (Redirect TCP and TProxy UDP)
|
||||||
|
# redir-port: 7892
|
||||||
|
|
||||||
|
# Transparent proxy server port for Linux (TProxy TCP and TProxy UDP)
|
||||||
|
# tproxy-port: 7893
|
||||||
|
|
||||||
|
# HTTP(S) and SOCKS4(A)/SOCKS5 server on the same port
|
||||||
|
# mixed-port: 7890
|
||||||
|
|
||||||
|
# authentication of local SOCKS5/HTTP(S) server
|
||||||
|
# authentication:
|
||||||
|
# - "user1:pass1"
|
||||||
|
# - "user2:pass2"
|
||||||
|
|
||||||
|
# Set to true to allow connections to the local-end server from
|
||||||
|
# other LAN IP addresses
|
||||||
|
# allow-lan: false
|
||||||
|
|
||||||
|
# This is only applicable when `allow-lan` is `true`
|
||||||
|
# '*': bind all IP addresses
|
||||||
|
# 192.168.122.11: bind a single IPv4 address
|
||||||
|
# "[aaaa::a8aa:ff:fe09:57d8]": bind a single IPv6 address
|
||||||
|
# bind-address: '*'
|
||||||
|
|
||||||
|
# Clash router working mode
|
||||||
|
# rule: rule-based packet routing
|
||||||
|
# global: all packets will be forwarded to a single endpoint
|
||||||
|
# direct: directly forward the packets to the Internet
|
||||||
|
mode: rule
|
||||||
|
|
||||||
|
# Clash by default prints logs to STDOUT
|
||||||
|
# info / warning / error / debug / silent
|
||||||
|
# log-level: info
|
||||||
|
|
||||||
|
# When set to false, resolver won't translate hostnames to IPv6 addresses
|
||||||
|
# ipv6: false
|
||||||
|
|
||||||
|
# RESTful web API listening address
|
||||||
|
external-controller: 127.0.0.1:9090
|
||||||
|
|
||||||
|
# A relative path to the configuration directory or an absolute path to a
|
||||||
|
# directory in which you put some static web resource. Clash core will then
|
||||||
|
# serve it at `http://{{external-controller}}/ui`.
|
||||||
|
# external-ui: folder
|
||||||
|
|
||||||
|
# Secret for the RESTful API (optional)
|
||||||
|
# Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
|
||||||
|
# ALWAYS set a secret if RESTful API is listening on 0.0.0.0
|
||||||
|
# secret: ""
|
||||||
|
|
||||||
|
# Outbound interface name
|
||||||
|
# interface-name: en0
|
||||||
|
|
||||||
|
# fwmark on Linux only
|
||||||
|
# routing-mark: 6666
|
||||||
|
|
||||||
|
# Static hosts for DNS server and connection establishment (like /etc/hosts)
|
||||||
|
#
|
||||||
|
# Wildcard hostnames are supported (e.g. *.clash.dev, *.foo.*.example.com)
|
||||||
|
# Non-wildcard domain names have a higher priority than wildcard domain names
|
||||||
|
# e.g. foo.example.com > *.example.com > .example.com
|
||||||
|
# P.S. +.foo.com equals to .foo.com and foo.com
|
||||||
|
# hosts:
|
||||||
|
# '*.clash.dev': 127.0.0.1
|
||||||
|
# '.dev': 127.0.0.1
|
||||||
|
# 'alpha.clash.dev': '::1'
|
||||||
|
|
||||||
|
# profile:
|
||||||
|
# Store the `select` results in $HOME/.config/clash/.cache
|
||||||
|
# set false If you don't want this behavior
|
||||||
|
# when two different configurations have groups with the same name, the selected values are shared
|
||||||
|
# store-selected: true
|
||||||
|
|
||||||
|
# persistence fakeip
|
||||||
|
# store-fake-ip: false
|
||||||
|
|
||||||
|
# DNS server settings
|
||||||
|
# This section is optional. When not present, the DNS server will be disabled.
|
||||||
|
dns:
|
||||||
|
enable: false
|
||||||
|
listen: 0.0.0.0:53
|
||||||
|
# ipv6: false # when the false, response to AAAA questions will be empty
|
||||||
|
|
||||||
|
# These nameservers are used to resolve the DNS nameserver hostnames below.
|
||||||
|
# Specify IP addresses only
|
||||||
|
default-nameserver:
|
||||||
|
- 114.114.114.114
|
||||||
|
- 8.8.8.8
|
||||||
|
# enhanced-mode: fake-ip
|
||||||
|
fake-ip-range: 198.18.0.1/16 # Fake IP addresses pool CIDR
|
||||||
|
# use-hosts: true # lookup hosts and return IP record
|
||||||
|
|
||||||
|
# search-domains: [local] # search domains for A/AAAA record
|
||||||
|
|
||||||
|
# Hostnames in this list will not be resolved with fake IPs
|
||||||
|
# i.e. questions to these domain names will always be answered with their
|
||||||
|
# real IP addresses
|
||||||
|
# fake-ip-filter:
|
||||||
|
# - '*.lan'
|
||||||
|
# - localhost.ptlogin2.qq.com
|
||||||
|
|
||||||
|
# Supports UDP, TCP, DoT, DoH. You can specify the port to connect to.
|
||||||
|
# All DNS questions are sent directly to the nameserver, without proxies
|
||||||
|
# involved. Clash answers the DNS question with the first result gathered.
|
||||||
|
nameserver:
|
||||||
|
- 114.114.114.114 # default value
|
||||||
|
- 8.8.8.8 # default value
|
||||||
|
- tls://dns.rubyfish.cn:853 # DNS over TLS
|
||||||
|
- https://1.1.1.1/dns-query # DNS over HTTPS
|
||||||
|
- dhcp://en0 # dns from dhcp
|
||||||
|
# - '8.8.8.8#en0'
|
||||||
|
|
||||||
|
# When `fallback` is present, the DNS server will send concurrent requests
|
||||||
|
# to the servers in this section along with servers in `nameservers`.
|
||||||
|
# The answers from fallback servers are used when the GEOIP country
|
||||||
|
# is not `CN`.
|
||||||
|
# fallback:
|
||||||
|
# - tcp://1.1.1.1
|
||||||
|
# - 'tcp://1.1.1.1#en0'
|
||||||
|
|
||||||
|
# If IP addresses resolved with servers in `nameservers` are in the specified
|
||||||
|
# subnets below, they are considered invalid and results from `fallback`
|
||||||
|
# servers are used instead.
|
||||||
|
#
|
||||||
|
# IP address resolved with servers in `nameserver` is used when
|
||||||
|
# `fallback-filter.geoip` is true and when GEOIP of the IP address is `CN`.
|
||||||
|
#
|
||||||
|
# If `fallback-filter.geoip` is false, results from `nameserver` nameservers
|
||||||
|
# are always used if not match `fallback-filter.ipcidr`.
|
||||||
|
#
|
||||||
|
# This is a countermeasure against DNS pollution attacks.
|
||||||
|
# fallback-filter:
|
||||||
|
# geoip: true
|
||||||
|
# geoip-code: CN
|
||||||
|
# ipcidr:
|
||||||
|
# - 240.0.0.0/4
|
||||||
|
# domain:
|
||||||
|
# - '+.google.com'
|
||||||
|
# - '+.facebook.com'
|
||||||
|
# - '+.youtube.com'
|
||||||
|
|
||||||
|
# Lookup domains via specific nameservers
|
||||||
|
# nameserver-policy:
|
||||||
|
# 'www.baidu.com': '114.114.114.114'
|
||||||
|
# '+.internal.crop.com': '10.0.0.1'
|
||||||
|
|
||||||
|
proxies:
|
||||||
|
# Shadowsocks
|
||||||
|
# The supported ciphers (encryption methods):
|
||||||
|
# aes-128-gcm aes-192-gcm aes-256-gcm
|
||||||
|
# aes-128-cfb aes-192-cfb aes-256-cfb
|
||||||
|
# aes-128-ctr aes-192-ctr aes-256-ctr
|
||||||
|
# rc4-md5 chacha20-ietf xchacha20
|
||||||
|
# chacha20-ietf-poly1305 xchacha20-ietf-poly1305
|
||||||
|
- name: "ss1"
|
||||||
|
type: ss
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
cipher: chacha20-ietf-poly1305
|
||||||
|
password: "password"
|
||||||
|
# udp: true
|
||||||
|
|
||||||
|
- name: "ss2"
|
||||||
|
type: ss
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
cipher: chacha20-ietf-poly1305
|
||||||
|
password: "password"
|
||||||
|
plugin: obfs
|
||||||
|
plugin-opts:
|
||||||
|
mode: tls # or http
|
||||||
|
# host: bing.com
|
||||||
|
|
||||||
|
- name: "ss3"
|
||||||
|
type: ss
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
cipher: chacha20-ietf-poly1305
|
||||||
|
password: "password"
|
||||||
|
plugin: v2ray-plugin
|
||||||
|
plugin-opts:
|
||||||
|
mode: websocket # no QUIC now
|
||||||
|
# tls: true # wss
|
||||||
|
# skip-cert-verify: true
|
||||||
|
# host: bing.com
|
||||||
|
# path: "/"
|
||||||
|
# mux: true
|
||||||
|
# headers:
|
||||||
|
# custom: value
|
||||||
|
|
||||||
|
# vmess
|
||||||
|
# cipher support auto/aes-128-gcm/chacha20-poly1305/none
|
||||||
|
- name: "vmess"
|
||||||
|
type: vmess
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
uuid: uuid
|
||||||
|
alterId: 32
|
||||||
|
cipher: auto
|
||||||
|
# udp: true
|
||||||
|
# tls: true
|
||||||
|
# skip-cert-verify: true
|
||||||
|
# servername: example.com # priority over wss host
|
||||||
|
# network: ws
|
||||||
|
# ws-opts:
|
||||||
|
# path: /path
|
||||||
|
# headers:
|
||||||
|
# Host: v2ray.com
|
||||||
|
# max-early-data: 2048
|
||||||
|
# early-data-header-name: Sec-WebSocket-Protocol
|
||||||
|
|
||||||
|
- name: "vmess-h2"
|
||||||
|
type: vmess
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
uuid: uuid
|
||||||
|
alterId: 32
|
||||||
|
cipher: auto
|
||||||
|
network: h2
|
||||||
|
tls: true
|
||||||
|
h2-opts:
|
||||||
|
host:
|
||||||
|
- http.example.com
|
||||||
|
- http-alt.example.com
|
||||||
|
path: /
|
||||||
|
|
||||||
|
- name: "vmess-http"
|
||||||
|
type: vmess
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
uuid: uuid
|
||||||
|
alterId: 32
|
||||||
|
cipher: auto
|
||||||
|
# udp: true
|
||||||
|
# network: http
|
||||||
|
# http-opts:
|
||||||
|
# # method: "GET"
|
||||||
|
# # path:
|
||||||
|
# # - '/'
|
||||||
|
# # - '/video'
|
||||||
|
# # headers:
|
||||||
|
# # Connection:
|
||||||
|
# # - keep-alive
|
||||||
|
|
||||||
|
- name: vmess-grpc
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
type: vmess
|
||||||
|
uuid: uuid
|
||||||
|
alterId: 32
|
||||||
|
cipher: auto
|
||||||
|
network: grpc
|
||||||
|
tls: true
|
||||||
|
servername: example.com
|
||||||
|
# skip-cert-verify: true
|
||||||
|
grpc-opts:
|
||||||
|
grpc-service-name: "example"
|
||||||
|
|
||||||
|
# socks5
|
||||||
|
- name: "socks"
|
||||||
|
type: socks5
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
# username: username
|
||||||
|
# password: password
|
||||||
|
# tls: true
|
||||||
|
# skip-cert-verify: true
|
||||||
|
# udp: true
|
||||||
|
|
||||||
|
# http
|
||||||
|
- name: "http"
|
||||||
|
type: http
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
# username: username
|
||||||
|
# password: password
|
||||||
|
# tls: true # https
|
||||||
|
# skip-cert-verify: true
|
||||||
|
# sni: custom.com
|
||||||
|
|
||||||
|
# Snell
|
||||||
|
# Beware that there's currently no UDP support yet
|
||||||
|
- name: "snell"
|
||||||
|
type: snell
|
||||||
|
server: server
|
||||||
|
port: 44046
|
||||||
|
psk: yourpsk
|
||||||
|
# version: 2
|
||||||
|
# obfs-opts:
|
||||||
|
# mode: http # or tls
|
||||||
|
# host: bing.com
|
||||||
|
|
||||||
|
# Trojan
|
||||||
|
- name: "trojan"
|
||||||
|
type: trojan
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
password: yourpsk
|
||||||
|
# udp: true
|
||||||
|
# sni: example.com # aka server name
|
||||||
|
# alpn:
|
||||||
|
# - h2
|
||||||
|
# - http/1.1
|
||||||
|
# skip-cert-verify: true
|
||||||
|
|
||||||
|
- name: trojan-grpc
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
type: trojan
|
||||||
|
password: "example"
|
||||||
|
network: grpc
|
||||||
|
sni: example.com
|
||||||
|
# skip-cert-verify: true
|
||||||
|
udp: true
|
||||||
|
grpc-opts:
|
||||||
|
grpc-service-name: "example"
|
||||||
|
|
||||||
|
- name: trojan-ws
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
type: trojan
|
||||||
|
password: "example"
|
||||||
|
network: ws
|
||||||
|
sni: example.com
|
||||||
|
# skip-cert-verify: true
|
||||||
|
udp: true
|
||||||
|
# ws-opts:
|
||||||
|
# path: /path
|
||||||
|
# headers:
|
||||||
|
# Host: example.com
|
||||||
|
|
||||||
|
# ShadowsocksR
|
||||||
|
# The supported ciphers (encryption methods): all stream ciphers in ss
|
||||||
|
# The supported obfses:
|
||||||
|
# plain http_simple http_post
|
||||||
|
# random_head tls1.2_ticket_auth tls1.2_ticket_fastauth
|
||||||
|
# The supported supported protocols:
|
||||||
|
# origin auth_sha1_v4 auth_aes128_md5
|
||||||
|
# auth_aes128_sha1 auth_chain_a auth_chain_b
|
||||||
|
- name: "ssr"
|
||||||
|
type: ssr
|
||||||
|
server: server
|
||||||
|
port: 443
|
||||||
|
cipher: chacha20-ietf
|
||||||
|
password: "password"
|
||||||
|
obfs: tls1.2_ticket_auth
|
||||||
|
protocol: auth_sha1_v4
|
||||||
|
# obfs-param: domain.tld
|
||||||
|
# protocol-param: "#"
|
||||||
|
# udp: true
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
# relay chains the proxies. proxies shall not contain a relay. No UDP support.
|
||||||
|
# Traffic: clash <-> http <-> vmess <-> ss1 <-> ss2 <-> Internet
|
||||||
|
- name: "relay"
|
||||||
|
type: relay
|
||||||
|
proxies:
|
||||||
|
- http
|
||||||
|
- vmess
|
||||||
|
- ss1
|
||||||
|
- ss2
|
||||||
|
|
||||||
|
# url-test select which proxy will be used by benchmarking speed to a URL.
|
||||||
|
- name: "auto"
|
||||||
|
type: url-test
|
||||||
|
proxies:
|
||||||
|
- ss1
|
||||||
|
- ss2
|
||||||
|
- vmess1
|
||||||
|
# tolerance: 150
|
||||||
|
# lazy: true
|
||||||
|
url: 'http://www.gstatic.com/generate_204'
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
# fallback selects an available policy by priority. The availability is tested by accessing an URL, just like an auto url-test group.
|
||||||
|
- name: "fallback-auto"
|
||||||
|
type: fallback
|
||||||
|
proxies:
|
||||||
|
- ss1
|
||||||
|
- ss2
|
||||||
|
- vmess1
|
||||||
|
url: 'http://www.gstatic.com/generate_204'
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
# load-balance: The request of the same eTLD+1 will be dial to the same proxy.
|
||||||
|
- name: "load-balance"
|
||||||
|
type: load-balance
|
||||||
|
proxies:
|
||||||
|
- ss1
|
||||||
|
- ss2
|
||||||
|
- vmess1
|
||||||
|
url: 'http://www.gstatic.com/generate_204'
|
||||||
|
interval: 300
|
||||||
|
# strategy: consistent-hashing # or round-robin
|
||||||
|
|
||||||
|
# select is used for selecting proxy or proxy group
|
||||||
|
# you can use RESTful API to switch proxy is recommended for use in GUI.
|
||||||
|
- name: Proxy
|
||||||
|
type: select
|
||||||
|
# disable-udp: true
|
||||||
|
# filter: 'someregex'
|
||||||
|
proxies:
|
||||||
|
- ss1
|
||||||
|
- ss2
|
||||||
|
- vmess1
|
||||||
|
- auto
|
||||||
|
|
||||||
|
# direct to another interfacename or fwmark, also supported on proxy
|
||||||
|
- name: en1
|
||||||
|
type: select
|
||||||
|
interface-name: en1
|
||||||
|
routing-mark: 6667
|
||||||
|
proxies:
|
||||||
|
- DIRECT
|
||||||
|
|
||||||
|
- name: UseProvider
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- provider1
|
||||||
|
proxies:
|
||||||
|
- Proxy
|
||||||
|
- DIRECT
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
provider1:
|
||||||
|
type: http
|
||||||
|
url: "url"
|
||||||
|
interval: 3600
|
||||||
|
path: ./provider1.yaml
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
interval: 600
|
||||||
|
# lazy: true
|
||||||
|
url: http://www.gstatic.com/generate_204
|
||||||
|
test:
|
||||||
|
type: file
|
||||||
|
path: /test.yaml
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
interval: 36000
|
||||||
|
url: http://www.gstatic.com/generate_204
|
||||||
|
|
||||||
|
tunnels:
|
||||||
|
# one line config
|
||||||
|
- tcp/udp,127.0.0.1:6553,114.114.114.114:53,proxy
|
||||||
|
- tcp,127.0.0.1:6666,rds.mysql.com:3306,vpn
|
||||||
|
# full yaml config
|
||||||
|
- network: [tcp, udp]
|
||||||
|
address: 127.0.0.1:7777
|
||||||
|
target: target.com
|
||||||
|
proxy: proxy
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- DOMAIN-SUFFIX,google.com,auto
|
||||||
|
- DOMAIN-KEYWORD,google,auto
|
||||||
|
- DOMAIN,google.com,auto
|
||||||
|
- DOMAIN-SUFFIX,ad.com,REJECT
|
||||||
|
- SRC-IP-CIDR,192.168.1.201/32,DIRECT
|
||||||
|
# optional param "no-resolve" for IP rules (GEOIP, IP-CIDR, IP-CIDR6)
|
||||||
|
- IP-CIDR,127.0.0.0/8,DIRECT
|
||||||
|
- GEOIP,CN,DIRECT
|
||||||
|
- DST-PORT,80,DIRECT
|
||||||
|
- SRC-PORT,7777,DIRECT
|
||||||
|
- RULE-SET,apple,REJECT # Premium only
|
||||||
|
- MATCH,auto
|
||||||
|
```
|
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