Compare commits

...

156 Commits

Author SHA1 Message Date
a76yyyy
c7e34bdc11
Docs(locales): add chinese locale support (#2772) 2023-07-14 22:20:15 +08:00
Dreamacro
24186a488a Chore: update dependencies 2023-06-30 21:03:58 +08:00
Kr328
5212aaf445
Fix: process resolving for udp (#2806) 2023-06-25 09:19:06 +08:00
MoonStrider
e26bed43de
Change: replace std regex with regexp2 (#2802) 2023-06-21 17:06:29 +08:00
Dreamacro
700ceed194 Fix: proxy health check should check not alive proxy on lazy 2023-06-18 18:30:02 +08:00
Kr328
154cb1d1f0
Improve: alloc using make if alloc size > 65536 (#2796) 2023-06-18 11:19:35 +08:00
Kr328
295b0da0e5
Fix: should check originDst is nil (#2797) 2023-06-18 11:16:40 +08:00
Dreamacro
31fe77ee69 Chore: add alive for proxy api 2023-06-16 21:19:10 +08:00
Kr328
9177645a89
Fix: windows process panic (#2793) 2023-06-15 21:37:26 +08:00
Kr328
355eb491ad
Fix: windows process panic (#2791) 2023-06-15 17:51:55 +08:00
Terry Chan
18c666a1ab
Fix: aysnc exchange with new context (#2788) 2023-06-13 23:44:48 +08:00
Kr328
13d9e960f7
Refactor: refactor find process (#2781) 2023-06-13 23:25:32 +08:00
Dreamacro
289025c6ee Fix: filterable provider should be touch 2023-05-28 14:12:03 +08:00
Dreamacro
369f2735a0 Fix: linter fix 2023-05-28 13:52:17 +08:00
Yonas Yanfa
2b6dd2a909
Feature: add REDIRECT IPv6 support for FreeBSD. (#2768)
Upstream patch from FreeBSD ports which adds IPv6 support.
2023-05-25 21:13:42 +08:00
znley
ccd6d321cd
Feature: add loong64 build (#2762) 2023-05-24 09:44:43 +08:00
a76yyyy
4d66da2277
Chore: update wiki URL in issue_template (#2763) 2023-05-23 19:46:01 +08:00
Akariln
1ab615852e
Docs: fix some mistakes (#2761) 2023-05-21 21:26:10 +08:00
Birkhoff Lee
46bb6c38ff
Docs(shortcuts): add expr and starlark (#2759)
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
2023-05-21 21:24:21 +08:00
Birkhoff Lee
c244229ffb
Docs(faq): add docs about amd64-v3 (#2752) 2023-05-21 21:23:35 +08:00
Birkhoff Lee
e8b2d0ecc8
Docs: fix previous/next page button (#2760)
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
2023-05-21 21:22:52 +08:00
Birkhoff Lee
acec0f5c89
Docs(rules): update about no-resolve (#2758) 2023-05-21 21:22:12 +08:00
Birkhoff Lee
4655bd4da8
Docs(script-shortcuts): add expr engine (#2757) 2023-05-20 22:52:50 +08:00
Birkhoff Lee
6b17fd2595
Docs(rules): fix IPSET example (#2756) 2023-05-20 22:52:31 +08:00
Birkhoff Lee
cbcbd0e085
Docs(outbound): fix some typos (#2755) 2023-05-20 22:52:11 +08:00
Birkhoff Lee
75c0254703
Docs(ebpf): add tailscaled conflict (#2754) 2023-05-20 22:51:46 +08:00
Dreamacro
e02d556bf4 Chore: upgrade test deps 2023-05-19 22:10:26 +08:00
Dreamacro
d006b0f2b4 Chore: update dependencies 2023-05-19 21:55:04 +08:00
Birkhoff Lee
d9753efe23
Docs: link logo to public directory (#2748)
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
2023-05-19 21:14:03 +08:00
Birkhoff Lee
6ecd96e5ac
Docs: fix logo url and update README (#2747)
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
2023-05-17 21:07:23 +08:00
Dreamacro
fdb1456c69 Fix: docs build path 2023-05-15 22:35:03 +08:00
Dreamacro
7d9723662c Chore: upgrade actions/deploy-pages version 2023-05-15 21:55:52 +08:00
Birkhoff Lee
ca42ca2ca8
Docs: new documentation site (#2723)
This commit adds a VitePress build to the main repository,
aiming to ditch GitHub Wiki. Moving further, we're going to
host our own documentation site eithor on GitHub Pages or
something alike.
2023-05-15 21:47:01 +08:00
江湖风轻
10dcb7a3ad
Fix: PacketConn's internal remote address is overwritten (#2727)
When using vmess + fake-ip, after receiving the first UDP response,
PacketConn's internal address will be rewritten to fake-ip, causing all
subsequent sending operations to return "ErrUDPRemoteAddrMismatch".

Signed-off-by: Hackerl <490021209@qq.com>
2023-05-11 18:42:24 +08:00
Dreamacro
4c3c64a34a Fix: potential token time attack 2023-05-06 14:51:28 +08:00
KaitoHH
257fcef0b8
Fix: adjust DNS TTL values based on minimum value (#2706)
This commit adds an updated function that adjusts
the TTL values of DNS records are based on the minimum TTL
the value found in the records list so that all records share the
same TTL value. This ensures consistency in the cache
expiry time for all records to prevent caching issues.
2023-04-30 12:18:20 +08:00
Georeth Chow
7f1b50f4a7
Chore: Fix ISSUE_TEMPLATE (DEBUG -> debug) (#2711) 2023-04-28 17:23:00 +08:00
Birkhoff Lee
4f5e74dad9
Chore: update bug_report.yml (#2708)
Signed-off-by: Birkhoff Lee <git@birkhoff.me>
2023-04-27 18:19:31 +08:00
yaling888
48b77b2847
Fix: socks4 server handshake (#2700) 2023-04-25 20:16:11 +08:00
major1201
6eee226965
Feature: support IPSET rule (#2693) 2023-04-22 20:07:47 +08:00
Dreamacro
765982e86a Chore: add new lint-fix for Makefile 2023-04-22 20:03:57 +08:00
yaling888
c5fe5235f7
Feature: add provider proxies API (#2668) 2023-04-22 19:16:51 +08:00
Dreamacro
63770b328f Fix: direct require protobytes 2023-04-21 21:13:13 +08:00
Dreamacro
85f4cb23fc Fix: put correctly pool 2023-04-20 11:07:21 +08:00
Xiaochao Dong
d71324069d
Feature: support basic authentication for DoH (#2684) 2023-04-19 11:30:57 +08:00
Dreamacro
b7aade5e11 Chore: use protobytes replace most of bytes.Buffer 2023-04-17 14:08:39 +08:00
M4rtin Hsu
df61a586c9
Fix: potential vulnerability in http provider (#2680) 2023-04-16 20:14:36 +08:00
Dreamacro
8e05fbfd6d Chore: update dependencies 2023-04-13 20:51:13 +08:00
yaling888
9b2b7c662d
Feature: add filter option to proxy group (#2518) 2023-04-08 19:23:19 +08:00
yaling888
20a521f02d
Feature: bind socket to interface by native API on Windows (#2662) 2023-04-08 19:20:14 +08:00
yaling888
95bbfe3945
Fix: should always drop packet when handle UDP packet (#2659) 2023-04-05 14:05:23 +08:00
Dreamacro
4cd4912749 Chore: more parse proxy group error detail 2023-04-04 16:50:41 +08:00
Dreamacro
5045ca4574 Chore: add some linters and clean up the code 2023-04-04 14:53:59 +08:00
RommHui
a7252a1576
Feature: allow http outbound to set custom headers (#2647) 2023-03-31 20:33:41 +08:00
包布丁
7e2974f02f
Fix: return pooled buffer when simple-obfs tls read error (#2643) 2023-03-26 16:22:23 +08:00
Jiahao Lu
8f9b39c62e
Fix: potential panic in putMsgToCache (#2634)
When the upstream DNS server returns a message that contains no
questions (i.e. QDCOUNT == 0), `putMsgToCache` will trigger an
out-of-range panic.

Issue: #2524
Comment: https://github.com/Dreamacro/clash/issues/2524#issuecomment-1477477601
2023-03-21 19:36:49 +08:00
Dreamacro
d808576f98 Chore: use the number of cpus in parallel make 2023-03-18 20:19:34 +08:00
Dreamacro
fcbe2f06cc Chore: update docker workflow 2023-03-18 19:57:31 +08:00
Dreamacro
148ebccb60 Fix: ignore meanDelay error 2023-03-17 16:35:50 +08:00
Jeff An
3b1d319820
Feature: add support for dns search domains (#2597) 2023-03-17 15:53:06 +08:00
Dreamacro
ff2f2b667b Chore: make test linter happy 2023-03-14 21:24:15 +08:00
Dreamacro
e5a2dbd9b5 Chore: update uuid to v5 2023-03-14 21:18:09 +08:00
Dreamacro
4d14dd65fa Chore: update dependencies 2023-03-14 21:08:09 +08:00
Dave Yu
4ffc999617
Fix: modify local ip to pass all test (#2595) 2023-03-09 10:44:36 +08:00
Dreamacro
71f8f0667f Fix: fakeip 4in 6 unmap 2023-03-04 16:27:36 +08:00
Dreamacro
f78a7cb2cb Feature: add meanDelay on URLTest 2023-02-28 13:28:42 +08:00
Dreamacro
8173d6681b Migration: go1.20 2023-02-16 21:43:40 +08:00
Dreamacro
fbf2f26516 Chore: update bug report template 2023-02-03 21:19:06 +08:00
Dreamacro
9af6d498e7 Change: remove redir-host as config 2023-02-01 15:19:36 +08:00
bobo liu
81b1e9f931
Feature: support vmess 'zero' security (#2513) 2023-01-30 14:14:42 +08:00
Dreamacro
58732ee8b1 Chore: update dependencies 2023-01-29 18:46:47 +08:00
yaling888
d16727e2bd
Fix: dns api panic on disable dns section (#2498) 2023-01-18 16:58:03 +08:00
Dreamacro
876653ebc8 Feature: add riscv64 build 2023-01-18 12:06:06 +08:00
Dreamacro
a26b670420 Feature: add dns query json api 2023-01-16 15:25:34 +08:00
Dreamacro
0489a7391b Chore: update test dependencies 2023-01-11 14:44:38 +08:00
Dreamacro
0c0d18a01c Chore: update dependencies 2023-01-11 14:43:34 +08:00
Dreamacro
e1fa343088 Change: set false as udp-fallback-match default value 2023-01-04 17:43:14 +08:00
Dreamacro
a5d54884e0 Feature: add udp-fallback-match option 2023-01-01 20:12:17 +08:00
Dreamacro
2301b909d2 Fix: immediately update provider when modtime too old 2022-12-31 16:32:30 +08:00
embeddedlove
fbca37c42b
Feature: REDIRECT support IPv6 (#2473) 2022-12-22 19:25:30 +08:00
ALICE
4a57917783
Chore: skip cache acme challenge dns msg (#2469) 2022-12-22 13:30:23 +08:00
wwqgtxx
cdc7d449a6
Fix: safeConnClose not working (#2463) 2022-12-22 12:42:38 +08:00
igoogolx
d8ac82be36
Fix: broken build badge (#2470) 2022-12-22 12:09:24 +08:00
Dreamacro
a6c144038b Chore: improve redir getorigdst 2022-12-22 12:00:56 +08:00
Sizhe Sun
90b40a8e5a
Fix: drop UDP packet which mismatched destination for VMess (#2410)
Co-authored-by: SUN Sizhe <sunsizhe@cmi.chinamobile.com>
2022-11-26 11:27:24 +08:00
Dreamacro
ed988dcdc5 Chore: update dependencies 2022-11-25 20:42:28 +08:00
Dreamacro
efa4b9e0b8 Fix: lint warning 2022-11-22 21:01:51 +08:00
Dreamacro
8c6e205c5a Fix: tunnel proxy match 2022-11-22 19:16:08 +08:00
Dreamacro
5b07d7b776 Feature: add tunnels 2022-11-20 21:30:55 +08:00
Dreamacro
de264c42a8 Chore: update test dependencies 2022-11-04 13:31:20 +08:00
Dreamacro
c2469162fb Chore: update dependencies 2022-11-04 13:28:51 +08:00
wwqgtxx
19b7c7f52a
Fix: a shared fastSingle.Do() may cause providers untouched (#2378) 2022-11-04 13:11:01 +08:00
Dreamacro
c8bc11d61d Fix: amd64 macOS Ventura process name match 2022-10-27 15:36:09 +08:00
Dreamacro
f29b54898f Fix: macOS Ventura process name match 2022-10-27 11:25:18 +08:00
Pan
3e2b08f9d0
Chore: upgrade go.mod go version to 1.19 (#2331) 2022-09-29 11:47:30 +08:00
AndyChen
fb85691fb9
Fix: uncorrect README link (#2325) 2022-09-27 14:22:21 +08:00
Dreamacro
d411394482 Chore: rename linux-armv8 to linux-arm64, windows-arm32v7 to windows-armv7 2022-09-21 21:18:24 +08:00
Adrian Gąsior
827d5289bc
Refactor: improve Dockerfile (#2246) 2022-09-21 21:09:11 +08:00
Kr328
6995e98181
Refactor: linux process resolving (#2305) 2022-09-18 12:53:51 +08:00
x2c3z4
4f291fa513
Chore: show the source ip in log (#2284)
Co-authored-by: Li Feng <fengli@smartx.com>
2022-09-02 16:59:00 +08:00
Kr328
22b9befbda
Fix: fake ip pool offset calculate (#2281) 2022-09-01 11:33:47 +08:00
Birkhoff Lee
425b6e0dc0
Chore: update README (#2276) 2022-08-27 12:16:25 +08:00
Dreamacro
2516169f61 Chore: update dependencies 2022-08-26 21:18:16 +08:00
Dreamacro
a3281712e2 Chore: reduce dhcp dns client cost 2022-08-24 21:36:19 +08:00
Dreamacro
bf079742cb Clean: use go 1.19 Appendf 2022-08-24 20:21:06 +08:00
Dreamacro
6e058f8581 Chore: remove old cache implementation 2022-08-17 11:43:20 +08:00
Dreamacro
3946d771e5 Feature: sync missing resolver logic from premium, but still net.IP on opensource 2022-08-13 13:07:35 +08:00
Dreamacro
5940f62794 Chore: http2 should use DialTLSContext and some tls handshake should with context 2022-08-13 12:35:39 +08:00
bobo liu
71cad51e8f
Fix: satisfy RFC4343 - DNS case insensitivity (#2260) 2022-08-12 13:47:51 +08:00
Dreamacro
50105f0559 Migration: go1.19 2022-08-07 21:45:50 +08:00
Dreamacro
6648793e40 Chore: reenable latest golangci-lint 2022-08-05 10:52:36 +08:00
archzi
95e3a88608
Chore: update bug_report.yml (#2240) 2022-07-28 20:27:53 +08:00
Kaming Chan
bec4df7b12
Fix: handle parse socks5 udp address properly (#2220) 2022-07-25 12:44:00 +08:00
Skyxim
93400cf44d
Fix: ALPN should on DoH instead of DoT (#2232) 2022-07-25 12:41:22 +08:00
Dreamacro
a794819869 Chore: upgrade actions and fixed golangci-lint version 2022-07-21 15:15:14 +08:00
Dreamacro
be8d63ba8f Fix: macOS udp find process should use unspecified fallback 2022-07-15 17:00:41 +08:00
Dreamacro
3b90e18047 Chore: update test dependencies 2022-07-15 16:07:18 +08:00
LJea
f0952b55d0
Fix: query string parse on ws-opts (#2213) 2022-07-10 14:56:34 +08:00
Dreamacro
8c7c8f4374 Chore: update dependencies 2022-07-07 22:15:50 +08:00
Kaming Chan
65a8e8f59c
Fix: process rule type (#2206) 2022-07-06 13:44:04 +08:00
Dreamacro
5497adaba1 Fix: fakeip udp should not replace with another ip 2022-07-05 21:09:29 +08:00
Dreamacro
aaf08dadff
Change: remove AddrType on Metadata (#2199) 2022-07-05 20:26:43 +08:00
Dreamacro
557297ac9a Chore: load balance hash need to have fallback strategy 2022-07-04 21:36:33 +08:00
Dreamacro
77a1e3a653 Chore: cleanup bind mark code 2022-06-30 17:27:57 +08:00
Dreamacro
27e1d6cdae Chore: cleanup code 2022-06-30 17:12:06 +08:00
Kaming Chan
91c22b16bf
Fix: proxy provider filter validation (#2198) 2022-06-30 17:08:53 +08:00
Dreamacro
fc5c9b931b Fix: try to unmap lAddr on tproxy udp listener 2022-06-29 23:36:45 +08:00
Dreamacro
c231fd1466 Chore: update dependencies 2022-06-19 13:01:43 +08:00
Dreamacro
fbb27b84d1 Chore: add redir-host deprecated warnning 2022-06-14 11:26:04 +08:00
Dreamacro
e0c5a85314 Fix: missing import 2022-06-12 21:22:02 +08:00
Dreamacro
2fa1a5c4b9 Chore: update tproxy udp packet read logic 2022-06-12 19:37:51 +08:00
Dreamacro
06d75da257 Chore: adjust Relay copy memory alloc logic 2022-06-11 20:38:16 +08:00
Dreamacro
09d49bac95 Chore: embed shadowsocks2 2022-06-01 21:43:20 +08:00
Dreamacro
3360839fe3 Chore: make CodeQL happy 2022-06-01 21:38:05 +08:00
Hongqi Yu
c1285adbf8
Feature: can set custom interface for dns nameserver (#2126) 2022-06-01 10:50:54 +08:00
Dreamacro
9d2fc976e2 Chore: upgrade to yaml v3 2022-05-26 17:47:05 +08:00
Dreamacro
7f41f94fff Fix: benchmark read bytes 2022-05-23 12:58:18 +08:00
Dreamacro
d1f0dac302 Fix: test broken on opensource repo 2022-05-23 12:30:54 +08:00
Dreamacro
afb3e00067 Chore: add benchmark r/w 2022-05-23 12:27:52 +08:00
Dreamacro
9a31ad6151 Chore: cleanup test go.mod 2022-05-21 17:46:34 +08:00
Dreamacro
09cc6b69e3 Chore: cleanup test code 2022-05-21 17:38:17 +08:00
Dreamacro
8603ac40a1 Chore: make linter happy 2022-05-17 19:58:33 +08:00
Kr328
b384449717
Fix: fix upgrade header detect (#2134) 2022-05-15 09:12:53 +08:00
Kaming Chan
da7ffc0da9
Fix: add length check for ssr auth_aes128_sha1 (#2129) 2022-05-13 11:21:39 +08:00
Dreamacro
5dd94c8298 Chore: update dependencies 2022-05-07 21:08:15 +08:00
Kaming Chan
412b44a981
Fix: decode nil value in slice decoder (#2102) 2022-05-07 11:00:58 +08:00
Dreamacro
aef4dd3fe7 Fix: make log api unblocked 2022-04-26 22:36:10 +08:00
Kr328
6a92c6af4e
Fix: http proxy Upgrade behavior (#2097) 2022-04-25 19:50:20 +08:00
Kr328
e010940b61
Improve: replace bootstrap dns (#2080) 2022-04-16 15:31:26 +08:00
Dreamacro
2c9a4d276a Chore: add more github action cache 2022-04-14 23:37:41 +08:00
Dreamacro
4dfba73e5c Fix: SyscallN should not use nargs 2022-04-14 23:37:19 +08:00
Dreamacro
c282d662ca Fix: make golangci lint support multi GOOS 2022-04-13 17:51:21 +08:00
Anankke
b3d7594813
Chore: add none alias to dummy on ShadowsocksR (#2056) 2022-04-13 10:06:06 +08:00
Guowei Zhao
dd9bdf4e2f
Fix: convert size to unit32 in getoridst to solve some mips64 devices cannot get redirect origin dst (#2041)
Change-Id: I40aa73dcea692132e38db980320a8a07ed427fe6

Co-authored-by: Zhao Guowei <zhaoguowei@bytedance.com>
2022-03-28 14:48:51 +08:00
落心
275cc7edf3
Chore: structure support weakly type from float to int (#2042) 2022-03-25 15:22:31 +08:00
Dreamacro
8c9e0b3884 Chore: use GOAMD64 v1 on build docker image 2022-03-20 11:32:18 +08:00
232 changed files with 10277 additions and 3748 deletions

View File

@ -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
View 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
View 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 语法)"

View File

@ -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

View File

@ -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
"

View 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?"

View 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: 此项非必须,但是如果你有想法的话欢迎提出。

View File

@ -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
View 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

View File

@ -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,12 +43,14 @@ 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
push: true push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev' tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Get all docker tags - name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -68,9 +70,11 @@ jobs:
- name: Build release and push - name: Build release and push
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true push: true
tags: ${{steps.tags.outputs.result}} tags: ${{steps.tags.outputs.result}}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -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

View File

@ -4,23 +4,21 @@ 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: ~/go/pkg/mod path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
@ -34,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

View File

@ -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
View File

@ -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

View File

@ -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'

View File

@ -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"]

View File

@ -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:
GOAMD64=v3 $(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,8 +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
golangci-lint run ./...
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
%-lint:
GOOS=$* golangci-lint run ./...
lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
%-lint-fix:
GOOS=$* golangci-lint run --fix ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

@ -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

View File

@ -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
} }

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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,

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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,
} }
} }

View File

@ -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

View File

@ -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
@ -92,6 +93,12 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
} }
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
password := option.Password password := option.Password
@ -103,13 +110,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int ivSize int
key []byte key []byte
) )
if option.Cipher == "dummy" { if option.Cipher == "dummy" {
ivSize = 0 ivSize = 0
key = core.Kdf(option.Password, 16) key = core.Kdf(option.Password, 16)
} else { } else {
ciph, ok := coreCiph.(*core.StreamCipher) ciph, ok := coreCiph.(*core.StreamCipher)
if !ok { if !ok {
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher) return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
} }
ivSize = ciph.IVSize() ivSize = ciph.IVSize()
key = ciph.Key key = ciph.Key

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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]
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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
View File

@ -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
}

View File

@ -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()
}

View File

@ -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),

View File

@ -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)

24
common/net/relay.go Normal file
View File

@ -0,0 +1,24 @@
package net
import (
"io"
"net"
"time"
)
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
ch := make(chan error)
go func() {
// Wrapping to avoid using *net.TCPConn.(ReadFrom)
// See also https://github.com/Dreamacro/clash/pull/1209
_, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
leftConn.SetReadDeadline(time.Now())
ch <- err
}()
io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
rightConn.SetReadDeadline(time.Now())
<-ch
}

View File

@ -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
} }

View File

@ -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) {

View File

@ -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)
}

View File

@ -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()

View File

@ -91,6 +91,8 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error
switch { switch {
case kind == reflect.Int: case kind == reflect.Int:
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
@ -157,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
for valSlice.Len() <= i { for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
} }
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i) fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil { if err := d.decode(fieldName, currentData, currentField); err != nil {
return err return err
} }

View File

@ -137,3 +137,45 @@ func TestStructure_Nest(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal) assert.Equal(t, s.BazOptional, goal)
} }
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
}

View File

@ -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...)
} }

View File

@ -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

View 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
}

View File

@ -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 {

View 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
}

View File

@ -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
} }
} }

View File

@ -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

View File

@ -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)),
} }
} }

View File

@ -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))
} }
} }

View 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
}

View 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
}

View File

@ -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)
} }

View File

@ -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

View 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
}

View File

@ -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
}

View 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{})))
}

View File

@ -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
} }

View File

@ -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
} }

View 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)
}
}

View File

@ -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.Syscall6(fn, 6, 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.Syscall6(
queryProcName, 4, err = windows.QueryFullProcessImageName(h, 0, &buf[0], &size)
uintptr(h), if err != nil {
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0, 0)
if r1 == 0 {
return "", err return "", err
} }
return syscall.UTF16ToString(buf[:size]), nil
return windows.UTF16ToString(buf[:size]), nil
} }

View File

@ -0,0 +1,12 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package resolver
import _ "unsafe"
//go:linkname defaultNS net.defaultNS
var defaultNS []string
func init() {
defaultNS = []string{"114.114.114.114:53", "8.8.8.8:53"}
}

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 &copy return &copy
} }

View File

@ -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)
}
}

View File

@ -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")
} }

View File

@ -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()
} }

View File

@ -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:

View File

@ -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 {

View File

@ -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"
) )

View File

@ -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 {

View File

@ -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"

View File

@ -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()

View File

@ -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"},
}, },
}, },
} }

View File

@ -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{

View File

@ -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))
} }

View File

@ -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 {

View File

@ -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
View 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: '切换'
}
}
}
}
},
}
}
},
})

View 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' },
]
}
]
}

View 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
}
}
})

View 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
}

View 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' },
]
}
]
}

View 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)
}
```

View 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.

Some files were not shown because too many files have changed in this diff Show More