Compare commits

..

194 Commits

Author SHA1 Message Date
ffaa40c120 Merge branch 'Alpha' into Meta 2023-09-25 19:32:51 +08:00
fdd327d58d fix: fail to set KeepAliveIntervall #715 2023-09-25 14:05:13 +08:00
0dfe696300 chore: ntp service support dialer-proxy 2023-09-25 09:11:35 +08:00
c0ba798708 chore: share N.dialer code 2023-09-25 09:11:35 +08:00
67d7e53f7a feat: recovering preHandleMetadata failure from sniffing (#769) 2023-09-24 19:27:55 +08:00
e6366f7442 chore: fix typo 2023-09-24 19:00:51 +08:00
89d9cb0539 Merge pull request #767 from PuerNya/fix-delay
chore: handle provider proxies  in proxies api
2023-09-24 15:55:33 +08:00
0d300a3540 chore: handle provider proxies in proxies api 2023-09-24 15:39:14 +08:00
7c59916c22 chore: update provider proxies api 2023-09-24 00:19:10 +08:00
8f515ecc05 chore: updateUI API return 501 when config incomplete 2023-09-23 18:00:07 +08:00
34f62a0919 feat: add provider proxies api 2023-09-23 17:54:20 +08:00
0207a7ac96 chore: resolver read system hosts file 2023-09-23 14:01:18 +08:00
bf619d8586 fix: socks5 udp not working on loopback 2023-09-22 23:33:24 +08:00
d48f9c2a6c chore: rebuild ca parsing 2023-09-22 14:45:34 +08:00
90a5aa609a fix: uot read failed 2023-09-22 00:11:57 +08:00
4fe7a463c5 chore: limit tuicv5's maxUdpRelayPacketSize up to 1200-PacketOverHead 2023-09-21 23:49:45 +08:00
7f49c91267 fix: hy2 udp not working 2023-09-21 23:36:40 +08:00
f6bf9c0857 feat: converter support hysteria2 2023-09-21 17:25:15 +08:00
da24810da2 chore: support set cwnd for hy2 too 2023-09-21 16:41:31 +08:00
ee3213c28f fix: tuicv5 panic in ReadFrom 2023-09-21 15:10:35 +08:00
233eeb0b38 feat: inbound support Hysteria2 2023-09-21 15:10:35 +08:00
6c3b973748 doc: add Hysteria2 doc 2023-09-21 10:43:45 +08:00
9b8e2d9343 feat: support Hysteria2 2023-09-21 10:28:28 +08:00
24fd577767 chore: Update dependencies 2023-09-21 08:57:38 +08:00
42b85de83e chore: Restore go1.20 support 2023-09-21 08:29:28 +08:00
62266010ac Revert "migration: go 1.21"
This reverts commit 33d41338ef.
2023-09-21 08:29:28 +08:00
0d7a57fa9d Chore: update github issue template 2023-09-21 03:40:46 +08:00
f909b3c0dc chore: Update android-ndk 2023-09-20 15:26:36 +08:00
8b518161a3 chore: update external-ui 2023-09-20 14:23:58 +08:00
20fafdca65 chore: cleanup code 2023-09-18 19:42:08 +08:00
fd96efd456 chore: ignore PR when Pre-releasing 2023-09-18 19:36:11 +08:00
7c21768e99 feat: update external-ui 2023-09-18 19:21:30 +08:00
6a5a94f48f chore: DNS cache policy follow upstream 2023-09-17 17:18:35 +08:00
33d41338ef migration: go 1.21 2023-09-17 17:05:13 +08:00
2d3b9364bf fix: caceh dns result 2023-09-16 12:30:11 +08:00
fa49fd7ba2 chore: use cmp in go 1.21
Co-authored-by: H1JK <hell0jack@protonmail.com>
2023-09-16 12:06:58 +08:00
c3d72f6883 feat: download/upgrade XD to external-ui 2023-09-16 11:44:15 +08:00
af99b52527 docs(README): update dashboard section 2023-09-09 13:06:49 +08:00
f241e1f81a chore: Update dependencies 2023-09-09 09:53:14 +08:00
90acce7fa1 feat: Add disable quic-go GSO to experimental 2023-09-08 22:58:59 +08:00
7286391883 feat: support users to customize download ua 2023-09-07 18:44:58 +08:00
a1eab125ee fix: ntp service panic 2023-09-04 18:35:06 +08:00
1d4af2d92b chore: TCPKeepAlive interval set to 15s by default 2023-09-03 20:42:54 +08:00
d6cf2a837f chore: ntp service dep with sing, optional synchronize system time 2023-09-03 17:49:56 +08:00
d6b80acfbc chore: Use xsync provided map size calculation 2023-09-02 20:17:43 +08:00
1cad615b25 chore: using xsync.MapOf replace sync.Map 2023-09-02 16:54:48 +08:00
73fa79bf3f feat: configurable TCPKeepAlive interval 2023-09-02 16:45:16 +08:00
d79c13064e chore: cleanup codes 2023-09-02 14:12:53 +08:00
427a377c2a refactor: Decouple .Cleanup from ReCreateTun
The listener.Cleanup method will be called during
executor.Shutdown and route.restart, so it should serve
all kinds of listeners rather than a single tun device.

Currently listener.ReCreateTun will call it to handle
some internal affairs, This should be decoupled.

In this way, the cleanup tasks for data outside the
process life cycle that other listeners will add here
in the future will not be accidentally triggered
by configuring tun.
2023-09-02 14:12:53 +08:00
9feb4d6668 fix: RESTful api missing TunConf.device
In commit 54fee7b, due to failure to take into account that
not all required parameters of `sing_tun.server.New` have
default values provided by `LC.Tun`, the name of the tun device
cannot be obtained when `TunConf.device` is not explicitly
configured. This commit fixed the issue.
2023-09-02 14:12:53 +08:00
a366e9a4b5 fix: ntp service panic 2023-09-02 12:37:43 +08:00
cbdf33c42c feat: ntp service 2023-09-02 02:15:46 +08:00
9ceaf20584 fix: concurrent map writes #707 2023-09-01 10:43:04 +08:00
54fee7bd3a Improve: nicer tun info for RESTful api
Let the restful api still get TunConf even when tun is off.
Otherwise the api will return the default values,
instead of the values that actually take effect after enable.

* Due to this problem, yacd changes the displayed value
back to gvisor immediately after the user selects tun stack.
2023-08-30 21:13:32 +08:00
414d8f2162 chore: use WaitGroup in dualStackDialContext 2023-08-30 17:28:36 +08:00
86cf1dd54b fix: dualStack confusing error on ipv4 failed connect 2023-08-30 17:28:36 +08:00
d099375200 chore: rename func name 2023-08-30 15:52:41 +08:00
9536372cfb fix: call shutdown before restart (#709) 2023-08-30 15:49:28 +08:00
630a17cf90 chore: cleanup codes 2023-08-26 21:20:20 +08:00
0a7b7894bd feat: proxies support direct type 2023-08-24 23:33:03 +08:00
3a9fc39cd9 chore: update quic-go to 0.38.0 2023-08-21 16:18:56 +08:00
1181fd4560 feat: add udp-over-stream for tuic
only work with meta tuic server or sing-box 1.4.0-beta.6
2023-08-21 12:37:39 +08:00
b8a60261ef chore: restore unselected
clear selected node in outboundgoup/URLtest when getGroupDelay triggered
2023-08-18 22:17:07 +08:00
db68d55a0e fix: sing-vmess panic 2023-08-17 22:33:07 +08:00
574efb4526 chore: Update dependencies 2023-08-16 21:30:12 +08:00
03b0252589 feat: bump restls to v0.1.6 (utls v1.4.3) (#692)
* feat: bump restls to v0.1.5 (utls v1.4.3)
* fix: rm dependency go-quic
2023-08-16 11:41:58 +08:00
ed09df4e13 fix: TLS ALPN support 2023-08-14 15:48:13 +08:00
f89ecd97d6 feat: Converter unofficial TUIC share link support 2023-08-14 15:11:33 +08:00
65071ea7d1 Merge branch 'Alpha' into Meta 2023-08-13 23:00:41 +08:00
3093fc4f33 chore: update go1.21.0 release 2023-08-09 17:26:24 +08:00
984fca4726 feat: add inbound-mptcp for listeners 2023-08-09 17:09:03 +08:00
cc42d787d4 feat: add mptcp for all proxy 2023-08-09 16:57:39 +08:00
e2e0fd4eba chore: using uint16 for ports in Metadata 2023-08-09 13:51:02 +08:00
bad9f2e6dc fix geodata-mode 2023-08-07 01:43:23 +08:00
68bf6f16ac refactor: Geodata initialization 2023-08-06 23:34:10 +08:00
cca701c641 chore: Update dependencies 2023-08-06 18:38:50 +08:00
09ec7c8a62 chore: update quic-go to 0.37.3 2023-08-06 09:45:51 +08:00
68f312288d chore: update quic-go to 0.37.2 and go1.21rc4 2023-08-05 12:53:49 +08:00
191243a1d2 chore: better tuicV5 deFragger 2023-08-03 23:07:30 +08:00
b0fed73236 Fix: mapping dns should not stale (#675)
* Fix: mapping dns should not stale

* Update enhancer.go
2023-08-01 17:30:57 +08:00
f125e1ce9e chore: Update dependencies 2023-08-01 13:54:22 +08:00
e2216b7824 chore: update quic-go to 0.37.1 2023-08-01 09:55:55 +08:00
7632827177 chore: Use Meta-geoip for default 2023-07-20 23:24:48 +08:00
b0e76ec791 feat: Add Meta-geoip V0 database support 2023-07-17 10:33:20 +08:00
a82745f544 chore: Remove legacy XTLS support (#645)
* chore: Remove legacy XTLS support

* chore: Rename function
2023-07-16 23:26:07 +08:00
cbb8ef5dfe fix: discard http unsuccessful status 2023-07-16 11:43:55 +08:00
a181e35865 chore: structure support decode pointer 2023-07-16 11:11:30 +08:00
014537e1ea fix: discard http unsuccessful status 2023-07-16 11:10:07 +08:00
9b50f56e7c fix: tunnel's handleUDPToLocal panic 2023-07-16 10:35:10 +08:00
9cbca162a0 feat: tuic outbound allow set an empty ALPN array 2023-07-16 10:29:43 +08:00
f73f32e41c fix: parse nested sub-rules failed 2023-07-16 10:15:43 +08:00
cfc30753af chore: Update go1.21rc3 2023-07-15 16:52:44 +08:00
081e94c738 feat: Add sing-geoip database support 2023-07-14 22:28:24 +08:00
5dd57bab67 chore: Update dependencies 2023-07-14 11:37:15 +08:00
492a731ec1 fix: DNS cache 2023-07-14 09:55:43 +08:00
0b1aff5759 chore: Update dependencies 2023-07-02 10:41:02 +08:00
8f1475d5d0 chore: update to go1.21rc2, drop support for go1.19 2023-07-02 09:59:18 +08:00
c6b84b0f20 chore: update quic-go to 0.36.1 2023-07-02 09:05:16 +08:00
02ba78ab90 chore: change geodata download url to fastly.jsdelivr.net (#636) 2023-06-30 18:52:39 +08:00
57db8dfe23 Chore: Something update from clash (#639)
Chore: add alive for proxy api
Improve: alloc using make if alloc size > 65536
2023-06-30 17:36:43 +08:00
8e16738465 chore: better env parsing 2023-06-29 16:40:08 +08:00
fa94403629 chore: better resolv.conf parsing 2023-06-29 14:25:25 +08:00
1beb2919e7 fix: panic when add 4in6 ipcidr 2023-06-29 14:25:25 +08:00
75c5d0482e chore: better close single connection in restful api 2023-06-29 14:25:25 +08:00
55bcabdf46 chore: statistic's Snapshot only contains TrackerInfo 2023-06-29 14:25:25 +08:00
abf80601e1 chore: avoid unneeded map copy when close connection in restful api 2023-06-29 14:25:25 +08:00
26f97b45d6 chore: update quic-go to 0.36.0 2023-06-29 14:25:25 +08:00
29315ce8e5 fix: tuic server cwnd parsing 2023-06-29 14:25:25 +08:00
65f84d21ea chore: tuic server can handle V4 and V5 in same port 2023-06-29 14:25:25 +08:00
e3ac58bc51 chore: fix TUIC cwnd parsing 2023-06-29 14:25:25 +08:00
c66438d794 Revert "chore: Refine adapter type name"
This reverts commit 61734e5cac.
2023-06-29 14:25:25 +08:00
411e587460 chore: function rename 2023-06-29 14:25:25 +08:00
6cc9c68458 chore: Update dependencies 2023-06-29 14:25:25 +08:00
d1c858d7ff update docs 2023-06-29 14:25:25 +08:00
3eef1ee064 chore: adjustable cwnd for cc in quic 2023-06-29 14:25:25 +08:00
514d374b8c chore: Refine adapter type name 2023-06-29 14:25:25 +08:00
a2334430c1 feat: optional provider path (#624) 2023-06-29 14:25:25 +08:00
c8a3d6edd9 Add REALITY ChaCha20-Poly1305 auth mode support 2023-06-29 14:25:25 +08:00
bda2ca3c13 fix: singmux return wrong supportUDP value 2023-06-29 14:25:25 +08:00
f4b734c74c fix: tuicV5's heartbeat should be a datagram packet 2023-06-29 14:25:25 +08:00
c2cdf43239 fix: dns concurrent not work 2023-06-29 14:25:25 +08:00
b939c81d3e feat: support tuicV5 2023-06-29 14:25:25 +08:00
0e92496eeb chore: Update dependencies 2023-06-29 14:25:25 +08:00
ea482598e0 chore: Disable cache for RCode client 2023-06-29 14:25:25 +08:00
16f3567ddc feat: Add RCode DNS client 2023-06-29 14:25:25 +08:00
73f8da091e chore: allow unsafe path for provider by environment variable 2023-06-29 14:25:25 +08:00
6bdaadc581 chore: Replace murmur3 with maphash 2023-06-29 14:25:24 +08:00
73a2cf593e chore: reduce process lookup attempts when process not exist #613 2023-06-29 14:25:24 +08:00
665bfcab2d fix: Disable XUDP global ID if source address invalid 2023-06-29 14:25:24 +08:00
8be860472a chore: init gopacket only when dial fake-tcp to decrease memory using 2023-06-29 14:25:24 +08:00
1ec74f13f7 feat: Add XUDP migration support 2023-06-29 14:25:24 +08:00
564b834e00 fix: Resolve delay omission in the presence of nested proxy-groups 2023-06-29 14:25:24 +08:00
da04e00767 When testing the delay through REST API, determine whether to store the delay data based on certain conditions instead of discarding it directly (#609) 2023-06-29 14:25:24 +08:00
e0faffbfbd fix: go1.19 compile 2023-06-29 14:25:24 +08:00
a0c7641ad5 chore: Something update from clash :) (#606) 2023-06-29 14:25:24 +08:00
1f592c43de fix: nil pointer in urltest (#603) 2023-06-29 14:25:24 +08:00
4d7350923c fix hysteria faketcp lookback in TUN mode (#601) 2023-06-29 14:25:24 +08:00
76a7945994 chore: Ignore PR in Docker build 2023-06-29 14:25:24 +08:00
a2bbd1cc8d ProxyProvider health check also supports specifying expected status (#600)
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-29 14:25:24 +08:00
4ec66d299a [Feature] Proxy stores delay data of different URLs. And supports specifying different test URLs and expected statue by group (#588)
Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com>
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-29 14:25:24 +08:00
4e46cbfbde fix: hysteria faketcp loopback in tun mode 2023-06-29 14:25:24 +08:00
1a44dcee55 chore: update proxy's udpConn when received a new packet 2023-06-29 14:25:24 +08:00
6c7d1657a5 chore: update quic-go to 0.35.1 2023-06-29 14:25:24 +08:00
38e210a851 chore: Update dependencies 2023-06-29 14:25:24 +08:00
359ee70daa chore: update quic-go to 0.34.0 2023-06-29 14:25:24 +08:00
db6b2b7702 chore: better resolv.conf parsing 2023-06-28 09:17:54 +08:00
603d0809b4 fix: panic when add 4in6 ipcidr 2023-06-26 21:04:54 +08:00
614cc93cac chore: better close single connection in restful api 2023-06-26 18:25:36 +08:00
1cb75350e2 chore: statistic's Snapshot only contains TrackerInfo 2023-06-26 18:13:17 +08:00
42ef4fedfa chore: avoid unneeded map copy when close connection in restful api 2023-06-26 17:46:14 +08:00
2284acce94 chore: update quic-go to 0.36.0 2023-06-26 12:08:38 +08:00
919daf0dbb fix: tuic server cwnd parsing 2023-06-21 14:00:49 +08:00
6d824c8745 chore: tuic server can handle V4 and V5 in same port 2023-06-21 13:53:37 +08:00
1d94546902 chore: fix TUIC cwnd parsing 2023-06-21 00:47:05 +08:00
ad7508f203 Revert "chore: Refine adapter type name"
This reverts commit 61734e5cac.
2023-06-19 14:28:06 +08:00
d391fda051 chore: function rename 2023-06-19 08:32:11 +08:00
fe0f2d9ef9 chore: Update dependencies 2023-06-19 08:23:48 +08:00
b9110c164d update docs 2023-06-18 01:50:32 +08:00
6c8631d5cc chore: adjustable cwnd for cc in quic 2023-06-18 00:47:26 +08:00
61734e5cac chore: Refine adapter type name 2023-06-17 00:05:03 +08:00
77fb9a9c01 feat: optional provider path (#624) 2023-06-15 22:45:02 +08:00
af28b99b2a Add REALITY ChaCha20-Poly1305 auth mode support 2023-06-14 17:17:46 +08:00
4f79bb7931 fix: singmux return wrong supportUDP value 2023-06-14 15:51:13 +08:00
644abcf071 fix: tuicV5's heartbeat should be a datagram packet 2023-06-13 17:50:10 +08:00
183f2d974c fix: dns concurrent not work 2023-06-12 18:42:46 +08:00
e914317bef feat: support tuicV5 2023-06-12 18:42:46 +08:00
5e20fedf5f chore: Update dependencies 2023-06-11 23:57:25 +08:00
54337ecdf3 chore: Disable cache for RCode client 2023-06-11 23:01:51 +08:00
c7de0e0253 feat: Add RCode DNS client 2023-06-11 23:01:45 +08:00
b72219c06a chore: allow unsafe path for provider by environment variable 2023-06-11 01:55:49 +00:00
64b23257db chore: Replace murmur3 with maphash 2023-06-10 17:35:19 +08:00
8d1251f128 chore: generate release note automatically 2023-06-09 12:59:59 +00:00
c57f17d094 chore: reduce process lookup attempts when process not exist #613 2023-06-08 18:07:56 +08:00
cd44901e90 fix: Disable XUDP global ID if source address invalid 2023-06-08 15:57:51 +08:00
766d08a8eb chore: init gopacket only when dial fake-tcp to decrease memory using 2023-06-08 11:58:51 +08:00
c3ef05b257 feat: Add XUDP migration support 2023-06-07 23:03:36 +08:00
093453582f fix: Resolve delay omission in the presence of nested proxy-groups 2023-06-07 13:20:45 +08:00
767aa182b9 When testing the delay through REST API, determine whether to store the delay data based on certain conditions instead of discarding it directly (#609) 2023-06-07 11:04:03 +08:00
ad11a2b813 fix: go1.19 compile 2023-06-06 10:47:50 +08:00
dafecebdc0 chore: Something update from clash :) (#606) 2023-06-06 09:45:05 +08:00
e7174866e5 fix: nil pointer in urltest (#603) 2023-06-05 12:40:46 +08:00
fdaa6a22a4 fix hysteria faketcp lookback in TUN mode (#601) 2023-06-04 23:43:54 +08:00
fd0c71a485 chore: Ignore PR in Docker build 2023-06-04 15:51:25 +08:00
3c1f9a9953 ProxyProvider health check also supports specifying expected status (#600)
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-04 14:00:24 +08:00
3ef81afc76 [Feature] Proxy stores delay data of different URLs. And supports specifying different test URLs and expected statue by group (#588)
Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com>
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-06-04 11:51:30 +08:00
03d0c8620e fix: hysteria faketcp loopback in tun mode 2023-06-03 22:15:09 +08:00
63b5387164 chore: update proxy's udpConn when received a new packet 2023-06-03 21:40:09 +08:00
fb6a032872 chore: genReleaseNote support verrsion range
This commit updates the release script to handle the input version range provided via command line options. Now, you can specify the version range using the '-v' option, such as '-v v1.14.1...v1.14.2', and the script will generate the release notes based on the specified range.

The script parses the command line options, extracts the version range, and uses it in the 'git log' commands to capture the relevant commits. The output is then organized into different sections, including 'What's Changed', 'BUG & Fix', and 'Maintenance', along with the full changelog link.

This enhancement improves the flexibility and usability of the release script, allowing users to generate release notes for specific version ranges easily.

Co-authored-by: ChatGPT
2023-06-03 10:25:00 +00:00
2af758e5f1 chore: Random only if the certificate and private-key are empty 2023-06-03 17:45:47 +08:00
2c44b4e170 chore: update quic-go to 0.35.1 2023-06-03 16:45:35 +08:00
7906fbfee6 chore: Update dependencies 2023-06-03 00:24:51 +08:00
17565ec93b chore: Reject packet conn implement wait read 2023-06-02 22:58:33 +08:00
26acaee424 fix: handle manually select in url-test 2023-06-02 18:26:51 +08:00
9b6e56a65e chore: update quic-go to 0.34.0 2023-06-01 16:25:02 +08:00
202 changed files with 6674 additions and 3191 deletions

View File

@ -1 +1,32 @@
git log --pretty=format:"* %s by @%an" v1.14.x..v1.14.y | sort -f | uniq > release.md
#!/bin/bash
while getopts "v:" opt; do
case $opt in
v)
version_range=$OPTARG
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
if [ -z "$version_range" ]; then
echo "Please provide the version range using -v option. Example: ./genReleashNote.sh -v v1.14.1...v1.14.2"
exit 1
fi
echo "## What's Changed" > release.md
git log --pretty=format:"* %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md
echo "" >> release.md
echo "## BUG & Fix" >> release.md
git log --pretty=format:"* %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md
echo "" >> release.md
echo "## Maintenance" >> release.md
git log --pretty=format:"* %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md
echo "" >> release.md
echo "**Full Changelog**: https://github.com/MetaCubeX/Clash.Meta/compare/$version_range" >> release.md

View File

@ -94,11 +94,6 @@ jobs:
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Beta'}}
run: echo "VERSION=beta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Meta'}}
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
@ -128,7 +123,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.20"
go-version: "1.21"
check-latest: true
- name: Test
@ -147,7 +142,7 @@ jobs:
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
id: setup-ndk
with:
ndk-version: r25b
ndk-version: r26
add-to-path: false
local-cache: true
@ -209,7 +204,7 @@ jobs:
Upload-Prerelease:
permissions: write-all
if: ${{ github.ref_type=='branch' }}
if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }}
needs: [Build]
runs-on: ubuntu-latest
steps:
@ -267,6 +262,23 @@ jobs:
needs: [Build]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get tags
run: |
echo "CURRENTVERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
git fetch --tags
echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_ENV
- name: Generate release notes
run: |
cp ./.github/genReleaseNote.sh ./
bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION}
rm ./genReleaseNote.sh
- uses: actions/download-artifact@v3
with:
name: artifact
@ -283,8 +295,10 @@ jobs:
tag_name: ${{ github.ref_name }}
files: bin/*
generate_release_notes: true
body_path: release.md
Docker:
if: ${{ github.event_name != 'pull_request' }}
permissions: write-all
needs: [Build]
runs-on: ubuntu-latest

View File

@ -4,9 +4,9 @@ RUN echo "I'm building for $TARGETPLATFORM"
RUN apk add --no-cache gzip && \
mkdir /clash-config && \
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O /clash-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \
wget -O /clash-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \
wget -O /clash-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat
COPY docker/file-name.sh /clash/file-name.sh
WORKDIR /clash

View File

@ -31,6 +31,8 @@ PLATFORM_LIST = \
linux-mips-hardfloat \
linux-mipsle-softfloat \
linux-mipsle-hardfloat \
linux-riscv64 \
linux-loong64 \
android-arm64 \
freebsd-386 \
freebsd-amd64 \
@ -103,6 +105,9 @@ linux-mips64le:
linux-riscv64:
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-loong64:
GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
android-arm64:
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@

View File

@ -24,13 +24,22 @@
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
- Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- 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 hard-coding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller
## Dashboard
We made an official web dashboard providing first class support for this project, check it out
at [metacubexd](https://github.com/MetaCubeX/metacubexd)
## Wiki
Configuration examples can be found at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml), while documentation can be found [Clash.Meta Wiki](https://clash-meta.wiki).
Configuration examples can be found
at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml), while documentation can be
found [Clash.Meta Wiki](https://clash-meta.wiki).
## Build
@ -43,7 +52,7 @@ git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download
```
If you can't visit github,you should set proxy first:
If you can't visit GitHub, you should set proxy first:
```shell
go env -w GOPROXY=https://goproxy.io,direct
@ -324,36 +333,27 @@ ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
WantedBy=multi-user.target
```
Launch clashd on system startup with:
Launch clash-meta daemon on system startup with:
```shell
$ systemctl enable Clash-Meta
```
Launch clashd immediately with:
Launch clash-meta daemon immediately with:
```shell
$ systemctl start Clash-Meta
```
### Display Process name
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name in GUI please use [Razord-meta](https://github.com/MetaCubeX/Razord-meta).
### Dashboard
We also made a custom fork of yacd provide better support for this project, check it out at [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta)
## Development
If you want to build an application that uses clash as a library, check out the
If you want to build an application that uses clash as a library, check out
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Debugging
Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug API.
Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug
API.
## Credits

View File

@ -3,25 +3,42 @@ package adapter
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/puzpuzpuz/xsync/v2"
)
var UnifiedDelay = atomic.NewBool(false)
const (
defaultHistoriesNum = 10
)
type extraProxyState struct {
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
}
type Proxy struct {
C.ProxyAdapter
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
url string
extra *xsync.MapOf[string, *extraProxyState]
}
// Alive implements C.Proxy
@ -29,6 +46,15 @@ func (p *Proxy) Alive() bool {
return p.alive.Load()
}
// AliveForTestUrl implements C.Proxy
func (p *Proxy) AliveForTestUrl(url string) bool {
if state, ok := p.extra.Load(url); ok {
return state.alive.Load()
}
return p.alive.Load()
}
// Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
@ -62,9 +88,51 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
// DelayHistoryForTestUrl implements C.Proxy
func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
var queueM []C.DelayHistory
if state, ok := p.extra.Load(url); ok {
queueM = state.history.Copy()
}
if queueM == nil {
queueM = p.history.Copy()
}
histories := []C.DelayHistory{}
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
func (p *Proxy) ExtraDelayHistory() map[string][]C.DelayHistory {
extraHistory := map[string][]C.DelayHistory{}
p.extra.Range(func(k string, v *extraProxyState) bool {
testUrl := k
state := v
histories := []C.DelayHistory{}
queueM := state.history.Copy()
for _, item := range queueM {
histories = append(histories, item)
}
extraHistory[testUrl] = histories
return true
})
return extraHistory
}
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
// implements C.Proxy
func (p *Proxy) LastDelay() (delay uint16) {
@ -80,6 +148,28 @@ func (p *Proxy) LastDelay() (delay uint16) {
return history.Delay
}
// LastDelayForTestUrl implements C.Proxy
func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
var max uint16 = 0xffff
alive := p.alive.Load()
history := p.history.Last()
if state, ok := p.extra.Load(url); ok {
alive = state.alive.Load()
history = state.history.Last()
}
if !alive {
return max
}
if history.Delay == 0 {
return max
}
return history.Delay
}
// MarshalJSON implements C.ProxyAdapter
func (p *Proxy) MarshalJSON() ([]byte, error) {
inner, err := p.ProxyAdapter.MarshalJSON()
@ -90,6 +180,8 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping := map[string]any{}
_ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["extra"] = p.ExtraDelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP()
@ -99,16 +191,53 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
// 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, expectedStatus utils.IntRanges[uint16], store C.DelayHistoryStoreType) (t uint16, err error) {
defer func() {
p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > 10 {
p.history.Pop()
alive := err == nil
store = p.determineFinalStoreType(store, url)
switch store {
case C.OriginalHistory:
p.alive.Store(alive)
record := C.DelayHistory{Time: time.Now()}
if alive {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum {
p.history.Pop()
}
// test URL configured by the proxy provider
if len(p.url) == 0 {
p.url = url
}
case C.ExtraHistory:
record := C.DelayHistory{Time: time.Now()}
if alive {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum {
p.history.Pop()
}
state, ok := p.extra.Load(url)
if !ok {
state = &extraProxyState{
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
}
p.extra.Store(url, state)
}
state.alive.Store(alive)
state.history.Put(record)
if state.history.Len() > defaultHistoriesNum {
state.history.Pop()
}
default:
log.Debugln("health check result will be discarded, url: %s alive: %t, delay: %d", url, alive, t)
}
}()
@ -172,12 +301,22 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
}
}
if expectedStatus != nil && !expectedStatus.Check(uint16(resp.StatusCode)) {
// maybe another value should be returned for differentiation
err = errors.New("response status is inconsistent with the expected status")
}
t = uint16(time.Since(start) / time.Millisecond)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
return &Proxy{
ProxyAdapter: adapter,
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
url: "",
extra: xsync.NewMapOf[*extraProxyState]()}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -198,11 +337,36 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return
}
}
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: port,
DstPort: uint16(uintPort),
}
return
}
func (p *Proxy) determineFinalStoreType(store C.DelayHistoryStoreType, url string) C.DelayHistoryStoreType {
if store != C.DropHistory {
return store
}
if len(p.url) == 0 || url == p.url {
return C.OriginalHistory
}
if p.extra.Size() < 2*C.DefaultMaxHealthCheckUrlNum {
return C.ExtraHistory
}
_, ok := p.extra.Load(url)
if ok {
return C.ExtraHistory
}
return store
}

View File

@ -17,6 +17,10 @@ func SetTfo(open bool) {
lc.DisableTFO = !open
}
func SetMPTCP(open bool) {
setMultiPathTCP(&lc.ListenConfig, open)
}
func ListenContext(ctx context.Context, network, address string) (net.Listener, error) {
return lc.Listen(ctx, network, address)
}

View File

@ -0,0 +1,10 @@
//go:build !go1.21
package inbound
import "net"
const multipathTCPAvailable = false
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
}

View File

@ -0,0 +1,11 @@
//go:build go1.21
package inbound
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
listenConfig.SetMultipathTCP(open)
}

View File

@ -3,6 +3,7 @@ package inbound
import (
"net"
"net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -37,7 +38,9 @@ func NewInner(conn net.Conn, address string) *context.ConnContext {
metadata.DNSMode = C.DNSNormal
metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(address); err == nil {
metadata.DstPort = port
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
metadata.DstPort = uint16(port)
}
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
} else {

View File

@ -20,14 +20,14 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
case socks5.AtypDomainName:
// trim for FQDN
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4:
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip6.Unmap()
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return metadata
@ -43,11 +43,16 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737)
host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: netip.Addr{},
DstPort: port,
DstPort: uint16Port,
}
ip, err := netip.ParseAddr(host)
@ -58,10 +63,10 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata
}
func parseAddr(addr net.Addr) (netip.Addr, string, error) {
func parseAddr(addr net.Addr) (netip.Addr, uint16, error) {
// Filter when net.Addr interface is nil
if addr == nil {
return netip.Addr{}, "", errors.New("nil addr")
return netip.Addr{}, 0, errors.New("nil addr")
}
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
ip, port, err := parseAddr(rawAddr.RawAddr())
@ -72,9 +77,14 @@ func parseAddr(addr net.Addr) (netip.Addr, string, error) {
addrStr := addr.String()
host, port, err := net.SplitHostPort(addrStr)
if err != nil {
return netip.Addr{}, "", err
return netip.Addr{}, 0, err
}
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
ip, err := netip.ParseAddr(host)
return ip, port, err
return ip, uint16Port, err
}

View File

@ -21,6 +21,7 @@ type Base struct {
udp bool
xudp bool
tfo bool
mpTcp bool
rmark int
id string
prefer C.DNSPrefer
@ -143,11 +144,16 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithTFO(true))
}
if b.mpTcp {
opts = append(opts, dialer.WithMPTCP(true))
}
return opts
}
type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
@ -161,6 +167,7 @@ type BaseOption struct {
UDP bool
XUDP bool
TFO bool
MPTCP bool
Interface string
RoutingMark int
Prefer C.DNSPrefer
@ -174,6 +181,7 @@ func NewBase(opt BaseOption) *Base {
udp: opt.UDP,
xudp: opt.XUDP,
tfo: opt.TFO,
mpTcp: opt.MPTCP,
iface: opt.Interface,
rmark: opt.RoutingMark,
prefer: opt.Prefer,

View File

@ -3,6 +3,9 @@ package outbound
import (
"context"
"errors"
"net/netip"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
@ -12,6 +15,11 @@ type Direct struct {
*Base
}
type DirectOption struct {
BasicOption
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
@ -19,7 +27,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
if err != nil {
return nil, err
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
return NewConn(c, d), nil
}
@ -33,13 +41,28 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
}
metadata.DstIP = ip
}
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...)
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort))
if err != nil {
return nil, err
}
return newPacketConn(pc, d), nil
}
func NewDirectWithOption(option DirectOption) *Direct {
return &Direct{
Base: &Base{
name: option.Name,
tp: C.Direct,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
}
}
func NewDirect() *Direct {
return &Direct{
Base: &Base{

View File

@ -7,14 +7,16 @@ import (
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
)
@ -74,7 +76,7 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -155,19 +157,13 @@ func NewHttp(option HttpOption) (*Http, error) {
if option.SNI != "" {
sni = option.SNI
}
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
})
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
}, option.Fingerprint); err != nil {
return nil, err
}
var err error
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
}, option.Fingerprint)
if err != nil {
return nil, err
}
}
@ -177,6 +173,7 @@ func NewHttp(option HttpOption) (*Http, error) {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -2,16 +2,11 @@ package outbound
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"net"
"net/netip"
"os"
"regexp"
"strconv"
"time"
@ -19,9 +14,9 @@ import (
"github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
@ -43,8 +38,6 @@ const (
DefaultHopInterval = 10
)
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct {
*Base
@ -120,12 +113,12 @@ type HysteriaOption struct {
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64
up = stringToBps(c.Up)
up = StringToBps(c.Up)
if up == 0 {
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
}
down = stringToBps(c.Down)
down = StringToBps(c.Down)
if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
}
@ -153,37 +146,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
MinVersion: tls.VersionTLS13,
}
var bs []byte
var err error
if len(option.CustomCA) > 0 {
bs, err = os.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
}
} else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString)
}
if len(bs) > 0 {
block, _ := pem.Decode(bs)
if block == nil {
return nil, fmt.Errorf("CA cert is not PEM")
}
fpBytes := sha256.Sum256(block.Bytes)
if len(option.Fingerprint) == 0 {
option.Fingerprint = hex.EncodeToString(fpBytes[:])
}
}
if len(option.Fingerprint) != 0 {
var err error
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if len(option.ALPN) > 0 {
@ -268,42 +234,6 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
}, nil
}
func stringToBps(s string) uint64 {
if s == "" {
return 0
}
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return stringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}
type hyPacketConn struct {
core.UDPConn
}

View File

@ -0,0 +1,157 @@
package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"runtime"
"strconv"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant"
tuicCommon "github.com/Dreamacro/clash/transport/tuic/common"
"github.com/metacubex/sing-quic/hysteria2"
M "github.com/sagernet/sing/common/metadata"
)
func init() {
hysteria2.SetCongestionController = tuicCommon.SetCongestionController
}
type Hysteria2 struct {
*Base
option *Hysteria2Option
client *hysteria2.Client
dialer proxydialer.SingDialer
}
type Hysteria2Option struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
}
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := h.Base.DialOptions(opts...)
h.dialer.SetDialer(dialer.NewDialer(options...))
c, err := h.client.DialConn(ctx, M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil {
return nil, err
}
return NewConn(CN.NewRefConn(c, h), h), nil
}
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := h.Base.DialOptions(opts...)
h.dialer.SetDialer(dialer.NewDialer(options...))
pc, err := h.client.ListenPacket(ctx)
if err != nil {
return nil, err
}
if pc == nil {
return nil, errors.New("packetConn is nil")
}
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil
}
func closeHysteria2(h *Hysteria2) {
if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed"))
}
}
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
var salamanderPassword string
if len(option.Obfs) > 0 {
if option.ObfsPassword == "" {
return nil, errors.New("missing obfs password")
}
switch option.Obfs {
case hysteria2.ObfsTypeSalamander:
salamanderPassword = option.ObfsPassword
default:
return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs)
}
}
serverName := option.Server
if option.SNI != "" {
serverName = option.SNI
}
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN
}
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
clientOptions := hysteria2.ClientOptions{
Context: context.TODO(),
Dialer: singDialer,
ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
SendBPS: StringToBps(option.Up),
ReceiveBPS: StringToBps(option.Down),
SalamanderPassword: salamanderPassword,
Password: option.Password,
TLSConfig: tlsConfig,
UDPDisabled: false,
CWND: option.CWND,
}
client, err := hysteria2.NewClient(clientOptions)
if err != nil {
return nil, err
}
outbound := &Hysteria2{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Hysteria2,
udp: true,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
client: client,
dialer: singDialer,
}
runtime.SetFinalizer(outbound, closeHysteria2)
return outbound, nil
}

View File

@ -19,7 +19,7 @@ import (
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
restlsC "github.com/3andne/restls-client-go"
"github.com/metacubex/sing-shadowsocks2"
shadowsocks "github.com/metacubex/sing-shadowsocks2"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
)
@ -146,7 +146,7 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -294,7 +294,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
}
restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
restlsConfig.SessionTicketsDisabled = true
if err != nil {
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
}
@ -315,6 +314,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
tp: C.Shadowsocks,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -80,7 +80,7 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -181,6 +181,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
tp: C.ShadowsocksR,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -3,7 +3,6 @@ package outbound
import (
"context"
"errors"
"net"
"runtime"
CN "github.com/Dreamacro/clash/common/net"
@ -15,14 +14,13 @@ import (
mux "github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingMux struct {
C.ProxyAdapter
base ProxyBase
client *mux.Client
dialer *muxSingDialer
dialer proxydialer.SingDialer
onlyTcp bool
}
@ -41,27 +39,9 @@ type ProxyBase interface {
DialOptions(opts ...dialer.Option) []dialer.Option
}
type muxSingDialer struct {
dialer dialer.Dialer
proxy C.ProxyAdapter
statistic bool
}
var _ N.Dialer = (*muxSingDialer)(nil)
func (d *muxSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
var cDialer C.Dialer = proxydialer.New(d.proxy, d.dialer, d.statistic)
return cDialer.DialContext(ctx, network, destination.String())
}
func (d *muxSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
var cDialer C.Dialer = proxydialer.New(d.proxy, d.dialer, d.statistic)
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
}
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := s.base.DialOptions(opts...)
s.dialer.dialer = dialer.NewDialer(options...)
s.dialer.SetDialer(dialer.NewDialer(options...))
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil {
return nil, err
@ -74,7 +54,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
}
options := s.base.DialOptions(opts...)
s.dialer.dialer = dialer.NewDialer(options...)
s.dialer.SetDialer(dialer.NewDialer(options...))
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
@ -97,7 +77,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
func (s *SingMux) SupportUDP() bool {
if s.onlyTcp {
return s.ProxyAdapter.SupportUOT()
return s.ProxyAdapter.SupportUDP()
}
return true
}
@ -114,7 +94,7 @@ func closeSingMux(s *SingMux) {
}
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) {
singDialer := &muxSingDialer{dialer: dialer.NewDialer(), proxy: proxy, statistic: option.Statistic}
singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(), option.Statistic)
client, err := mux.NewClient(mux.Options{
Dialer: singDialer,
Protocol: option.Protocol,

View File

@ -6,6 +6,7 @@ import (
"net"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
@ -59,8 +60,7 @@ func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return c, err
}
@ -72,8 +72,7 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return nil, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil {
c.Close()
return nil, err
}
@ -95,7 +94,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -123,7 +122,7 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil {
return nil, err
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
@ -183,6 +182,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
tp: C.Snell,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
@ -208,7 +208,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
return nil, err
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
})
}

View File

@ -9,9 +9,10 @@ import (
"net"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
@ -80,7 +81,7 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -126,7 +127,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
N.TCPKeepAlive(c)
var user *socks5.User
if ss.user != "" {
user = &socks5.User{
@ -155,7 +156,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
bindUDPAddr.IP = serverAddr.IP
}
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...)
pc, err := cDialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort())
if err != nil {
return
}
@ -179,13 +180,10 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
ServerName: option.Server,
}
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
var err error
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
}
@ -196,6 +194,7 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
tp: C.Socks5,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -8,13 +8,14 @@ import (
"net/http"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
)
type Trojan struct {
@ -45,8 +46,6 @@ type TrojanOption struct {
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
}
@ -95,11 +94,6 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
return nil, err
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
@ -117,12 +111,6 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return nil, err
}
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
c.Close()
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close()
return nil, err
@ -145,7 +133,7 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
@ -198,7 +186,7 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
N.TCPKeepAlive(c)
c, err = t.plainStream(ctx, c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@ -237,24 +225,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow,
Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
}
switch option.Network {
case "", "tcp":
if len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
tOption.Flow = option.Flow
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
}
}
if option.SNI != "" {
tOption.ServerName = option.SNI
}
@ -266,6 +240,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tp: C.Trojan,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
@ -295,7 +270,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
return c, nil
}
@ -306,13 +281,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName,
}
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
var err error
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)

View File

@ -2,24 +2,25 @@ package outbound
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"math"
"net"
"os"
"strconv"
"time"
"github.com/metacubex/quic-go"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/tuic"
"github.com/gofrs/uuid/v5"
"github.com/metacubex/quic-go"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
)
type Tuic struct {
@ -33,7 +34,9 @@ type TuicOption struct {
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Token string `proxy:"token"`
Token string `proxy:"token,omitempty"`
UUID string `proxy:"uuid,omitempty"`
Password string `proxy:"password,omitempty"`
Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
@ -46,6 +49,7 @@ type TuicOption struct {
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
@ -55,6 +59,9 @@ type TuicOption struct {
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
}
// DialContext implements C.ProxyAdapter
@ -78,6 +85,32 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata
uotMetadata.Host = uotDestination.Fqdn
uotMetadata.DstPort = uotDestination.Port
c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata)
if err != nil {
return nil, err
}
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil
} else {
return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil
}
}
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
@ -90,11 +123,7 @@ func (t *Tuic) SupportWithDialer() C.NetWork {
return C.ALLNet
}
func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (pc net.PacketConn, addr net.Addr, err error) {
return t.dialWithDialer(ctx, dialer.NewDialer(opts...))
}
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) {
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) {
if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
if err != nil {
@ -106,10 +135,14 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.Pack
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, nil, err
}
transport = &quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
return
}
@ -125,40 +158,13 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.ServerName = option.SNI
}
var bs []byte
var err error
if len(option.CustomCA) > 0 {
bs, err = os.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
}
} else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString)
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if len(bs) > 0 {
block, _ := pem.Decode(bs)
if block == nil {
return nil, fmt.Errorf("CA cert is not PEM")
}
fpBytes := sha256.Sum256(block.Bytes)
if len(option.Fingerprint) == 0 {
option.Fingerprint = hex.EncodeToString(fpBytes[:])
}
}
if len(option.Fingerprint) != 0 {
var err error
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
}
if len(option.ALPN) > 0 {
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
tlsConfig.NextProtos = option.ALPN
} else {
tlsConfig.NextProtos = []string{"h3"}
@ -172,8 +178,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
option.HeartbeatInterval = 10000
}
udpRelayMode := tuic.QUIC
if option.UdpRelayMode != "quic" {
option.UdpRelayMode = "native"
udpRelayMode = tuic.NATIVE
}
if option.MaxUdpRelayPacketSize == 0 {
@ -184,14 +191,23 @@ func NewTuic(option TuicOption) (*Tuic, error) {
option.MaxOpenStreams = 100
}
if option.CWND == 0 {
option.CWND = 32
}
packetOverHead := tuic.PacketOverHeadV4
if len(option.Token) == 0 {
packetOverHead = tuic.PacketOverHeadV5
}
if option.MaxDatagramFrameSize == 0 {
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + tuic.PacketOverHead
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead
}
if option.MaxDatagramFrameSize > 1400 {
option.MaxDatagramFrameSize = 1400
}
option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - tuic.PacketOverHead
option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead
// ensure server's incoming stream can handle correctly, increase to 1.1x
quicMaxOpenStreams := int64(option.MaxOpenStreams)
@ -220,12 +236,18 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Ip) > 0 {
addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port))
}
host := option.Server
if option.DisableSni {
host = ""
tlsConfig.ServerName = ""
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
}
switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion:
case 0:
option.UDPOverStreamVersion = uot.LegacyVersion
default:
return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion)
}
tkn := tuic.GenTKN(option.Token)
t := &Tuic{
Base: &Base{
@ -251,21 +273,44 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1
}
clientOption := &tuic.ClientOption{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Host: host,
Token: tkn,
UdpRelayMode: option.UdpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
}
t.client = tuic.NewPoolClient(clientOption)
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV4(clientOption)
} else {
maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV5(clientOption)
}
return t, nil
}

View File

@ -4,12 +4,12 @@ import (
"bytes"
"context"
"crypto/tls"
xtls "github.com/xtls/go"
"fmt"
"net"
"net/netip"
"regexp"
"strconv"
"sync"
"time"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
@ -17,18 +17,10 @@ import (
)
var (
globalClientSessionCache tls.ClientSessionCache
globalClientXSessionCache xtls.ClientSessionCache
once sync.Once
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
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)
@ -36,18 +28,11 @@ func getClientSessionCache() tls.ClientSessionCache {
return globalClientSessionCache
}
func getClientXSessionCache() xtls.ClientSessionCache {
once.Do(func() {
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
})
return globalClientXSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
addrType := metadata.AddrType()
aType := uint8(addrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
p := uint(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType {
case socks5.AtypDomainName:
@ -138,3 +123,41 @@ func safeConnClose(c net.Conn, err error) {
_ = c.Close()
}
}
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
func StringToBps(s string) uint64 {
if s == "" {
return 0
}
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return StringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}

View File

@ -14,6 +14,8 @@ import (
"github.com/Dreamacro/clash/common/convert"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
@ -25,8 +27,8 @@ import (
"github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess"
vmessSing "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr"
vmessSing "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
)
@ -55,8 +57,8 @@ type VlessOption struct {
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
@ -109,13 +111,9 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
NextProtos: []string{"http/1.1"},
}
if len(v.option.Fingerprint) == 0 {
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
if v.option.ServerName != "" {
@ -132,7 +130,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
c, err = v.streamTLSOrXTLSConn(ctx, c, false)
c, err = v.streamTLSConn(ctx, c, false)
if err != nil {
return nil, err
}
@ -147,7 +145,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = vmess.StreamHTTPConn(c, httpOpts)
case "h2":
c, err = v.streamTLSOrXTLSConn(ctx, c, true)
c, err = v.streamTLSConn(ctx, c, true)
if err != nil {
return nil, err
}
@ -162,8 +160,8 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default:
// default tcp network
// handle TLS And XTLS
c, err = v.streamTLSOrXTLSConn(ctx, c, false)
// handle TLS
c, err = v.streamTLSConn(ctx, c, false)
}
if err != nil {
@ -179,7 +177,7 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
metadata = &C.Metadata{
NetWork: C.UDP,
Host: packetaddr.SeqPacketMagicAddress,
DstPort: "443",
DstPort: 443,
}
} else {
metadata = &C.Metadata{ // a clear metadata only contains ip
@ -201,29 +199,17 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
return
}
func (v *Vless) streamTLSOrXTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr)
func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
if v.isLegacyXTLSEnabled() && !isH2 {
xtlsOpts := vless.XTLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
Fingerprint: v.option.Fingerprint,
}
if v.option.ServerName != "" {
xtlsOpts.Host = v.option.ServerName
}
return vless.StreamXTLSConn(ctx, conn, &xtlsOpts)
} else if v.option.TLS {
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if isH2 {
@ -240,10 +226,6 @@ func (v *Vless) streamTLSOrXTLSConn(ctx context.Context, conn net.Conn, isH2 boo
return conn, nil
}
func (v *Vless) isLegacyXTLSEnabled() bool {
return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV
}
// DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
@ -278,7 +260,7 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
@ -343,7 +325,7 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
@ -373,8 +355,14 @@ func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada
}
if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
return newPacketConn(N.NewThreadSafePacketConn(
vmessSing.NewXUDPConn(c, M.SocksaddrFromNet(metadata.UDPAddr())),
vmessSing.NewXUDPConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
} else if v.option.PacketAddr {
return newPacketConn(N.NewThreadSafePacketConn(
@ -410,12 +398,11 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
copy(addr[1:], metadata.Host)
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: uint16(port),
Port: metadata.DstPort,
Mux: metadata.NetWork == C.UDP && xudp,
}
}
@ -519,11 +506,11 @@ func NewVless(option VlessOption) (*Vless, error) {
switch option.Flow {
case vless.XRV:
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
fallthrough
case vless.XRO, vless.XRD, vless.XRS:
addons = &vless.Addons{
Flow: option.Flow,
}
case vless.XRO, vless.XRD, vless.XRS:
log.Fatalln("Legacy XTLS protocol %s is deprecated and no longer supported", option.Flow)
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
@ -542,7 +529,7 @@ func NewVless(option VlessOption) (*Vless, error) {
option.PacketAddr = false
}
client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
client, err := vless.NewClient(option.UUID, addons)
if err != nil {
return nil, err
}
@ -555,6 +542,7 @@ func NewVless(option VlessOption) (*Vless, error) {
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
@ -587,7 +575,7 @@ func NewVless(option VlessOption) (*Vless, error) {
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
return c, nil
}
@ -601,7 +589,7 @@ func NewVless(option VlessOption) (*Vless, error) {
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
})

View File

@ -12,16 +12,19 @@ import (
"sync"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/ntp"
"github.com/Dreamacro/clash/transport/gun"
clashVMess "github.com/Dreamacro/clash/transport/vmess"
vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr"
vmess "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
)
@ -51,6 +54,7 @@ type VmessOption struct {
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"`
@ -124,12 +128,9 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
NextProtos: []string{"http/1.1"},
}
if len(v.option.Fingerprint) == 0 {
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
return nil, err
}
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
if v.option.ServerName != "" {
@ -148,6 +149,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
@ -204,6 +206,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
if v.option.ServerName != "" {
@ -223,30 +226,44 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if metadata.NetWork == C.UDP {
if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
if N.NeedHandshake(c) {
conn = v.client.DialEarlyXUDPPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr()))
conn = v.client.DialEarlyXUDPPacketConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else {
conn, err = v.client.DialXUDPPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr()))
conn, err = v.client.DialXUDPPacketConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr()))
}
} else if v.option.PacketAddr {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyPacketConn(c, M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
conn = v.client.DialEarlyPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
} else {
conn, err = v.client.DialPacketConn(c, M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
conn, err = v.client.DialPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
}
conn = packetaddr.NewBindConn(conn)
} else {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr()))
conn = v.client.DialEarlyPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else {
conn, err = v.client.DialPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr()))
conn, err = v.client.DialPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
}
}
} else {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
conn = v.client.DialEarlyConn(c,
M.ParseSocksaddr(metadata.RemoteAddress()))
} else {
conn, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
conn, err = v.client.DialConn(c,
M.ParseSocksaddr(metadata.RemoteAddress()))
}
}
if err != nil {
@ -289,7 +306,7 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
@ -350,7 +367,7 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
@ -398,6 +415,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if option.AuthenticatedLength {
options = append(options, vmess.ClientWithAuthenticatedLength())
}
options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
if err != nil {
return nil, err
@ -421,6 +439,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
@ -448,7 +467,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
N.TCPKeepAlive(c)
return c, nil
}
@ -462,7 +481,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
})

View File

@ -27,7 +27,6 @@ import (
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/device"
)
@ -36,7 +35,7 @@ type WireGuard struct {
bind *wireguard.ClientBind
device *device.Device
tunDevice wireguard.Device
dialer *wgSingDialer
dialer proxydialer.SingDialer
startOnce sync.Once
startErr error
resolver *dns.Resolver
@ -70,37 +69,6 @@ type WireGuardPeerOption struct {
AllowedIPs []string `proxy:"allowed-ips,omitempty"`
}
type wgSingDialer struct {
dialer dialer.Dialer
proxyName string
}
var _ N.Dialer = (*wgSingDialer)(nil)
func (d *wgSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
var cDialer C.Dialer = d.dialer
if len(d.proxyName) > 0 {
pd, err := proxydialer.NewByName(d.proxyName, d.dialer)
if err != nil {
return nil, err
}
cDialer = pd
}
return cDialer.DialContext(ctx, network, destination.String())
}
func (d *wgSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
var cDialer C.Dialer = d.dialer
if len(d.proxyName) > 0 {
pd, err := proxydialer.NewByName(d.proxyName, d.dialer)
if err != nil {
return nil, err
}
cDialer = pd
}
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
}
type wgSingErrorHandler struct {
name string
}
@ -168,7 +136,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
dialer: &wgSingDialer{dialer: dialer.NewDialer(), proxyName: option.DialerProxy},
dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()),
}
runtime.SetFinalizer(outbound, closeWireGuard)
@ -302,7 +270,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{
outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
},
@ -355,7 +323,7 @@ func closeWireGuard(w *WireGuard) {
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := w.Base.DialOptions(opts...)
w.dialer.dialer = dialer.NewDialer(options...)
w.dialer.SetDialer(dialer.NewDialer(options...))
var conn net.Conn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
@ -374,8 +342,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
} else {
port, _ := strconv.Atoi(metadata.DstPort)
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
}
if err != nil {
return nil, err
@ -388,7 +355,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := w.Base.DialOptions(opts...)
w.dialer.dialer = dialer.NewDialer(options...)
w.dialer.SetDialer(dialer.NewDialer(options...))
var pc net.PacketConn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
@ -412,8 +379,7 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
}
metadata.DstIP = ip
}
port, _ := strconv.Atoi(metadata.DstPort)
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
if err != nil {
return nil, err
}

View File

@ -9,6 +9,7 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/callback"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -16,9 +17,10 @@ import (
type Fallback struct {
*GroupBase
disableUDP bool
testUrl string
selected string
disableUDP bool
testUrl string
selected string
expectedStatus string
}
func (f *Fallback) Now() string {
@ -82,9 +84,11 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
"type": f.Type().String(),
"now": f.Now(),
"all": all,
"testUrl": f.testUrl,
"expected": f.expectedStatus,
})
}
@ -98,12 +102,14 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch)
for _, proxy := range proxies {
if len(f.selected) == 0 {
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
}
} else {
if proxy.Name() == f.selected {
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
} else {
f.selected = ""
@ -129,10 +135,12 @@ func (f *Fallback) Set(name string) error {
}
f.selected = name
if !p.Alive() {
// if !p.Alive() {
if !p.AliveForTestUrl(f.testUrl) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
_, _ = p.URLTest(ctx, f.testUrl)
expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus)
_, _ = p.URLTest(ctx, f.testUrl, expectedStatus, C.ExtraHistory)
}
return nil
@ -156,7 +164,8 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
option.ExcludeType,
providers,
}),
disableUDP: option.DisableUDP,
testUrl: option.URL,
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
}
}

View File

@ -9,6 +9,7 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
types "github.com/Dreamacro/clash/constant/provider"
@ -192,7 +193,7 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
return proxies
}
func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
var wg sync.WaitGroup
var lock sync.Mutex
mp := map[string]uint16{}
@ -201,7 +202,7 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
proxy := proxy
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url)
delay, err := proxy.URLTest(ctx, url, expectedStatus, C.DropHistory)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay

View File

@ -12,8 +12,8 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/callback"
"github.com/Dreamacro/clash/common/murmur3"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -25,8 +25,10 @@ type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Pr
type LoadBalance struct {
*GroupBase
disableUDP bool
strategyFn strategyFn
disableUDP bool
strategyFn strategyFn
testUrl string
expectedStatus string
}
var errStrategy = errors.New("unsupported strategy")
@ -129,7 +131,7 @@ func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool {
return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
}
func strategyRoundRobin() strategyFn {
func strategyRoundRobin(url string) strategyFn {
idx := 0
idxMutex := sync.Mutex{}
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
@ -148,7 +150,8 @@ func strategyRoundRobin() strategyFn {
for ; i < length; i++ {
id := (idx + i) % length
proxy := proxies[id]
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
i++
return proxy
}
@ -158,22 +161,24 @@ func strategyRoundRobin() strategyFn {
}
}
func strategyConsistentHashing() strategyFn {
func strategyConsistentHashing(url string) strategyFn {
maxRetry := 5
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
key := utils.MapHash(getKey(metadata))
buckets := int32(len(proxies))
for i := 0; i < maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := proxies[idx]
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
return proxy
}
}
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
return proxy
}
}
@ -182,14 +187,14 @@ func strategyConsistentHashing() strategyFn {
}
}
func strategyStickySessions() strategyFn {
func strategyStickySessions(url string) strategyFn {
ttl := time.Minute * 10
maxRetry := 5
lruCache := cache.New[uint64, int](
cache.WithAge[uint64, int](int64(ttl.Seconds())),
cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
key := utils.MapHash(getKeyWithSrcAndDst(metadata))
length := len(proxies)
idx, has := lruCache.Get(key)
if !has {
@ -199,7 +204,8 @@ func strategyStickySessions() strategyFn {
nowIdx := idx
for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx]
if proxy.Alive() {
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx)
@ -230,8 +236,10 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": lb.Type().String(),
"all": all,
"type": lb.Type().String(),
"all": all,
"testUrl": lb.testUrl,
"expectedStatus": lb.expectedStatus,
})
}
@ -239,11 +247,11 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
strategyFn = strategyConsistentHashing()
strategyFn = strategyConsistentHashing(option.URL)
case "round-robin":
strategyFn = strategyRoundRobin()
strategyFn = strategyRoundRobin(option.URL)
case "sticky-sessions":
strategyFn = strategyStickySessions()
strategyFn = strategyStickySessions(option.URL)
default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
@ -260,7 +268,9 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
option.ExcludeType,
providers,
}),
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
}, nil
}

View File

@ -3,35 +3,37 @@ package outboundgroup
import (
"errors"
"fmt"
"strings"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
var (
errFormat = errors.New("format error")
errType = errors.New("unsupport type")
errType = errors.New("unsupported type")
errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("duplicate provider name")
)
type GroupCommonOption struct {
outbound.BasicOption
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
ExpectedStatus string `group:"expected-status,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
@ -53,30 +55,36 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
}
expectedStatus, err := utils.NewIntRanges[uint16](groupOption.ExpectedStatus)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
status := strings.TrimSpace(groupOption.ExpectedStatus)
if status == "" {
status = "*"
}
groupOption.ExpectedStatus = status
testUrl := groupOption.URL
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 _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider
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
}
var url string
var interval uint
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
// select don't need health check
if groupOption.Type != "select" && groupOption.Type != "relay" {
if groupOption.URL == "" {
groupOption.URL = "https://cp.cloudflare.com/generate_204"
}
@ -85,22 +93,29 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
groupOption.Interval = 300
}
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
url = groupOption.URL
interval = uint(groupOption.Interval)
}
hc := provider.NewHealthCheck(ps, url, interval, true, expectedStatus)
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)
}
// different proxy groups use different test URL
addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
providers = append(providers, list...)
} else {
groupOption.Filter = ""
@ -154,3 +169,13 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
}
return ps, nil
}
func addTestUrlToProviders(providers []types.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
if len(providers) == 0 || len(url) == 0 {
return
}
for _, pd := range providers {
pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval)
}
}

View File

@ -25,12 +25,13 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct {
*GroupBase
selected string
testUrl string
tolerance uint16
disableUDP bool
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
selected string
testUrl string
expectedStatus string
tolerance uint16
disableUDP bool
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
}
func (u *URLTest) Now() string {
@ -112,7 +113,8 @@ func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
fast := proxies[0]
min := fast.LastDelay()
// min := fast.LastDelay()
min := fast.LastDelayForTestUrl(u.testUrl)
fastNotExist := true
for _, proxy := range proxies[1:] {
@ -120,11 +122,13 @@ func (u *URLTest) fast(touch bool) C.Proxy {
fastNotExist = false
}
if !proxy.Alive() {
// if !proxy.Alive() {
if !proxy.AliveForTestUrl(u.testUrl) {
continue
}
delay := proxy.LastDelay()
// delay := proxy.LastDelay()
delay := proxy.LastDelayForTestUrl(u.testUrl)
if delay < min {
fast = proxy
min = delay
@ -132,7 +136,8 @@ func (u *URLTest) fast(touch bool) C.Proxy {
}
// tolerance
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
// if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance {
u.fastNode = fast
}
return u.fastNode, nil
@ -164,9 +169,11 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
"type": u.Type().String(),
"now": u.Now(),
"all": all,
"testUrl": u.testUrl,
"expected": u.expectedStatus,
})
}
@ -198,9 +205,10 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
option.ExcludeType,
providers,
}),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP,
testUrl: option.URL,
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
}
for _, option := range options {

View File

@ -1,17 +1,5 @@
package outboundgroup
import (
"net"
"time"
)
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
type SelectAble interface {
Set(string) error
ForceSet(name string)

View File

@ -92,6 +92,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break
}
proxy, err = outbound.NewHysteria(*hyOption)
case "hysteria2":
hyOption := &outbound.Hysteria2Option{}
err = decoder.Decode(mapping, hyOption)
if err != nil {
break
}
proxy, err = outbound.NewHysteria2(*hyOption)
case "wireguard":
wgOption := &outbound.WireGuardOption{}
err = decoder.Decode(mapping, wgOption)
@ -106,6 +113,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break
}
proxy, err = outbound.NewTuic(*tuicOption)
case "direct":
directOption := &outbound.DirectOption{}
err = decoder.Decode(mapping, directOption)
if err != nil {
break
}
proxy = outbound.NewDirectWithOption(*directOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}

View File

@ -2,6 +2,8 @@ package provider
import (
"context"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/common/atomic"
@ -10,6 +12,8 @@ import (
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/dlclark/regexp2"
)
const (
@ -21,18 +25,33 @@ type HealthCheckOption struct {
Interval uint
}
type extraOption struct {
expectedStatus utils.IntRanges[uint16]
filters map[string]struct{}
}
type HealthCheck struct {
url string
proxies []C.Proxy
interval uint
lazy bool
lastTouch *atomic.Int64
done chan struct{}
singleDo *singledo.Single[struct{}]
url string
extra map[string]*extraOption
mu sync.Mutex
started *atomic.Bool
proxies []C.Proxy
interval uint
lazy bool
expectedStatus utils.IntRanges[uint16]
lastTouch *atomic.Int64
done chan struct{}
singleDo *singledo.Single[struct{}]
}
func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
hc.start()
for {
select {
case <-ticker.C:
@ -44,6 +63,7 @@ func (hc *HealthCheck) process() {
}
case <-hc.done:
ticker.Stop()
hc.stop()
return
}
}
@ -53,6 +73,63 @@ func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies
}
func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
url = strings.TrimSpace(url)
if len(url) == 0 || url == hc.url {
log.Debugln("ignore invalid health check url: %s", url)
return
}
hc.mu.Lock()
defer hc.mu.Unlock()
// if the provider has not set up health checks, then modify it to be the same as the group's interval
if hc.interval == 0 {
hc.interval = interval
}
if hc.extra == nil {
hc.extra = make(map[string]*extraOption)
}
// prioritize the use of previously registered configurations, especially those from provider
if _, ok := hc.extra[url]; ok {
// provider default health check does not set filter
if url != hc.url && len(filter) != 0 {
splitAndAddFiltersToExtra(filter, hc.extra[url])
}
log.Debugln("health check url: %s exists", url)
return
}
// due to the time-consuming nature of health checks, a maximum of defaultMaxTestURLNum URLs can be set for testing
if len(hc.extra) > C.DefaultMaxHealthCheckUrlNum {
log.Debugln("skip add url: %s to health check because it has reached the maximum limit: %d", url, C.DefaultMaxHealthCheckUrlNum)
return
}
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
filter = strings.TrimSpace(filter)
if len(filter) != 0 {
for _, regex := range strings.Split(filter, "`") {
regex = strings.TrimSpace(regex)
if len(regex) != 0 {
option.filters[regex] = struct{}{}
}
}
}
}
func (hc *HealthCheck) auto() bool {
return hc.interval != 0
}
@ -61,41 +138,102 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
}
func (hc *HealthCheck) check() {
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
id := utils.NewUUIDV4().String()
log.Debugln("Start New Health Checking {%s}", id)
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range hc.proxies {
p := proxy
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
log.Debugln("Health Checking %s {%s}", p.Name(), id)
_, _ = p.URLTest(ctx, hc.url)
log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id)
return false, nil
})
}
// execute default health check
option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
hc.execute(b, hc.url, id, option)
// execute extra health check
if len(hc.extra) != 0 {
for url, option := range hc.extra {
hc.execute(b, url, id, option)
}
}
b.Wait()
log.Debugln("Finish A Health Checking {%s}", id)
return struct{}{}, nil
})
}
func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
url = strings.TrimSpace(url)
if len(url) == 0 {
log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
return
}
var filterReg *regexp2.Regexp
var store = C.OriginalHistory
var expectedStatus utils.IntRanges[uint16]
if option != nil {
if url != hc.url {
store = C.ExtraHistory
}
expectedStatus = option.expectedStatus
if len(option.filters) != 0 {
filters := make([]string, 0, len(option.filters))
for filter := range option.filters {
filters = append(filters, filter)
}
filterReg = regexp2.MustCompile(strings.Join(filters, "|"), 0)
}
}
for _, proxy := range hc.proxies {
// skip proxies that do not require health check
if filterReg != nil {
if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil {
continue
}
}
p := proxy
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
_, _ = p.URLTest(ctx, url, expectedStatus, store)
log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
return false, nil
})
}
}
func (hc *HealthCheck) close() {
hc.done <- struct{}{}
}
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck {
if len(url) == 0 {
interval = 0
expectedStatus = nil
}
return &HealthCheck{
proxies: proxies,
url: url,
interval: interval,
lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
proxies: proxies,
url: url,
extra: map[string]*extraOption{},
started: atomic.NewBool(false),
interval: interval,
lazy: lazy,
expectedStatus: expectedStatus,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
}
}

View File

@ -6,23 +6,28 @@ import (
"time"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
var errVehicleType = errors.New("unsupport vehicle type")
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct {
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
ExpectedStatus string `provider:"expected-status,omitempty"`
}
type proxyProviderSchema struct {
Type string `provider:"type"`
Path string `provider:"path"`
Path string `provider:"path,omitempty"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
@ -44,20 +49,33 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
return nil, err
}
expectedStatus, err := utils.NewIntRanges[uint16](schema.HealthCheck.ExpectedStatus)
if err != nil {
return nil, err
}
var hcInterval uint
if schema.HealthCheck.Enable {
hcInterval = uint(schema.HealthCheck.Interval)
}
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
path := C.Path.Resolve(schema.Path)
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy, expectedStatus)
var vehicle types.Vehicle
switch schema.Type {
case "file":
path := C.Path.Resolve(schema.Path)
vehicle = resource.NewFileVehicle(path)
case "http":
vehicle = resource.NewHTTPVehicle(schema.URL, path)
if schema.Path != "" {
path := C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
vehicle = resource.NewHTTPVehicle(schema.URL, path)
} else {
path := C.Path.GetPathByHash("proxies", schema.URL)
vehicle = resource.NewHTTPVehicle(schema.URL, path)
}
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/common/utils"
clashHttp "github.com/Dreamacro/clash/component/http"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant"
@ -50,6 +51,7 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
"testUrl": pp.healthCheck.url,
"updatedAt": pp.UpdatedAt,
"subscriptionInfo": pp.subscriptionInfo,
})
@ -98,6 +100,10 @@ func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
@ -141,15 +147,15 @@ func (pp *proxySetProvider) getSubscriptionInfo() {
}
func (pp *proxySetProvider) closeAllConnections() {
snapshot := statistic.DefaultManager.Snapshot()
for _, c := range snapshot.Connections {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
for _, chain := range c.Chains() {
if chain == pp.Name() {
_ = c.Close()
break
}
}
}
return true
})
}
func stopProxyProvider(pd *ProxySetProvider) {
@ -210,6 +216,7 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
"testUrl": cp.healthCheck.url,
})
}
@ -249,6 +256,10 @@ func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
@ -288,7 +299,7 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%s, %w", err.Error(), err1)
return nil, fmt.Errorf("%w, %w", err, err1)
}
schema.Proxies = proxies
}

View File

@ -11,18 +11,9 @@ type Buffer = buf.Buffer
var New = buf.New
var NewSize = buf.NewSize
var StackNew = buf.StackNew
var StackNewSize = buf.StackNewSize
var With = buf.With
var As = buf.As
var KeepAlive = common.KeepAlive
//go:norace
func Dup[T any](obj T) T {
return common.Dup(obj)
}
var (
Must = common.Must
Error = common.Error

View File

@ -7,6 +7,8 @@ import (
"time"
"github.com/Dreamacro/clash/common/generics/list"
"github.com/samber/lo"
)
// Option is part of Functional Options Pattern
@ -82,9 +84,27 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
// Get returns the any representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
return getZero[V](), false
return lo.Empty[V](), false
}
value := el.value
return value, true
}
func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
value := constructor()
c.set(key, value)
return value, false
}
value := el.value
@ -96,9 +116,12 @@ func (c *LruCache[K, V]) Get(key K) (V, bool) {
// 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[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
return getZero[V](), time.Time{}, false
return lo.Empty[V](), time.Time{}, false
}
return el.value, time.Unix(el.expires, 0), true
@ -115,11 +138,18 @@ func (c *LruCache[K, V]) Exist(key K) bool {
// Set stores the any representation of a response for a given key.
func (c *LruCache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.set(key, value)
}
func (c *LruCache[K, V]) set(key K, value V) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
}
c.SetWithExpire(key, value, time.Unix(expires, 0))
c.setWithExpire(key, value, time.Unix(expires, 0))
}
// SetWithExpire stores the any representation of a response for a given key and given expires.
@ -128,6 +158,10 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.setWithExpire(key, value, expires)
}
func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le)
e := le.Value
@ -165,9 +199,6 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
}
func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
le, ok := c.cache[key]
if !ok {
return nil
@ -191,12 +222,11 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
// Delete removes the value associated with a key.
func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok {
c.deleteElement(le)
}
c.mu.Unlock()
}
func (c *LruCache[K, V]) maybeDeleteOldest() {
@ -219,10 +249,10 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
func (c *LruCache[K, V]) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil
}
@ -231,8 +261,3 @@ type entry[K comparable, V any] struct {
value V
expires int64
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -21,7 +21,7 @@ func TestSplitArgs(t *testing.T) {
func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" {
_, err := ExecCmd("dir")
_, err := ExecCmd("cmd -c 'dir'")
assert.Nil(t, err)
return
}

View File

@ -50,7 +50,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs")
hysteria["alpn"] = []string{query.Get("alpn")}
if alpn := query.Get("alpn"); alpn != "" {
hysteria["alpn"] = strings.Split(alpn, ",")
}
hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol")
up := query.Get("up")
@ -66,6 +68,79 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria)
case "hysteria2":
urlHysteria2, err := url.Parse(line)
if err != nil {
continue
}
query := urlHysteria2.Query()
name := uniqueName(names, urlHysteria2.Fragment)
hysteria2 := make(map[string]any, 20)
hysteria2["name"] = name
hysteria2["type"] = scheme
hysteria2["server"] = urlHysteria2.Hostname()
if port := urlHysteria2.Port(); port != "" {
hysteria2["port"] = port
} else {
hysteria2["port"] = "443"
}
hysteria2["obfs"] = query.Get("obfs")
hysteria2["obfs-password"] = query.Get("obfs-password")
hysteria2["sni"] = query.Get("sni")
hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
if alpn := query.Get("alpn"); alpn != "" {
hysteria2["alpn"] = strings.Split(alpn, ",")
}
if auth := urlHysteria2.User.String(); auth != "" {
hysteria2["password"] = auth
}
hysteria2["fingerprint"] = query.Get("pinSHA256")
hysteria2["down"] = query.Get("down")
hysteria2["up"] = query.Get("up")
proxies = append(proxies, hysteria2)
case "tuic":
// A temporary unofficial TUIC share link standard
// Modified from https://github.com/daeuniverse/dae/discussions/182
// Changes:
// 1. Support TUICv4, just replace uuid:password with token
// 2. Remove `allow_insecure` field
urlTUIC, err := url.Parse(line)
if err != nil {
continue
}
query := urlTUIC.Query()
tuic := make(map[string]any, 20)
tuic["name"] = uniqueName(names, urlTUIC.Fragment)
tuic["type"] = scheme
tuic["server"] = urlTUIC.Hostname()
tuic["port"] = urlTUIC.Port()
tuic["udp"] = true
password, v5 := urlTUIC.User.Password()
if v5 {
tuic["uuid"] = urlTUIC.User.Username()
tuic["password"] = password
} else {
tuic["token"] = urlTUIC.User.Username()
}
if cc := query.Get("congestion_control"); cc != "" {
tuic["congestion-controller"] = cc
}
if alpn := query.Get("alpn"); alpn != "" {
tuic["alpn"] = strings.Split(alpn, ",")
}
if sni := query.Get("sni"); sni != "" {
tuic["sni"] = sni
}
if query.Get("disable_sni") == "1" {
tuic["disable-sni"] = true
}
if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" {
tuic["udp-relay-mode"] = udpRelayMode
}
case "trojan":
urlTrojan, err := url.Parse(line)
@ -86,10 +161,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["udp"] = true
trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure"))
sni := query.Get("sni")
if sni != "" {
if sni := query.Get("sni"); sni != "" {
trojan["sni"] = sni
}
if alpn := query.Get("alpn"); alpn != "" {
trojan["alpn"] = strings.Split(alpn, ",")
}
network := strings.ToLower(query.Get("type"))
if network != "" {
@ -217,6 +294,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if strings.HasSuffix(tls, "tls") {
vmess["tls"] = true
}
if alpn, ok := values["alpn"].(string); ok {
vmess["alpn"] = strings.Split(alpn, ",")
}
}
switch network {
@ -332,6 +412,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
}
}
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := encRaw.DecodeString(body)
if err != nil {

View File

@ -0,0 +1,35 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
)
// https://v2.hysteria.network/zh/docs/developers/URI-Scheme/
func TestConvertsV2Ray_normal(t *testing.T) {
hy2test := "hysteria2://letmein@example.com:8443/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com&up=114&down=514&alpn=h3,h4#hy2test"
expected := []map[string]interface{}{
{
"name": "hy2test",
"type": "hysteria2",
"server": "example.com",
"port": "8443",
"sni": "real.example.com",
"obfs": "salamander",
"obfs-password": "gawrgura",
"alpn": []string{"h3", "h4"},
"password": "letmein",
"up": "114",
"down": "514",
"skip-cert-verify": true,
"fingerprint": "deadbeef",
},
}
proxies, err := ConvertsV2Ray([]byte(hy2test))
assert.Nil(t, err)
assert.Equal(t, expected, proxies)
}

View File

@ -24,8 +24,6 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
proxy["port"] = url.Port()
proxy["uuid"] = url.User.Username()
proxy["udp"] = true
proxy["skip-cert-verify"] = false
proxy["tls"] = false
tls := strings.ToLower(query.Get("security"))
if strings.HasSuffix(tls, "tls") || tls == "reality" {
proxy["tls"] = true
@ -34,6 +32,9 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
} else {
proxy["client-fingerprint"] = fingerprint
}
if alpn := query.Get("alpn"); alpn != "" {
proxy["alpn"] = strings.Split(alpn, ",")
}
}
if sni := query.Get("sni"); sni != "" {
proxy["servername"] = sni

View File

@ -4,8 +4,11 @@ import (
"fmt"
"net"
"strings"
"time"
)
var KeepAliveInterval = 15 * time.Second
func SplitNetworkType(s string) (string, string, error) {
var (
shecme string
@ -44,3 +47,10 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
host, port, err = net.SplitHostPort(temp)
return
}
func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(KeepAliveInterval)
}
}

View File

@ -47,6 +47,7 @@ func (p *Picker[T]) Wait() T {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
return p.result
}
@ -69,6 +70,7 @@ func (p *Picker[T]) Go(f func() (T, error)) {
p.result = ret
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
})
} else {
@ -78,3 +80,13 @@ func (p *Picker[T]) Go(f func() (T, error)) {
}
}()
}
// Close cancels the picker context and releases resources associated with it.
// If Wait has been called, then there is no need to call Close.
func (p *Picker[T]) Close() error {
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@ -15,7 +16,7 @@ func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, err
case <-timer.C:
return input, nil
case <-ctx.Done():
return getZero[T](), ctx.Err()
return lo.Empty[T](), ctx.Err()
}
}
}
@ -35,11 +36,6 @@ func TestPicker_Timeout(t *testing.T) {
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.Equal(t, number, getZero[int]())
assert.Equal(t, number, lo.Empty[int]())
assert.NotNil(t, picker.Error())
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -32,23 +32,32 @@ 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")
}

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

View File

@ -2,6 +2,8 @@ package queue
import (
"sync"
"github.com/samber/lo"
)
// Queue is a simple concurrent safe queue
@ -24,7 +26,7 @@ func (q *Queue[T]) Put(items ...T) {
// Pop returns the head of items.
func (q *Queue[T]) Pop() T {
if len(q.items) == 0 {
return GetZero[T]()
return lo.Empty[T]()
}
q.lock.Lock()
@ -37,7 +39,7 @@ func (q *Queue[T]) Pop() T {
// Last returns the last of item.
func (q *Queue[T]) Last() T {
if len(q.items) == 0 {
return GetZero[T]()
return lo.Empty[T]()
}
q.lock.RLock()
@ -69,8 +71,3 @@ func New[T any](hint int64) *Queue[T] {
items: make([]T, 0, hint),
}
}
func GetZero[T any]() T {
var result T
return result
}

View File

@ -96,6 +96,11 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error {
return d.decodeFloat(name, data, val)
}
switch kind {
case reflect.Pointer:
if val.IsNil() {
val.Set(reflect.New(val.Type().Elem()))
}
return d.decode(name, data, val.Elem())
case reflect.String:
return d.decodeString(name, data, val)
case reflect.Bool:
@ -282,6 +287,9 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
}
valSlice := val
// make a new slice with cap(val)==cap(dataVal)
// the caller can determine whether the original configuration contains this item by judging whether the value is nil.
valSlice = reflect.MakeSlice(valType, 0, dataVal.Len())
for i := 0; i < dataVal.Len(); i++ {
currentData := dataVal.Index(i).Interface()
for valSlice.Len() <= i {

17
common/utils/global_id.go Normal file
View File

@ -0,0 +1,17 @@
package utils
import (
"hash/maphash"
"unsafe"
)
var globalSeed = maphash.MakeSeed()
func GlobalID(material string) (id [8]byte) {
*(*uint64)(unsafe.Pointer(&id[0])) = maphash.String(globalSeed, material)
return
}
func MapHash(material string) uint64 {
return maphash.String(globalSeed, material)
}

View File

@ -9,36 +9,36 @@ type Range[T constraints.Ordered] struct {
end T
}
func NewRange[T constraints.Ordered](start, end T) *Range[T] {
func NewRange[T constraints.Ordered](start, end T) Range[T] {
if start > end {
return &Range[T]{
return Range[T]{
start: end,
end: start,
}
}
return &Range[T]{
return Range[T]{
start: start,
end: end,
}
}
func (r *Range[T]) Contains(t T) bool {
func (r Range[T]) Contains(t T) bool {
return t >= r.start && t <= r.end
}
func (r *Range[T]) LeftContains(t T) bool {
func (r Range[T]) LeftContains(t T) bool {
return t >= r.start && t < r.end
}
func (r *Range[T]) RightContains(t T) bool {
func (r Range[T]) RightContains(t T) bool {
return t > r.start && t <= r.end
}
func (r *Range[T]) Start() T {
func (r Range[T]) Start() T {
return r.start
}
func (r *Range[T]) End() T {
func (r Range[T]) End() T {
return r.end
}

77
common/utils/ranges.go Normal file
View File

@ -0,0 +1,77 @@
package utils
import (
"errors"
"fmt"
"strconv"
"strings"
"golang.org/x/exp/constraints"
)
type IntRanges[T constraints.Integer] []Range[T]
var errIntRanges = errors.New("intRanges error")
func NewIntRanges[T constraints.Integer](expected string) (IntRanges[T], error) {
// example: 200 or 200/302 or 200-400 or 200/204/401-429/501-503
expected = strings.TrimSpace(expected)
if len(expected) == 0 || expected == "*" {
return nil, nil
}
list := strings.Split(expected, "/")
if len(list) > 28 {
return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges)
}
return NewIntRangesFromList[T](list)
}
func NewIntRangesFromList[T constraints.Integer](list []string) (IntRanges[T], error) {
var ranges IntRanges[T]
for _, s := range list {
if s == "" {
continue
}
status := strings.Split(s, "-")
statusLen := len(status)
if statusLen > 2 {
return nil, errIntRanges
}
start, err := strconv.ParseInt(strings.Trim(status[0], "[ ]"), 10, 64)
if err != nil {
return nil, errIntRanges
}
switch statusLen {
case 1:
ranges = append(ranges, NewRange(T(start), T(start)))
case 2:
end, err := strconv.ParseUint(strings.Trim(status[1], "[ ]"), 10, 64)
if err != nil {
return nil, errIntRanges
}
ranges = append(ranges, NewRange(T(start), T(end)))
}
}
return ranges, nil
}
func (ranges IntRanges[T]) Check(status T) bool {
if len(ranges) == 0 {
return true
}
for _, segment := range ranges {
if segment.Contains(status) {
return true
}
}
return false
}

View File

@ -0,0 +1,21 @@
package utils
import "unsafe"
// ImmutableBytesFromString is equivalent to []byte(s), except that it uses the
// same memory backing s instead of making a heap-allocated copy. This is only
// valid if the returned slice is never mutated.
func ImmutableBytesFromString(s string) []byte {
b := unsafe.StringData(s)
return unsafe.Slice(b, len(s))
}
// StringFromImmutableBytes is equivalent to string(bs), except that it uses
// the same memory backing bs instead of making a heap-allocated copy. This is
// only valid if bs is never mutated after StringFromImmutableBytes returns.
func StringFromImmutableBytes(bs []byte) string {
if len(bs) == 0 {
return ""
}
return unsafe.String(&bs[0], len(bs))
}

View File

@ -1,7 +1,7 @@
package auth
import (
"sync"
"github.com/puzpuzpuz/xsync/v2"
)
type Authenticator interface {
@ -15,7 +15,7 @@ type AuthUser struct {
}
type inMemoryAuthenticator struct {
storage *sync.Map
storage *xsync.MapOf[string, string]
usernames []string
}
@ -31,13 +31,13 @@ func NewAuthenticator(users []AuthUser) Authenticator {
return nil
}
au := &inMemoryAuthenticator{storage: &sync.Map{}}
au := &inMemoryAuthenticator{storage: xsync.NewMapOf[string]()}
for _, user := range users {
au.storage.Store(user.User, user.Pass)
}
usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value any) bool {
usernames = append(usernames, key.(string))
au.storage.Range(func(key string, value string) bool {
usernames = append(usernames, key)
return true
})
au.usernames = usernames

View File

@ -1,4 +1,4 @@
package tls
package ca
import (
"bytes"
@ -8,16 +8,15 @@ import (
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"sync"
xtls "github.com/xtls/go"
)
var trustCerts []*x509.Certificate
var certPool *x509.CertPool
var globalCertPool *x509.CertPool
var mutex sync.RWMutex
var errNotMacth error = errors.New("certificate fingerprints do not match")
var errNotMatch = errors.New("certificate fingerprints do not match")
func AddCertificate(certificate string) error {
mutex.Lock()
@ -35,12 +34,12 @@ func AddCertificate(certificate string) error {
func initializeCertPool() {
var err error
certPool, err = x509.SystemCertPool()
globalCertPool, err = x509.SystemCertPool()
if err != nil {
certPool = x509.NewCertPool()
globalCertPool = x509.NewCertPool()
}
for _, cert := range trustCerts {
certPool.AddCert(cert)
globalCertPool.AddCert(cert)
}
}
@ -55,15 +54,15 @@ func getCertPool() *x509.CertPool {
if len(trustCerts) == 0 {
return nil
}
if certPool == nil {
if globalCertPool == nil {
mutex.Lock()
defer mutex.Unlock()
if certPool != nil {
return certPool
if globalCertPool != nil {
return globalCertPool
}
initializeCertPool()
}
return certPool
return globalCertPool
}
func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@ -79,7 +78,7 @@ func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedCh
}
}
}
return errNotMacth
return errNotMatch
}
}
@ -96,53 +95,49 @@ func convertFingerprint(fingerprint string) (*[32]byte, error) {
return (*[32]byte)(fpByte), nil
}
func GetDefaultTLSConfig() *tls.Config {
return GetGlobalTLSConfig(nil)
// GetTLSConfig specified fingerprint, customCA and customCAString
func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (*tls.Config, error) {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
var certificate []byte
var err error
if len(customCA) > 0 {
certificate, err = os.ReadFile(customCA)
if err != nil {
return nil, fmt.Errorf("load ca error: %w", err)
}
} else if customCAString != "" {
certificate = []byte(customCAString)
}
if len(certificate) > 0 {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(certificate) {
return nil, fmt.Errorf("failed to parse certificate:\n\n %s", certificate)
}
tlsConfig.RootCAs = certPool
} else {
tlsConfig.RootCAs = getCertPool()
}
if len(fingerprint) > 0 {
var fingerprintBytes *[32]byte
fingerprintBytes, err = convertFingerprint(fingerprint)
if err != nil {
return nil, err
}
tlsConfig = GetGlobalTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true
}
return tlsConfig, nil
}
// GetSpecifiedFingerprintTLSConfig specified fingerprint
func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) {
if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil {
return nil, err
} else {
tlsConfig = GetGlobalTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true
return tlsConfig, nil
}
return GetTLSConfig(tlsConfig, fingerprint, "", "")
}
func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config {
certPool := getCertPool()
if tlsConfig == nil {
return &tls.Config{
RootCAs: certPool,
}
}
tlsConfig.RootCAs = certPool
return tlsConfig
}
// GetSpecifiedFingerprintXTLSConfig specified fingerprint
func GetSpecifiedFingerprintXTLSConfig(tlsConfig *xtls.Config, fingerprint string) (*xtls.Config, error) {
if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil {
return nil, err
} else {
tlsConfig = GetGlobalXTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true
return tlsConfig, nil
}
}
func GetGlobalXTLSConfig(tlsConfig *xtls.Config) *xtls.Config {
certPool := getCertPool()
if tlsConfig == nil {
return &xtls.Config{
RootCAs: certPool,
}
}
tlsConfig.RootCAs = certPool
tlsConfig, _ = GetTLSConfig(tlsConfig, "", "", "")
return tlsConfig
}

51
component/dialer/bind.go Normal file
View File

@ -0,0 +1,51 @@
package dialer
import (
"net"
"net/netip"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func LookupLocalAddrFromIfaceName(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *netip.Prefix
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination.IsValid() {
if destination.Is4() || destination.Is4In6() {
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.Addr().AsSlice(),
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
}

View File

@ -7,52 +7,8 @@ import (
"net/netip"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return nil, err
}
var addr *netip.Prefix
switch network {
case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination)
default:
if destination.IsValid() {
if destination.Is4() {
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.Addr().AsSlice(),
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
}
return nil, iface.ErrAddrNotFound
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() {
return nil
@ -66,7 +22,7 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, des
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
if err != nil {
return err
}
@ -84,7 +40,7 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
if err != nil {
return "", err
}

View File

@ -20,3 +20,20 @@ func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) {
return fn(context.Background(), network, address, c)
}
}
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
switch {
case ld.ControlContext != nil:
if err = ld.ControlContext(ctx, network, address, c); err != nil {
return
}
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(ctx, network, address, c)
}
}

View File

@ -1,22 +0,0 @@
//go:build !go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.Control = func(network, address string, c syscall.RawConn) (err error) {
switch {
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(context.Background(), network, address, c)
}
}

View File

@ -1,26 +0,0 @@
//go:build go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
switch {
case ld.ControlContext != nil:
if err = ld.ControlContext(ctx, network, address, c); err != nil {
return
}
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(ctx, network, address, c)
}
}

View File

@ -2,6 +2,7 @@ package dialer
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
@ -131,6 +132,9 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
if opt.mpTcp {
setMultiPathTCP(dialer)
}
if opt.tfo {
return dialTFO(ctx, *dialer, network, address)
}
@ -158,14 +162,22 @@ func concurrentDualStackDialContext(ctx context.Context, network string, ips []n
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
ipv4s, ipv6s := resolver.SortationAddr(ips)
preferIPVersion := opt.prefer
if len(ipv4s) == 0 && len(ipv6s) == 0 {
return nil, ErrorNoIpAddress
}
preferIPVersion := opt.prefer
fallbackTicker := time.NewTicker(fallbackTimeout)
defer fallbackTicker.Stop()
results := make(chan dialResult)
returned := make(chan struct{})
defer close(returned)
var wg sync.WaitGroup
racer := func(ips []netip.Addr, isPrimary bool) {
defer wg.Done()
result := dialResult{isPrimary: isPrimary}
defer func() {
select {
@ -178,18 +190,36 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
}()
result.Conn, result.error = dialFn(ctx, network, ips, port, opt)
}
go racer(ipv4s, preferIPVersion != 6)
go racer(ipv6s, preferIPVersion != 4)
if len(ipv4s) != 0 {
wg.Add(1)
go racer(ipv4s, preferIPVersion != 6)
}
if len(ipv6s) != 0 {
wg.Add(1)
go racer(ipv6s, preferIPVersion != 4)
}
go func() {
wg.Wait()
close(results)
}()
var fallback dialResult
var errs []error
for i := 0; i < 2; {
loop:
for {
select {
case <-fallbackTicker.C:
if fallback.error == nil && fallback.Conn != nil {
return fallback.Conn, nil
}
case res := <-results:
i++
case res, ok := <-results:
if !ok {
break loop
}
if res.error == nil {
if res.isPrimary {
return res.Conn, nil
@ -204,10 +234,11 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
}
}
}
if fallback.error == nil && fallback.Conn != nil {
return fallback.Conn, nil
}
return nil, errorsJoin(errs...)
return nil, errors.Join(errs...)
}
func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
@ -244,7 +275,7 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr,
}
if len(errs) > 0 {
return nil, errorsJoin(errs...)
return nil, errors.Join(errs...)
}
return nil, os.ErrDeadlineExceeded
}
@ -261,7 +292,7 @@ func serialDialContext(ctx context.Context, network string, ips []netip.Addr, po
errs = append(errs, err)
}
}
return nil, errorsJoin(errs...)
return nil, errors.Join(errs...)
}
type dialResult struct {

View File

@ -2,17 +2,9 @@ package dialer
import (
"errors"
E "github.com/sagernet/sing/common/exceptions"
)
var (
ErrorNoIpAddress = errors.New("no ip address")
ErrorInvalidedNetworkStack = errors.New("invalided network stack")
)
func errorsJoin(errs ...error) error {
// compatibility with golang<1.20
// maybe use errors.Join(errs...) is better after we drop the old version's support
return E.Errors(errs...)
}

View File

@ -0,0 +1,12 @@
//go:build !go1.21
package dialer
import (
"net"
)
const multipathTCPAvailable = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@ -0,0 +1,11 @@
//go:build go1.21
package dialer
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)
}

View File

@ -25,6 +25,7 @@ type option struct {
network int
prefer int
tfo bool
mpTcp bool
resolver resolver.Resolver
netDialer NetDialer
}
@ -83,6 +84,12 @@ func WithTFO(tfo bool) Option {
}
}
func WithMPTCP(mpTcp bool) Option {
return func(opt *option) {
opt.mpTcp = mpTcp
}
}
func WithNetDialer(netDialer NetDialer) Option {
return func(opt *option) {
opt.netDialer = netDialer

View File

@ -2,6 +2,7 @@ package http
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
@ -9,15 +10,13 @@ import (
"strings"
"time"
"github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/component/ca"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/inner"
)
const (
UA = "clash.meta"
)
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
UA := C.UA
method = strings.ToUpper(method)
urlRes, err := URL.Parse(url)
if err != nil {
@ -60,7 +59,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
return d.DialContext(ctx, network, address)
}
},
TLSClientConfig: tls.GetDefaultTLSConfig(),
TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}),
}
client := http.Client{Transport: transport}

View File

@ -4,6 +4,7 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"
"github.com/Dreamacro/clash/common/singledo"
@ -37,12 +38,21 @@ func ResolveInterface(name string) (*Interface, error) {
if err != nil {
continue
}
// if not available device like Meta, dummy0, docker0, etc.
if (iface.Flags&net.FlagMulticast == 0) || (iface.Flags&net.FlagPointToPoint != 0) || (iface.Flags&net.FlagRunning == 0) {
continue
}
ipNets := make([]*netip.Prefix, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
ip, _ := netip.AddrFromSlice(ipNet.IP)
//unavailable IPv6 Address
if ip.Is6() && strings.HasPrefix(ip.String(), "fe80") {
continue
}
ones, bits := ipNet.Mask.Size()
if bits == 32 {
ip = ip.Unmap()

View File

@ -12,42 +12,68 @@ import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/oschwald/geoip2-golang"
"github.com/oschwald/maxminddb-golang"
)
type databaseType = uint8
const (
typeMaxmind databaseType = iota
typeSing
typeMetaV0
)
var (
mmdb *geoip2.Reader
once sync.Once
reader Reader
once sync.Once
)
func LoadFromBytes(buffer []byte) {
once.Do(func() {
var err error
mmdb, err = geoip2.FromBytes(buffer)
mmdb, err := maxminddb.FromBytes(buffer)
if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error())
}
reader = Reader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
reader.databaseType = typeSing
case "Meta-geoip0":
reader.databaseType = typeMetaV0
default:
reader.databaseType = typeMaxmind
}
})
}
func Verify() bool {
instance, err := geoip2.Open(C.Path.MMDB())
instance, err := maxminddb.Open(C.Path.MMDB())
if err == nil {
instance.Close()
}
return err == nil
}
func Instance() *geoip2.Reader {
func Instance() Reader {
once.Do(func() {
var err error
mmdb, err = geoip2.Open(C.Path.MMDB())
mmdbPath := C.Path.MMDB()
log.Debugln("Load MMDB file: %s", mmdbPath)
mmdb, err := maxminddb.Open(mmdbPath)
if err != nil {
log.Fatalln("Can't load mmdb: %s", err.Error())
log.Fatalln("Can't load MMDB: %s", err.Error())
}
reader = Reader{Reader: mmdb}
switch mmdb.Metadata.DatabaseType {
case "sing-geoip":
reader.databaseType = typeSing
case "Meta-geoip0":
reader.databaseType = typeMetaV0
default:
reader.databaseType = typeMaxmind
}
})
return mmdb
return reader
}
func DownloadMMDB(path string) (err error) {

56
component/mmdb/reader.go Normal file
View File

@ -0,0 +1,56 @@
package mmdb
import (
"fmt"
"net"
"github.com/oschwald/maxminddb-golang"
"github.com/sagernet/sing/common"
)
type geoip2Country struct {
Country struct {
IsoCode string `maxminddb:"iso_code"`
} `maxminddb:"country"`
}
type Reader struct {
*maxminddb.Reader
databaseType
}
func (r Reader) LookupCode(ipAddress net.IP) []string {
switch r.databaseType {
case typeMaxmind:
var country geoip2Country
_ = r.Lookup(ipAddress, &country)
if country.Country.IsoCode == "" {
return []string{}
}
return []string{country.Country.IsoCode}
case typeSing:
var code string
_ = r.Lookup(ipAddress, &code)
if code == "" {
return []string{}
}
return []string{code}
case typeMetaV0:
var record any
_ = r.Lookup(ipAddress, &record)
switch record := record.(type) {
case string:
return []string{record}
case []any: // lookup returned type of slice is []any
return common.Map(record, func(it any) string {
return it.(string)
})
}
return []string{}
default:
panic(fmt.Sprint("unknown geoip database type:", r.databaseType))
}
}

26
component/nat/proxy.go Normal file
View File

@ -0,0 +1,26 @@
package nat
import (
"net"
"github.com/Dreamacro/clash/common/atomic"
C "github.com/Dreamacro/clash/constant"
)
type writeBackProxy struct {
wb atomic.TypedValue[C.WriteBack]
}
func (w *writeBackProxy) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return w.wb.Load().WriteBack(b, addr)
}
func (w *writeBackProxy) UpdateWriteBack(wb C.WriteBack) {
w.wb.Store(wb)
}
func NewWriteBackProxy(wb C.WriteBack) C.WriteBackProxy {
w := &writeBackProxy{}
w.UpdateWriteBack(wb)
return w
}

View File

@ -5,42 +5,53 @@ import (
"sync"
C "github.com/Dreamacro/clash/constant"
"github.com/puzpuzpuz/xsync/v2"
)
type Table struct {
mapping sync.Map
mapping *xsync.MapOf[string, *Entry]
lockMap *xsync.MapOf[string, *sync.Cond]
}
type Entry struct {
PacketConn C.PacketConn
LocalUDPConnMap sync.Map
WriteBackProxy C.WriteBackProxy
LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn]
LocalLockMap *xsync.MapOf[string, *sync.Cond]
}
func (t *Table) Set(key string, e C.PacketConn) {
func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) {
t.mapping.Store(key, &Entry{
PacketConn: e,
LocalUDPConnMap: sync.Map{},
WriteBackProxy: w,
LocalUDPConnMap: xsync.NewMapOf[*net.UDPConn](),
LocalLockMap: xsync.NewMapOf[*sync.Cond](),
})
}
func (t *Table) Get(key string) C.PacketConn {
func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) {
entry, exist := t.getEntry(key)
if !exist {
return nil
return nil, nil
}
return entry.PacketConn
return entry.PacketConn, entry.WriteBackProxy
}
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {
item, loaded := t.mapping.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item.(*sync.Cond), loaded
item, loaded := t.lockMap.LoadOrCompute(key, makeLock)
return item, loaded
}
func (t *Table) Delete(key string) {
t.mapping.Delete(key)
}
func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn {
func (t *Table) DeleteLock(lockKey string) {
t.lockMap.Delete(lockKey)
}
func (t *Table) GetForLocalConn(lAddr, rAddr string) *net.UDPConn {
entry, exist := t.getEntry(lAddr)
if !exist {
return nil
@ -49,10 +60,10 @@ func (t *Table) GetLocalConn(lAddr, rAddr string) *net.UDPConn {
if !exist {
return nil
}
return item.(*net.UDPConn)
return item
}
func (t *Table) AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
func (t *Table) AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
entry, exist := t.getEntry(lAddr)
if !exist {
return false
@ -61,7 +72,7 @@ func (t *Table) AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool {
return true
}
func (t *Table) RangeLocalConn(lAddr string, f func(key, value any) bool) {
func (t *Table) RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool) {
entry, exist := t.getEntry(lAddr)
if !exist {
return
@ -74,11 +85,11 @@ func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool
if !loaded {
return nil, false
}
item, loaded := entry.LocalUDPConnMap.LoadOrStore(key, sync.NewCond(&sync.Mutex{}))
return item.(*sync.Cond), loaded
item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock)
return item, loaded
}
func (t *Table) DeleteLocalConnMap(lAddr, key string) {
func (t *Table) DeleteForLocalConn(lAddr, key string) {
entry, loaded := t.getEntry(lAddr)
if !loaded {
return
@ -86,17 +97,26 @@ func (t *Table) DeleteLocalConnMap(lAddr, key string) {
entry.LocalUDPConnMap.Delete(key)
}
func (t *Table) getEntry(key string) (*Entry, bool) {
item, ok := t.mapping.Load(key)
// This should not happen usually since this function called after PacketConn created
if !ok {
return nil, false
func (t *Table) DeleteLockForLocalConn(lAddr, key string) {
entry, loaded := t.getEntry(lAddr)
if !loaded {
return
}
entry, ok := item.(*Entry)
return entry, ok
entry.LocalLockMap.Delete(key)
}
func (t *Table) getEntry(key string) (*Entry, bool) {
return t.mapping.Load(key)
}
func makeLock() *sync.Cond {
return sync.NewCond(&sync.Mutex{})
}
// New return *Cache
func New() *Table {
return &Table{}
return &Table{
mapping: xsync.NewMapOf[*Entry](),
lockMap: xsync.NewMapOf[*sync.Cond](),
}
}

View File

@ -67,7 +67,7 @@ func findProcessName(network string, ip netip.Addr, srcPort int) (uint32, string
err := initWin32API()
if err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
log.Warnln("All PROCESS-NAMES rules will be skiped")
log.Warnln("All PROCESS-NAMES rules will be skipped")
return
}
})

View File

@ -0,0 +1,82 @@
package proxydialer
import (
"context"
"net"
C "github.com/Dreamacro/clash/constant"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingDialer interface {
N.Dialer
SetDialer(dialer C.Dialer)
}
type singDialer proxyDialer
var _ N.Dialer = (*singDialer)(nil)
func (d *singDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return (*proxyDialer)(d).DialContext(ctx, network, destination.String())
}
func (d *singDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return (*proxyDialer)(d).ListenPacket(ctx, "udp", "", destination.AddrPort())
}
func (d *singDialer) SetDialer(dialer C.Dialer) {
(*proxyDialer)(d).dialer = dialer
}
func NewSingDialer(proxy C.ProxyAdapter, dialer C.Dialer, statistic bool) SingDialer {
return (*singDialer)(&proxyDialer{
proxy: proxy,
dialer: dialer,
statistic: statistic,
})
}
type byNameSingDialer struct {
dialer C.Dialer
proxyName string
}
var _ N.Dialer = (*byNameSingDialer)(nil)
func (d *byNameSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
var cDialer C.Dialer = d.dialer
if len(d.proxyName) > 0 {
pd, err := NewByName(d.proxyName, d.dialer)
if err != nil {
return nil, err
}
cDialer = pd
}
return cDialer.DialContext(ctx, network, destination.String())
}
func (d *byNameSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
var cDialer C.Dialer = d.dialer
if len(d.proxyName) > 0 {
pd, err := NewByName(d.proxyName, d.dialer)
if err != nil {
return nil, err
}
cDialer = pd
}
return cDialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
}
func (d *byNameSingDialer) SetDialer(dialer C.Dialer) {
d.dialer = dialer
}
func NewByNameSingDialer(proxyName string, dialer C.Dialer) SingDialer {
return &byNameSingDialer{
dialer: dialer,
proxyName: proxyName,
}
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"net/netip"
"strings"
_ "unsafe"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/trie"
@ -20,28 +21,39 @@ func NewHosts(hosts *trie.DomainTrie[HostValue]) Hosts {
}
}
// lookupStaticHost looks up the addresses and the canonical name for the given host from /etc/hosts.
//
//go:linkname lookupStaticHost net.lookupStaticHost
func lookupStaticHost(host string) ([]string, string)
// Return the search result and whether to match the parameter `isDomain`
func (h *Hosts) Search(domain string, isDomain bool) (*HostValue, bool) {
value := h.DomainTrie.Search(domain)
if value == nil {
return nil, false
}
hostValue := value.Data()
for {
if isDomain && hostValue.IsDomain {
return &hostValue, true
} else {
if node := h.DomainTrie.Search(hostValue.Domain); node != nil {
hostValue = node.Data()
if value := h.DomainTrie.Search(domain); value != nil {
hostValue := value.Data()
for {
if isDomain && hostValue.IsDomain {
return &hostValue, true
} else {
break
if node := h.DomainTrie.Search(hostValue.Domain); node != nil {
hostValue = node.Data()
} else {
break
}
}
}
if isDomain == hostValue.IsDomain {
return &hostValue, true
}
return &hostValue, false
}
if isDomain == hostValue.IsDomain {
return &hostValue, true
if !isDomain {
addr, _ := lookupStaticHost(domain)
if hostValue, err := NewHostValue(addr); err == nil {
return &hostValue, true
}
}
return &hostValue, false
return nil, false
}
type HostValue struct {

View File

@ -0,0 +1,19 @@
//go:build !go1.22
// a simple standard lib fix from: https://github.com/golang/go/commit/33d4a5105cf2b2d549922e909e9239a48b8cefcc
package resolver
import (
"golang.org/x/sys/windows"
_ "unsafe"
)
//go:linkname testHookHostsPath net.testHookHostsPath
var testHookHostsPath string
func init() {
if dir, err := windows.GetSystemDirectory(); err == nil {
testHookHostsPath = dir + "/Drivers/etc/hosts"
}
}

View File

@ -9,6 +9,8 @@ import (
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
"github.com/samber/lo"
)
var (
@ -65,7 +67,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
}
if err != nil {
return getZero[V](), err
return lo.Empty[V](), err
}
var contents V
@ -85,18 +87,18 @@ func (f *Fetcher[V]) Initial() (V, error) {
if err != nil {
if !isLocal {
return getZero[V](), err
return lo.Empty[V](), err
}
// parse local file error, fallback to remote
buf, err = f.vehicle.Read()
if err != nil {
return getZero[V](), err
return lo.Empty[V](), err
}
contents, err = f.parser(buf)
if err != nil {
return getZero[V](), err
return lo.Empty[V](), err
}
isLocal = false
@ -104,7 +106,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return getZero[V](), err
return lo.Empty[V](), err
}
}
@ -121,7 +123,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
func (f *Fetcher[V]) Update() (V, bool, error) {
buf, err := f.vehicle.Read()
if err != nil {
return getZero[V](), false, err
return lo.Empty[V](), false, err
}
now := time.Now()
@ -129,17 +131,17 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
if bytes.Equal(f.hash[:], hash[:]) {
f.UpdatedAt = &now
_ = os.Chtimes(f.vehicle.Path(), now, now)
return getZero[V](), true, nil
return lo.Empty[V](), true, nil
}
contents, err := f.parser(buf)
if err != nil {
return getZero[V](), false, err
return lo.Empty[V](), false, err
}
if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return getZero[V](), false, err
return lo.Empty[V](), false, err
}
}
@ -210,8 +212,3 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
interval: interval,
}
}
func getZero[V any]() V {
var result V
return result
}

View File

@ -2,12 +2,14 @@ package resource
import (
"context"
clashHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
"errors"
"io"
"net/http"
"os"
"time"
clashHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
)
type FileVehicle struct {
@ -54,8 +56,10 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, errors.New(resp.Status)
}
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err

View File

@ -10,11 +10,11 @@ import (
type SnifferConfig struct {
OverrideDest bool
Ports []utils.Range[uint16]
Ports utils.IntRanges[uint16]
}
type BaseSniffer struct {
ports []utils.Range[uint16]
ports utils.IntRanges[uint16]
supportNetworkType constant.NetWork
}
@ -35,15 +35,10 @@ func (bs *BaseSniffer) SupportNetwork() constant.NetWork {
// SupportPort implements sniffer.Sniffer
func (bs *BaseSniffer) SupportPort(port uint16) bool {
for _, portRange := range bs.ports {
if portRange.Contains(port) {
return true
}
}
return false
return bs.ports.Check(port)
}
func NewBaseSniffer(ports []utils.Range[uint16], networkType constant.NetWork) *BaseSniffer {
func NewBaseSniffer(ports utils.IntRanges[uint16], networkType constant.NetWork) *BaseSniffer {
return &BaseSniffer{
ports: ports,
supportNetworkType: networkType,

View File

@ -5,7 +5,6 @@ import (
"fmt"
"net"
"net/netip"
"strconv"
"sync"
"time"
@ -26,29 +25,24 @@ var (
var Dispatcher *SnifferDispatcher
type SnifferDispatcher struct {
enable bool
sniffers map[sniffer.Sniffer]SnifferConfig
forceDomain *trie.DomainSet
skipSNI *trie.DomainSet
skipList *cache.LruCache[string, uint8]
rwMux sync.RWMutex
forceDnsMapping bool
parsePureIp bool
enable bool
sniffers map[sniffer.Sniffer]SnifferConfig
forceDomain *trie.DomainSet
skipSNI *trie.DomainSet
skipList *cache.LruCache[string, uint8]
rwMux sync.RWMutex
forceDnsMapping bool
parsePureIp bool
}
func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) {
// TCPSniff returns true if the connection is sniffed to have a domain
func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool {
if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) {
port, err := strconv.ParseUint(metadata.DstPort, 10, 16)
if err != nil {
log.Debugln("[Sniffer] Dst port is error")
return
}
inWhitelist := false
overrideDest := false
for sniffer, config := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet {
inWhitelist = sniffer.SupportPort(uint16(port))
inWhitelist = sniffer.SupportPort(metadata.DstPort)
if inWhitelist {
overrideDest = config.OverrideDest
break
@ -57,26 +51,26 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
}
if !inWhitelist {
return
return false
}
sd.rwMux.RLock()
dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort)
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
if count, ok := sd.skipList.Get(dst); ok && count > 5 {
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
defer sd.rwMux.RUnlock()
return
return false
}
sd.rwMux.RUnlock()
if host, err := sd.sniffDomain(conn, metadata); err != nil {
sd.cacheSniffFailed(metadata)
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return false
} else {
if sd.skipSNI.Has(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return
return false
}
sd.rwMux.RLock()
@ -84,20 +78,23 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
sd.rwMux.RUnlock()
sd.replaceDomain(metadata, host, overrideDest)
return true
}
}
return false
}
func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
// show log early, since the following code may mutate `metadata.Host`
log.Debugln("[Sniffer] Sniff TCP [%s]-->[%s] success, replace domain [%s]-->[%s]",
metadata.SourceDetail(),
metadata.RemoteAddress(),
metadata.Host, host)
metadata.SniffHost = host
if overrideDest {
metadata.Host = host
}
metadata.DNSMode = C.DNSNormal
log.Debugln("[Sniffer] Sniff TCP [%s]-->[%s] success, replace domain [%s]-->[%s]",
metadata.SourceDetail(),
metadata.RemoteAddress(),
metadata.Host, host)
}
func (sd *SnifferDispatcher) Enable() bool {
@ -149,7 +146,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) {
sd.rwMux.Lock()
dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort)
dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
count, _ := sd.skipList.Get(dst)
if count <= 5 {
count++

View File

@ -34,11 +34,9 @@ type HTTPSniffer struct {
var _ sniffer.Sniffer = (*HTTPSniffer)(nil)
func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) {
ports := make([]utils.Range[uint16], 0)
if len(snifferConfig.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](80, 80))
} else {
ports = append(ports, snifferConfig.Ports...)
ports := snifferConfig.Ports
if len(ports) == 0 {
ports = utils.IntRanges[uint16]{utils.NewRange[uint16](80, 80)}
}
return &HTTPSniffer{
BaseSniffer: NewBaseSniffer(ports, C.TCP),

View File

@ -22,11 +22,9 @@ type TLSSniffer struct {
}
func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) {
ports := make([]utils.Range[uint16], 0)
if len(snifferConfig.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](443, 443))
} else {
ports = append(ports, snifferConfig.Ports...)
ports := snifferConfig.Ports
if len(ports) == 0 {
ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)}
}
return &TLSSniffer{
BaseSniffer: NewBaseSniffer(ports, C.TCP),

View File

@ -22,9 +22,11 @@ import (
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/ntp"
utls "github.com/sagernet/utls"
"github.com/zhangyunhao116/fastrand"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"golang.org/x/net/http2"
@ -37,6 +39,9 @@ type RealityConfig struct {
ShortID [RealityMaxShortIDLen]byte
}
//go:linkname aesgcmPreferred crypto/tls.aesgcmPreferred
func aesgcmPreferred(ciphers []uint16) bool
func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
if fingerprint, exists := GetFingerprint(ClientFingerprint); exists {
verifier := &realityVerifier{
@ -61,17 +66,17 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
}
hello := uConn.HandshakeState.Hello
for i := range hello.SessionId { // https://github.com/golang/go/issues/5373
hello.SessionId[i] = 0
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
for i := range rawSessionID { // https://github.com/golang/go/issues/5373
rawSessionID[i] = 0
}
copy(hello.Raw[39:], hello.SessionId)
binary.BigEndian.PutUint64(hello.SessionId, uint64(time.Now().Unix()))
binary.BigEndian.PutUint64(hello.SessionId, uint64(ntp.Now().Unix()))
copy(hello.SessionId[8:], realityConfig.ShortID[:])
hello.SessionId[0] = 1
hello.SessionId[1] = 8
hello.SessionId[2] = 0
copy(hello.SessionId[8:], realityConfig.ShortID[:])
hello.SessionId[2] = 2
//log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16])
@ -84,9 +89,14 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
if err != nil {
return nil, err
}
aesBlock, _ := aes.NewCipher(authKey)
aesGcmCipher, _ := cipher.NewGCM(aesBlock)
aesGcmCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)
var aeadCipher cipher.AEAD
if aesgcmPreferred(hello.CipherSuites) {
aesBlock, _ := aes.NewCipher(authKey)
aeadCipher, _ = cipher.NewGCM(aesBlock)
} else {
aeadCipher, _ = chacha20poly1305.New(authKey)
}
aeadCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw)
copy(hello.Raw[39:], hello.SessionId)
//log.Debugln("REALITY hello.sessionId: %v", hello.SessionId)
//log.Debugln("REALITY uConn.AuthKey: %v", authKey)
@ -96,7 +106,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
return nil, err
}
log.Debugln("REALITY Authentication: %v", verifier.verified)
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
if !verifier.verified {
go realityClientFallback(uConn, uConfig.ServerName, clientID)
@ -137,7 +147,7 @@ type realityVerifier struct {
verified bool
}
var pOffset = utils.MustOK(reflect.TypeOf((*utls.UConn)(nil)).Elem().FieldByName("peerCertificates")).Offset
var pOffset = utils.MustOK(reflect.TypeOf((*utls.Conn)(nil)).Elem().FieldByName("peerCertificates")).Offset
func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
//p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates")

View File

@ -23,6 +23,8 @@ type DomainSet struct {
ranks, selects []int32
}
type qElt struct{ s, e, col int }
// NewDomainSet creates a new *DomainSet struct, from a DomainTrie.
func (t *DomainTrie[T]) NewDomainSet() *DomainSet {
reserveDomains := make([]string, 0)
@ -39,7 +41,6 @@ func (t *DomainTrie[T]) NewDomainSet() *DomainSet {
ss := &DomainSet{}
lIdx := 0
type qElt struct{ s, e, col int }
queue := []qElt{{0, len(keys), 0}}
for i := 0; i < len(queue); i++ {
elt := queue[i]

View File

@ -1,8 +1,9 @@
package trie
import (
"github.com/Dreamacro/clash/log"
"net"
"github.com/Dreamacro/clash/log"
)
type IPV6 bool
@ -47,11 +48,10 @@ func (trie *IpCidrTrie) AddIpCidrForString(ipCidr string) error {
}
func (trie *IpCidrTrie) IsContain(ip net.IP) bool {
ip, isIpv4 := checkAndConverterIp(ip)
if ip == nil {
return false
}
isIpv4 := len(ip) == net.IPv4len
var groupValues []uint32
var ipCidrNode *IpCidrNode
@ -71,7 +71,13 @@ func (trie *IpCidrTrie) IsContain(ip net.IP) bool {
}
func (trie *IpCidrTrie) IsContainForString(ipString string) bool {
return trie.IsContain(net.ParseIP(ipString))
ip := net.ParseIP(ipString)
// deal with 4in6
actualIp := ip.To4()
if actualIp == nil {
actualIp = ip
}
return trie.IsContain(actualIp)
}
func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) {
@ -82,9 +88,8 @@ func ipCidrToSubIpCidr(ipNet *net.IPNet) ([]net.IP, int, bool, error) {
isIpv4 bool
err error
)
ip, isIpv4 := checkAndConverterIp(ipNet.IP)
ipList, newMaskSize, err = subIpCidr(ip, maskSize, isIpv4)
isIpv4 = len(ipNet.IP) == net.IPv4len
ipList, newMaskSize, err = subIpCidr(ipNet.IP, maskSize, isIpv4)
return ipList, newMaskSize, isIpv4, err
}
@ -238,18 +243,3 @@ func search(root *IpCidrNode, groupValues []uint32) *IpCidrNode {
return nil
}
// return net.IP To4 or To16 and is ipv4
func checkAndConverterIp(ip net.IP) (net.IP, bool) {
ipResult := ip.To4()
if ipResult == nil {
ipResult = ip.To16()
if ipResult == nil {
return nil, false
}
return ipResult, false
}
return ipResult, true
}

View File

@ -3,8 +3,9 @@ package trie
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
)
import "github.com/stretchr/testify/assert"
func TestIpv4AddSuccess(t *testing.T) {
trie := NewIpCidrTrie()
@ -96,5 +97,11 @@ func TestIpv6Search(t *testing.T) {
assert.Equal(t, true, trie.IsContainForString("2001:67c:4e8:9666::1213"))
assert.Equal(t, false, trie.IsContain(net.ParseIP("22233:22")))
}
func TestIpv4InIpv6(t *testing.T) {
trie := NewIpCidrTrie()
// Boundary testing
assert.NoError(t, trie.AddIpCidrForString("::ffff:198.18.5.138/128"))
}

View File

@ -8,8 +8,8 @@ import (
"net/netip"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"time"
@ -17,6 +17,7 @@ import (
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/dialer"
@ -52,6 +53,7 @@ type General struct {
IPv6 bool `json:"ipv6"`
Interface string `json:"interface-name"`
RoutingMark int `json:"-"`
GeoXUrl GeoXUrl `json:"geox-url"`
GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"`
@ -59,6 +61,7 @@ type General struct {
Sniffing bool `json:"sniffing"`
EBpf EBpf `json:"-"`
GlobalClientFingerprint string `json:"global-client-fingerprint"`
GlobalUA string `json:"global-ua"`
}
// Inbound config
@ -76,6 +79,7 @@ type Inbound struct {
AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"`
InboundTfo bool `json:"inbound-tfo"`
InboundMPTCP bool `json:"inbound-mptcp"`
}
// Controller config
@ -86,6 +90,16 @@ type Controller struct {
Secret string `json:"-"`
}
// NTP config
type NTP struct {
Enable bool `yaml:"enable"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Interval int `yaml:"interval"`
DialerProxy string `yaml:"dialer-proxy"`
WriteToSystem bool `yaml:"write-to-system"`
}
// DNS config
type DNS struct {
Enable bool `yaml:"enable"`
@ -143,13 +157,15 @@ type Sniffer struct {
// Experimental config
type Experimental struct {
Fingerprints []string `yaml:"fingerprints"`
Fingerprints []string `yaml:"fingerprints"`
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"`
}
// Config is clash config manager
type Config struct {
General *General
IPTables *IPTables
NTP *NTP
DNS *DNS
Experimental *Experimental
Hosts *trie.DomainTrie[resolver.HostValue]
@ -166,6 +182,15 @@ type Config struct {
TLS *TLS
}
type RawNTP struct {
Enable bool `yaml:"enable"`
Server string `yaml:"server"`
ServerPort int `yaml:"server-port"`
Interval int `yaml:"interval"`
DialerProxy string `yaml:"dialer-proxy"`
WriteToSystem bool `yaml:"write-to-system"`
}
type RawDNS struct {
Enable bool `yaml:"enable"`
PreferH3 bool `yaml:"prefer-h3"`
@ -220,16 +245,18 @@ type RawTun struct {
}
type RawTuicServer struct {
Enable bool `yaml:"enable" json:"enable"`
Listen string `yaml:"listen" json:"listen"`
Token []string `yaml:"token" json:"token"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
Enable bool `yaml:"enable" json:"enable"`
Listen string `yaml:"listen" json:"listen"`
Token []string `yaml:"token" json:"token"`
Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
}
type RawConfig struct {
@ -241,6 +268,7 @@ type RawConfig struct {
ShadowSocksConfig string `yaml:"ss-config"`
VmessConfig string `yaml:"vmess-config"`
InboundTfo bool `yaml:"inbound-tfo"`
InboundMPTCP bool `yaml:"inbound-mptcp"`
Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"`
@ -251,6 +279,8 @@ type RawConfig struct {
ExternalController string `yaml:"external-controller"`
ExternalControllerTLS string `yaml:"external-controller-tls"`
ExternalUI string `yaml:"external-ui"`
ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"`
ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"`
Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"`
RoutingMark int `yaml:"routing-mark"`
@ -260,11 +290,14 @@ type RawConfig struct {
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
FindProcessMode P.FindProcessMode `yaml:"find-process-mode" json:"find-process-mode"`
GlobalClientFingerprint string `yaml:"global-client-fingerprint"`
GlobalUA string `yaml:"global-ua"`
KeepAliveInterval int `yaml:"keep-alive-interval"`
Sniffer RawSniffer `yaml:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]any `yaml:"hosts"`
NTP RawNTP `yaml:"ntp"`
DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"`
TuicServer RawTuicServer `yaml:"tuic-server"`
@ -272,7 +305,7 @@ type RawConfig struct {
IPTables IPTables `yaml:"iptables"`
Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"`
GeoXUrl RawGeoXUrl `yaml:"geox-url"`
GeoXUrl GeoXUrl `yaml:"geox-url"`
Proxy []map[string]any `yaml:"proxies"`
ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"`
@ -281,7 +314,7 @@ type RawConfig struct {
Listeners []map[string]any `yaml:"listeners"`
}
type RawGeoXUrl struct {
type GeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"`
GeoSite string `yaml:"geosite" json:"geosite"`
@ -344,6 +377,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
ProxyGroup: []map[string]any{},
TCPConcurrent: false,
FindProcessMode: P.FindProcessStrict,
GlobalUA: "clash.meta",
Tun: RawTun{
Enable: false,
Device: "",
@ -356,6 +390,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
TuicServer: RawTuicServer{
Enable: false,
Token: nil,
Users: nil,
Certificate: "",
PrivateKey: "",
Listen: "",
@ -374,6 +409,13 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
InboundInterface: "lo",
Bypass: []string{},
},
NTP: RawNTP{
Enable: false,
WriteToSystem: false,
Server: "time.apple.com",
ServerPort: 123,
Interval: 30,
},
DNS: RawDNS{
Enable: false,
IPv6: false,
@ -416,11 +458,12 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Profile: Profile{
StoreSelected: true,
},
GeoXUrl: RawGeoXUrl{
Mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb",
GeoIp: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat",
GeoSite: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat",
GeoXUrl: GeoXUrl{
Mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb",
GeoIp: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat",
GeoSite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat",
},
ExternalUIURL: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
}
if err := yaml.Unmarshal(buf, rawCfg); err != nil {
@ -446,7 +489,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.General = general
if len(config.General.GlobalClientFingerprint) != 0 {
log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint)
log.Debugln("GlobalClientFingerprint: %s", config.General.GlobalClientFingerprint)
tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint)
}
@ -488,6 +531,9 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.Hosts = hosts
ntpCfg := paresNTP(rawCfg)
config.NTP = ntpCfg
dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders)
if err != nil {
return nil, err
@ -528,15 +574,40 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
func parseGeneral(cfg *RawConfig) (*General, error) {
externalUI := cfg.ExternalUI
geodata.SetLoader(cfg.GeodataLoader)
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
C.MmdbUrl = cfg.GeoXUrl.Mmdb
C.GeodataMode = cfg.GeodataMode
C.UA = cfg.GlobalUA
if cfg.KeepAliveInterval != 0 {
N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second
}
ExternalUIPath = cfg.ExternalUI
// checkout externalUI exist
if externalUI != "" {
externalUI = C.Path.Resolve(externalUI)
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
if ExternalUIPath != "" {
ExternalUIPath = C.Path.Resolve(ExternalUIPath)
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
defaultUIpath := path.Join(C.Path.HomeDir(), "ui")
log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath)
if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil {
return nil, err
}
ExternalUIPath = defaultUIpath
cfg.ExternalUI = defaultUIpath
}
}
// checkout UIpath/name exist
if cfg.ExternalUIName != "" {
ExternalUIName = cfg.ExternalUIName
} else {
ExternalUIFolder = ExternalUIPath
}
if cfg.ExternalUIURL != "" {
ExternalUIURL = cfg.ExternalUIURL
}
cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun
return &General{
Inbound: Inbound{
@ -550,6 +621,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress,
InboundTfo: cfg.InboundTfo,
InboundMPTCP: cfg.InboundMPTCP,
},
Controller: Controller{
ExternalController: cfg.ExternalController,
@ -563,12 +635,14 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
IPv6: cfg.IPv6,
Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark,
GeoXUrl: cfg.GeoXUrl,
GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent,
FindProcessMode: cfg.FindProcessMode,
EBpf: cfg.EBpf,
GlobalClientFingerprint: cfg.GlobalClientFingerprint,
GlobalUA: cfg.GlobalUA,
}, nil
}
@ -655,7 +729,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
}
ps = append(ps, proxies[v])
}
hc := provider.NewHealthCheck(ps, "", 0, true)
hc := provider.NewHealthCheck(ps, "", 0, true, nil)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)
providersMap[provider.ReservedName] = pd
@ -710,6 +784,9 @@ func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.
func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) {
subRules = map[string][]C.Rule{}
for name := range cfg.SubRules {
subRules[name] = make([]C.Rule, 0)
}
for name, rawRules := range cfg.SubRules {
if len(name) == 0 {
return nil, fmt.Errorf("sub-rule name is empty")
@ -914,7 +991,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
addr, err = hostWithDefaultPort(u.Host, "443")
if err == nil {
proxyName = ""
clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path}
clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path, User: u.User}
addr = clearURL.String()
dnsNetType = "https" // DNS over HTTPS
if len(u.Fragment) != 0 {
@ -940,6 +1017,19 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
dnsNetType = "quic" // DNS over QUIC
case "system":
dnsNetType = "system" // System DNS
case "rcode":
dnsNetType = "rcode"
addr = u.Host
switch addr {
case "success",
"format_error",
"server_failure",
"name_error",
"not_implemented",
"refused":
default:
err = fmt.Errorf("unsupported RCode type: %s", addr)
}
default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
}
@ -1105,6 +1195,19 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil
}
func paresNTP(rawCfg *RawConfig) *NTP {
cfg := rawCfg.NTP
ntpCfg := &NTP{
Enable: cfg.Enable,
Server: cfg.Server,
Port: cfg.ServerPort,
Interval: cfg.Interval,
DialerProxy: cfg.DialerProxy,
WriteToSystem: cfg.WriteToSystem,
}
return ntpCfg
}
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 {
@ -1282,6 +1385,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error {
Enable: rawTuic.Enable,
Listen: rawTuic.Listen,
Token: rawTuic.Token,
Users: rawTuic.Users,
Certificate: rawTuic.Certificate,
PrivateKey: rawTuic.PrivateKey,
CongestionController: rawTuic.CongestionController,
@ -1289,6 +1393,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error {
AuthenticationTimeout: rawTuic.AuthenticationTimeout,
ALPN: rawTuic.ALPN,
MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize,
CWND: rawTuic.CWND,
}
return nil
}
@ -1304,7 +1409,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
if len(snifferRaw.Sniff) != 0 {
for sniffType, sniffConfig := range snifferRaw.Sniff {
find := false
ports, err := parsePortRange(sniffConfig.Ports)
ports, err := utils.NewIntRangesFromList[uint16](sniffConfig.Ports)
if err != nil {
return nil, err
}
@ -1331,7 +1436,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
// Deprecated: Use Sniff instead
log.Warnln("Deprecated: Use Sniff instead")
}
globalPorts, err := parsePortRange(snifferRaw.Ports)
globalPorts, err := utils.NewIntRangesFromList[uint16](snifferRaw.Ports)
if err != nil {
return nil, err
}
@ -1376,28 +1481,3 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
return sniffer, nil
}
func parsePortRange(portRanges []string) ([]utils.Range[uint16], error) {
ports := make([]utils.Range[uint16], 0)
for _, portRange := range portRanges {
portRaws := strings.Split(portRange, "-")
p, err := strconv.ParseUint(portRaws[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
start := uint16(p)
if len(portRaws) > 1 {
p, err = strconv.ParseUint(portRaws[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
end := uint16(p)
ports = append(ports, *utils.NewRange(start, end))
} else {
ports = append(ports, *utils.NewRange(start, start))
}
}
return ports, nil
}

View File

@ -2,7 +2,6 @@ package config
import (
"fmt"
"github.com/Dreamacro/clash/component/geodata"
"os"
C "github.com/Dreamacro/clash/constant"
@ -28,23 +27,6 @@ func Init(dir string) error {
f.Write([]byte(`mixed-port: 7890`))
f.Close()
}
buf, _ := os.ReadFile(C.Path.Config())
rawCfg, err := UnmarshalRawConfig(buf)
if err != nil {
log.Errorln(err.Error())
fmt.Printf("configuration file %s test failed\n", C.Path.Config())
os.Exit(1)
}
if !C.GeodataMode {
C.GeodataMode = rawCfg.GeodataMode
}
C.GeoIpUrl = rawCfg.GeoXUrl.GeoIp
C.GeoSiteUrl = rawCfg.GeoXUrl.GeoSite
C.MmdbUrl = rawCfg.GeoXUrl.Mmdb
// initial GeoIP
if err := geodata.InitGeoIP(); err != nil {
return fmt.Errorf("can't initial GeoIP: %w", err)
}
return nil
}

View File

@ -1,20 +1,14 @@
package config
import (
"context"
"fmt"
"io"
"net/http"
"os"
"runtime"
"time"
"github.com/Dreamacro/clash/component/geodata"
_ "github.com/Dreamacro/clash/component/geodata/standard"
clashHttp "github.com/Dreamacro/clash/component/http"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
"github.com/oschwald/maxminddb-golang"
)
func UpdateGeoDatabases() error {
@ -44,7 +38,7 @@ func UpdateGeoDatabases() error {
return fmt.Errorf("can't download MMDB database file: %w", err)
}
instance, err := geoip2.FromBytes(data)
instance, err := maxminddb.FromBytes(data)
if err != nil {
return fmt.Errorf("invalid MMDB database file: %s", err)
}
@ -72,19 +66,3 @@ func UpdateGeoDatabases() error {
return nil
}
func downloadForBytes(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := clashHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func saveFile(bytes []byte, path string) error {
return os.WriteFile(path, bytes, 0o644)
}

145
config/update_ui.go Normal file
View File

@ -0,0 +1,145 @@
package config
import (
"archive/zip"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
C "github.com/Dreamacro/clash/constant"
)
var (
ExternalUIURL string
ExternalUIPath string
ExternalUIFolder string
ExternalUIName string
)
var (
ErrIncompleteConf = errors.New("ExternalUI configure incomplete")
)
var xdMutex sync.Mutex
func UpdateUI() error {
xdMutex.Lock()
defer xdMutex.Unlock()
err := prepare()
if err != nil {
return err
}
data, err := downloadForBytes(ExternalUIURL)
if err != nil {
return fmt.Errorf("can't download file: %w", err)
}
saved := path.Join(C.Path.HomeDir(), "download.zip")
if saveFile(data, saved) != nil {
return fmt.Errorf("can't save zip file: %w", err)
}
defer os.Remove(saved)
err = cleanup(ExternalUIFolder)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("cleanup exist file error: %w", err)
}
}
unzipFolder, err := unzip(saved, C.Path.HomeDir())
if err != nil {
return fmt.Errorf("can't extract zip file: %w", err)
}
err = os.Rename(unzipFolder, ExternalUIFolder)
if err != nil {
return fmt.Errorf("can't rename folder: %w", err)
}
return nil
}
func prepare() error {
if ExternalUIPath == "" || ExternalUIURL == "" {
return ErrIncompleteConf
}
if ExternalUIName != "" {
ExternalUIFolder = filepath.Clean(path.Join(ExternalUIPath, ExternalUIName))
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
if err := os.MkdirAll(ExternalUIPath, os.ModePerm); err != nil {
return err
}
}
} else {
ExternalUIFolder = ExternalUIPath
}
return nil
}
func unzip(src, dest string) (string, error) {
r, err := zip.OpenReader(src)
if err != nil {
return "", err
}
defer r.Close()
var extractedFolder string
for _, f := range r.File {
fpath := filepath.Join(dest, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath)
}
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return "", err
}
rc, err := f.Open()
if err != nil {
return "", err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return "", err
}
if extractedFolder == "" {
extractedFolder = filepath.Dir(fpath)
}
}
return extractedFolder, nil
}
func cleanup(root string) error {
if _, err := os.Stat(root); os.IsNotExist(err) {
return nil
}
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if err := os.RemoveAll(path); err != nil {
return err
}
} else {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}

View File

@ -1,15 +1,37 @@
package config
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"os"
"strings"
"time"
"github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/common/structure"
clashHttp "github.com/Dreamacro/clash/component/http"
)
func downloadForBytes(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := clashHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func saveFile(bytes []byte, path string) error {
return os.WriteFile(path, bytes, 0o644)
}
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))

View File

@ -10,6 +10,7 @@ import (
"time"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
)
@ -35,14 +36,16 @@ const (
Vless
Trojan
Hysteria
Hysteria2
WireGuard
Tuic
)
const (
DefaultTCPTimeout = 5 * time.Second
DefaultUDPTimeout = DefaultTCPTimeout
DefaultTLSTimeout = DefaultTCPTimeout
DefaultTCPTimeout = 5 * time.Second
DefaultUDPTimeout = DefaultTCPTimeout
DefaultTLSTimeout = DefaultTCPTimeout
DefaultMaxHealthCheckUrlNum = 16
)
var ErrNotSupport = errors.New("no support")
@ -132,7 +135,7 @@ type ProxyAdapter interface {
}
type Group interface {
URLTest(ctx context.Context, url string) (mp map[string]uint16, err error)
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
GetProxies(touch bool) []Proxy
Touch()
}
@ -142,12 +145,23 @@ type DelayHistory struct {
Delay uint16 `json:"delay"`
}
type DelayHistoryStoreType int
const (
OriginalHistory DelayHistoryStoreType = iota
ExtraHistory
DropHistory
)
type Proxy interface {
ProxyAdapter
Alive() bool
AliveForTestUrl(url string) bool
DelayHistory() []DelayHistory
ExtraDelayHistory() map[string][]DelayHistory
LastDelay() uint16
URLTest(ctx context.Context, url string) (uint16, error)
LastDelayForTestUrl(url string) uint16
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store DelayHistoryStoreType) (uint16, error)
// Deprecated: use DialContext instead.
Dial(metadata *Metadata) (Conn, error)
@ -187,6 +201,8 @@ func (at AdapterType) String() string {
return "Trojan"
case Hysteria:
return "Hysteria"
case Hysteria2:
return "Hysteria2"
case WireGuard:
return "WireGuard"
case Tuic:
@ -217,7 +233,7 @@ type UDPPacket interface {
// - variable source IP/Port is important to STUN
// - if addr is not provided, WriteBack will write out UDP packet with SourceIP/Port equals to original Target,
// this is important when using Fake-IP.
WriteBack(b []byte, addr net.Addr) (n int, err error)
WriteBack
// Drop call after packet is used, could recycle buffer in this function.
Drop()
@ -236,22 +252,35 @@ type PacketAdapter interface {
Metadata() *Metadata
}
type NatTable interface {
Set(key string, e PacketConn)
type WriteBack interface {
WriteBack(b []byte, addr net.Addr) (n int, err error)
}
Get(key string) PacketConn
type WriteBackProxy interface {
WriteBack
UpdateWriteBack(wb WriteBack)
}
type NatTable interface {
Set(key string, e PacketConn, w WriteBackProxy)
Get(key string) (PacketConn, WriteBackProxy)
GetOrCreateLock(key string) (*sync.Cond, bool)
Delete(key string)
GetLocalConn(lAddr, rAddr string) *net.UDPConn
DeleteLock(key string)
AddLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool
GetForLocalConn(lAddr, rAddr string) *net.UDPConn
RangeLocalConn(lAddr string, f func(key, value any) bool)
AddForLocalConn(lAddr, rAddr string, conn *net.UDPConn) bool
GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool)
RangeForLocalConn(lAddr string, f func(key string, value *net.UDPConn) bool)
DeleteLocalConnMap(lAddr, key string)
GetOrCreateLockForLocalConn(lAddr string, key string) (*sync.Cond, bool)
DeleteForLocalConn(lAddr, key string)
DeleteLockForLocalConn(lAddr, key string)
}

5
constant/http.go Normal file
View File

@ -0,0 +1,5 @@
package constant
var (
UA string
)

View File

@ -30,6 +30,7 @@ const (
TUNNEL
TUN
TUIC
HYSTERIA2
INNER
)
@ -78,6 +79,8 @@ func (t Type) String() string {
return "Tun"
case TUIC:
return "Tuic"
case HYSTERIA2:
return "Hysteria2"
case INNER:
return "Inner"
default:
@ -110,6 +113,8 @@ func ParseType(t string) (*Type, error) {
res = TUN
case "TUIC":
res = TUIC
case "HYSTERIA2":
res = HYSTERIA2
case "INNER":
res = INNER
default:
@ -128,10 +133,10 @@ type Metadata struct {
Type Type `json:"type"`
SrcIP netip.Addr `json:"sourceIP"`
DstIP netip.Addr `json:"destinationIP"`
SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"`
SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output
DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output
InIP netip.Addr `json:"inboundIP"`
InPort string `json:"inboundPort"`
InPort uint16 `json:"inboundPort,string"` // `,string` is used to compatible with old version json output
InName string `json:"inboundName"`
InUser string `json:"inboundUser"`
Host string `json:"host"`
@ -147,11 +152,11 @@ type Metadata struct {
}
func (m *Metadata) RemoteAddress() string {
return net.JoinHostPort(m.String(), m.DstPort)
return net.JoinHostPort(m.String(), strconv.FormatUint(uint64(m.DstPort), 10))
}
func (m *Metadata) SourceAddress() string {
return net.JoinHostPort(m.SrcIP.String(), m.SrcPort)
return net.JoinHostPort(m.SrcIP.String(), strconv.FormatUint(uint64(m.SrcPort), 10))
}
func (m *Metadata) SourceDetail() string {
@ -171,6 +176,10 @@ func (m *Metadata) SourceDetail() string {
}
}
func (m *Metadata) SourceValid() bool {
return m.SrcPort != 0 && m.SrcIP.IsValid()
}
func (m *Metadata) AddrType() int {
switch true {
case m.Host != "" || !m.DstIP.IsValid():
@ -207,8 +216,7 @@ func (m *Metadata) Pure() *Metadata {
}
func (m *Metadata) AddrPort() netip.AddrPort {
port, _ := strconv.ParseUint(m.DstPort, 10, 16)
return netip.AddrPortFrom(m.DstIP.Unmap(), uint16(port))
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
}
func (m *Metadata) UDPAddr() *net.UDPAddr {
@ -238,6 +246,11 @@ func (m *Metadata) SetRemoteAddress(rawAddress string) error {
return err
}
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
if ip, err := netip.ParseAddr(host); err != nil {
m.Host = host
m.DstIP = netip.Addr{}
@ -245,7 +258,7 @@ func (m *Metadata) SetRemoteAddress(rawAddress string) error {
m.Host = ""
m.DstIP = ip.Unmap()
}
m.DstPort = port
m.DstPort = uint16Port
return nil
}

View File

@ -1,9 +1,12 @@
package constant
import (
"crypto/md5"
"encoding/hex"
"os"
P "path"
"path/filepath"
"strconv"
"strings"
)
@ -20,14 +23,15 @@ var Path = func() *path {
if err != nil {
homeDir, _ = os.Getwd()
}
allowUnsafePath, _ := strconv.ParseBool(os.Getenv("SKIP_SAFE_PATH_CHECK"))
homeDir = P.Join(homeDir, ".config", Name)
return &path{homeDir: homeDir, configFile: "config.yaml"}
return &path{homeDir: homeDir, configFile: "config.yaml", allowUnsafePath: allowUnsafePath}
}()
type path struct {
homeDir string
configFile string
homeDir string
configFile string
allowUnsafePath bool
}
// SetHomeDir is used to set the configuration path
@ -56,6 +60,27 @@ func (p *path) Resolve(path string) string {
return path
}
// IsSafePath return true if path is a subpath of homedir
func (p *path) IsSafePath(path string) bool {
if p.allowUnsafePath {
return true
}
homedir := p.HomeDir()
path = p.Resolve(path)
rel, err := filepath.Rel(homedir, path)
if err != nil {
return false
}
return !strings.Contains(rel, "..")
}
func (p *path) GetPathByHash(prefix, name string) string {
hash := md5.Sum([]byte(name))
filename := hex.EncodeToString(hash[:])
return filepath.Join(p.HomeDir(), prefix, filename)
}
func (p *path) MMDB() string {
files, err := os.ReadDir(p.homeDir)
if err != nil {
@ -66,13 +91,15 @@ func (p *path) MMDB() string {
// 目录则直接跳过
continue
} else {
if strings.EqualFold(fi.Name(), "Country.mmdb") {
if strings.EqualFold(fi.Name(), "Country.mmdb") ||
strings.EqualFold(fi.Name(), "geoip.db") ||
strings.EqualFold(fi.Name(), "geoip.metadb") {
GeoipName = fi.Name()
return P.Join(p.homeDir, fi.Name())
}
}
}
return P.Join(p.homeDir, "Country.mmdb")
return P.Join(p.homeDir, "geoip.metadb")
}
func (p *path) OldCache() string {

View File

@ -1,6 +1,7 @@
package provider
import (
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/constant"
)
@ -71,6 +72,7 @@ type ProxyProvider interface {
Touch()
HealthCheck()
Version() uint32
RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint)
}
// RuleProvider interface

View File

@ -10,12 +10,14 @@ var StackTypeMapping = map[string]TUNStack{
strings.ToLower(TunGvisor.String()): TunGvisor,
strings.ToLower(TunSystem.String()): TunSystem,
strings.ToLower(TunLWIP.String()): TunLWIP,
strings.ToLower(TunMixed.String()): TunMixed,
}
const (
TunGvisor TUNStack = iota
TunSystem
TunLWIP
TunMixed
)
type TUNStack int
@ -64,6 +66,8 @@ func (e TUNStack) String() string {
return "System"
case TunLWIP:
return "LWIP"
case TunMixed:
return "Mixed"
default:
return "unknown"
}

View File

@ -9,9 +9,9 @@ import (
"strings"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
D "github.com/miekg/dns"
@ -99,7 +99,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
ch := make(chan result, 1)
go func() {
if strings.HasSuffix(c.Client.Net, "tls") {
conn = tls.Client(conn, tlsC.GetGlobalTLSConfig(c.Client.TLSConfig))
conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig))
}
msg, _, err := c.Client.ExchangeWithConn(m, &D.Conn{

View File

@ -59,7 +59,8 @@ func (d *dhcpClient) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg,
return nil, err
}
return batchExchange(ctx, clients, m)
msg, _, err = batchExchange(ctx, clients, m)
return
}
func (d *dhcpClient) resolve(ctx context.Context) ([]dnsClient, error) {

View File

@ -15,7 +15,7 @@ import (
"sync"
"time"
tlsC "github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/component/ca"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/metacubex/quic-go"
@ -382,7 +382,7 @@ func (doh *dnsOverHTTPS) createClient(ctx context.Context) (*http.Client, error)
// HTTP3 is enabled in the upstream options). If this attempt is successful,
// it returns an HTTP3 transport, otherwise it returns the H1/H2 transport.
func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripper, err error) {
tlsConfig := tlsC.GetGlobalTLSConfig(
tlsConfig := ca.GetGlobalTLSConfig(
&tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
@ -543,7 +543,17 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.
if err != nil {
return nil, err
}
return quic.DialEarlyContext(ctx, conn, &udpAddr, doh.url.Host, tlsCfg, cfg)
transport := quic.Transport{Conn: conn}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
tlsCfg = tlsCfg.Clone()
if host, _, err := net.SplitHostPort(doh.url.Host); err == nil {
tlsCfg.ServerName = host
} else {
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
tlsCfg.ServerName = doh.url.Host
}
return transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
}
// probeH3 runs a test to check whether QUIC is faster than TLS for this

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