Compare commits

...

288 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
Kr328
30d4668008
Chore: fix typo (#2033) 2022-03-19 13:58:51 +08:00
Dreamacro
02333a859a Chore: split amd64 v3 to special release 2022-03-19 13:42:06 +08:00
risetechlab
f9cc1cc363
Fix: routing-mark option doesn't work on proxies (#2028) 2022-03-19 13:29:30 +08:00
Dreamacro
fb7d340233 Fix: docker build makefile 2022-03-16 12:13:59 +08:00
Dreamacro
6a661bff0c Migration: go 1.18 2022-03-16 12:10:13 +08:00
suyar
d1dd21417b
Feature: add tzdata to Dockerfile (#2027)
Co-authored-by: suyaqi <suyaqi@wy.net>
2022-03-15 11:30:52 +08:00
Kr328
b866f06414
Chore: move find connection process to tunnel (#2016) 2022-03-12 19:07:53 +08:00
Kr328
9683c297a7
Chore: add more details to process resolving (#2017) 2022-03-09 13:41:50 +08:00
Dreamacro
f6c7281bb7 Chore: update github action workflow 2022-03-06 21:48:37 +08:00
Kr328
83bfe521b1
Fix: should split linux process name with space (#2008) 2022-03-05 18:25:16 +08:00
Dreamacro
b52d0c16e9 Chore: vmess test remove all alterid 2022-02-27 18:00:04 +08:00
Kaming Chan
132a6a6a2f
Fix: listener tcp keepalive & reuse net.BufferedConn (#1987) 2022-02-23 11:22:46 +08:00
Dreamacro
03e4b5d525 Chore: use golangci-lint config file 2022-02-19 00:08:51 +08:00
Dreamacro
a0221bf897 Fix: routing-mark should effect on root 2022-02-17 14:23:47 +08:00
Dreamacro
b1a639feae Fix: domain trie search 2022-01-26 22:28:13 +08:00
Kr328
cfe7354c07
Improve: change provider file modify time when updated (#1918) 2022-01-18 13:32:47 +08:00
thank243
9732efe938
Fix: tls handshake requires a timeout (#1893) 2022-01-15 19:33:21 +08:00
Digital Pencil
8f3385bbb6
Feature: support snell v3 (#1884) 2022-01-10 20:24:20 +08:00
Dreamacro
d237b041b3 Fix: ignore empty dns server error 2022-01-05 11:41:31 +08:00
Dreamacro
3cb87e083c Fix: duplicate provider err typo 2022-01-03 17:21:27 +08:00
Dreamacro
8c6d0c6757 Chore: fix docker dependencies security warning 2022-01-02 11:15:40 +08:00
Dreamacro
cb95326aca Chore: update dependencies 2022-01-02 01:15:49 +08:00
HamsterReserved
8679968ab0
Fix: multiple port string parsing overflow (#1868)
Ports in TCP and UDP should be parsed as an unsigned integer,
otherwise ports > 32767 get truncated to 32767. As this is
the case with Metadata.UDPAddr(), this fundamentally breaks
UDP connections where demand for high port numbers is high.

This commit fixes all known cases where ParseInt is used for ports,
and has been verified to fix Discord voice connections on port
50001~50004.

Fixes: d40e5e4fe6c11311ee1de82779a985c3ca47d03a

Co-authored-by: Hamster Tian <haotia@gmail.com>
2022-01-02 01:09:29 +08:00
Dreamacro
204a72bbd3 Chore: remove forward compatible code 2022-01-02 00:48:57 +08:00
Kr328
7267c58913
Chore: ReCreate* do side effect job (#1849) 2021-12-26 22:08:53 +08:00
Kr328
14ae87fcd0
Chore: remove reduce regex compile (#1855) 2021-12-26 20:47:12 +08:00
Fan
ee6fc12709
Fix: when both providers and proxies are present, use the health check configuration for proxies (#1821)
Co-authored-by: Ho <ho@fluidex.com>
2021-12-12 20:37:30 +08:00
bobo liu
78e105f3b2
Chore: builtin right mime of .js (#1808) 2021-12-08 13:38:25 +08:00
Rick
08607fb6b4
Feature: add linux/arm/v6 for the container image (#1771) 2021-12-02 21:12:45 +08:00
Dreamacro
075d8ed094 Fix: fakeip pool cycle used 2021-11-23 22:01:49 +08:00
Dreamacro
b1bed7623d Fix: provider filter potential panic 2021-11-21 17:44:03 +08:00
beyondkmp
1401a82bb0
Feature: add filter on proxy provider (#1511) 2021-11-20 23:38:49 +08:00
Dreamacro
4524cf4418 Fix: should return io.EOF immediately 2021-11-20 12:44:31 +08:00
Dreamacro
0db15d46c3 Change: use nop packet conn for reject 2021-11-20 12:34:14 +08:00
Dreamacro
08c43b8876 Fix: revert ssr udp fix 2021-11-14 14:48:00 +08:00
Dreamacro
499beb7344 Fix: bind iface should throw control error 2021-11-10 22:19:11 +08:00
Dreamacro
c9be614821 Fix: windows arm7 build 2021-11-08 21:24:39 +08:00
Dreamacro
b56d35040d Chore: update dependencies and rename profile props 2021-11-08 20:48:29 +08:00
bobo liu
bd2ea2b917
Feature: mark on socket (#1705) 2021-11-08 16:59:48 +08:00
Dreamacro
e622d8dd38 Fix: parse dial interface option 2021-11-08 13:31:08 +08:00
Dreamacro
d40e5e4fe6 Fix: codeql alerts 2021-11-08 00:32:21 +08:00
Dreamacro
1a7830f18e
Feature: dial different NIC for all proxies (#1714) 2021-11-07 16:48:51 +08:00
Dreamacro
bcb301b730 Chore: adjust all udp alloc size 2021-11-03 22:29:24 +08:00
Dreamacro
ebbc9604ce Chore: use uber max procs 2021-10-27 21:27:19 +08:00
Blaise Wang
a7aea12aa6
Fix: remove ResponseHeaderTimeout limitation (#1690) 2021-10-20 13:44:05 +08:00
Dreamacro
c6cceeb0c5 Chore: use alpn http 1.1 only on trojan websocket by default 2021-10-19 22:34:18 +08:00
Dreamacro
967932d02c Fix: set dnsmode behavior 2021-10-18 23:03:25 +08:00
Dreamacro
81d5da51a3 Fix: unexpected proxy dial behavior on mapping mode 2021-10-18 21:08:27 +08:00
Dreamacro
fea9d1c5e2 Fix: replace vmess grpc test image 2021-10-16 20:35:06 +08:00
Dreamacro
df3a491d40 Feature: support trojan websocket 2021-10-16 20:19:59 +08:00
Dreamacro
68753b4ae1 Chore: contexify ProxyAdapter ListenPacket 2021-10-15 21:44:53 +08:00
Dreamacro
583b2a5ace Change: use interface HardwareAddr for dhcp discovery 2021-10-14 22:54:43 +08:00
Dreamacro
13bd601cac Fix: #1660 panic 2021-10-11 21:05:38 +08:00
Dreamacro
3d5681cffd
Feature: persistence fakeip (#1662) 2021-10-11 20:48:58 +08:00
Dreamacro
a1c2478e74 Chore: actions split lint and release 2021-10-11 20:08:18 +08:00
Dreamacro
f1cf7e9269 Style: use gofumpt for fmt 2021-10-10 23:44:09 +08:00
Dreamacro
4ce35870fe Chore: remove deprecated ioutil 2021-10-09 20:35:06 +08:00
beyondkmp
1996bef9e6
Chore: doh request should with id 0 (#1660) 2021-10-07 22:57:55 +08:00
Dreamacro
66cb0b1218 Fix: cache kv db should not block on init 2021-10-05 22:47:26 +08:00
Dreamacro
b9d470cf79 Fix: dhcp client should request special interface 2021-10-05 13:31:19 +08:00
Dreamacro
4f1fac02ab Chore: add remove TODO 2021-10-05 12:42:21 +08:00
Dreamacro
537b672fcf Change: use bbolt as cache db 2021-10-04 19:20:11 +08:00
Kr328
ced9749104
Fix: http proxy should response correct http version (#1651) 2021-09-30 16:30:07 +08:00
bobo liu
9aeb4c8cfe
Improve: avoid bufconn twice (#1650) 2021-09-28 23:15:53 +08:00
Dreamacro
70c8605cca Improve: use one bytes.Buffer pool 2021-09-20 21:02:18 +08:00
Dreamacro
5b1a0a523f Chore: update README.md 2021-09-20 17:22:40 +08:00
Dreamacro
b398f1e6f3 Chore: force set latest go version to action 2021-09-18 00:18:47 +08:00
Dreamacro
b3cd4ebbd3 Fix: use 1.17.x on github actions 2021-09-15 20:21:30 +08:00
Excited Codes
b0f83e401f
Fix: socks4 request continues after authentication failed (#1624) 2021-09-15 16:45:57 +08:00
Xuen Li
f5806d9263
Fix: http/https proxy authentication (#1613) 2021-09-14 00:08:23 +08:00
Dreamacro
55600c49c9 Fix: potential pitfalls 2021-09-13 23:58:48 +08:00
Kr328
beb88cc46f
Fix: should not trust address of http.Client (#1616) 2021-09-13 23:46:39 +08:00
Kr328
d49b38b00f
Fix: should not unmarshal to pointer (#1615) 2021-09-13 23:43:28 +08:00
bobo liu
0c79d1207e
Fix: potential overflow in ssr (#1600) 2021-09-09 20:30:34 +08:00
maskedeken
400dc923e0
Fix: vmess ws headers not set properly (#1595) 2021-09-08 14:44:24 +08:00
Dreamacro
5b7f0de48b Chore: update dependencies 2021-09-07 20:16:07 +08:00
Kr328
a5b950a779
Feature: add dhcp type dns client (#1509) 2021-09-06 23:07:34 +08:00
Excited Codes
a2d59d6ef5
Feature: skip DIRECT proxies in relay (#1583) 2021-09-06 21:39:28 +08:00
Dreamacro
8ef5cdb8be Test: use local clash pkg 2021-09-05 14:38:07 +08:00
Dreamacro
c7b718f651 Test: release resources correctly 2021-09-05 14:25:55 +08:00
Dreamacro
ff56e5c5de Test: fix direct listen fail 2021-09-04 22:44:18 +08:00
Dreamacro
661c417fce Test: use shadowsocks-rust for ss benchmark 2021-09-04 22:31:08 +08:00
Dreamacro
7d20097465 Fix: ssr auth aes128 udp hmac verify 2021-08-30 00:15:57 +08:00
Loyalsoldier
a20b9a3960
Chore: make geoip match case-insensitive (#1574) 2021-08-29 22:19:22 +08:00
Dreamacro
e0d3f926b7 Feature: add geoip-code option 2021-08-25 15:15:13 +08:00
Dreamacro
121bc910f6 Chore: adjust vmess 0rtt code and split xray test 2021-08-22 16:16:45 +08:00
wwqgtxx
4522cdc551
Feature: support xray's ws-0rtt path (#1558) 2021-08-22 16:03:46 +08:00
Dreamacro
410772e81c Test: add vmess ws 0-rtt test 2021-08-22 01:17:29 +08:00
秋のかえで
0267b2efad
Feature: add vmess WebSocket early data (#1505)
Co-authored-by: ShinyGwyn <79344143+ShinyGwyn@users.noreply.github.com>
2021-08-22 00:25:29 +08:00
Digital Pencil
c6d375eda2
Fix: HTTP proxy internal linkage signature (#1555) 2021-08-20 23:38:47 +08:00
Dreamacro
847f41952e Fix: grpc transport path should not escape 2021-08-19 22:11:56 +08:00
Dreamacro
47044ec0d8 Fix: dependabot alerts 2021-08-18 20:20:00 +08:00
Dreamacro
426ca36118 Chore: upgrade test package 2021-08-18 13:31:34 +08:00
Dreamacro
571d2a0075 Migration: go 1.17 2021-08-18 13:26:23 +08:00
Dreamacro
1be09f5751 Chore: fix issue template render type 2021-08-13 22:44:22 +08:00
Dreamacro
2663cb2e6e Chore: update github issue template 2021-08-13 22:35:48 +08:00
Dreamacro
9b0bbb90ff Chore: upgrade github actions 2021-08-07 22:27:23 +08:00
Dreamacro
588645a2c3 Fix: interface nil check panic from previous commit 2021-08-04 23:52:50 +08:00
Dreamacro
1bfebd0d03 Fix: listener patch diff 2021-08-01 00:35:37 +08:00
xᴊᴀsᴏɴʟʏᴜ
3705996974
Chore: split SOCKS version inbound metadata type (#1513) 2021-07-27 13:58:29 +08:00
Dreamacro
09697b7679 Chore: adjust batch 2021-07-23 00:30:23 +08:00
duama
4578b2c826
Fix: remove Content-Length from CONNECT response (#1502) 2021-07-22 18:06:03 +08:00
Dreamacro
b3a293ab07 Chore: benchmark explanation 2021-07-22 00:01:11 +08:00
Dreamacro
507ba16065 Fix: incorrect use batch 2021-07-21 23:53:31 +08:00
Dreamacro
aa9f8a39a3 Fix: socks inbound packet typo 2021-07-21 23:08:52 +08:00
Dreamacro
8d37220566 Fix: limit concurrency number of provider health check 2021-07-21 17:01:15 +08:00
ayanamist
53e17a916b
Chore: logging remote port on request (#1494) 2021-07-19 15:31:38 +08:00
ayanamist
247dd84970
Chore: logging real listen port (#1492) 2021-07-19 14:07:51 +08:00
Dreamacro
c2f3111922 Test: add direct benchmark 2021-07-18 19:26:51 +08:00
Dreamacro
44872300e9 Test: ss use aes-256-gcm and vmess-aead for benchmark 2021-07-18 17:37:23 +08:00
Dreamacro
91ed0118f6 Test: add basic protocol benchmark 2021-07-18 17:23:30 +08:00
xᴊᴀsᴏɴʟʏᴜ
a461c2306a
Feature: SOCKS4/SOCKS4A Inbound Compatible Support (#1491) 2021-07-18 16:09:09 +08:00
Dreamacro
46f4f84442 Chore: use iife replace init in some cases 2021-07-11 19:43:25 +08:00
Dreamacro
250a9f4f84 Fix: reorder apply config to ensure update proxies and rules 2021-07-10 17:01:40 +08:00
Dreamacro
b4292d0972 Fix: staticcheck error 2021-07-06 00:33:13 +08:00
Dreamacro
d755383e39 Chore: move provider interface to constant 2021-07-06 00:31:13 +08:00
Dreamacro
dff1e8f1ce Chore: update dependencies 2021-07-03 21:01:41 +08:00
Dreamacro
995aa7a8fc Fix: remove ClientSessionCache and add NextProtos for vmess to fix #1468 2021-07-03 20:34:44 +08:00
Indust
3ca5d17c40
Fix: enable DNS server message compression (#1451) 2021-06-24 13:38:44 +08:00
ayanamist
244cb370a4
Change: config reload API use default path when both path and payload don't exist (#1447) 2021-06-21 17:33:34 +08:00
Dreamacro
c35cb24bda Chore: use unix.ByteSliceToString transform cstring 2021-06-15 21:03:47 +08:00
Kr328
b6ff08074c
Refactor: plain http proxy (#1443) 2021-06-15 17:13:40 +08:00
Dreamacro
70d53fd45a Chore: update development wiki to README.md 2021-06-13 23:11:49 +08:00
Dreamacro
f231a63e93 Chore: Listener should not expose original net.Listener 2021-06-13 23:05:22 +08:00
Dreamacro
6091fcdfec Style: code style 2021-06-13 17:23:10 +08:00
Fndroid
bcfc15e398
chore: expose udp field to proxies API (#1441) 2021-06-10 15:08:33 +08:00
Dreamacro
045edc188c Style: code style 2021-06-10 14:05:56 +08:00
Rusty Pen
0778591524
Feature: dns resolve domain through nameserver-policy (#1406) 2021-05-19 11:17:35 +08:00
Dreamacro
d5e52bed43 Feature: add protocol test 2021-05-17 20:33:00 +08:00
Dreamacro
06fdd3abe0 Fix: vmess http should use Host header on request 2021-05-16 20:05:41 +08:00
Dreamacro
4e5898197a Fix: build broken 2021-05-13 22:39:33 +08:00
Dreamacro
f96ebab99f Chore: split component to transport 2021-05-13 22:19:34 +08:00
359 changed files with 17875 additions and 5207 deletions

View File

@ -1,100 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug]"
labels: ''
assignees: ''
---
<!--
感谢你向 Clash Core 提交 issue
在提交之前,请确认:
- [ ] 如果你可以自己 debug 并解决的话,提交 PR 吧!
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的问题
- [ ] 我已经使用 dev 分支版本测试过,问题依旧存在
- [ ] 我已经仔细看过 [Documentation](https://github.com/Dreamacro/clash/wiki/) 并无法自行解决问题
- [ ] 这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
Thanks for opening an issue towards the Clash core!
But before so, please do the following checklist:
- [ ] Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
- [ ] I have searched on the [issue tracker](……/) for a related issue.
- [ ] I have tested using the dev branch, and the issue still exists.
- [ ] I have read the [documentation](https://github.com/Dreamacro/clash/wiki/) and was unable to solve the issue
- [ ] This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash
Please understand that we close issues that fail to follow this issue template.
-->
------------------------------------------------------------------
<!--
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information.
-->
### Clash config
<!--
在下方附上 Clash core 脱敏后配置文件的内容
Paste the Clash core configuration below.
-->
<details>
<summary>config.yaml</summary>
```yaml
……
```
</details>
### Clash log
<!--
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* 操作系统 (the OS that the Clash core is running on)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他 (any other information that would be useful)
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?
-->
### 重现问题的具体布骤 Steps to Reproduce
1. [First Step]
2. [Second Step]
3. ……
**我预期会发生……?**
<!-- **Expected behavior:** [What you expected to happen] -->
**实际上发生了什么?**
<!-- **Actual behavior:** [What actually happened] -->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

124
.github/ISSUE_TEMPLATE/bug_report_en.yml vendored Normal file
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 语法)"

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,9 @@
blank_issues_enabled: false
contact_links:
- name: (中文)阅读 Wiki
url: https://dreamacro.github.io/clash/zh_CN/
about: 如果你是新手,或者想要了解 Clash 的更多信息,请阅读我们撰写的官方 Wiki
- name: (English) Read our Wiki page
url: https://dreamacro.github.io/clash/
about: If you are new to Clash, or want to know more about Clash, please read our Wiki page

View File

@ -1,78 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
labels: ''
assignees: ''
---
<!-- The English version is available. -->
感谢你向 Clash Core 提交 Feature Request
在提交之前,请确认:
- [ ] 我已经在 [Issue Tracker](……/) 中找过我要提出的请求
请注意,如果你并没有遵照这个 issue template 填写内容,我们将直接关闭这个 issue。
<!--
Thanks for submitting a feature request towards the Clash core!
But before so, please do the following checklist:
- [ ] I have searched on the [issue tracker](……/) before creating the issue.
Please understand that we close issues that fail to follow the issue template.
-->
我都确认过了,我要继续提交。
<!-- None of the above, create a feature request -->
------------------------------------------------------------------
请附上任何可以帮助我们解决这个问题的信息,如果我们收到的信息不足,我们将对这个 issue 加上 *Needs more information* 标记并在收到更多资讯之前关闭 issue。
<!-- Make sure to add **all the information needed to understand the bug** so that someone can help. If the info is missing we'll add the 'Needs more information' label and close the issue until there is enough information. -->
### Clash core config
<!--
在下方附上 Clash Core 脱敏后的配置内容
Paste the Clash core configuration below.
-->
```
……
```
### Clash log
<!--
在下方附上 Clash Core 的日志log level 请使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
-->
```
……
```
### 环境 Environment
* Clash Core 的操作系统 (the OS that the Clash core is running on)
……
* 使用者的操作系统 (the OS running on the client)
……
* 网路环境或拓扑 (network conditions/topology)
……
* iptables如果适用 (if applicable)
……
* ISP 有没有进行 DNS 污染 (is your ISP performing DNS pollution?)
……
* 其他
……
### 说明 Description
<!--
请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash Core 的行为是什麽?
-->
### 可能的解决方案 Possible Solution
<!-- 此项非必须,但是如果你有想法的话欢迎提出。 -->
<!-- Not obligatory, but suggest a fix/reason for the bug, -->
<!-- or ideas how to implement the addition or change -->
### 更多信息 More Information

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

@ -1,8 +1,8 @@
name: "CodeQL"
name: CodeQL
on:
push:
branches: [ master, dev ]
branches: [master, dev]
jobs:
analyze:
@ -12,19 +12,19 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
language: ['go']
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
- 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

@ -13,29 +13,29 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up docker buildx
id: buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v2
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Github Package
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: Dreamacro
@ -43,20 +43,22 @@ jobs:
- name: Build dev branch and push
if: github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v3
uses: actions/github-script@v6
id: tags
with:
script: |
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
const ref = context.payload.ref.replace(/\/?refs\/tags\//, '')
const tags = [
'dreamacro/clash:latest',
`dreamacro/clash:${ref}`,
@ -68,9 +70,11 @@ jobs:
- name: Build release and push
if: startsWith(github.ref, 'refs/tags/')
uses: docker/build-push-action@v2
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm/v7,linux/arm64
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: ${{steps.tags.outputs.result}}
cache-from: type=gha
cache-to: type=gha,mode=max

18
.github/workflows/linter.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Linter
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v4
with:
check-latest: true
go-version: '1.20'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

View File

@ -1,46 +1,42 @@
name: Go
on: [push, pull_request]
name: Release
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Setup Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.16
check-latest: true
go-version: '1.20'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Cache go module
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Get dependencies, run test and static check
- name: Get dependencies, run test
run: |
go test ./...
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -- $(go list ./...)
- name: Build
if: startsWith(github.ref, 'refs/tags/')
env:
NAME: clash
BINDIR: bin
run: make -j releases
run: make -j $(go run ./test/main.go) releases
- name: Upload Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: bin/*
draft: true

View File

@ -11,9 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
- uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days'
days-before-stale: 60
days-before-close: 5

16
.gitignore vendored
View File

@ -12,7 +12,7 @@ bin/*
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# dep
# go mod vendor
vendor
# GoLand
@ -20,3 +20,17 @@ vendor
# macOS file
.DS_Store
# test suite
test/config/cache*
# docs site generator
node_modules
package-lock.json
pnpm-lock.yaml
# docs site cache
docs/.vitepress/cache
# docs site build files
docs/.vitepress/dist

23
.golangci.yaml Normal file
View File

@ -0,0 +1,23 @@
linters:
disable-all: true
enable:
- gci
- gofumpt
- gosimple
- govet
- ineffassign
- misspell
- staticcheck
- unconvert
- unused
- usestdlibvars
linters-settings:
gci:
custom-order: true
sections:
- standard
- prefix(github.com/Dreamacro/clash)
- default
staticcheck:
go: '1.20'

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
WORKDIR /clash-src
WORKDIR /workdir
COPY --from=tonistiigi/xx:golang / /
COPY . /clash-src
RUN go mod download && \
make docker && \
mv ./bin/clash-docker /clash
ARG TARGETOS TARGETARCH TARGETVARIANT
RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
make BINDIR= ${TARGETOS}-${TARGETARCH}${TARGETVARIANT} && \
mv /clash* /clash
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
RUN apk add --no-cache ca-certificates
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
ENTRYPOINT ["/clash"]

View File

@ -8,36 +8,43 @@ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clas
PLATFORM_LIST = \
darwin-amd64 \
darwin-amd64-v3 \
darwin-arm64 \
linux-386 \
linux-amd64 \
linux-amd64-v3 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
linux-armv8 \
linux-arm64 \
linux-mips-softfloat \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
linux-mips64 \
linux-mips64le \
linux-riscv64 \
linux-loong64 \
freebsd-386 \
freebsd-amd64 \
freebsd-amd64-v3 \
freebsd-arm64
WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64 \
windows-arm32v7
windows-amd64-v3 \
windows-arm64 \
windows-armv7
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -47,6 +54,9 @@ linux-386:
linux-amd64:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -56,7 +66,7 @@ linux-armv6:
linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv8:
linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat:
@ -77,12 +87,21 @@ linux-mips64:
linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-riscv64:
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-loong64:
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64-v3:
GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -91,8 +110,14 @@ windows-386:
windows-amd64:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm32v7:
windows-amd64-v3:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-armv7:
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
@ -108,5 +133,16 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases)
LINT_OS_LIST := darwin windows linux freebsd openbsd
lint: $(foreach os,$(LINT_OS_LIST),$(os)-lint)
%-lint:
GOOS=$* golangci-lint run ./...
lint-fix: $(foreach os,$(LINT_OS_LIST),$(os)-lint-fix)
%-lint-fix:
GOOS=$* golangci-lint run --fix ./...
clean:
rm $(BINDIR)/*

View File

@ -7,53 +7,47 @@
<p align="center">
<a href="https://github.com/Dreamacro/clash/actions">
<img src="https://img.shields.io/github/workflow/status/Dreamacro/clash/Go?style=flat-square" alt="Github Actions">
<img src="https://img.shields.io/github/actions/workflow/status/Dreamacro/clash/release.yml?branch=master&style=flat-square" alt="Github Actions">
</a>
<a href="https://goreportcard.com/report/github.com/Dreamacro/clash">
<img src="https://goreportcard.com/badge/github.com/Dreamacro/clash?style=flat-square">
</a>
<img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/Dreamacro/clash/releases">
<img src="https://img.shields.io/github/release/Dreamacro/clash/all.svg?style=flat-square">
</a>
<a href="https://github.com/Dreamacro/clash/releases/tag/premium">
<img src="https://img.shields.io/badge/release-Premium-00b4f0?style=flat-square">
</a>
</p>
## Features
- Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IP CIDR or ports to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller
This is a general overview of the features that comes with Clash.
## 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)
- 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)
*Some of the features may only be available in the [Premium core](https://dreamacro.github.io/clash/premium/introduction.html).*
## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Documentation
## Premium Release
[Release](https://github.com/Dreamacro/clash/releases/tag/premium)
You can find the latest documentation at [https://dreamacro.github.io/clash/](https://dreamacro.github.io/clash/).
## Credits
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
## License
This software is released under the GPL-3.0 license.
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)
## TODO
- [x] Complementing the necessary rule operators
- [x] Redir proxy
- [x] UDP support
- [x] Connection manager

View File

@ -1,110 +1,21 @@
package outbound
package adapter
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
)
type Base struct {
name string
addr string
tp C.AdapterType
udp bool
}
// Name implements C.ProxyAdapter
func (b *Base) Name() string {
return b.name
}
// Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// DialUDP implements C.ProxyAdapter
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
// Addr implements C.ProxyAdapter
func (b *Base) Addr() string {
return b.addr
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
return &Base{name, addr, tp, udp}
}
type conn struct {
net.Conn
chain C.Chain
}
// Chains implements C.Connection
func (c *conn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}}
}
type packetConn struct {
net.PacketConn
chain C.Chain
}
// Chains implements C.Connection
func (c *packetConn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}
type Proxy struct {
C.ProxyAdapter
history *queue.Queue
@ -118,20 +29,32 @@ func (p *Proxy) Alive() bool {
// Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
defer cancel()
return p.DialContext(ctx, metadata)
}
// DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
if err != nil {
p.alive.Store(false)
}
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return conn, err
}
// DialUDP implements C.ProxyAdapter
func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
defer cancel()
return p.ListenPacketContext(ctx, metadata)
}
// ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err
}
// DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy()
@ -168,21 +91,24 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
return inner, err
}
mapping := map[string]interface{}{}
mapping := map[string]any{}
json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
return json.Marshal(mapping)
}
// URLTest get the delay for the specified URL
// 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() {
p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = t
record.Delay = delay
record.MeanDelay = meanDelay
}
p.history.Put(record)
if p.history.Len() > 10 {
@ -225,15 +151,53 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return
}
resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
delay = uint16(time.Since(start) / time.Millisecond)
resp, err = client.Do(req)
if err != nil {
// ignore error because some server will hijack the connection and close immediately
return delay, 0, nil
}
resp.Body.Close()
meanDelay = uint16(time.Since(start) / time.Millisecond / 2)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}

27
adapter/inbound/http.go Normal file
View File

@ -0,0 +1,27 @@
package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, originTarget net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = C.HTTP
if ip, port, err := parseAddr(source.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if originTarget != nil {
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
metadata.OriginDst = addrPort
}
}
return context.NewConnContext(conn, metadata)
}

View File

@ -3,6 +3,7 @@ package inbound
import (
"net"
"net/http"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -16,5 +17,8 @@ func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort
}
return context.NewConnContext(conn, metadata)
}

View File

@ -1,8 +1,11 @@
package inbound
import (
"github.com/Dreamacro/clash/component/socks5"
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
@ -17,7 +20,7 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
}
// NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
func NewPacket(target socks5.Addr, originTarget net.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
metadata := parseSocksAddr(target)
metadata.NetWork = C.UDP
metadata.Type = source
@ -25,7 +28,11 @@ func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAda
metadata.SrcIP = ip
metadata.SrcPort = port
}
if originTarget != nil {
if addrPort, err := netip.ParseAddrPort(originTarget.String()); err == nil {
metadata.OriginDst = addrPort
}
}
return &PacketAdapter{
UDPPacket: packet,
metadata: metadata,

View File

@ -2,10 +2,11 @@ package inbound
import (
"net"
"net/netip"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
)
// NewSocket receive TCP inbound and return ConnContext
@ -17,6 +18,8 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
metadata.SrcIP = ip
metadata.SrcPort = port
}
if addrPort, err := netip.ParseAddrPort(conn.LocalAddr().String()); err == nil {
metadata.OriginDst = addrPort
}
return context.NewConnContext(conn, metadata)
}

View File

@ -6,14 +6,12 @@ import (
"strconv"
"strings"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{
AddrType: int(target[0]),
}
metadata := &C.Metadata{}
switch target[0] {
case socks5.AtypDomainName:
@ -44,21 +42,13 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
host = strings.TrimRight(host, ".")
metadata := &C.Metadata{
NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstPort: port,
NetWork: C.TCP,
Host: host,
DstIP: nil,
DstPort: port,
}
ip := net.ParseIP(host)
if ip != nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
if ip := net.ParseIP(host); ip != nil {
metadata.DstIP = ip
}

138
adapter/outbound/base.go Normal file
View File

@ -0,0 +1,138 @@
package outbound
import (
"context"
"encoding/json"
"errors"
"net"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Base struct {
name string
addr string
iface string
tp C.AdapterType
udp bool
rmark int
}
// Name implements C.ProxyAdapter
func (b *Base) Name() string {
return b.name
}
// Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support")
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": b.Type().String(),
})
}
// Addr implements C.ProxyAdapter
func (b *Base) Addr() string {
return b.addr
}
// Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil
}
// DialOptions return []dialer.Option from struct
func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
if b.iface != "" {
opts = append(opts, dialer.WithInterface(b.iface))
}
if b.rmark != 0 {
opts = append(opts, dialer.WithRoutingMark(b.rmark))
}
return opts
}
type BasicOption struct {
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
}
type BaseOption struct {
Name string
Addr string
Type C.AdapterType
UDP bool
Interface string
RoutingMark int
}
func NewBase(opt BaseOption) *Base {
return &Base{
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
iface: opt.Interface,
rmark: opt.RoutingMark,
}
}
type conn struct {
net.Conn
chain C.Chain
}
// Chains implements C.Connection
func (c *conn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}}
}
type packetConn struct {
net.PacketConn
chain C.Chain
}
// Chains implements C.Connection
func (c *packetConn) Chains() C.Chain {
return c.chain
}
// AppendToChains implements C.Connection
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}}
}

View File

@ -13,10 +13,8 @@ type Direct struct {
}
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
c, err := dialer.DialContext(ctx, "tcp", address)
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
@ -24,9 +22,9 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
return NewConn(c, d), nil
}
// DialUDP implements C.ProxyAdapter
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "")
// ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}

View File

@ -22,24 +22,29 @@ type Http struct {
user string
pass string
tlsConfig *tls.Config
Headers http.Header
}
type HttpOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig)
err := cc.Handshake()
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
@ -53,14 +58,16 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
}
// DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr)
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = h.StreamConn(c, metadata)
if err != nil {
@ -77,12 +84,12 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
URL: &url.URL{
Host: addr,
},
Host: addr,
Header: http.Header{
"Proxy-Connection": []string{"Keep-Alive"},
},
Host: addr,
Header: h.Headers.Clone(),
}
req.Header.Add("Proxy-Connection", "Keep-Alive")
if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
@ -125,19 +132,26 @@ func NewHttp(option HttpOption) *Http {
}
tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: sni,
}
}
headers := http.Header{}
for name, value := range option.Headers {
headers.Add(name, value)
}
return &Http{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
iface: option.Interface,
rmark: option.RoutingMark,
},
user: option.UserName,
pass: option.Password,
tlsConfig: tlsConfig,
Headers: headers,
}
}

View File

@ -0,0 +1,62 @@
package outbound
import (
"context"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
type Reject struct {
*Base
}
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(&nopConn{}, r), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(&nopPacketConn{}, r), nil
}
func NewReject() *Reject {
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
udp: true,
},
}
}
type nopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *nopConn) Close() error { return nil }
func (rw *nopConn) LocalAddr() net.Addr { return nil }
func (rw *nopConn) RemoteAddr() net.Addr { return nil }
func (rw *nopConn) SetDeadline(time.Time) error { return nil }
func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
type nopPacketConn struct{}
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc *nopPacketConn) Close() error { return nil }
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
func (npc *nopPacketConn) SetDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetReadDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -2,7 +2,6 @@ package outbound
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
@ -10,12 +9,11 @@ import (
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/socks5"
v2rayObfs "github.com/Dreamacro/clash/component/v2ray-plugin"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
)
type ShadowSocks struct {
@ -29,14 +27,15 @@ type ShadowSocks struct {
}
type ShadowSocksOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
}
type simpleObfsOption struct {
@ -75,22 +74,24 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
}
// DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err
}
// DialUDP implements C.ProxyAdapter
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "")
// ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
@ -105,13 +106,6 @@ func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
}
// MarshalJSON implements C.ProxyAdapter
func (ss *ShadowSocks) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ss.Type().String(),
})
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
@ -157,16 +151,17 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
if opts.TLS {
v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.SessionCache = getClientSessionCache()
}
}
return &ShadowSocks{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
cipher: ciph,

View File

@ -2,19 +2,17 @@ package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/ssr/obfs"
"github.com/Dreamacro/clash/component/ssr/protocol"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
)
type ShadowSocksR struct {
@ -25,6 +23,7 @@ type ShadowSocksR struct {
}
type ShadowSocksROption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -60,22 +59,24 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
}
// DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err
}
// DialUDP implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := dialer.ListenPacket("udp", "")
// ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
@ -91,14 +92,13 @@ func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
}
// MarshalJSON implements C.ProxyAdapter
func (ssr *ShadowSocksR) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": ssr.Type().String(),
})
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher
password := option.Password
@ -110,13 +110,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int
key []byte
)
if option.Cipher == "dummy" {
ivSize = 0
key = core.Kdf(option.Password, 16)
} else {
ciph, ok := coreCiph.(*core.StreamCipher)
if !ok {
return nil, fmt.Errorf("%s is not 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()
key = ciph.Key
@ -144,10 +145,12 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
cipher: coreCiph,
obfs: obfs,

View File

@ -8,9 +8,9 @@ import (
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
obfs "github.com/Dreamacro/clash/component/simple-obfs"
"github.com/Dreamacro/clash/component/snell"
C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/snell"
)
type Snell struct {
@ -22,12 +22,14 @@ type Snell struct {
}
type SnellOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Psk string `proxy:"psk"`
UDP bool `proxy:"udp,omitempty"`
Version int `proxy:"version,omitempty"`
ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
}
type streamOption struct {
@ -51,20 +53,20 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
port, _ := strconv.Atoi(metadata.DstPort)
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
}
// DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
if s.version == snell.Version2 {
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 {
c, err := s.pool.Get()
if err != nil {
return nil, err
}
port, _ := strconv.Atoi(metadata.DstPort)
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err
@ -72,18 +74,38 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
return NewConn(c, s), err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err
}
// ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
}
func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)
@ -105,15 +127,24 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == 0 {
option.Version = snell.DefaultSnellVersion
}
if option.Version != snell.Version1 && option.Version != snell.Version2 {
switch option.Version {
case snell.Version1, snell.Version2:
if option.UDP {
return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
}
case snell.Version3:
default:
return nil, fmt.Errorf("snell version error: %d", option.Version)
}
s := &Snell{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Snell,
name: option.Name,
addr: addr,
tp: C.Snell,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
psk: psk,
obfsOption: obfsOption,
@ -122,7 +153,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
c, err := dialer.DialContext(ctx, "tcp", addr)
c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
if err != nil {
return nil, err
}

View File

@ -6,13 +6,12 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
type Socks5 struct {
@ -25,6 +24,7 @@ type Socks5 struct {
}
type Socks5Option struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -39,7 +39,9 @@ type Socks5Option struct {
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err := cc.Handshake()
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
@ -60,14 +62,16 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
}
// DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata)
if err != nil {
@ -77,11 +81,9 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Co
return NewConn(c, ss), nil
}
// DialUDP implements C.ProxyAdapter
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
// ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return
@ -89,11 +91,15 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.tls {
cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake()
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err = cc.HandshakeContext(ctx)
c = cc
}
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
var user *socks5.User
@ -110,13 +116,13 @@ func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
return
}
pc, err := dialer.ListenPacket("udp", "")
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil {
return
}
go func() {
io.Copy(ioutil.Discard, c)
io.Copy(io.Discard, c)
c.Close()
// A UDP association terminates when the TCP connection that the UDP
// ASSOCIATE request arrived on terminates. RFC1928
@ -145,17 +151,18 @@ func NewSocks5(option Socks5Option) *Socks5 {
if option.TLS {
tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
ServerName: option.Server,
}
}
return &Socks5{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
udp: option.UDP,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
user: option.UserName,
pass: option.Password,

View File

@ -3,15 +3,15 @@ package outbound
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/gun"
"github.com/Dreamacro/clash/component/trojan"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"golang.org/x/net/http2"
)
@ -19,6 +19,7 @@ import (
type Trojan struct {
*Base
instance *trojan.Trojan
option *TrojanOption
// for gun mux
gunTLSConfig *tls.Config
@ -27,6 +28,7 @@ type Trojan struct {
}
type TrojanOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
@ -37,6 +39,34 @@ type TrojanOption struct {
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
Host: host,
Port: port,
Path: t.option.WSOpts.Path,
}
if t.option.SNI != "" {
wsOpts.Host = t.option.SNI
}
if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
}
return t.instance.StreamWebsocketConn(c, wsOpts)
}
return t.instance.StreamConn(c)
}
// StreamConn implements C.ProxyAdapter
@ -45,7 +75,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.instance.StreamConn(c)
c, err = t.plainStream(c)
}
if err != nil {
@ -57,9 +87,9 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if t.transport != nil {
if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
@ -73,13 +103,15 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return NewConn(c, t), nil
}
c, err := dialer.DialContext(ctx, "tcp", t.addr)
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.StreamConn(c, metadata)
if err != nil {
@ -89,27 +121,29 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
return NewConn(c, t), err
}
// DialUDP implements C.ProxyAdapter
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
// ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn
// grpc transport
if t.transport != nil {
if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
} else {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", t.addr)
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
c, err = t.instance.StreamConn(c)
c, err = t.plainStream(c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
@ -124,21 +158,14 @@ func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(pc, t), err
}
func (t *Trojan) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
"type": t.Type().String(),
})
}
func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
ClientSessionCache: getClientSessionCache(),
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
}
if option.SNI != "" {
@ -147,17 +174,20 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t := &Trojan{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
instance: trojan.New(tOption),
option: &option,
}
if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
}
@ -170,7 +200,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tOption.SkipCertVerify,
ServerName: tOption.ServerName,
ClientSessionCache: getClientSessionCache(),
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)

60
adapter/outbound/util.go Normal file
View File

@ -0,0 +1,60 @@
package outbound
import (
"net"
"strconv"
"time"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/protobytes"
)
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
buf := protobytes.BytesWriter{}
addrType := metadata.AddrType()
buf.PutUint8(uint8(addrType))
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
switch addrType {
case socks5.AtypDomainName:
buf.PutUint8(uint8(len(metadata.Host)))
buf.PutString(metadata.Host)
case socks5.AtypIPv4:
buf.PutSlice(metadata.DstIP.To4())
case socks5.AtypIPv6:
buf.PutSlice(metadata.DstIP.To16())
}
buf.PutUint16be(uint16(p))
return buf.Bytes()
}
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := resolver.ResolveIP(host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func safeConnClose(c net.Conn, err error) {
if err != nil {
c.Close()
}
}

View File

@ -11,14 +11,17 @@ import (
"strings"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/gun"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/vmess"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
type Vmess struct {
*Base
client *vmess.Client
@ -31,22 +34,22 @@ type Vmess struct {
}
type VmessOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
}
type HTTPOptions struct {
@ -64,6 +67,13 @@ type GrpcOptions struct {
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
}
type WSOptions struct {
Path string `proxy:"path,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
}
// StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
@ -71,14 +81,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSPath,
Host: host,
Port: port,
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
}
if len(v.option.WSHeaders) != 0 {
if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSHeaders {
for key, value := range v.option.WSOpts.Headers {
header.Add(key, value)
}
wsOpts.Headers = header
@ -86,9 +98,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if v.option.TLS {
wsOpts.TLS = true
wsOpts.SessionCache = getClientSessionCache()
wsOpts.SkipCertVerify = v.option.SkipCertVerify
wsOpts.ServerName = v.option.ServerName
wsOpts.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
}
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
@ -98,7 +117,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
}
if v.option.ServerName != "" {
@ -125,7 +143,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
NextProtos: []string{"h2"},
}
@ -153,7 +170,6 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
SessionCache: getClientSessionCache(),
}
if v.option.ServerName != "" {
@ -172,14 +188,16 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
}
// DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if v.transport != nil {
if v.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil {
@ -189,19 +207,21 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn
return NewConn(c, v), nil
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
// DialUDP implements C.ProxyAdapter
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host)
@ -213,23 +233,25 @@ func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
var c net.Conn
// gun transport
if v.transport != nil {
if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil {
return nil, err
}
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
ctx, cancel := context.WithTimeout(context.Background(), tcpTimeout)
defer cancel()
c, err = dialer.DialContext(ctx, "tcp", v.addr)
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata)
}
@ -264,10 +286,12 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v := &Vmess{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
client: client,
option: &option,
@ -280,7 +304,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
case "grpc":
dialFn := func(network, addr string) (net.Conn, error) {
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
@ -314,23 +338,23 @@ func NewVmess(option VmessOption) (*Vmess, error) {
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
switch metadata.AddrType() {
case socks5.AtypIPv4:
addrType = vmess.AtypIPv4
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
case socks5.AtypIPv6:
addrType = vmess.AtypIPv6
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
case socks5.AtypDomainName:
addrType = vmess.AtypDomainName
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.Atoi(metadata.DstPort)
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
@ -344,7 +368,14 @@ type vmessPacketConn struct {
rAddr net.Addr
}
// WriteTo implments C.PacketConn.WriteTo
// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not.
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
allowedAddr := uc.rAddr.(*net.UDPAddr)
destAddr := addr.(*net.UDPAddr)
if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) {
return 0, ErrUDPRemoteAddrMismatch
}
return uc.Conn.Write(b)
}

View File

@ -3,22 +3,27 @@ package outboundgroup
import (
"time"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
const (
defaultGetProxiesDuration = time.Second * 5
)
func touchProviders(providers []provider.ProxyProvider) {
for _, provider := range providers {
provider.Touch()
}
}
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
provider.Touch()
}
proxies = append(proxies, provider.Proxies()...)
}
return proxies
}

View File

@ -4,10 +4,11 @@ import (
"context"
"encoding/json"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Fallback struct {
@ -23,19 +24,19 @@ func (f *Fallback) Now() string {
}
// DialContext implements C.ProxyAdapter
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata)
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(f)
}
return c, err
}
// DialUDP implements C.ProxyAdapter
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
// ListenPacketContext implements C.ProxyAdapter
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata)
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(f)
}
@ -58,7 +59,7 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
@ -72,7 +73,7 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) {
elm, _, _ := f.single.Do(func() (any, error) {
return getProvidersProxies(f.providers, touch), nil
})
@ -90,11 +91,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
return proxies[0]
}
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: options.DisableUDP,
disableUDP: option.DisableUDP,
}
}

View File

@ -7,11 +7,12 @@ import (
"fmt"
"net"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"golang.org/x/net/publicsuffix"
)
@ -28,11 +29,9 @@ type LoadBalance struct {
var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]interface{}) string {
if elm, ok := config["strategy"]; ok {
if strategy, ok := elm.(string); ok {
return strategy
}
func parseStrategy(config map[string]any) string {
if strategy, ok := config["strategy"].(string); ok {
return strategy
}
return "consistent-hashing"
}
@ -69,7 +68,7 @@ func jumpHash(key uint64, buckets int32) int32 {
}
// DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
defer func() {
if err == nil {
c.AppendToChains(lb)
@ -78,12 +77,12 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return
}
// DialUDP implements C.ProxyAdapter
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
// ListenPacketContext implements C.ProxyAdapter
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
defer func() {
if err == nil {
pc.AppendToChains(lb)
@ -91,8 +90,7 @@ func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error
}()
proxy := lb.Unwrap(metadata)
return proxy.DialUDP(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
}
// SupportUDP implements C.ProxyAdapter
@ -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]
}
}
@ -140,7 +145,7 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) {
elm, _, _ := lb.single.Do(func() (any, error) {
return getProvidersProxies(lb.providers, touch), nil
})
@ -153,13 +158,13 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"type": lb.Type().String(),
"all": all,
})
}
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
@ -170,10 +175,15 @@ func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvid
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
return &LoadBalance{
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: options.DisableUDP,
disableUDP: option.DisableUDP,
}, nil
}

View File

@ -4,9 +4,13 @@ import (
"errors"
"fmt"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
regexp "github.com/dlclark/regexp2"
)
var (
@ -14,10 +18,11 @@ var (
errType = errors.New("unsupport type")
errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("`duplicate provider name")
errDuplicateProvider = errors.New("duplicate provider name")
)
type GroupCommonOption struct {
outbound.BasicOption
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
@ -26,9 +31,10 @@ type GroupCommonOption struct {
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
}
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{
@ -42,67 +48,72 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
return nil, errFormat
}
groupName := groupOption.Name
var (
groupName = groupOption.Name
filterReg *regexp.Regexp
)
providers := []provider.ProxyProvider{}
if groupOption.Filter != "" {
f, err := regexp.Compile(groupOption.Filter, regexp.None)
if err != nil {
return nil, fmt.Errorf("%s: invalid filter regex: %w", groupName, err)
}
filterReg = f
}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
}
providers := []types.ProxyProvider{}
if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s: %w", groupName, err)
}
// if Use not empty, drop health check options
if len(groupOption.Use) != 0 {
if _, ok := providersMap[groupName]; ok {
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
}
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s: %w", groupName, err)
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, fmt.Errorf("%s: %w", groupName, errMissHealthCheck)
}
// select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" || groupOption.Interval == 0 {
return nil, errMissHealthCheck
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
providers = append(providers, pd)
providersMap[groupName] = pd
}
}
if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use)
if err != nil {
return nil, 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
@ -120,7 +131,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
case "relay":
group = NewRelay(groupOption, providers)
default:
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
return nil, fmt.Errorf("%s %w: %s", groupName, errType, groupOption.Type)
}
return group, nil
@ -138,15 +149,15 @@ func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
return ps, nil
}
func getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) {
var ps []provider.ProxyProvider
func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
var ps []types.ProxyProvider
for _, name := range list {
p, ok := mapping[name]
if !ok {
return nil, fmt.Errorf("'%s' not found", name)
}
if p.VehicleType() == provider.Compatible {
if p.VehicleType() == types.Compatible {
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
}
ps = append(ps, p)

View File

@ -3,14 +3,13 @@ package outboundgroup
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Relay struct {
@ -20,15 +19,25 @@ type Relay struct {
}
// DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxies := r.proxies(metadata, true)
if len(proxies) == 0 {
return nil, errors.New("proxy does not exist")
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct {
proxies = append(proxies, proxy)
}
}
switch len(proxies) {
case 0:
return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
first := proxies[0]
last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
@ -63,14 +72,14 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"type": r.Type().String(),
"all": all,
})
}
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (interface{}, error) {
elm, _, _ := r.single.Do(func() (any, error) {
return getProvidersProxies(r.providers, touch), nil
})
@ -91,9 +100,14 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
return proxies
}
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
Base: outbound.NewBase(options.Name, "", C.Relay, false),
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
}

View File

@ -5,10 +5,11 @@ import (
"encoding/json"
"errors"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Selector struct {
@ -20,17 +21,17 @@ type Selector struct {
}
// DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(s)
}
return c, err
}
// DialUDP implements C.ProxyAdapter
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).DialUDP(metadata)
// ListenPacketContext implements C.ProxyAdapter
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(s)
}
@ -53,7 +54,7 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"type": s.Type().String(),
"now": s.Now(),
"all": all,
@ -82,7 +83,7 @@ func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) {
elm, _, _ := s.single.Do(func() (any, error) {
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
@ -96,13 +97,18 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
return elm.(C.Proxy)
}
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
selected := providers[0].Proxies()[0].Name()
return &Selector{
Base: outbound.NewBase(options.Name, "", C.Selector, false),
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: selected,
disableUDP: options.DisableUDP,
disableUDP: option.DisableUDP,
}
}

View File

@ -5,10 +5,11 @@ import (
"encoding/json"
"time"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type urlTestOption func(*URLTest)
@ -34,17 +35,17 @@ func (u *URLTest) Now() string {
}
// DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata)
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(u)
}
return c, err
}
// DialUDP implements C.ProxyAdapter
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast(true).DialUDP(metadata)
// ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(u)
}
@ -57,7 +58,7 @@ func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) {
elm, _, _ := u.single.Do(func() (any, error) {
return getProvidersProxies(u.providers, touch), nil
})
@ -65,7 +66,7 @@ func (u *URLTest) proxies(touch bool) []C.Proxy {
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
elm, _, shared := u.fastSingle.Do(func() (any, error) {
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
@ -94,6 +95,9 @@ func (u *URLTest) fast(touch bool) C.Proxy {
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)
}
@ -113,33 +117,36 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{}
// tolerance
if elm, ok := config["tolerance"]; ok {
if tolerance, ok := elm.(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
if tolerance, ok := config["tolerance"].(int); ok {
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
}
return opts
}
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: commonOptions.DisableUDP,
disableUDP: option.DisableUDP,
}
for _, option := range options {

View File

@ -18,27 +18,24 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
ip := net.ParseIP(host)
if ip == nil {
addr = &C.Metadata{
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstPort: port,
Host: host,
DstIP: nil,
DstPort: port,
}
return
} else if ip4 := ip.To4(); ip4 != nil {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip4,
DstPort: port,
Host: "",
DstIP: ip4,
DstPort: port,
}
return
}
addr = &C.Metadata{
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
Host: "",
DstIP: ip,
DstPort: port,
}
return
}

View File

@ -1,13 +1,14 @@
package outbound
package adapter
import (
"fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
func ParseProxy(mapping map[string]any) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string)
if !existType {
@ -20,36 +21,36 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
)
switch proxyType {
case "ss":
ssOption := &ShadowSocksOption{}
ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption)
if err != nil {
break
}
proxy, err = NewShadowSocks(*ssOption)
proxy, err = outbound.NewShadowSocks(*ssOption)
case "ssr":
ssrOption := &ShadowSocksROption{}
ssrOption := &outbound.ShadowSocksROption{}
err = decoder.Decode(mapping, ssrOption)
if err != nil {
break
}
proxy, err = NewShadowSocksR(*ssrOption)
proxy, err = outbound.NewShadowSocksR(*ssrOption)
case "socks5":
socksOption := &Socks5Option{}
socksOption := &outbound.Socks5Option{}
err = decoder.Decode(mapping, socksOption)
if err != nil {
break
}
proxy = NewSocks5(*socksOption)
proxy = outbound.NewSocks5(*socksOption)
case "http":
httpOption := &HttpOption{}
httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption)
if err != nil {
break
}
proxy = NewHttp(*httpOption)
proxy = outbound.NewHttp(*httpOption)
case "vmess":
vmessOption := &VmessOption{
HTTPOpts: HTTPOptions{
vmessOption := &outbound.VmessOption{
HTTPOpts: outbound.HTTPOptions{
Method: "GET",
Path: []string{"/"},
},
@ -58,21 +59,21 @@ func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
if err != nil {
break
}
proxy, err = NewVmess(*vmessOption)
proxy, err = outbound.NewVmess(*vmessOption)
case "snell":
snellOption := &SnellOption{}
snellOption := &outbound.SnellOption{}
err = decoder.Decode(mapping, snellOption)
if err != nil {
break
}
proxy, err = NewSnell(*snellOption)
proxy, err = outbound.NewSnell(*snellOption)
case "trojan":
trojanOption := &TrojanOption{}
trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption)
if err != nil {
break
}
proxy, err = NewTrojan(*trojanOption)
proxy, err = outbound.NewTrojan(*trojanOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}

View File

@ -3,51 +3,54 @@ package provider
import (
"bytes"
"crypto/md5"
"io/ioutil"
"os"
"path/filepath"
"time"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
)
var (
fileMode os.FileMode = 0666
dirMode os.FileMode = 0755
fileMode os.FileMode = 0o666
dirMode os.FileMode = 0o755
)
type parser = func([]byte) (interface{}, error)
type parser = func([]byte) (any, error)
type fetcher struct {
name string
vehicle Vehicle
vehicle types.Vehicle
interval time.Duration
updatedAt *time.Time
ticker *time.Ticker
done chan struct{}
hash [16]byte
parser parser
onUpdate func(interface{})
onUpdate func(any)
}
func (f *fetcher) Name() string {
return f.name
}
func (f *fetcher) VehicleType() VehicleType {
func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type()
}
func (f *fetcher) Initial() (interface{}, error) {
func (f *fetcher) Initial() (any, error) {
var (
buf []byte
err error
isLocal bool
buf []byte
err error
isLocal bool
immediatelyUpdate bool
)
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = ioutil.ReadFile(f.vehicle.Path())
buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime()
f.updatedAt = &modTime
isLocal = true
immediatelyUpdate = time.Since(modTime) > f.interval
} else {
buf, err = f.vehicle.Read()
}
@ -76,7 +79,7 @@ func (f *fetcher) Initial() (interface{}, error) {
isLocal = false
}
if f.vehicle.Type() != File && !isLocal {
if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err
}
@ -86,13 +89,13 @@ func (f *fetcher) Initial() (interface{}, error) {
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
go f.pullLoop(immediatelyUpdate)
}
return proxies, nil
}
func (f *fetcher) Update() (interface{}, bool, error) {
func (f *fetcher) Update() (any, bool, error) {
buf, err := f.vehicle.Read()
if err != nil {
return nil, false, err
@ -102,6 +105,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil
}
@ -110,7 +114,7 @@ func (f *fetcher) Update() (interface{}, bool, error) {
return nil, false, err
}
if f.vehicle.Type() != File {
if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
}
@ -129,25 +133,33 @@ func (f *fetcher) Destroy() error {
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 {
select {
case <-f.ticker.C:
elm, same, err := f.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)
}
update()
case <-f.done:
f.ticker.Stop()
return
@ -164,10 +176,10 @@ func safeWrite(path string, buf []byte) error {
}
}
return ioutil.WriteFile(path, buf, fileMode)
return os.WriteFile(path, buf, fileMode)
}
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
var ticker *time.Ticker
if interval != 0 {
ticker = time.NewTicker(interval)
@ -177,6 +189,7 @@ func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser par
name: name,
ticker: ticker,
vehicle: vehicle,
interval: interval,
parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate,

View File

@ -2,11 +2,12 @@ package provider
import (
"context"
"sync"
"time"
"github.com/Dreamacro/clash/common/batch"
C "github.com/Dreamacro/clash/constant"
"github.com/samber/lo"
"go.uber.org/atomic"
)
@ -31,13 +32,20 @@ type HealthCheck struct {
func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go hc.check()
go hc.checkAll()
for {
select {
case <-ticker.C:
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
hc.checkAll()
} else { // lazy but still need to check not alive proxies
notAliveProxies := lo.Filter(hc.proxies, func(proxy C.Proxy, _ int) bool {
return !proxy.Alive()
})
if len(notAliveProxies) != 0 {
hc.check(notAliveProxies)
}
}
case <-hc.done:
ticker.Stop()
@ -58,21 +66,22 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
wg := &sync.WaitGroup{}
func (hc *HealthCheck) checkAll() {
hc.check(hc.proxies)
}
for _, proxy := range hc.proxies {
wg.Add(1)
go func(p C.Proxy) {
func (hc *HealthCheck) check(proxies []C.Proxy) {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range proxies {
p := proxy
b.Go(p.Name(), func() (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)
wg.Done()
}(proxy)
return nil, nil
})
}
wg.Wait()
cancel()
b.Wait()
}
func (hc *HealthCheck) close() {

View File

@ -7,10 +7,12 @@ import (
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct {
@ -25,10 +27,11 @@ type proxyProviderSchema struct {
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
}
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{
@ -48,16 +51,20 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
path := C.Path.Resolve(schema.Path)
var vehicle Vehicle
var vehicle types.Vehicle
switch schema.Type {
case "file":
vehicle = NewFileVehicle(path)
case "http":
if !C.Path.IsSubPath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
vehicle = NewHTTPVehicle(schema.URL, path)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}
interval := time.Duration(uint(schema.Interval)) * time.Second
return NewProxySetProvider(name, interval, vehicle, hc), nil
filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc)
}

View File

@ -0,0 +1,322 @@
package provider
import (
"encoding/json"
"errors"
"fmt"
"runtime"
"time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
regexp "github.com/dlclark/regexp2"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)
var reject = adapter.NewProxy(outbound.NewReject())
const (
ReservedName = "default"
)
type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"`
}
// for auto gc
type ProxySetProvider struct {
*proxySetProvider
}
type proxySetProvider struct {
*fetcher
proxies []C.Proxy
healthCheck *HealthCheck
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": pp.Name(),
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
"updatedAt": pp.updatedAt,
})
}
func (pp *proxySetProvider) Name() string {
return pp.name
}
func (pp *proxySetProvider) HealthCheck() {
pp.healthCheck.checkAll()
}
func (pp *proxySetProvider) Update() error {
elm, same, err := pp.fetcher.Update()
if err == nil && !same {
pp.onUpdate(elm)
}
return err
}
func (pp *proxySetProvider) Initial() error {
elm, err := pp.fetcher.Initial()
if err != nil {
return err
}
pp.onUpdate(elm)
return nil
}
func (pp *proxySetProvider) Type() types.ProviderType {
return types.Proxy
}
func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.checkAll()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
filterReg, err := regexp.Compile(filter, regexp.None)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
proxies: []C.Proxy{},
healthCheck: hc,
}
onUpdate := func(elm any) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"].(string); ok && len(filter) > 0 {
matched, err := filterReg.MatchString(name)
if err != nil {
return nil, fmt.Errorf("regex filter failed: %w", err)
}
if !matched {
continue
}
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
}
// for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
})
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.checkAll()
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (cp *compatibleProvider) Type() types.ProviderType {
return types.Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
name: name,
proxies: proxies,
healthCheck: hc,
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
}
var _ types.ProxyProvider = (*FilterableProvider)(nil)
type FilterableProvider struct {
name string
providers []types.ProxyProvider
filterReg *regexp.Regexp
single *singledo.Single
}
func (fp *FilterableProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": fp.Name(),
"type": fp.Type().String(),
"vehicleType": fp.VehicleType().String(),
"proxies": fp.Proxies(),
})
}
func (fp *FilterableProvider) Name() string {
return fp.name
}
func (fp *FilterableProvider) HealthCheck() {
}
func (fp *FilterableProvider) Update() error {
return nil
}
func (fp *FilterableProvider) Initial() error {
return nil
}
func (fp *FilterableProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (fp *FilterableProvider) Type() types.ProviderType {
return types.Proxy
}
func (fp *FilterableProvider) Proxies() []C.Proxy {
elm, _, _ := fp.single.Do(func() (any, error) {
proxies := lo.FlatMap(
fp.providers,
func(item types.ProxyProvider, _ int) []C.Proxy {
return lo.Filter(
item.Proxies(),
func(item C.Proxy, _ int) bool {
matched, _ := fp.filterReg.MatchString(item.Name())
return matched
})
})
if len(proxies) == 0 {
proxies = append(proxies, reject)
}
return proxies, nil
})
return elm.([]C.Proxy)
}
func (fp *FilterableProvider) Touch() {
for _, provider := range fp.providers {
provider.Touch()
}
}
func NewFilterableProvider(name string, providers []types.ProxyProvider, filterReg *regexp.Regexp) *FilterableProvider {
return &FilterableProvider{
name: name,
providers: providers,
filterReg: filterReg,
single: singledo.NewSingle(time.Second * 10),
}
}

View File

@ -2,49 +2,23 @@ package provider
import (
"context"
"io/ioutil"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
"github.com/Dreamacro/clash/component/dialer"
types "github.com/Dreamacro/clash/constant/provider"
)
// Vehicle Type
const (
File VehicleType = iota
HTTP
Compatible
)
// VehicleType defined
type VehicleType int
func (v VehicleType) String() string {
switch v {
case File:
return "File"
case HTTP:
return "HTTP"
case Compatible:
return "Compatible"
default:
return "Unknown"
}
}
type Vehicle interface {
Read() ([]byte, error)
Path() string
Type() VehicleType
}
type FileVehicle struct {
path string
}
func (f *FileVehicle) Type() VehicleType {
return File
func (f *FileVehicle) Type() types.VehicleType {
return types.File
}
func (f *FileVehicle) Path() string {
@ -52,7 +26,7 @@ func (f *FileVehicle) Path() string {
}
func (f *FileVehicle) Read() ([]byte, error) {
return ioutil.ReadFile(f.path)
return os.ReadFile(f.path)
}
func NewFileVehicle(path string) *FileVehicle {
@ -64,8 +38,8 @@ type HTTPVehicle struct {
path string
}
func (h *HTTPVehicle) Type() VehicleType {
return HTTP
func (h *HTTPVehicle) Type() types.VehicleType {
return types.HTTP
}
func (h *HTTPVehicle) Path() string {
@ -99,7 +73,9 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DialContext,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
},
}
client := http.Client{Transport: transport}
@ -109,7 +85,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
}
defer resp.Body.Close()
buf, err := ioutil.ReadAll(resp.Body)
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

View File

@ -1,61 +0,0 @@
package inbound
import (
"net"
"net/http"
"strings"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
)
// NewHTTP receive normal http request and return HTTPContext
func NewHTTP(request *http.Request, conn net.Conn) *context.HTTPContext {
metadata := parseHTTPAddr(request)
metadata.Type = C.HTTP
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
return context.NewHTTPContext(conn, request, metadata)
}
// RemoveHopByHopHeaders remove hop-by-hop header
func RemoveHopByHopHeaders(header http.Header) {
// Strip hop-by-hop header based on RFC:
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
header.Del("Proxy-Connection")
header.Del("Proxy-Authenticate")
header.Del("Proxy-Authorization")
header.Del("TE")
header.Del("Trailers")
header.Del("Transfer-Encoding")
header.Del("Upgrade")
connections := header.Get("Connection")
header.Del("Connection")
if len(connections) == 0 {
return
}
for _, h := range strings.Split(connections, ",") {
header.Del(strings.TrimSpace(h))
}
}
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
func RemoveExtraHTTPHostPort(req *http.Request) {
host := req.Host
if host == "" {
host = req.URL.Host
}
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
host = pHost
}
req.Host = host
req.URL.Host = host
}

View File

@ -1,63 +0,0 @@
package outbound
import (
"context"
"errors"
"io"
"net"
"time"
C "github.com/Dreamacro/clash/constant"
)
type Reject struct {
*Base
}
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
return NewConn(&NopConn{}, r), nil
}
// DialUDP implements C.ProxyAdapter
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
return nil, errors.New("match reject rule")
}
func NewReject() *Reject {
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
udp: true,
},
}
}
type NopConn struct{}
func (rw *NopConn) Read(b []byte) (int, error) {
return 0, io.EOF
}
func (rw *NopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
// Close is fake function for net.Conn
func (rw *NopConn) Close() error { return nil }
// LocalAddr is fake function for net.Conn
func (rw *NopConn) LocalAddr() net.Addr { return nil }
// RemoteAddr is fake function for net.Conn
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
// SetDeadline is fake function for net.Conn
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
// SetReadDeadline is fake function for net.Conn
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
// SetWriteDeadline is fake function for net.Conn
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }

View File

@ -1,106 +0,0 @@
package outbound
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/url"
"strconv"
"sync"
"time"
"github.com/Dreamacro/clash/component/resolver"
"github.com/Dreamacro/clash/component/socks5"
C "github.com/Dreamacro/clash/constant"
)
const (
tcpTimeout = 5 * time.Second
)
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return
}
port := u.Port()
if port == "" {
switch u.Scheme {
case "https":
port = "443"
case "http":
port = "80"
default:
err = fmt.Errorf("%s scheme not Support", rawURL)
return
}
}
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstPort: port,
}
return
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
aType := uint8(metadata.AddrType)
p, _ := strconv.Atoi(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks5.AtypDomainName:
len := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
case socks5.AtypIPv4:
host := metadata.DstIP.To4()
buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6:
host := metadata.DstIP.To16()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
}
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := resolver.ResolveIP(host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func safeConnClose(c net.Conn, err error) {
if err != nil {
c.Close()
}
}

View File

@ -1,261 +0,0 @@
package provider
import (
"encoding/json"
"errors"
"fmt"
"runtime"
"time"
"github.com/Dreamacro/clash/adapters/outbound"
C "github.com/Dreamacro/clash/constant"
"gopkg.in/yaml.v2"
)
const (
ReservedName = "default"
)
// Provider Type
const (
Proxy ProviderType = iota
Rule
)
// ProviderType defined
type ProviderType int
func (pt ProviderType) String() string {
switch pt {
case Proxy:
return "Proxy"
case Rule:
return "Rule"
default:
return "Unknown"
}
}
// Provider interface
type Provider interface {
Name() string
VehicleType() VehicleType
Type() ProviderType
Initial() error
Update() error
}
// ProxyProvider interface
type ProxyProvider interface {
Provider
Proxies() []C.Proxy
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in Dial and DialUDP
ProxiesWithTouch() []C.Proxy
HealthCheck()
}
type ProxySchema struct {
Proxies []map[string]interface{} `yaml:"proxies"`
}
// for auto gc
type ProxySetProvider struct {
*proxySetProvider
}
type proxySetProvider struct {
*fetcher
proxies []C.Proxy
healthCheck *HealthCheck
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"name": pp.Name(),
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
"updatedAt": pp.updatedAt,
})
}
func (pp *proxySetProvider) Name() string {
return pp.name
}
func (pp *proxySetProvider) HealthCheck() {
pp.healthCheck.check()
}
func (pp *proxySetProvider) Update() error {
elm, same, err := pp.fetcher.Update()
if err == nil && !same {
pp.onUpdate(elm)
}
return err
}
func (pp *proxySetProvider) Initial() error {
elm, err := pp.fetcher.Initial()
if err != nil {
return err
}
pp.onUpdate(elm)
return nil
}
func (pp *proxySetProvider) Type() ProviderType {
return Proxy
}
func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(buf []byte) (interface{}, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
proxy, err := outbound.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
return nil, errors.New("file doesn't have any valid proxy")
}
return proxies, nil
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.check()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
proxies: []C.Proxy{},
healthCheck: hc,
}
onUpdate := func(elm interface{}) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
}
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate)
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper
}
// for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
})
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
return nil
}
func (cp *compatibleProvider) VehicleType() VehicleType {
return Compatible
}
func (cp *compatibleProvider) Type() ProviderType {
return Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("Provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
name: name,
proxies: proxies,
healthCheck: hc,
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
}

105
common/batch/batch.go Normal file
View File

@ -0,0 +1,105 @@
package batch
import (
"context"
"sync"
)
type Option = func(b *Batch)
type Result struct {
Value any
Err error
}
type Error struct {
Key string
Err error
}
func WithConcurrencyNum(n int) Option {
return func(b *Batch) {
q := make(chan struct{}, n)
for i := 0; i < n; i++ {
q <- struct{}{}
}
b.queue = q
}
}
// Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct {
result map[string]Result
queue chan struct{}
wg sync.WaitGroup
mux sync.Mutex
err *Error
once sync.Once
cancel func()
}
func (b *Batch) Go(key string, fn func() (any, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()
if b.queue != nil {
<-b.queue
defer func() {
b.queue <- struct{}{}
}()
}
value, err := fn()
if err != nil {
b.once.Do(func() {
b.err = &Error{key, err}
if b.cancel != nil {
b.cancel()
}
})
}
ret := Result{value, err}
b.mux.Lock()
defer b.mux.Unlock()
b.result[key] = ret
}()
}
func (b *Batch) Wait() *Error {
b.wg.Wait()
if b.cancel != nil {
b.cancel()
}
return b.err
}
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
err := b.Wait()
return b.Result(), err
}
func (b *Batch) Result() map[string]Result {
b.mux.Lock()
defer b.mux.Unlock()
copy := map[string]Result{}
for k, v := range b.result {
copy[k] = v
}
return copy
}
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
result: map[string]Result{},
}
for _, o := range opts {
o(b)
}
b.cancel = cancel
return b, ctx
}

View File

@ -0,0 +1,83 @@
package batch
import (
"context"
"errors"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
now := time.Now()
b.Go("foo", func() (any, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (any, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
result, err := b.WaitAndGetResult()
assert.Nil(t, err)
duration := time.Since(now)
assert.Less(t, duration, time.Millisecond*200)
assert.Equal(t, 2, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
context.Background(),
WithConcurrencyNum(3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (any, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
}
result, _ := b.WaitAndGetResult()
duration := time.Since(now)
assert.Greater(t, duration, time.Millisecond*260)
assert.Equal(t, 7, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b.Go("error", func() (any, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
})
b.Go("ctx", func() (any, error) {
<-ctx.Done()
return nil, ctx.Err()
})
result, err := b.WaitAndGetResult()
assert.NotNil(t, err)
assert.Equal(t, "error", err.Key)
assert.Equal(t, ctx.Err(), result["ctx"].Err)
}

106
common/cache/cache.go vendored
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 interface{}
}
// Put element in Cache with its ttl
func (c *cache) Put(key interface{}, payload interface{}, ttl time.Duration) {
c.mapping.Store(key, &element{
Payload: payload,
Expired: time.Now().Add(ttl),
})
}
// Get element in Cache, and drop when it expired
func (c *cache) Get(key interface{}) interface{} {
item, exist := c.mapping.Load(key)
if !exist {
return nil
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return nil
}
return elm.Payload
}
// GetWithExpire element in Cache with Expire Time
func (c *cache) GetWithExpire(key interface{}) (payload interface{}, expired time.Time) {
item, exist := c.mapping.Load(key)
if !exist {
return
}
elm := item.(*element)
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return
}
return elm.Payload, elm.Expired
}
func (c *cache) cleanup() {
c.mapping.Range(func(k, v interface{}) bool {
key := k.(string)
elm := v.(*element)
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
}
return true
})
}
type janitor struct {
interval time.Duration
stop chan struct{}
}
func (j *janitor) process(c *cache) {
ticker := time.NewTicker(j.interval)
for {
select {
case <-ticker.C:
c.cleanup()
case <-j.stop:
ticker.Stop()
return
}
}
}
func stopJanitor(c *Cache) {
c.janitor.stop <- struct{}{}
}
// New return *Cache
func New(interval time.Duration) *Cache {
j := &janitor{
interval: interval,
stop: make(chan struct{}),
}
c := &cache{janitor: j}
go j.process(c)
C := &Cache{c}
runtime.SetFinalizer(C, stopJanitor)
return C
}

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

@ -12,7 +12,7 @@ import (
type Option func(*LruCache)
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key interface{}, value interface{})
type EvictCallback = func(key any, value any)
// WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option {
@ -57,18 +57,18 @@ type LruCache struct {
maxAge int64
maxSize int
mu sync.Mutex
cache map[interface{}]*list.Element
cache map[any]*list.Element
lru *list.List // Front is least-recent
updateAgeOnGet bool
staleReturn bool
onEvict EvictCallback
}
// NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache {
// New creates an LruCache
func New(options ...Option) *LruCache {
lc := &LruCache{
lru: list.New(),
cache: make(map[interface{}]*list.Element),
cache: make(map[any]*list.Element),
}
for _, option := range options {
@ -78,9 +78,9 @@ func NewLRUCache(options ...Option) *LruCache {
return lc
}
// Get returns the interface{} representation of a cached response and a bool
// Get returns the any representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache) Get(key interface{}) (interface{}, bool) {
func (c *LruCache) Get(key any) (any, bool) {
entry := c.get(key)
if entry == nil {
return nil, false
@ -90,11 +90,11 @@ func (c *LruCache) Get(key interface{}) (interface{}, bool) {
return value, true
}
// GetWithExpire returns the interface{} representation of a cached response,
// GetWithExpire returns the any representation of a cached response,
// a time.Time Give expected expires,
// and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) {
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
entry := c.get(key)
if entry == nil {
return nil, time.Time{}, false
@ -104,7 +104,7 @@ func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool)
}
// Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key interface{}) bool {
func (c *LruCache) Exist(key any) bool {
c.mu.Lock()
defer c.mu.Unlock()
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
return ok
}
// Set stores the interface{} representation of a response for a given key.
func (c *LruCache) Set(key interface{}, value interface{}) {
// Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key any, value any) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
@ -121,9 +121,9 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
c.SetWithExpire(key, value, time.Unix(expires, 0))
}
// SetWithExpire stores the interface{} representation of a response for a given key and given expires.
// SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second.
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) {
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
@ -155,7 +155,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
defer n.mu.Unlock()
n.lru = list.New()
n.cache = make(map[interface{}]*list.Element)
n.cache = make(map[any]*list.Element)
for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry)
@ -163,7 +163,7 @@ func (c *LruCache) CloneTo(n *LruCache) {
}
}
func (c *LruCache) get(key interface{}) *entry {
func (c *LruCache) get(key any) *entry {
c.mu.Lock()
defer c.mu.Unlock()
@ -188,7 +188,7 @@ func (c *LruCache) get(key interface{}) *entry {
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key interface{}) {
func (c *LruCache) Delete(key any) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {
@ -217,7 +217,7 @@ func (c *LruCache) deleteElement(le *list.Element) {
}
type entry struct {
key interface{}
value interface{}
key any
value any
expires int64
}

View File

@ -19,7 +19,7 @@ var entries = []struct {
}
func TestLRUCache(t *testing.T) {
c := NewLRUCache()
c := New()
for _, e := range entries {
c.Set(e.key, e.value)
@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
}
func TestLRUMaxAge(t *testing.T) {
c := NewLRUCache(WithAge(86400))
c := New(WithAge(86400))
now := time.Now().Unix()
expected := now + 86400
@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
}
func TestLRUpdateOnGet(t *testing.T) {
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
c := New(WithAge(86400), WithUpdateAgeOnGet())
now := time.Now().Unix()
expires := now + 86400/2
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
}
func TestMaxSize(t *testing.T) {
c := NewLRUCache(WithSize(2))
c := New(WithSize(2))
// Add one expired entry
c.Set("foo", "bar")
_, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
}
func TestExist(t *testing.T) {
c := NewLRUCache(WithSize(1))
c := New(WithSize(1))
c.Set(1, 2)
assert.True(t, c.Exist(1))
c.Set(2, 3)
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) {
temp := 0
evict := func(key interface{}, value interface{}) {
evict := func(key any, value any) {
temp = key.(int) + value.(int)
}
c := NewLRUCache(WithEvict(evict), WithSize(1))
c := New(WithEvict(evict), WithSize(1))
c.Set(1, 2)
c.Set(2, 3)
@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
}
func TestSetWithExpire(t *testing.T) {
c := NewLRUCache(WithAge(1))
c := New(WithAge(1))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -149,11 +149,10 @@ func TestSetWithExpire(t *testing.T) {
assert.Equal(t, nil, res)
assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist)
}
func TestStale(t *testing.T) {
c := NewLRUCache(WithAge(1), WithStale(true))
c := New(WithAge(1), WithStale(true))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -166,11 +165,11 @@ func TestStale(t *testing.T) {
}
func TestCloneTo(t *testing.T) {
o := NewLRUCache(WithSize(10))
o := New(WithSize(10))
o.Set("1", 1)
o.Set("2", 2)
n := NewLRUCache(WithSize(2))
n := New(WithSize(2))
n.Set("3", 3)
n.Set("4", 4)

View File

@ -67,7 +67,6 @@ func (d *digest32) bmix(p []byte) (tail []byte) {
}
func (d *digest32) Sum32() (h1 uint32) {
h1 = d.h1
var k1 uint32

View File

@ -1,4 +1,4 @@
package mixed
package net
import (
"bufio"
@ -11,6 +11,9 @@ type BufferedConn struct {
}
func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok {
return bc
}
return &BufferedConn{bufio.NewReader(c), c}
}

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

@ -1,3 +1,3 @@
package observable
type Iterable <-chan interface{}
type Iterable <-chan any

View File

@ -9,8 +9,8 @@ import (
"go.uber.org/atomic"
)
func iterator(item []interface{}) chan interface{} {
ch := make(chan interface{})
func iterator(item []any) chan any {
ch := make(chan any)
go func() {
time.Sleep(100 * time.Millisecond)
for _, elm := range item {
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
}
func TestObservable(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
assert.Nil(t, err)
@ -34,15 +34,15 @@ func TestObservable(t *testing.T) {
}
func TestObservable_MultiSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe()
var count = atomic.NewInt32(0)
count := atomic.NewInt32(0)
var wg sync.WaitGroup
wg.Add(2)
waitCh := func(ch <-chan interface{}) {
waitCh := func(ch <-chan any) {
for range ch {
count.Inc()
}
@ -55,7 +55,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
}
func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
data, err := src.Subscribe()
assert.Nil(t, err)
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
}
func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1})
iter := iterator([]any{1})
src := NewObservable(iter)
data, _ := src.Subscribe()
<-data
@ -75,14 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
}
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{}))
iter := iterator([]interface{}{1})
sub := Subscription(make(chan any))
iter := iterator([]any{1})
src := NewObservable(iter)
src.UnSubscribe(sub)
}
func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5})
iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter)
max := 100
@ -94,7 +94,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
var wg sync.WaitGroup
wg.Add(max)
waitCh := func(ch <-chan interface{}) {
waitCh := func(ch <-chan any) {
for range ch {
}
wg.Done()
@ -115,7 +115,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
}
func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan interface{})
ch := make(chan any)
o := NewObservable(ch)
num := 1000

View File

@ -4,14 +4,14 @@ import (
"sync"
)
type Subscription <-chan interface{}
type Subscription <-chan any
type Subscriber struct {
buffer chan interface{}
buffer chan any
once sync.Once
}
func (s *Subscriber) Emit(item interface{}) {
func (s *Subscriber) Emit(item any) {
s.buffer <- item
}
@ -27,7 +27,7 @@ func (s *Subscriber) Close() {
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: make(chan interface{}, 200),
buffer: make(chan any, 200),
}
return sub
}

View File

@ -17,7 +17,7 @@ type Picker struct {
once sync.Once
errOnce sync.Once
result interface{}
result any
err error
}
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
// Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them.
func (p *Picker) Wait() interface{} {
func (p *Picker) Wait() any {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
// Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (interface{}, error)) {
func (p *Picker) Go(f func() (any, error)) {
p.wg.Add(1)
go func() {

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert"
)
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) {
return func() (interface{}, error) {
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (any, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select {
case <-timer.C:

View File

@ -8,11 +8,7 @@ import (
"sync"
)
var defaultAllocator *Allocator
func init() {
defaultAllocator = NewAllocator()
}
var defaultAllocator = NewAllocator()
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct {
@ -27,7 +23,7 @@ func NewAllocator() *Allocator {
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
for k := range alloc.buffers {
i := k
alloc.buffers[k].New = func() interface{} {
alloc.buffers[k].New = func() any {
return make([]byte, 1<<uint32(i))
}
}
@ -36,26 +32,36 @@ func NewAllocator() *Allocator {
// Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte {
if size <= 0 || size > 65536 {
switch {
case size < 0:
panic("alloc.Get: len out of range")
case size == 0:
return nil
}
case size > 65536:
return make([]byte, size)
default:
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
bits := msb(size)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
return alloc.buffers[bits+1].Get().([]byte)[:size]
}
return alloc.buffers[bits+1].Get().([]byte)[:size]
}
// Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}
bits := msb(cap(buf))
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits {
if cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size")
}
//nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf)
return nil

View File

@ -19,17 +19,17 @@ func TestAllocGet(t *testing.T) {
assert.Equal(t, 1024, cap(alloc.Get(1023)))
assert.Equal(t, 1024, len(alloc.Get(1024)))
assert.Equal(t, 65536, len(alloc.Get(65536)))
assert.Nil(t, alloc.Get(65537))
assert.Equal(t, 65537, len(alloc.Get(65537)))
}
func TestAllocPut(t *testing.T) {
alloc := NewAllocator()
assert.NotNil(t, alloc.Put(nil), "put nil misbehavior")
assert.Nil(t, alloc.Put(nil), "put nil misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 3)), "put elem:3 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 4)), "put elem:4 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 1023, 1024)), "put elem:1024 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65536)), "put elem:65536 []bytes misbehavior")
assert.NotNil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
assert.Nil(t, alloc.Put(make([]byte, 65537)), "put elem:65537 []bytes misbehavior")
}
func TestAllocPutThenGet(t *testing.T) {

31
common/pool/buffer.go Normal file
View File

@ -0,0 +1,31 @@
package pool
import (
"bytes"
"sync"
"github.com/Dreamacro/protobytes"
)
var (
bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
bytesBufferPool = sync.Pool{New: func() any { return &protobytes.BytesWriter{} }}
)
func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer)
}
func PutBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
func GetBytesBuffer() *protobytes.BytesWriter {
return bytesBufferPool.Get().(*protobytes.BytesWriter)
}
func PutBytesBuffer(buf *protobytes.BytesWriter) {
buf.Reset()
bytesBufferPool.Put(buf)
}

View File

@ -5,6 +5,11 @@ const (
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024
)
func Get(size int) []byte {

View File

@ -6,12 +6,12 @@ import (
// Queue is a simple concurrent safe queue
type Queue struct {
items []interface{}
items []any
lock sync.RWMutex
}
// Put add the item to the queue.
func (q *Queue) Put(items ...interface{}) {
func (q *Queue) Put(items ...any) {
if len(items) == 0 {
return
}
@ -22,7 +22,7 @@ func (q *Queue) Put(items ...interface{}) {
}
// Pop returns the head of items.
func (q *Queue) Pop() interface{} {
func (q *Queue) Pop() any {
if len(q.items) == 0 {
return nil
}
@ -35,7 +35,7 @@ func (q *Queue) Pop() interface{} {
}
// Last returns the last of item.
func (q *Queue) Last() interface{} {
func (q *Queue) Last() any {
if len(q.items) == 0 {
return nil
}
@ -47,8 +47,8 @@ func (q *Queue) Last() interface{} {
}
// Copy get the copy of queue.
func (q *Queue) Copy() []interface{} {
items := []interface{}{}
func (q *Queue) Copy() []any {
items := []any{}
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()
@ -66,6 +66,6 @@ func (q *Queue) Len() int64 {
// New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue {
return &Queue{
items: make([]interface{}, 0, hint),
items: make([]any, 0, hint),
}
}

View File

@ -7,7 +7,7 @@ import (
type call struct {
wg sync.WaitGroup
val interface{}
val any
err error
}
@ -20,13 +20,12 @@ type Single struct {
}
type Result struct {
Val interface{}
Val any
Err error
}
// Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
s.mux.Lock()
now := time.Now()
if now.Before(s.last.Add(s.wait)) {

View File

@ -12,8 +12,8 @@ import (
func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
var shardCount = atomic.NewInt32(0)
call := func() (interface{}, error) {
shardCount := atomic.NewInt32(0)
call := func() (any, error) {
foo++
time.Sleep(time.Millisecond * 5)
return nil, nil
@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
call := func() (interface{}, error) {
call := func() (any, error) {
foo++
return nil, nil
}
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
foo := 0
call := func() (interface{}, error) {
call := func() (any, error) {
foo++
return nil, nil
}

View File

@ -1,4 +1,4 @@
// +build !linux
//go:build !linux
package sockopt

View File

@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
return &Decoder{option: &option}
}
// Decode transform a map[string]interface{} to a struct
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
// Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct")
}
@ -37,14 +37,16 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
v := reflect.ValueOf(dst).Elem()
for idx := 0; idx < v.NumField(); idx++ {
field := t.Field(idx)
if field.Anonymous {
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
return err
}
continue
}
tag := field.Tag.Get(d.option.TagName)
str := strings.SplitN(tag, ",", 2)
key := str[0]
omitempty := false
if len(str) > 1 {
omitempty = str[1] == "omitempty"
}
key, omitKey, found := strings.Cut(tag, ",")
omitempty := found && omitKey == "omitempty"
value, ok := src[key]
if !ok || value == nil {
@ -62,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
return nil
}
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error {
func (d *Decoder) decode(name string, data any, val reflect.Value) error {
switch val.Kind() {
case reflect.Int:
return d.decodeInt(name, data, val)
@ -83,12 +85,14 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
}
}
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) {
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case kind == reflect.Int:
val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
@ -106,7 +110,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (e
return err
}
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) {
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
@ -123,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
return err
}
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) {
func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
@ -140,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
return err
}
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error {
func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
valType := val.Type()
valElemType := valType.Elem()
@ -155,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
}
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil {
return err
}
@ -167,7 +181,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
return nil
}
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error {
func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error {
valType := val.Type()
valKeyType := valType.Key()
valElemType := valType.Elem()
@ -239,7 +253,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
return nil
}
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error {
func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data))
// If the type of the value to write to and the data match directly,
@ -267,7 +281,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
}
dataValKeys := make(map[reflect.Value]struct{})
dataValKeysUnused := make(map[interface{}]struct{})
dataValKeysUnused := make(map[any]struct{})
for _, dataValKey := range dataVal.MapKeys() {
dataValKeys[dataValKey] = struct{}{}
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
@ -392,7 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
return nil
}
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) {
func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
val.Set(dataVal)
return nil

View File

@ -1,12 +1,15 @@
package structure
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
var decoder = NewDecoder(Option{TagName: "test"})
var weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
var (
decoder = NewDecoder(Option{TagName: "test"})
weakTypeDecoder = NewDecoder(Option{TagName: "test", WeaklyTypedInput: true})
)
type Baz struct {
Foo int `test:"foo"`
@ -24,7 +27,7 @@ type BazOptional struct {
}
func TestStructure_Basic(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": 1,
"bar": "test",
"extra": false,
@ -37,16 +40,12 @@ func TestStructure_Basic(t *testing.T) {
s := &Baz{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
assert.Nil(t, err)
assert.Equal(t, goal, s)
}
func TestStructure_Slice(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": 1,
"bar": []string{"one", "two"},
}
@ -58,16 +57,12 @@ func TestStructure_Slice(t *testing.T) {
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
assert.Nil(t, err)
assert.Equal(t, goal, s)
}
func TestStructure_Optional(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": 1,
}
@ -77,50 +72,40 @@ func TestStructure_Optional(t *testing.T) {
s := &BazOptional{}
err := decoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
assert.Nil(t, err)
assert.Equal(t, goal, s)
}
func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": 1,
}
s := &Baz{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
assert.NotNilf(t, err, "should throw error: %#v", s)
}
func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]interface{}{}
rawMap := map[string]any{}
s := Baz{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
assert.NotNilf(t, err, "should throw error: %#v", s)
}
func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": 1,
"bar": []int{1, 2},
}
s := &BazSlice{}
err := decoder.Decode(rawMap, s)
if err == nil {
t.Fatalf("should throw error: %#v", s)
}
assert.NotNilf(t, err, "should throw error: %#v", s)
}
func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]interface{}{
rawMap := map[string]any{
"foo": "1",
"bar": []int{1},
}
@ -132,10 +117,65 @@ func TestStructure_WeakType(t *testing.T) {
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
if err != nil {
t.Fatal(err.Error())
}
if !reflect.DeepEqual(s, goal) {
t.Fatalf("bad: %#v", s)
}
assert.Nil(t, err)
assert.Equal(t, goal, s)
}
func TestStructure_Nest(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
}
goal := BazOptional{
Foo: 1,
}
s := &struct {
BazOptional
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal)
}
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
}

View File

@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
au.storage.Store(user.User, user.Pass)
}
usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value interface{}) bool {
au.storage.Range(func(key, value any) bool {
usernames = append(usernames, key.(string))
return true
})

28
component/dhcp/conn.go Normal file
View File

@ -0,0 +1,28 @@
package dhcp
import (
"context"
"net"
"runtime"
"github.com/Dreamacro/clash/component/dialer"
)
func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, error) {
listenAddr := "0.0.0.0:68"
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
options := []dialer.Option{
dialer.WithInterface(ifaceName),
dialer.WithAddrReuse(true),
}
// fallback bind on windows, because syscall bind can not receive broadcast
if runtime.GOOS == "windows" {
options = append(options, dialer.WithFallbackBind(true))
}
return dialer.ListenPacket(ctx, "udp4", listenAddr, options...)
}

88
component/dhcp/dhcp.go Normal file
View File

@ -0,0 +1,88 @@
package dhcp
import (
"context"
"errors"
"net"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
)
var (
ErrNotResponding = errors.New("DHCP not responding")
ErrNotFound = errors.New("DNS option not found")
)
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
conn, err := ListenDHCPClient(context, ifaceName)
if err != nil {
return nil, err
}
defer conn.Close()
result := make(chan []net.IP, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
discovery, err := dhcpv4.NewDiscovery(ifaceObj.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
if err != nil {
return nil, err
}
go receiveOffer(conn, discovery.TransactionID, result)
_, err = conn.WriteTo(discovery.ToBytes(), &net.UDPAddr{IP: net.IPv4bcast, Port: 67})
if err != nil {
return nil, err
}
select {
case r, ok := <-result:
if !ok {
return nil, ErrNotFound
}
return r, nil
case <-context.Done():
return nil, ErrNotResponding
}
}
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
return
}
pkt, err := dhcpv4.FromBytes(buf[:n])
if err != nil {
continue
}
if pkt.MessageType() != dhcpv4.MessageTypeOffer {
continue
}
if pkt.TransactionID != id {
continue
}
dns := pkt.DNS()
if len(dns) == 0 {
return
}
result <- dns
return
}
}

View File

@ -1,118 +0,0 @@
package dialer
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
// In some OS, such as Windows, it takes a little longer to get interface information
var ifaceSingle = singledo.NewSingle(time.Second * 20)
var (
errPlatformNotSupport = errors.New("unsupport platform")
)
func lookupTCPAddr(ip net.IP, addrs []net.Addr) (*net.TCPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.TCPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func lookupUDPAddr(ip net.IP, addrs []net.Addr) (*net.UDPAddr, error) {
ipv4 := ip.To4() != nil
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok {
continue
}
addrV4 := addr.IP.To4() != nil
if addrV4 && ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
} else if !addrV4 && !ipv4 {
return &net.UDPAddr{IP: addr.IP, Port: 0}, nil
}
}
return nil, ErrAddrNotFound
}
func fallbackBindToDialer(dialer *net.Dialer, network string, ip net.IP, name string) error {
if !ip.IsGlobalUnicast() {
return nil
}
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return err
}
switch network {
case "tcp", "tcp4", "tcp6":
if addr, err := lookupTCPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
case "udp", "udp4", "udp6":
if addr, err := lookupUDPAddr(ip, addrs); err == nil {
dialer.LocalAddr = addr
} else {
return err
}
}
return nil
}
func fallbackBindToListenConfig(name string) (string, error) {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(name)
})
if err != nil {
return "", err
}
addrs, err := iface.(*net.Interface).Addrs()
if err != nil {
return "", err
}
for _, elm := range addrs {
addr, ok := elm.(*net.IPNet)
if !ok || addr.IP.To4() == nil {
continue
}
return net.JoinHostPort(addr.IP.String(), "0"), nil
}
return "", ErrAddrNotFound
}

View File

@ -3,51 +3,64 @@ package dialer
import (
"net"
"syscall"
"github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/unix"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceIdx int) controlFn {
return func(network, address string, c syscall.RawConn) error {
func bindControl(ifaceIdx int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return nil
return
}
}
return c.Control(func(fd uintptr) {
var innerErr error
err = c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_BOUND_IF, ifaceIdx)
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6, syscall.IPV6_BOUND_IF, ifaceIdx)
innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx)
}
})
if innerErr != nil {
err = innerErr
}
return
}
}
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err
}
dialer.Control = bindControl(iface.(*net.Interface).Index)
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
iface, err, _ := ifaceSingle.Do(func() (interface{}, error) {
return net.InterfaceByName(ifaceName)
})
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err
return "", err
}
lc.Control = bindControl(iface.(*net.Interface).Index)
return nil
lc.Control = bindControl(ifaceObj.Index, lc.Control)
return address, nil
}

View File

@ -3,34 +3,49 @@ package dialer
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
type controlFn = func(network, address string, c syscall.RawConn) error
func bindControl(ifaceName string) controlFn {
return func(network, address string, c syscall.RawConn) error {
func bindControl(ifaceName string, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return nil
return
}
}
return c.Control(func(fd uintptr) {
syscall.BindToDevice(int(fd), ifaceName)
var innerErr error
err = c.Control(func(fd uintptr) {
innerErr = unix.BindToDevice(int(fd), ifaceName)
})
if innerErr != nil {
err = innerErr
}
return
}
}
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
dialer.Control = bindControl(ifaceName)
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
dialer.Control = bindControl(ifaceName, dialer.Control)
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
lc.Control = bindControl(ifaceName)
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
lc.Control = bindControl(ifaceName, lc.Control)
return nil
return address, nil
}

View File

@ -1,13 +1,47 @@
// +build !linux,!darwin
//go:build !linux && !darwin && !windows
package dialer
import "net"
import (
"net"
"strconv"
)
func bindIfaceToDialer(dialer *net.Dialer, ifaceName string) error {
return errPlatformNotSupport
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
if !destination.IsGlobalUnicast() {
return nil
}
local := uint64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
}
func bindIfaceToListenConfig(lc *net.ListenConfig, ifaceName string) error {
return errPlatformNotSupport
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
}

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

@ -8,22 +8,7 @@ import (
"github.com/Dreamacro/clash/component/resolver"
)
func Dialer() (*net.Dialer, error) {
dialer := &net.Dialer{}
if DialerHook != nil {
if err := DialerHook(dialer); err != nil {
return nil, err
}
}
return dialer, nil
}
func Dial(network, address string) (net.Conn, error) {
return DialContext(context.Background(), network, address)
}
func DialContext(ctx context.Context, network, address string) (net.Conn, error) {
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
switch network {
case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address)
@ -31,11 +16,6 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
return nil, err
}
dialer, err := Dialer()
if err != nil {
return nil, err
}
var ip net.IP
switch network {
case "tcp4", "udp4":
@ -43,38 +23,92 @@ func DialContext(ctx context.Context, network, address string) (net.Conn, error)
default:
ip, err = resolver.ResolveIPv6(host)
}
if err != nil {
return nil, err
}
if DialHook != nil {
if err := DialHook(dialer, network, ip); err != nil {
return nil, err
}
}
return dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
return dialContext(ctx, network, ip, port, options)
case "tcp", "udp":
return dualStackDialContext(ctx, network, address)
return dualStackDialContext(ctx, network, address, options)
default:
return nil, errors.New("network invalid")
}
}
func ListenPacket(network, address string) (net.PacketConn, error) {
cfg := &net.ListenConfig{}
if ListenPacketHook != nil {
var err error
address, err = ListenPacketHook(cfg, address)
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
cfg := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(cfg)
}
for _, o := range options {
o(cfg)
}
lc := &net.ListenConfig{}
if cfg.interfaceName != "" {
var (
addr string
err error
)
if cfg.fallbackBind {
addr, err = fallbackBindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
} else {
addr, err = bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
}
if err != nil {
return nil, err
}
address = addr
}
if cfg.addrReuse {
addrReuseToListenConfig(lc)
}
if cfg.routingMark != 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address)
}
return cfg.ListenPacket(context.Background(), network, address)
return lc.ListenPacket(ctx, network, address)
}
func dualStackDialContext(ctx context.Context, network, address string) (net.Conn, error) {
func dialContext(ctx context.Context, network string, destination net.IP, port string, options []Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(opt)
}
for _, o := range options {
o(opt)
}
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if opt.fallbackBind {
if err := fallbackBindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
} else {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err
}
}
}
if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}
func dualStackDialContext(ctx context.Context, network, address string, options []Option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
@ -105,12 +139,6 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
}
}()
dialer, err := Dialer()
if err != nil {
result.error = err
return
}
var ip net.IP
if ipv6 {
ip, result.error = resolver.ResolveIPv6(host)
@ -122,12 +150,7 @@ func dualStackDialContext(ctx context.Context, network, address string) (net.Con
}
result.resolved = true
if DialHook != nil {
if result.error = DialHook(dialer, network, ip); result.error != nil {
return
}
}
result.Conn, result.error = dialer.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
result.Conn, result.error = dialContext(ctx, network, ip, port, options)
}
go startRacer(ctx, network+"4", host, false)

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

@ -1,43 +0,0 @@
package dialer
import (
"errors"
"net"
)
type DialerHookFunc = func(dialer *net.Dialer) error
type DialHookFunc = func(dialer *net.Dialer, network string, ip net.IP) error
type ListenPacketHookFunc = func(lc *net.ListenConfig, address string) (string, error)
var (
DialerHook DialerHookFunc
DialHook DialHookFunc
ListenPacketHook ListenPacketHookFunc
)
var (
ErrAddrNotFound = errors.New("addr not found")
ErrNetworkNotSupport = errors.New("network not support")
)
func ListenPacketWithInterface(name string) ListenPacketHookFunc {
return func(lc *net.ListenConfig, address string) (string, error) {
err := bindIfaceToListenConfig(lc, name)
if err == errPlatformNotSupport {
address, err = fallbackBindToListenConfig(name)
}
return address, err
}
}
func DialerWithInterface(name string) DialHookFunc {
return func(dialer *net.Dialer, network string, ip net.IP) error {
err := bindIfaceToDialer(dialer, name)
if err == errPlatformNotSupport {
err = fallbackBindToDialer(dialer, network, ip, name)
}
return err
}
}

View File

@ -0,0 +1,43 @@
//go:build linux
package dialer
import (
"net"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
func bindMarkToControl(mark int, chain controlFn) controlFn {
return func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
}
var innerErr error
err = c.Control(func(fd uintptr) {
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
})
if innerErr != nil {
err = innerErr
}
return
}
}

View File

@ -0,0 +1,26 @@
//go:build !linux
package dialer
import (
"net"
"sync"
"github.com/Dreamacro/clash/log"
)
var printMarkWarnOnce sync.Once
func printMarkWarn() {
printMarkWarnOnce.Do(func() {
log.Warnln("Routing mark on socket is not supported on current platform")
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
printMarkWarn()
}

View File

@ -0,0 +1,42 @@
package dialer
import "go.uber.org/atomic"
var (
DefaultOptions []Option
DefaultInterface = atomic.NewString("")
DefaultRoutingMark = atomic.NewInt32(0)
)
type option struct {
interfaceName string
fallbackBind bool
addrReuse bool
routingMark int
}
type Option func(opt *option)
func WithInterface(name string) Option {
return func(opt *option) {
opt.interfaceName = name
}
}
func WithFallbackBind(fallback bool) Option {
return func(opt *option) {
opt.fallbackBind = fallback
}
}
func WithAddrReuse(reuse bool) Option {
return func(opt *option) {
opt.addrReuse = reuse
}
}
func WithRoutingMark(mark int) Option {
return func(opt *option) {
opt.routingMark = mark
}
}

View File

@ -0,0 +1,9 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
package dialer
import (
"net"
)
func addrReuseToListenConfig(*net.ListenConfig) {}

View File

@ -0,0 +1,27 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
}
}

View File

@ -0,0 +1,24 @@
package dialer
import (
"net"
"syscall"
"golang.org/x/sys/windows"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
chain := lc.Control
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
return c.Control(func(fd uintptr) {
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
}
}

View File

@ -0,0 +1,55 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
type cachefileStore struct {
cache *cachefile.CacheFile
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
if elm == nil {
return "", false
}
return string(elm), true
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip net.IP) {
ip = ip.To4()
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
_, exist := c.GetByIP(ip)
return exist
}
// CloneTo implements store.CloneTo
// already persistence
func (c *cachefileStore) CloneTo(store store) {}

View File

@ -0,0 +1,69 @@
package fakeip
import (
"net"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
return ip, true
}
return nil, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
m.cache.Get(host)
return host, true
}
return "", false
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
}
// DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) {
ipNum := ipToUint(ip.To4())
if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
}
m.cache.Delete(ipNum)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
}
}

View File

@ -3,13 +3,25 @@ package fakeip
import (
"errors"
"net"
"strings"
"sync"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
)
// Pool is a implementation about fake ip generator without storage
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
DelByIP(ip net.IP)
Exist(ip net.IP) bool
CloneTo(store)
}
// Pool is an implementation about fake ip generator without storage
type Pool struct {
max uint32
min uint32
@ -18,25 +30,22 @@ type Pool struct {
mux sync.Mutex
host *trie.DomainTrie
ipnet *net.IPNet
cache *cache.LruCache
store store
}
// Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP {
p.mux.Lock()
defer p.mux.Unlock()
if elm, exist := p.cache.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list
n := ipToUint(ip.To4())
offset := n - p.min + 1
p.cache.Get(offset)
// RFC4343: DNS Case Insensitive, we SHOULD return result with all cases.
host = strings.ToLower(host)
if ip, exist := p.store.GetByHost(host); exist {
return ip
}
ip := p.get(host)
p.cache.Set(host, ip)
p.store.PutByHost(host, ip)
return ip
}
@ -49,22 +58,11 @@ func (p *Pool) LookBack(ip net.IP) (string, bool) {
return "", false
}
n := ipToUint(ip.To4())
offset := n - p.min + 1
if elm, exist := p.cache.Get(offset); exist {
host := elm.(string)
// ensure host --> ip on head of linked list
p.cache.Get(host)
return host, true
}
return "", false
return p.store.GetByIP(ip)
}
// LookupHost return if domain in host
func (p *Pool) LookupHost(domain string) bool {
// ShouldSkipped return if domain should be skipped
func (p *Pool) ShouldSkipped(domain string) bool {
if p.host == nil {
return false
}
@ -80,9 +78,7 @@ func (p *Pool) Exist(ip net.IP) bool {
return false
}
n := ipToUint(ip.To4())
offset := n - p.min + 1
return p.cache.Exist(offset)
return p.store.Exist(ip)
}
// Gateway return gateway ip
@ -95,26 +91,30 @@ func (p *Pool) IPNet() *net.IPNet {
return p.ipnet
}
// PatchFrom clone cache from old pool
func (p *Pool) PatchFrom(o *Pool) {
o.cache.CloneTo(p.cache)
// CloneFrom clone cache from old pool
func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store)
}
func (p *Pool) get(host string) net.IP {
current := p.offset
for {
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
ip := uintToIP(p.min + p.offset)
if !p.store.Exist(ip) {
break
}
if !p.cache.Exist(p.offset) {
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset)
p.store.DelByIP(ip)
break
}
}
ip := uintToIP(p.min + p.offset - 1)
p.cache.Set(p.offset, host)
ip := uintToIP(p.min + p.offset)
p.store.PutByIP(ip, host)
return ip
}
@ -130,11 +130,24 @@ func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
}
// New return Pool instance
func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
min := ipToUint(ipnet.IP) + 2
type Options struct {
IPNet *net.IPNet
Host *trie.DomainTrie
ones, bits := ipnet.Mask.Size()
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
Size int
// Persistence will save the data to disk.
// Size will not work and record will be fully stored.
Persistence bool
}
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 2
ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 2
if total <= 0 {
@ -142,12 +155,22 @@ func New(ipnet *net.IPNet, size int, host *trie.DomainTrie) (*Pool, error) {
}
max := min + uint32(total) - 1
return &Pool{
pool := &Pool{
min: min,
max: max,
gateway: min - 1,
host: host,
ipnet: ipnet,
cache: cache.NewLRUCache(cache.WithSize(size * 2)),
}, nil
host: options.Host,
ipnet: options.IPNet,
}
if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.New(cache.WithSize(options.Size * 2)),
}
}
return pool, nil
}

View File

@ -2,38 +2,143 @@ package fakeip
import (
"net"
"os"
"testing"
"time"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
"github.com/stretchr/testify/assert"
"go.etcd.io/bbolt"
)
func createPools(options Options) ([]*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
filePool, tempfile, err := createCachefileStore(options)
if err != nil {
return nil, "", err
}
return []*Pool{pool, filePool}, tempfile, nil
}
func createCachefileStore(options Options) (*Pool, string, error) {
pool, err := New(options)
if err != nil {
return nil, "", err
}
f, err := os.CreateTemp("", "clash")
if err != nil {
return nil, "", err
}
db, err := bbolt.Open(f.Name(), 0o666, &bbolt.Options{Timeout: time.Second})
if err != nil {
return nil, "", err
}
pool.store = &cachefileStore{
cache: &cachefile.CacheFile{DB: db},
}
return pool, f.Name(), nil
}
func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pool, _ := New(ipnet, 10, nil)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
for _, pool := range pools {
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 2})
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 3}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
}
}
func TestPool_Cycle(t *testing.T) {
func TestPool_Case_Insensitive(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/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) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("2.com"))
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
assert.Equal(t, net.IP{192, 168, 0, 4}, pool.Lookup("4.com"))
assert.Equal(t, net.IP{192, 168, 0, 5}, pool.Lookup("5.com"))
assert.Equal(t, net.IP{192, 168, 0, 6}, pool.Lookup("6.com"))
assert.Equal(t, net.IP{192, 168, 0, 2}, pool.Lookup("12.com"))
assert.Equal(t, net.IP{192, 168, 0, 3}, pool.Lookup("3.com"))
}
}
func TestPool_Skip(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/30")
pool, _ := New(ipnet, 10, nil)
tree := trie.New()
tree.Insert("example.com", tree)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: tree,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
first := pool.Lookup("foo.com")
same := pool.Lookup("baz.com")
assert.True(t, first.Equal(same))
for _, pool := range pools {
assert.True(t, pool.ShouldSkipped("example.com"))
assert.False(t, pool.ShouldSkipped("foo.com"))
}
}
func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil)
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
pool.Lookup("bar.com")
@ -45,7 +150,10 @@ func TestPool_MaxCacheSize(t *testing.T) {
func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(ipnet, 2, nil)
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
// fill cache
fooIP := pool.Lookup("foo.com")
@ -70,9 +178,35 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazIP.Equal(newBazIP))
}
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 2}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 3}))
newPool, _ := New(Options{
IPNet: ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
_, firstExist := newPool.LookBack(first)
_, lastExist := newPool.LookBack(last)
assert.True(t, firstExist)
assert.True(t, lastExist)
}
func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
_, err := New(ipnet, 10, nil)
_, err := New(Options{
IPNet: ipnet,
Size: 10,
})
assert.Error(t, err)
}

115
component/iface/iface.go Normal file
View File

@ -0,0 +1,115 @@
package iface
import (
"errors"
"net"
"time"
"github.com/Dreamacro/clash/common/singledo"
)
type Interface struct {
Index int
Name string
Addrs []*net.IPNet
HardwareAddr net.HardwareAddr
}
var (
ErrIfaceNotFound = errors.New("interface not found")
ErrAddrNotFound = errors.New("addr not found")
)
var interfaces = singledo.NewSingle(time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (any, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
r := map[string]*Interface{}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
ipNets := make([]*net.IPNet, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
if v4 := ipNet.IP.To4(); v4 != nil {
ipNet.IP = v4
}
ipNets = append(ipNets, ipNet)
}
r[iface.Name] = &Interface{
Index: iface.Index,
Name: iface.Name,
Addrs: ipNets,
HardwareAddr: iface.HardwareAddr,
}
}
return r, nil
})
if err != nil {
return nil, err
}
ifaces := value.(map[string]*Interface)
iface, ok := ifaces[name]
if !ok {
return nil, ErrIfaceNotFound
}
return iface, nil
}
func FlushCache() {
interfaces.Reset()
}
func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() != nil
})
}
func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() == nil
})
}
func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) {
var fallback *net.IPNet
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}
if fallback == nil && !addr.IP.IsLinkLocalUnicast() {
fallback = addr
if destination == nil {
break
}
}
if destination != nil && addr.Contains(destination) {
return addr, nil
}
}
if fallback == nil {
return nil, ErrAddrNotFound
}
return fallback, nil
}

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
}

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