Compare commits

..

290 Commits

Author SHA1 Message Date
8c3557e96b chore: support v2ray http upgrade server too 2023-11-03 13:58:53 +08:00
228990472d fix: avoid tls panic 2023-11-03 12:04:22 +08:00
09e7866a5c fix: gvisor panic 2023-11-03 11:50:25 +08:00
665ba7f9f1 chore: do websocket client upgrade directly instead of gobwas/ws 2023-11-03 11:50:25 +08:00
ee3038d5e4 chore: add SetupContextForConn for common/net 2023-11-03 11:50:25 +08:00
885ee7a820 fix: v2ray http upgrade Hosts header not working 2023-11-03 11:50:25 +08:00
ef303b11f2 action: trigger CMFA PR update in every commit 2023-11-02 16:01:35 +08:00
a82ce85707 chore: add route exclude support 2023-11-02 11:37:40 +08:00
5bfe7ba169 chore: better tls handshake 2023-11-02 11:22:01 +08:00
ceac5bfaa4 feat: add v2ray-http-upgrade support 2023-11-02 11:11:35 +08:00
b0638cfc49 chore: better bufio.Reader warp 2023-11-02 11:11:35 +08:00
96220aa8ea feat: cancel RULE-SET nested SUB-RULE restrictions 2023-10-31 11:10:38 +00:00
8ff476a3a1 fix: remote logic rules cannot be parsed (#837) 2023-10-31 19:07:01 +08:00
261b6e8dce action: small fix to cmfa core-update trigger 2023-10-30 20:00:15 +08:00
2b9141e0e5 chore: geo link replaced with github 2023-10-30 19:46:56 +08:00
55255faa52 chore: modify configuration fields 2023-10-27 17:49:12 +08:00
d42e3f74ad action: add question issue guidance 2023-10-26 19:08:42 +08:00
81a8a63861 build: more go120 build 2023-10-26 11:39:54 +08:00
c3a61e2db5 build: add go120 build for win7/8.1 2023-10-26 11:09:19 +08:00
bffe47a974 chore: netip.Prefix should not using pointer 2023-10-26 11:02:53 +08:00
4314b37d04 fix: dhcp not working on windows 2023-10-26 10:27:38 +08:00
cf93f69f40 chore: cleanup error using of dialer.DefaultInterface 2023-10-26 09:07:49 +08:00
55f626424f chore: better dns batchExchange 2023-10-25 20:16:44 +08:00
431d52f250 chore: system resolver can autoupdate 2023-10-25 19:21:20 +08:00
c1f24d8f0e chore: code cleanup 2023-10-25 18:07:45 +08:00
fc5a3cf80c action: ban black issues 2023-10-25 18:06:10 +08:00
e1e999180a chore: inMemoryAuthenticator unneed sync map 2023-10-24 21:25:03 +08:00
8755618910 fix: reality panic 2023-10-23 23:33:59 +08:00
aede97571f Merge branch 'Alpha' of https://github.com/MetaCubeX/Clash.Meta into Alpha 2023-10-23 17:02:08 +08:00
01bc84db02 chore: add labels to issue template 2023-10-23 17:02:04 +08:00
3564e96a00 chore: share some code 2023-10-23 16:45:22 +08:00
f6f8f27668 action: update sync 2023-10-23 15:39:56 +08:00
dff54464c6 Add auto sync Alpha rebase android-open -> android-real (#817)
* chore: add android branch auto sync

* chore: fix

* chore: fix missing

* chore: fix actions

* chore: write branch auto sync
2023-10-23 15:39:56 +08:00
e987cdaaae chore: add CMFA auto update-dependencies trigger 2023-10-23 15:39:56 +08:00
6cd0e58fd0 fix: ssr panic 2023-10-23 15:39:56 +08:00
f794c090a5 chore: update sing-tun 2023-10-23 15:39:56 +08:00
0d3197e437 chore: fix sniffer log error 2023-10-20 22:36:29 +08:00
150bf7fc65 chore: decrease memory copy in sing listener 2023-10-20 08:39:04 +08:00
51004b14d9 docs: update readme.md 2023-10-20 00:34:10 +08:00
ea7e15b447 chore: decrease memory copy in quic sniffer 2023-10-19 23:51:37 +08:00
8e637a2ec7 chore: code cleanup 2023-10-19 20:44:49 +08:00
96d886380a Merge pull request #810 from 5aaee9/Alpha
feat: add quic sniffer
2023-10-19 19:34:45 +08:00
981c69040f docs: update about quic sniffer 2023-10-19 19:09:13 +08:00
de90c276af feat(sniffer): add quic sniffer 2023-10-19 18:30:20 +08:00
0129a8579f chore: merge some quic-go fix 2023-10-19 11:08:14 +08:00
11ed4a56bd chore: code cleanup 2023-10-17 12:46:41 +08:00
d75a0e69a0 chore: Update dependencies 2023-10-16 09:56:41 +08:00
1faad73381 fix: socks5 udp associate 2023-10-16 09:27:55 +08:00
d2499cd69d feature: add xdg base support (#2913) 2023-10-16 09:23:31 +08:00
98df77439c feature: add environs startup option support (#2909) 2023-10-16 09:22:16 +08:00
81bbbe4eec fix: DNS NCACHE TTL and OPT RRs (#2900)
* Fix: DNS NCACHE TTL and OPT RRs

1. DNS NCACHE was not correctly implemented.
2. OPT RRs must not be cached or forwarded.

Closes #2889.
2023-10-16 09:21:06 +08:00
9f530525d7 fix: method in vmess http-opts is not used 2023-10-16 09:16:36 +08:00
129283066f chore: code cleanup 2023-10-11 22:54:19 +08:00
0dc6a726c1 fix: unmap 4in6 ip 2023-10-11 18:17:39 +08:00
4636499439 chore: support reject proxy type 2023-10-11 13:01:14 +08:00
9a16eb2895 fix: BBR memory leak
from: 7c46e845a6
2023-10-11 11:01:17 +08:00
270a080b55 fix: sing listener panic 2023-10-11 10:55:12 +08:00
1cf9a55e3e chore: code cleanup 2023-10-10 21:29:12 +08:00
6bcd91a801 feat: add skip-auth-prefixes 2023-10-10 21:29:12 +08:00
7ed25ddc74 chore: better atomic using 2023-10-10 21:28:46 +08:00
ae557c30d3 fix: quic-go min MTU 2023-10-08 13:15:17 +08:00
5a1800d642 fix: BBR bandwidth estimation edge case
from 89429598bf
2023-10-08 07:26:28 +08:00
d8fe7a52d6 feat: add certificate and private-key to vmess listener 2023-10-08 07:26:28 +08:00
791ecfbb32 feat: add ws-path to vmess listener 2023-10-08 07:26:28 +08:00
5ff4473083 chore: migrate from gorilla/websocket to gobwas/ws 2023-10-06 17:44:36 +08:00
d1e88a30cb fix: gVisor UDP 6to4 check 2023-10-03 16:00:03 +08:00
7eae7756f5 chore: update gvisor 2023-10-01 19:15:26 +08:00
4e3cd01aad chore: merge some quic-go fix 2023-10-01 13:44:56 +08:00
dbaee284e4 fix: hy2/tuic inbound cert isn't path
Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
2023-10-01 12:04:34 +08:00
8253bfe2e0 add quic-go-disable-ecn to experimental 2023-10-01 09:10:11 +08:00
828b5ad8bb chore: add new bbr implementation 2023-10-01 00:01:32 +08:00
fedad26c13 chore: support relative path for hy2/tuic inbound cert 2023-10-01 00:01:32 +08:00
a526bb70ea chore: fix bbr bugs 2023-09-30 13:40:07 +08:00
5f6de610e1 Fix: should check all ips need to fallback (#2915) 2023-09-29 13:42:22 +08:00
02397868fc docs: support reload in service 2023-09-29 13:26:59 +08:00
265a6b9b68 chore: reduce string split immediately after string concat (#773) 2023-09-29 08:51:13 +08:00
10e7c533d7 feat: support clash premium's structured log stream (#735)
* feat: support clash premium's structured log stream

New version of Clash for Windows uses `ws://external-controller/logs?token=&level=info&format=structured` to get real time log. When Clash Premium Core reveices `format=structured`, it returns a different form of JSON log entry. Supporting this feature will allow better Clash for Windows integration

Signed-off-by: Misty <gyc990326@gmail.com>
2023-09-29 08:50:50 +08:00
0ed3c5a5ec chore: improve subscription userinfo parsing (#781)
do not use regex parsing for `Subscription-UserInfo` header field
2023-09-29 08:42:57 +08:00
c2b06a02bf feat: add reload signal support (#780)
Backport Clash feature by @septs, see Dreamacro/clash#2908
2023-09-29 08:36:25 +08:00
e0458a8fde chore: decrease goroutine used in core tunnel 2023-09-28 18:59:31 +08:00
21fb5f75b8 fix: gvisor panic 2023-09-26 09:06:00 +08:00
fb99412193 chore: update quic-go to 0.39.0 2023-09-26 08:51:25 +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
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
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
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
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
7fa3d3aa0b chore: cleanup system dns code 2023-06-01 12:36:53 +08:00
1120c8185d chore: Use API to create windows firewall rule 2023-05-31 15:54:36 +08:00
41af94ea66 fix: deadline reader cause panic 2023-05-30 20:21:51 +08:00
36539bb670 fix: sing-ss2's Reader not set buffer end 2023-05-30 07:59:55 +08:00
8e88e0b9f5 chore: add WaitReadFrom support in ssr 2023-05-28 22:51:44 +08:00
097f3e250c chore: slightly improve quic-bbr performance 2023-05-28 20:15:11 +08:00
9c2972afb0 chore: add IN-USER and IN-NAME rules 2023-05-28 17:19:57 +08:00
7aae781569 chore: add WaitReadFrom support in quicStreamPacketConn 2023-05-28 15:22:08 +08:00
92f71fd25f chore: add WaitReadFrom support in hyPacketConn 2023-05-28 09:33:42 +08:00
f44ba26f0c chore: switch ss uot default back to version 1 2023-05-28 08:50:02 +08:00
73140ab826 fix: udp panic when server return a domain name 2023-05-27 13:43:41 +08:00
4971b9d804 chore: Add vision splice support 2023-05-27 11:26:13 +08:00
654e76d91e refactor: Move vision implementation to a new package 2023-05-26 20:11:06 +08:00
984bf27d9b chore: using internal socks5.ReadAddr0 in trojan 2023-05-20 18:35:04 +08:00
546b2bc24b chore: Decrease UoT read memory 2023-05-20 17:01:52 +08:00
d4e4f6d2d7 chore: rebuild ref and threadSafe packetConn 2023-05-20 16:57:42 +08:00
b047ca0294 chore: packet deadline support CreateReadWaiter interface 2023-05-20 11:44:11 +08:00
2b1e69153b chore: better packet deadline 2023-05-19 23:29:59 +08:00
ae8d42fb82 Fix: update action to support Node 16 (#565) 2023-05-19 21:00:00 +08:00
89ae640487 fix: ensure group not empty 2023-05-19 19:57:55 +08:00
6e0c3a368f chore: upgrade dependencies 2023-05-19 11:08:14 +08:00
033f902ace chore: more context passing in outbounds 2023-05-18 13:15:08 +08:00
6b1a4385b2 chore: better updater 2023-05-17 00:33:59 +08:00
e552b5475f fix: tfoConn panic 2023-05-16 14:55:50 +08:00
8b631f11b8 chore: better sing's udp api support 2023-05-15 22:45:08 +08:00
1a9104c003 fix: UDP packet should not return io.EOF 2023-05-15 19:06:58 +08:00
872a28a5eb Fix: deprecated action commands (#556)
Co-authored-by: 8Mi_Yile <admin@8mi.tech>
2023-05-14 13:43:25 +08:00
c7557b8e48 feat: Updater detect and download AMD64v3 artifact
Co-authored-by: Larvan2 <78135608+larvan2@users.noreply.github.com>
2023-05-14 12:34:47 +08:00
c6fed3e97f fix: TLS certificate pool initialize
Co-authored-by: Skyxim <noreply@skyxim.dev>
2023-05-14 00:21:59 +08:00
ed17478961 feat: Support insecure gRPC 2023-05-13 09:38:14 +08:00
b674983034 chore: improve read waiter interface 2023-05-12 12:12:22 +08:00
a22b1cd69e fix: sing-based listener panic 2023-05-12 09:14:27 +08:00
f1be9b3f4a fix: tuic server return error udp address 2023-05-11 22:45:27 +08:00
534282839c chore: better tproxy error logging 2023-05-11 21:31:29 +08:00
8dd7632d0a chore: update docs 2023-05-11 21:24:38 +08:00
51e9f3598e fix: shadowsocks rc4-md5 not working 2023-05-11 20:42:36 +08:00
76caab19bf fix: Deadline not apply on EnhancePacketConn 2023-05-11 19:58:50 +08:00
234f7dbd3b chore: decrease shadowsocks udp read memory used for no-windows platform 2023-05-11 19:01:41 +08:00
e404695a0d fix: mux's udp should add write lock 2023-05-11 15:34:28 +08:00
75cd72385a chore: decrease direct udp read memory used for no-windows platform 2023-05-11 13:47:51 +08:00
d9fa051dd8 chore: drop bufio.Reader in BufferedConn to let gc can clean up its internal buf 2023-05-11 11:30:20 +08:00
98394095e4 fix: udp can't auto close 2023-05-11 00:03:40 +08:00
c58400572c chore: sing inbound support WaitReadPacket 2023-05-10 22:35:50 +08:00
3b291d3fbf fix: sing inbound should check needAdditionReadDeadline on udp too 2023-05-10 16:03:28 +08:00
15a8d7c473 chore: better tuic earlyConn impl 2023-05-10 09:36:06 +08:00
67b9314693 fix: tuic can't work with proxy-dialer 2023-05-10 09:03:49 +08:00
99f7c4f821 fix: ss aead udp problem 2023-05-10 08:31:16 +08:00
0cb594dd5d chore: upgrade dependencies 2023-05-10 07:23:49 +08:00
bd431fbf49 fix: Update unsafe pointer add usage 2023-05-06 15:49:10 +08:00
8c0168d3a8 chore: upgrade dependencies 2023-05-05 07:27:36 +00:00
7ae3e78b15 Feat: rewrite http outbound 2023-05-03 22:00:06 +08:00
d61a5af335 chore: update release note 2023-05-02 22:06:07 +08:00
f90066f286 chore: Update dependencies 2023-05-01 23:38:02 +08:00
463da578dd fixes #512: geo download failed when startup (#538)
* fixes #512: geo download failed when startup

- 启动阶段,executor还未初始化tunnel,tcpIn==nil导致geo下载失败,阻塞在
  tcpIn <- context

* chore: handled by the upper layer

* chore: remove useless parameters

---------

Co-authored-by: Skyxim <noreply@skyxim.dev>
2023-05-01 21:27:55 +08:00
1eefa71e1f chore: Make slash optional for system resolver 2023-05-01 12:58:02 +08:00
969c235490 chore: Remove default DNS in system resolver 2023-05-01 12:41:36 +08:00
f35ff24d0c docs: update config.yaml 2023-04-30 17:10:57 +00:00
94f990da31 feat: support system dns for windows 2023-05-01 00:46:57 +08:00
d6931ec491 feat: support system dns 2023-04-30 23:59:54 +08:00
19b403da86 refactor: Switch to sing-shadowsocks2 client 2023-04-30 18:57:16 +08:00
6ecd1c31e5 fix: tuic connection error using fast_open 2023-04-29 15:44:07 +00:00
a7233f6036 fix: wildcard matching problem 2023-04-28 16:55:35 +00:00
309 changed files with 13677 additions and 5547 deletions

View File

@ -1,6 +1,7 @@
name: Bug report name: Bug report
description: Create a report to help us improve description: Create a report to help us improve
title: "[Bug] " title: "[Bug] "
labels: ["bug"]
body: body:
- type: checkboxes - type: checkboxes
id: ensure id: ensure

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

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Clash.Meta Community Support
url: https://github.com/MetaCubeX/Clash.Meta/discussions
about: Please ask and answer questions about Clash.Meta here.

View File

@ -1,6 +1,7 @@
name: Feature request name: Feature request
description: Suggest an idea for this project description: Suggest an idea for this project
title: "[Feature] " title: "[Feature] "
labels: ["enhancement"]
body: body:
- type: checkboxes - type: checkboxes
id: ensure id: ensure

View File

@ -1 +0,0 @@
git log --pretty=format:"* %s by @%an" v1.14.x..v1.14.y | sort -f | uniq > release.md

12
.github/rename-go120.sh vendored Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
FILENAMES=$(ls)
for FILENAME in $FILENAMES
do
if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then
mv $FILENAME ${FILENAME}-go120
elif [[ $FILENAME =~ ".exe" ]];then
mv $FILENAME ${FILENAME%.*}-go120.exe
else echo "skip $FILENAME"
fi
done

View File

@ -0,0 +1,69 @@
name: Android Branch Auto Sync
on:
workflow_dispatch:
push:
paths-ignore:
- "docs/**"
- "README.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- Alpha
- android-open
tags:
- "v*"
pull_request_target:
branches:
- Alpha
- android-open
jobs:
update-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Configure Git
run: |
git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com'
- name: Sync android-real with Alpha rebase android-open
run: |
git fetch origin
git checkout origin/Alpha -b android-real
git merge --squash origin/android-open
git commit -m "Android: patch"
- name: Check for conflicts
run: |
CONFLICTS=$(git diff --name-only --diff-filter=U)
if [ ! -z "$CONFLICTS" ]; then
echo "There are conflicts in the following files:"
echo $CONFLICTS
exit 1
fi
- name: Push changes
run: |
git push origin android-real --force
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies
trigger-CMFA-update:
needs: update-dependencies
runs-on: ubuntu-latest
steps:
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.MAINTAINER_APPID }}
private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }}
- name: Trigger update-dependencies
run: |
curl -X POST https://api.github.com/repos/MetaCubeX/ClashMetaForAndroid/dispatches \
-H "Accept: application/vnd.github.everest-preview+json" \
-H "Authorization: token ${{ steps.generate-token.outputs.token }}" \
-d '{"event_type": "core-updated"}'

View File

@ -69,6 +69,12 @@ jobs:
target: "darwin-amd64 darwin-arm64 android-arm64", target: "darwin-amd64 darwin-arm64 android-arm64",
id: "9", id: "9",
} }
# only for test
- { type: "WithoutCGO-GO120", target: "linux-amd64 linux-amd64-compatible",id: "1" }
# Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
- { type: "WithoutCGO-GO120", target: "windows-amd64-compatible windows-amd64 windows-386",id: "2" }
# Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
- { type: "WithoutCGO-GO120", target: "darwin-amd64 darwin-arm64 android-arm64",id: "3" }
- { type: "WithCGO", target: "windows/*", id: "1" } - { type: "WithCGO", target: "windows/*", id: "1" }
- { type: "WithCGO", target: "linux/386", id: "2" } - { type: "WithCGO", target: "linux/386", id: "2" }
- { type: "WithCGO", target: "linux/amd64", id: "3" } - { type: "WithCGO", target: "linux/amd64", id: "3" }
@ -126,18 +132,26 @@ jobs:
shell: bash shell: bash
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 if: ${{ matrix.job.type!='WithoutCGO-GO120' }}
uses: actions/setup-go@v4
with:
go-version: "1.21"
check-latest: true
- name: Setup Go
if: ${{ matrix.job.type=='WithoutCGO-GO120' }}
uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.20"
check-latest: true check-latest: true
- name: Test - name: Test
if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }} if: ${{ matrix.job.id=='1' && matrix.job.type!='WithCGO' }}
run: | run: |
go test ./... go test ./...
- name: Build WithoutCGO - name: Build WithoutCGO
if: ${{ matrix.job.type=='WithoutCGO' }} if: ${{ matrix.job.type!='WithCGO' }}
env: env:
NAME: Clash.Meta NAME: Clash.Meta
BINDIR: bin BINDIR: bin
@ -147,7 +161,7 @@ jobs:
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }} if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
id: setup-ndk id: setup-ndk
with: with:
ndk-version: r25b ndk-version: r26
add-to-path: false add-to-path: false
local-cache: true local-cache: true
@ -185,6 +199,17 @@ jobs:
ls -la ls -la
cd .. cd ..
- name: Rename
if: ${{ matrix.job.type=='WithoutCGO-GO120' }}
run: |
cd bin
ls -la
cp ../.github/rename-go120.sh ./
bash ./rename-go120.sh
rm ./rename-go120.sh
ls -la
cd ..
- name: Zip - name: Zip
if: ${{ success() }} if: ${{ success() }}
run: | run: |
@ -209,7 +234,7 @@ jobs:
Upload-Prerelease: Upload-Prerelease:
permissions: write-all permissions: write-all
if: ${{ github.ref_type=='branch' }} if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }}
needs: [Build] needs: [Build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -223,7 +248,7 @@ jobs:
working-directory: bin working-directory: bin
- name: Delete current release assets - name: Delete current release assets
uses: andreaswilli/delete-release-assets-action@v2.0.0 uses: 8Mi-Tech/delete-release-assets-action@main
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
tag: Prerelease-${{ github.ref_name }} tag: Prerelease-${{ github.ref_name }}
@ -246,18 +271,14 @@ jobs:
Release created at ${{ env.BUILDTIME }} Release created at ${{ env.BUILDTIME }}
Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version
<br> <br>
### release version [我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/Clash.Meta/wiki/FAQ)
`default(not specified in file name)`: compiled with GOAMD64=v3 [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/)
`cgo`: support lwip tun stack, compiled with GOAMD64=v1
`compatible`: compiled with GOAMD64=v1
Check details between different architectural levels [here](https://github.com/golang/go/wiki/MinimumRequirements#amd64).
EOF EOF
- name: Upload Prerelease - name: Upload Prerelease
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ success() }} if: ${{ success() }}
with: with:
tag: ${{ github.ref_name }}
tag_name: Prerelease-${{ github.ref_name }} tag_name: Prerelease-${{ github.ref_name }}
files: | files: |
bin/* bin/*
@ -284,12 +305,12 @@ jobs:
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ success() }} if: ${{ success() }}
with: with:
tag: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
files: bin/* files: bin/*
generate_release_notes: true generate_release_notes: true
Docker: Docker:
if: ${{ github.event_name != 'pull_request' }}
permissions: write-all permissions: write-all
needs: [Build] needs: [Build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -309,10 +330,10 @@ jobs:
working-directory: bin working-directory: bin
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
with: with:
version: latest version: latest
@ -320,7 +341,7 @@ jobs:
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}} images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
- name: Show files - name: Show files
@ -329,7 +350,7 @@ jobs:
ls bin/ ls bin/
- name: Log into registry - name: Log into registry
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
@ -339,7 +360,7 @@ jobs:
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile

View File

@ -1,15 +0,0 @@
name: Delete old workflow runs
on:
schedule:
- cron: "0 0 * * SUN"
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
token: ${{ secrets.AUTH_PAT }}
repository: ${{ github.repository }}
retain_days: 30

View File

@ -4,9 +4,9 @@ RUN echo "I'm building for $TARGETPLATFORM"
RUN apk add --no-cache gzip && \ RUN apk add --no-cache gzip && \
mkdir /clash-config && \ mkdir /clash-config && \
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \ 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://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \ 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://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.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 COPY docker/file-name.sh /clash/file-name.sh
WORKDIR /clash WORKDIR /clash

View File

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

318
README.md
View File

@ -21,261 +21,52 @@
## Features ## Features
- Local HTTP/HTTPS/SOCKS server with authentication support - Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, Shadowsocks, Trojan, Snell protocol support for remote connections - VMess, VLESS, Shadowsocks, Trojan, Snell, TUIC, Hysteria protocol support
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP. - 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 - 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 groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config 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`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller - Comprehensive HTTP RESTful API controller
## Wiki ## Dashboard
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 A web dashboard with first-class support for this project has been created; it can be checked out at [metacubexd](https://github.com/MetaCubeX/metacubexd).
You should install [golang](https://go.dev) first. ## Configration example
Then get the source code of Clash.Meta: Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml).
## Docs
Documentation can be found in [Clash.Meta Docs](https://clash-meta.wiki).
## For development
Requirements:
[Go 1.20 or newer](https://go.dev/dl/)
Build Clash.Meta:
```shell ```shell
git clone https://github.com/MetaCubeX/Clash.Meta.git git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download cd Clash.Meta && go mod download
go build
``` ```
If you can't visit github,you should set proxy first: Set go proxy if a connection to GitHub is not possible:
```shell ```shell
go env -w GOPROXY=https://goproxy.io,direct go env -w GOPROXY=https://goproxy.io,direct
``` ```
Now you can build it: Build with gvisor tun stack:
```shell
go build
```
If you need gvisor for tun stack, build with:
```shell ```shell
go build -tags with_gvisor go build -tags with_gvisor
``` ```
<!-- ## Advanced usage of this fork -->
<!-- ### DNS configuration
Support `geosite` with `fallback-filter`.
Restore `Redir remote resolution`.
Support resolve ip with a `Proxy Tunnel`.
```yaml
proxy-groups:
- name: DNS
type: url-test
use:
- HK
url: http://cp.cloudflare.com
interval: 180
lazy: true
```
```yaml
dns:
enable: true
use-hosts: true
ipv6: false
enhanced-mode: redir-host
fake-ip-range: 198.18.0.1/16
listen: 127.0.0.1:6868
default-nameserver:
- 119.29.29.29
- 114.114.114.114
nameserver:
- https://doh.pub/dns-query
- tls://223.5.5.5:853
fallback:
- "https://1.0.0.1/dns-query#DNS" # append the proxy adapter name or group name to the end of DNS URL with '#' prefix.
- "tls://8.8.4.4:853#DNS"
fallback-filter:
geoip: false
geosite:
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
domain:
- +.example.com
ipcidr:
- 0.0.0.0/32
```
### TUN configuration
Supports macOS, Linux and Windows.
Built-in [Wintun](https://www.wintun.net) driver.
```yaml
# Enable the TUN listener
tun:
enable: true
stack: system # system/gvisor
dns-hijack:
- 0.0.0.0:53 # additional dns server listen on TUN
auto-route: true # auto set global route
```
### Rules configuration
- Support rule `GEOSITE`.
- Support rule-providers `RULE-SET`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules.
- Support source IPCIDR condition for all rules, just append to the end.
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
```yaml
rules:
# network(tcp/udp) condition for all rules
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp
# multiport condition for rules SRC-PORT and DST-PORT
- DST-PORT,123/136/137-139,DIRECT,udp
# rule GEOSITE
- GEOSITE,category-ads-all,REJECT
- GEOSITE,icloud@cn,DIRECT
- GEOSITE,apple@cn,DIRECT
- GEOSITE,apple-cn,DIRECT
- GEOSITE,microsoft@cn,DIRECT
- GEOSITE,facebook,PROXY
- GEOSITE,youtube,PROXY
- GEOSITE,geolocation-cn,DIRECT
- GEOSITE,geolocation-!cn,PROXY
# source IPCIDR condition for all rules in gateway proxy
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,private,DIRECT,no-resolve
- GEOIP,cn,DIRECT
- MATCH,PROXY
```
### Proxies configuration
Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node)
Support `Policy Group Filter`
```yaml
proxy-groups:
- name: 🚀 HK Group
type: select
use:
- ALL
filter: "HK"
- name: 🚀 US Group
type: select
use:
- ALL
filter: "US"
proxy-providers:
ALL:
type: http
url: "xxxxx"
interval: 3600
path: "xxxxx"
health-check:
enable: true
interval: 600
url: http://www.gstatic.com/generate_204
```
Support outbound transport protocol `VLESS`.
The XTLS support (TCP/UDP) transport by the XRAY-CORE.
```yaml
proxies:
- name: "vless"
type: vless
server: server
port: 443
uuid: uuid
servername: example.com # AKA SNI
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
# skip-cert-verify: true
- name: "vless-ws"
type: vless
server: server
port: 443
uuid: uuid
tls: true
udp: true
network: ws
servername: example.com # priority over wss host
# skip-cert-verify: true
ws-opts:
path: /path
headers: { Host: example.com, Edge: "12a00c4.fm.huawei.com:82897" }
- name: "vless-grpc"
type: vless
server: server
port: 443
uuid: uuid
tls: true
udp: true
network: grpc
servername: example.com # priority over wss host
# skip-cert-verify: true
grpc-opts:
grpc-service-name: grpcname
```
Support outbound transport protocol `Wireguard`
```yaml
proxies:
- name: "wg"
type: wireguard
server: 162.159.192.1
port: 2480
ip: 172.16.0.2
ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5
private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=
public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=
udp: true
```
Support outbound transport protocol `Tuic`
```yaml
proxies:
- name: "tuic"
server: www.example.com
port: 10443
type: tuic
token: TOKEN
# ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server'
# heartbeat-interval: 10000
# alpn: [h3]
# disable-sni: true
reduce-rtt: true
# request-timeout: 8000
udp-relay-mode: native # Available: "native", "quic". Default: "native"
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
# max-udp-relay-packet-size: 1500
# fast-open: true
# skip-cert-verify: true
``` -->
### IPTABLES configuration ### IPTABLES configuration
Work on Linux OS which supported `iptables` Work on Linux OS which supported `iptables`
@ -289,71 +80,10 @@ iptables:
inbound-interface: eth0 # detect the inbound interface, default is 'lo' inbound-interface: eth0 # detect the inbound interface, default is 'lo'
``` ```
### General installation guide for Linux
- Create user given name `clash-meta`
- Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
- Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
- Create folder `/etc/Clash-Meta/` as working directory
Run Meta Kernel by user `clash-meta` as a daemon.
Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`:
```
[Unit]
Description=Clash-Meta Daemon, Another Clash Kernel.
After=network.target NetworkManager.service systemd-networkd.service iwd.service
[Service]
Type=simple
User=clash-meta
Group=clash-meta
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
Restart=always
ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
[Install]
WantedBy=multi-user.target
```
Launch clashd on system startup with:
```shell
$ systemctl enable Clash-Meta
```
Launch clashd 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
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Debugging ## 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://wiki.metacubex.one/api/#debug) to get an instruction on using debug
API.
## Credits ## Credits

View File

@ -3,25 +3,42 @@ package adapter
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"strconv"
"time" "time"
"github.com/Dreamacro/clash/common/atomic" "github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/puzpuzpuz/xsync/v2"
) )
var UnifiedDelay = atomic.NewBool(false) var UnifiedDelay = atomic.NewBool(false)
const (
defaultHistoriesNum = 10
)
type extraProxyState struct {
history *queue.Queue[C.DelayHistory]
alive atomic.Bool
}
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue[C.DelayHistory] history *queue.Queue[C.DelayHistory]
alive *atomic.Bool alive atomic.Bool
url string
extra *xsync.MapOf[string, *extraProxyState]
} }
// Alive implements C.Proxy // Alive implements C.Proxy
@ -29,6 +46,15 @@ func (p *Proxy) Alive() bool {
return p.alive.Load() 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 // Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout) ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
@ -62,9 +88,51 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
for _, item := range queueM { for _, item := range queueM {
histories = append(histories, item) histories = append(histories, item)
} }
return histories 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. // LastDelay return last history record. if proxy is not alive, return the max value of uint16.
// implements C.Proxy // implements C.Proxy
func (p *Proxy) LastDelay() (delay uint16) { func (p *Proxy) LastDelay() (delay uint16) {
@ -80,6 +148,28 @@ func (p *Proxy) LastDelay() (delay uint16) {
return history.Delay 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 // MarshalJSON implements C.ProxyAdapter
func (p *Proxy) MarshalJSON() ([]byte, error) { func (p *Proxy) MarshalJSON() ([]byte, error) {
inner, err := p.ProxyAdapter.MarshalJSON() inner, err := p.ProxyAdapter.MarshalJSON()
@ -90,6 +180,8 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping := map[string]any{} mapping := map[string]any{}
_ = json.Unmarshal(inner, &mapping) _ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["extra"] = p.ExtraDelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP() mapping["xudp"] = p.SupportXUDP()
@ -99,17 +191,54 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
// URLTest get the delay for the specified URL // URLTest get the delay for the specified URL
// implements C.Proxy // implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) { func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store C.DelayHistoryStoreType) (t uint16, err error) {
defer func() { defer func() {
p.alive.Store(err == nil) alive := err == nil
store = p.determineFinalStoreType(store, url)
switch store {
case C.OriginalHistory:
p.alive.Store(alive)
record := C.DelayHistory{Time: time.Now()} record := C.DelayHistory{Time: time.Now()}
if err == nil { if alive {
record.Delay = t record.Delay = t
} }
p.history.Put(record) p.history.Put(record)
if p.history.Len() > 10 { if p.history.Len() > defaultHistoriesNum {
p.history.Pop() 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)
}
}() }()
unifiedDelay := UnifiedDelay.Load() unifiedDelay := UnifiedDelay.Load()
@ -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) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { 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) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -198,11 +337,36 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return return
} }
} }
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{ addr = C.Metadata{
Host: u.Hostname(), Host: u.Hostname(),
DstIP: netip.Addr{}, DstIP: netip.Addr{},
DstPort: port, DstPort: uint16(uintPort),
} }
return 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

@ -1,13 +1,17 @@
package inbound package inbound
import ( import (
"net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
type Addition func(metadata *C.Metadata) type Addition func(metadata *C.Metadata)
func (a Addition) Apply(metadata *C.Metadata) { func ApplyAdditions(metadata *C.Metadata, additions ...Addition) {
a(metadata) for _, addition := range additions {
addition(metadata)
}
} }
func WithInName(name string) Addition { func WithInName(name string) Addition {
@ -16,6 +20,12 @@ func WithInName(name string) Addition {
} }
} }
func WithInUser(user string) Addition {
return func(metadata *C.Metadata) {
metadata.InUser = user
}
}
func WithSpecialRules(specialRules string) Addition { func WithSpecialRules(specialRules string) Addition {
return func(metadata *C.Metadata) { return func(metadata *C.Metadata) {
metadata.SpecialRules = specialRules metadata.SpecialRules = specialRules
@ -27,3 +37,29 @@ func WithSpecialProxy(specialProxy string) Addition {
metadata.SpecialProxy = specialProxy metadata.SpecialProxy = specialProxy
} }
} }
func WithDstAddr(addr net.Addr) Addition {
return func(metadata *C.Metadata) {
_ = metadata.SetRemoteAddr(addr)
}
}
func WithSrcAddr(addr net.Addr) Addition {
return func(metadata *C.Metadata) {
m := C.Metadata{}
if err := m.SetRemoteAddr(addr);err ==nil{
metadata.SrcIP = m.DstIP
metadata.SrcPort = m.DstPort
}
}
}
func WithInAddr(addr net.Addr) Addition {
return func(metadata *C.Metadata) {
m := C.Metadata{}
if err := m.SetRemoteAddr(addr);err ==nil{
metadata.InIP = m.DstIP
metadata.InPort = m.DstPort
}
}
}

45
adapter/inbound/auth.go Normal file
View File

@ -0,0 +1,45 @@
package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
)
var skipAuthPrefixes []netip.Prefix
func SetSkipAuthPrefixes(prefixes []netip.Prefix) {
skipAuthPrefixes = prefixes
}
func SkipAuthPrefixes() []netip.Prefix {
return skipAuthPrefixes
}
func SkipAuthRemoteAddr(addr net.Addr) bool {
m := C.Metadata{}
if err := m.SetRemoteAddr(addr); err != nil {
return false
}
return skipAuth(m.AddrPort().Addr())
}
func SkipAuthRemoteAddress(addr string) bool {
m := C.Metadata{}
if err := m.SetRemoteAddress(addr); err != nil {
return false
}
return skipAuth(m.AddrPort().Addr())
}
func skipAuth(addr netip.Addr) bool {
if addr.IsValid() {
for _, prefix := range skipAuthPrefixes {
if prefix.Contains(addr.Unmap()) {
return true
}
}
}
return false
}

View File

@ -4,25 +4,15 @@ import (
"net" "net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
// NewHTTP receive normal http request and return HTTPContext // NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext { func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.HTTP metadata.Type = C.HTTP
for _, addition := range additions { ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
addition.Apply(metadata) ApplyAdditions(metadata, additions...)
} return conn, metadata
if ip, port, err := parseAddr(source); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata)
} }

View File

@ -5,23 +5,13 @@ import (
"net/http" "net/http"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
) )
// NewHTTPS receive CONNECT request and return ConnContext // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext { func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPS metadata.Type = C.HTTPS
for _, addition := range additions { ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
addition.Apply(metadata) ApplyAdditions(metadata, additions...)
} return conn, metadata
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata)
} }

View File

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

@ -5,38 +5,16 @@ import (
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
type PacketAdapter struct {
C.UDPPacket
metadata *C.Metadata
}
// Metadata returns destination metadata
func (s *PacketAdapter) Metadata() *C.Metadata {
return s.metadata
}
// NewPacket is PacketAdapter generator // NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter { func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.UDP metadata.NetWork = C.UDP
metadata.Type = source metadata.Type = source
for _, addition := range additions { ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr()))
addition.Apply(metadata)
}
if ip, port, err := parseAddr(packet.LocalAddr()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if p, ok := packet.(C.UDPPacketInAddr); ok { if p, ok := packet.(C.UDPPacketInAddr); ok {
if ip, port, err := parseAddr(p.InAddr()); err == nil { ApplyAdditions(metadata, WithInAddr(p.InAddr()))
metadata.InIP = ip
metadata.InPort = port
}
} }
ApplyAdditions(metadata, additions...)
return &PacketAdapter{ return packet, metadata
packet,
metadata,
}
} }

View File

@ -2,51 +2,17 @@ package inbound
import ( import (
"net" "net"
"net/netip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
// NewSocket receive TCP inbound and return ConnContext // NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext { func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
for _, addition := range additions { ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
addition.Apply(metadata) ApplyAdditions(metadata, additions...)
} return conn, metadata
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata)
}
func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
metadata := &C.Metadata{}
metadata.NetWork = C.TCP
metadata.Type = C.INNER
metadata.DNSMode = C.DNSNormal
metadata.Host = host
metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(dst); err == nil {
metadata.DstPort = port
if host == "" {
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
} else {
metadata.Host = h
}
}
}
return context.NewConnContext(conn, metadata)
} }

View File

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

View File

@ -21,6 +21,7 @@ type Base struct {
udp bool udp bool
xudp bool xudp bool
tfo bool tfo bool
mpTcp bool
rmark int rmark int
id string id string
prefer C.DNSPrefer prefer C.DNSPrefer
@ -45,8 +46,8 @@ func (b *Base) Type() C.AdapterType {
return b.tp return b.tp
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, C.ErrNotSupport return c, C.ErrNotSupport
} }
@ -143,11 +144,16 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithTFO(true)) opts = append(opts, dialer.WithTFO(true))
} }
if b.mpTcp {
opts = append(opts, dialer.WithMPTCP(true))
}
return opts return opts
} }
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` 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"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
@ -161,6 +167,7 @@ type BaseOption struct {
UDP bool UDP bool
XUDP bool XUDP bool
TFO bool TFO bool
MPTCP bool
Interface string Interface string
RoutingMark int RoutingMark int
Prefer C.DNSPrefer Prefer C.DNSPrefer
@ -174,6 +181,7 @@ func NewBase(opt BaseOption) *Base {
udp: opt.UDP, udp: opt.UDP,
xudp: opt.XUDP, xudp: opt.XUDP,
tfo: opt.TFO, tfo: opt.TFO,
mpTcp: opt.MPTCP,
iface: opt.Interface, iface: opt.Interface,
rmark: opt.RoutingMark, rmark: opt.RoutingMark,
prefer: opt.Prefer, prefer: opt.Prefer,
@ -220,7 +228,7 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
} }
type packetConn struct { type packetConn struct {
net.PacketConn N.EnhancePacketConn
chain C.Chain chain C.Chain
adapterName string adapterName string
connID string connID string
@ -242,15 +250,28 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
} }
func (c *packetConn) LocalAddr() net.Addr { func (c *packetConn) LocalAddr() net.Addr {
lAddr := c.PacketConn.LocalAddr() lAddr := c.EnhancePacketConn.LocalAddr()
return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy
} }
func (c *packetConn) Upstream() any {
return c.EnhancePacketConn
}
func (c *packetConn) WriterReplaceable() bool {
return true
}
func (c *packetConn) ReaderReplaceable() bool {
return true
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
pc = N.NewDeadlinePacketConn(pc) // most conn from outbound can't handle readDeadline correctly epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
} }
return &packetConn{pc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())} return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
} }
func parseRemoteDestination(addr string) string { func parseRemoteDestination(addr string) string {

View File

@ -3,8 +3,9 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"net" "net/netip"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -14,6 +15,11 @@ type Direct struct {
*Base *Base
} }
type DirectOption struct {
BasicOption
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver)) opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
@ -21,7 +27,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
if err != nil { if err != nil {
return nil, err return nil, err
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
return NewConn(c, d), nil return NewConn(c, d), nil
} }
@ -35,15 +41,26 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
metadata.DstIP = ip 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 { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(&directPacketConn{pc}, d), nil return newPacketConn(pc, d), nil
} }
type directPacketConn struct { func NewDirectWithOption(option DirectOption) *Direct {
net.PacketConn 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 { func NewDirect() *Direct {

View File

@ -7,15 +7,16 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -41,12 +42,10 @@ type HttpOption struct {
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil { if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig) cc := tls.Client(c, h.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx) err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
@ -77,13 +76,13 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = h.StreamConn(c, metadata) c, err = h.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -98,34 +97,36 @@ func (h *Http) SupportWithDialer() C.NetWork {
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
addr := metadata.RemoteAddress() addr := metadata.RemoteAddress()
req := &http.Request{ HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n"
Method: http.MethodConnect, tempHeaders := map[string]string{
URL: &url.URL{ "Host": addr,
Host: addr, "User-Agent": "Go-http-client/1.1",
}, "Proxy-Connection": "Keep-Alive",
Host: addr,
Header: http.Header{
"Proxy-Connection": []string{"Keep-Alive"},
},
} }
//增加headers
if len(h.option.Headers) != 0 {
for key, value := range h.option.Headers { for key, value := range h.option.Headers {
req.Header.Add(key, value) tempHeaders[key] = value
}
} }
if h.user != "" && h.pass != "" { if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
} }
if err := req.Write(rw); err != nil { for key, value := range tempHeaders {
HeaderString += key + ": " + value + "\r\n"
}
HeaderString += "\r\n"
_, err := rw.Write([]byte(HeaderString))
if err != nil {
return err return err
} }
resp, err := http.ReadResponse(bufio.NewReader(rw), req) resp, err := http.ReadResponse(bufio.NewReader(rw), nil)
if err != nil { if err != nil {
return err return err
} }
@ -156,21 +157,15 @@ func NewHttp(option HttpOption) (*Http, error) {
if option.SNI != "" { if option.SNI != "" {
sni = option.SNI sni = option.SNI
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
})
} else {
var err error var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{ tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni, ServerName: sni,
}, option.Fingerprint); err != nil { }, option.Fingerprint)
if err != nil {
return nil, err return nil, err
} }
} }
}
return &Http{ return &Http{
Base: &Base{ Base: &Base{
@ -178,6 +173,7 @@ func NewHttp(option HttpOption) (*Http, error) {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -2,16 +2,11 @@ package outbound
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"os"
"regexp"
"strconv" "strconv"
"time" "time"
@ -19,9 +14,9 @@ import (
"github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion" hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
@ -43,8 +38,6 @@ const (
DefaultHopInterval = 10 DefaultHopInterval = 10
) )
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct { type Hysteria struct {
*Base *Base
@ -53,7 +46,7 @@ type Hysteria struct {
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), h.genHdc(ctx, opts...)) tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx, opts...))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -120,12 +113,12 @@ type HysteriaOption struct {
func (c *HysteriaOption) Speed() (uint64, uint64, error) { func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64 var up, down uint64
up = stringToBps(c.Up) up = StringToBps(c.Up)
if up == 0 { if up == 0 {
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
} }
down = stringToBps(c.Down) down = StringToBps(c.Down)
if down == 0 { if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
} }
@ -153,38 +146,11 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
MinVersion: tls.VersionTLS13, MinVersion: tls.VersionTLS13,
} }
var bs []byte
var err error var err error
if len(option.CustomCA) > 0 { tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
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 { if err != nil {
return nil, err return nil, err
} }
} else {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
}
if len(option.ALPN) > 0 { if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN tlsConfig.NextProtos = option.ALPN
@ -268,42 +234,6 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
}, nil }, 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 { type hyPacketConn struct {
core.UDPConn core.UDPConn
} }
@ -318,6 +248,16 @@ func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return return
} }
func (c *hyPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
b, addrStr, err := c.UDPConn.ReadFrom()
if err != nil {
return
}
data = b
addr = M.ParseSocksaddr(addrStr).UDPAddr()
return
}
func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String()) err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
if err != nil { if err != nil {

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.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
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

@ -15,6 +15,10 @@ type Reject struct {
*Base *Base
} }
type RejectOption struct {
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(nopConn{}, r), nil return NewConn(nopConn{}, r), nil
@ -25,6 +29,16 @@ func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return newPacketConn(nopPacketConn{}, r), nil return newPacketConn(nopPacketConn{}, r), nil
} }
func NewRejectWithOption(option RejectOption) *Reject {
return &Reject{
Base: &Base{
name: option.Name,
tp: C.Direct,
udp: true,
},
}
}
func NewReject() *Reject { func NewReject() *Reject {
return &Reject{ return &Reject{
Base: &Base{ Base: &Base{
@ -78,6 +92,9 @@ type nopPacketConn struct{}
func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil } func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF } func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
return nil, nil, nil, io.EOF
}
func (npc nopPacketConn) Close() error { return nil } func (npc nopPacketConn) Close() error { return nil }
func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified } func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
func (npc nopPacketConn) SetDeadline(time.Time) error { return nil } func (npc nopPacketConn) SetDeadline(time.Time) error { return nil }

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
"time"
N "github.com/Dreamacro/clash/common/net" N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
@ -17,13 +16,10 @@ import (
"github.com/Dreamacro/clash/transport/restls" "github.com/Dreamacro/clash/transport/restls"
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls" shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
restlsC "github.com/3andne/restls-client-go" restlsC "github.com/3andne/restls-client-go"
shadowsocks "github.com/metacubex/sing-shadowsocks" shadowsocks "github.com/metacubex/sing-shadowsocks2"
"github.com/metacubex/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/uot"
) )
@ -70,6 +66,7 @@ type v2rayObfsOption struct {
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"`
} }
type shadowTLSOption struct { type shadowTLSOption struct {
@ -87,14 +84,7 @@ type restlsOption struct {
RestlsScript string `obfs:"restls-script,omitempty"` RestlsScript string `obfs:"restls-script,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// fix tls handshake not timeout
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
return ss.StreamConnContext(ctx, c, metadata)
}
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
useEarly := false useEarly := false
switch ss.obfsMode { switch ss.obfsMode {
@ -105,7 +95,7 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket": case "websocket":
var err error var err error
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption) c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
@ -134,9 +124,9 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
} }
} }
if useEarly { if useEarly {
return ss.method.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil return ss.method.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)), nil
} else { } else {
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) return ss.method.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
} }
} }
@ -157,7 +147,7 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
@ -196,7 +186,7 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc = ss.method.DialPacketConn(bufio.NewBindPacketConn(pc, addr)) pc = ss.method.DialPacketConn(N.NewBindPacketConn(pc, addr))
return newPacketConn(pc, ss), nil return newPacketConn(pc, ss), nil
} }
@ -234,7 +224,9 @@ func (ss *ShadowSocks) SupportUOT() bool {
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password, time.Now) method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{
Password: option.Password,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
} }
@ -272,6 +264,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
Path: opts.Path, Path: opts.Path,
Headers: opts.Headers, Headers: opts.Headers,
Mux: opts.Mux, Mux: opts.Mux,
V2rayHttpUpgrade: opts.V2rayHttpUpgrade,
} }
if opts.TLS { if opts.TLS {
@ -303,7 +296,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
} }
restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint) restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
restlsConfig.SessionTicketsDisabled = true
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
} }
@ -312,7 +304,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
switch option.UDPOverTCPVersion { switch option.UDPOverTCPVersion {
case uot.Version, uot.LegacyVersion: case uot.Version, uot.LegacyVersion:
case 0: case 0:
option.UDPOverTCPVersion = uot.Version option.UDPOverTCPVersion = uot.LegacyVersion
default: default:
return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion) return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion)
} }
@ -324,6 +316,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -338,36 +331,3 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
restlsConfig: restlsConfig, restlsConfig: restlsConfig,
}, nil }, nil
} }
type ssPacketConn struct {
net.PacketConn
rAddr net.Addr
}
func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
if err != nil {
return
}
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.PacketConn.ReadFrom(b)
if e != nil {
return 0, nil, e
}
addr := socks5.SplitAddr(b[:n])
if addr == nil {
return 0, nil, errors.New("parse addr error")
}
udpAddr := addr.UDPAddr()
if udpAddr == nil {
return 0, nil, errors.New("parse addr error")
}
copy(b, b[len(addr):])
return n - len(addr), udpAddr, e
}

View File

@ -2,16 +2,19 @@ package outbound
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core" "github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead" "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream" "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol" "github.com/Dreamacro/clash/transport/ssr/protocol"
) )
@ -38,8 +41,8 @@ type ShadowSocksROption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = ssr.obfs.StreamConn(c) c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c) c = ssr.cipher.StreamConn(c)
var ( var (
@ -77,13 +80,13 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = ssr.StreamConn(c, metadata) c, err = ssr.StreamConnContext(ctx, c, metadata)
return NewConn(c, ssr), err return NewConn(c, ssr), err
} }
@ -110,9 +113,9 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err return nil, err
} }
pc = ssr.cipher.PacketConn(pc) epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc))
pc = ssr.protocol.PacketConn(pc) epc = ssr.protocol.PacketConn(epc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil
} }
// SupportWithDialer implements C.ProxyAdapter // SupportWithDialer implements C.ProxyAdapter
@ -178,6 +181,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -188,3 +192,62 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
protocol: protocol, protocol: protocol,
}, nil }, nil
} }
type ssrPacketConn struct {
N.EnhancePacketConn
rAddr net.Addr
}
func (spc *ssrPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
if err != nil {
return
}
return spc.EnhancePacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssrPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.EnhancePacketConn.ReadFrom(b)
if e != nil {
return 0, nil, e
}
addr := socks5.SplitAddr(b[:n])
if addr == nil {
return 0, nil, errors.New("parse addr error")
}
udpAddr := addr.UDPAddr()
if udpAddr == nil {
return 0, nil, errors.New("parse addr error")
}
copy(b, b[len(addr):])
return n - len(addr), udpAddr, e
}
func (spc *ssrPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
data, put, _, err = spc.EnhancePacketConn.WaitReadFrom()
if err != nil {
return nil, nil, nil, err
}
_addr := socks5.SplitAddr(data)
if _addr == nil {
if put != nil {
put()
}
return nil, nil, nil, errors.New("parse addr error")
}
addr = _addr.UDPAddr()
if addr == nil {
if put != nil {
put()
}
return nil, nil, nil, errors.New("parse addr error")
}
data = data[len(_addr):]
return
}

View File

@ -3,7 +3,6 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"net"
"runtime" "runtime"
CN "github.com/Dreamacro/clash/common/net" CN "github.com/Dreamacro/clash/common/net"
@ -15,14 +14,13 @@ import (
mux "github.com/sagernet/sing-mux" mux "github.com/sagernet/sing-mux"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type SingMux struct { type SingMux struct {
C.ProxyAdapter C.ProxyAdapter
base ProxyBase base ProxyBase
client *mux.Client client *mux.Client
dialer *muxSingDialer dialer proxydialer.SingDialer
onlyTcp bool onlyTcp bool
} }
@ -41,28 +39,10 @@ type ProxyBase interface {
DialOptions(opts ...dialer.Option) []dialer.Option 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) { func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := s.base.DialOptions(opts...) 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())) c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -74,7 +54,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
} }
options := s.base.DialOptions(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 // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
@ -92,12 +72,12 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if pc == nil { if pc == nil {
return nil, E.New("packetConn is nil") return nil, E.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(pc, s), s.ProxyAdapter), nil return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), s), s.ProxyAdapter), nil
} }
func (s *SingMux) SupportUDP() bool { func (s *SingMux) SupportUDP() bool {
if s.onlyTcp { if s.onlyTcp {
return s.ProxyAdapter.SupportUOT() return s.ProxyAdapter.SupportUDP()
} }
return true return true
} }
@ -114,7 +94,7 @@ func closeSingMux(s *SingMux) {
} }
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) { 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{ client, err := mux.NewClient(mux.Options{
Dialer: singDialer, Dialer: singDialer,
Protocol: option.Protocol, Protocol: option.Protocol,

View File

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

View File

@ -7,11 +7,13 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
@ -39,12 +41,10 @@ type Socks5Option struct {
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx) err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
@ -82,13 +82,13 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -128,7 +128,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
tcpKeepAlive(c) N.TCPKeepAlive(c)
var user *socks5.User var user *socks5.User
if ss.user != "" { if ss.user != "" {
user = &socks5.User{ user = &socks5.User{
@ -137,7 +137,8 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
} }
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user) udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
if err != nil { if err != nil {
err = fmt.Errorf("client hanshake error: %w", err) err = fmt.Errorf("client hanshake error: %w", err)
return return
@ -157,7 +158,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
bindUDPAddr.IP = serverAddr.IP 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 { if err != nil {
return return
} }
@ -181,15 +182,12 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
ServerName: option.Server, ServerName: option.Server,
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
var err error var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err return nil, err
} }
} }
}
return &Socks5{ return &Socks5{
Base: &Base{ Base: &Base{
@ -198,6 +196,7 @@ func NewSocks5(option Socks5Option) (*Socks5, error) {
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),

View File

@ -8,13 +8,14 @@ import (
"net/http" "net/http"
"strconv" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls" tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
) )
type Trojan struct { type Trojan struct {
@ -45,18 +46,17 @@ type TrojanOption struct {
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" { if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr) host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{ wsOpts := &trojan.WebsocketOption{
Host: host, Host: host,
Port: port, Port: port,
Path: t.option.WSOpts.Path, Path: t.option.WSOpts.Path,
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
} }
if t.option.SNI != "" { if t.option.SNI != "" {
@ -71,14 +71,14 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
wsOpts.Headers = header wsOpts.Headers = header
} }
return t.instance.StreamWebsocketConn(c, wsOpts) return t.instance.StreamWebsocketConn(ctx, c, wsOpts)
} }
return t.instance.StreamConn(c) return t.instance.StreamConn(ctx, c)
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 { if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
@ -88,18 +88,13 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
if t.transport != nil { if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
} else { } else {
c, err = t.plainStream(c) c, err = t.plainStream(ctx, c)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
return nil, err
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err return c, err
@ -117,12 +112,6 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return nil, err 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 { if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close() c.Close()
return nil, err return nil, err
@ -145,13 +134,13 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = t.StreamConn(c, metadata) c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -198,8 +187,8 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
tcpKeepAlive(c) N.TCPKeepAlive(c)
c, err = t.plainStream(c) c, err = t.plainStream(ctx, c)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
@ -237,24 +226,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ALPN: option.ALPN, ALPN: option.ALPN,
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow,
Fingerprint: option.Fingerprint, Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint, 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 != "" { if option.SNI != "" {
tOption.ServerName = option.SNI tOption.ServerName = option.SNI
} }
@ -266,6 +241,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -295,7 +271,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
return c, nil return c, nil
} }
@ -306,14 +282,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
var err error var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil { tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err return nil, err
} }
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)

View File

@ -2,24 +2,25 @@ package outbound
import ( import (
"context" "context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/hex" "errors"
"encoding/pem"
"fmt" "fmt"
"math" "math"
"net" "net"
"os"
"strconv" "strconv"
"time" "time"
"github.com/metacubex/quic-go" "github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "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" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/tuic" "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 { type Tuic struct {
@ -33,7 +34,9 @@ type TuicOption struct {
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` 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"` Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"` HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
@ -46,6 +49,7 @@ type TuicOption struct {
FastOpen bool `proxy:"fast-open,omitempty"` FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"` MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"` CustomCA string `proxy:"ca,omitempty"`
@ -55,6 +59,9 @@ type TuicOption struct {
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,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 // DialContext implements C.ProxyAdapter
@ -78,6 +85,32 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
// ListenPacketWithDialer implements C.ProxyAdapter // ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { 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) pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil { if err != nil {
return nil, err return nil, err
@ -90,11 +123,7 @@ func (t *Tuic) SupportWithDialer() C.NetWork {
return C.ALLNet return C.ALLNet
} }
func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (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) {
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) {
if len(t.option.DialerProxy) > 0 { if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer) dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
if err != nil { if err != nil {
@ -106,10 +135,14 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.Pack
return nil, nil, err return nil, nil, err
} }
addr = udpAddr addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
transport = &quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
return return
} }
@ -125,40 +158,13 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.ServerName = option.SNI tlsConfig.ServerName = option.SNI
} }
var bs []byte
var err error var err error
if len(option.CustomCA) > 0 { tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
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)
}
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 { if err != nil {
return nil, err 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 tlsConfig.NextProtos = option.ALPN
} else { } else {
tlsConfig.NextProtos = []string{"h3"} tlsConfig.NextProtos = []string{"h3"}
@ -172,8 +178,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
option.HeartbeatInterval = 10000 option.HeartbeatInterval = 10000
} }
udpRelayMode := tuic.QUIC
if option.UdpRelayMode != "quic" { if option.UdpRelayMode != "quic" {
option.UdpRelayMode = "native" udpRelayMode = tuic.NATIVE
} }
if option.MaxUdpRelayPacketSize == 0 { if option.MaxUdpRelayPacketSize == 0 {
@ -184,14 +191,23 @@ func NewTuic(option TuicOption) (*Tuic, error) {
option.MaxOpenStreams = 100 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 { if option.MaxDatagramFrameSize == 0 {
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + tuic.PacketOverHead option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead
} }
if option.MaxDatagramFrameSize > 1400 { if option.MaxDatagramFrameSize > 1400 {
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 // ensure server's incoming stream can handle correctly, increase to 1.1x
quicMaxOpenStreams := int64(option.MaxOpenStreams) quicMaxOpenStreams := int64(option.MaxOpenStreams)
@ -220,12 +236,18 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Ip) > 0 { if len(option.Ip) > 0 {
addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port)) addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port))
} }
host := option.Server
if option.DisableSni { if option.DisableSni {
host = ""
tlsConfig.ServerName = "" 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{ t := &Tuic{
Base: &Base{ Base: &Base{
@ -251,21 +273,44 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if clientMaxOpenStreams < 1 { if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1 clientMaxOpenStreams = 1
} }
clientOption := &tuic.ClientOption{
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsConfig, TlsConfig: tlsConfig,
QuicConfig: quicConfig, QuicConfig: quicConfig,
Host: host,
Token: tkn, Token: tkn,
UdpRelayMode: option.UdpRelayMode, UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController, CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt, ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond, RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize, MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen, FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams, MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
} }
t.client = tuic.NewPoolClient(clientOption) 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 return t, nil
} }

View File

@ -4,12 +4,12 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
xtls "github.com/xtls/go" "fmt"
"net" "net"
"net/netip" "net/netip"
"regexp"
"strconv" "strconv"
"sync" "sync"
"time"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -18,17 +18,9 @@ import (
var ( var (
globalClientSessionCache tls.ClientSessionCache globalClientSessionCache tls.ClientSessionCache
globalClientXSessionCache xtls.ClientSessionCache
once sync.Once 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 { func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() { once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128) globalClientSessionCache = tls.NewLRUClientSessionCache(128)
@ -36,18 +28,11 @@ func getClientSessionCache() tls.ClientSessionCache {
return globalClientSessionCache return globalClientSessionCache
} }
func getClientXSessionCache() xtls.ClientSessionCache {
once.Do(func() {
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
})
return globalClientXSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() addrType := metadata.AddrType()
aType := uint8(addrType) aType := uint8(addrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16) p := uint(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch addrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
@ -138,3 +123,41 @@ func safeConnClose(c net.Conn, err error) {
_ = c.Close() _ = 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

@ -13,6 +13,9 @@ import (
"sync" "sync"
"github.com/Dreamacro/clash/common/convert" "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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
@ -24,8 +27,8 @@ import (
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
vmessSing "github.com/sagernet/sing-vmess" vmessSing "github.com/metacubex/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr" "github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
) )
@ -54,8 +57,8 @@ type VlessOption struct {
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"` Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"` PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"` XUDP bool `proxy:"xudp,omitempty"`
@ -74,7 +77,7 @@ type VlessOption struct {
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 { if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
@ -90,6 +93,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -108,14 +112,10 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if len(v.option.Fingerprint) == 0 { wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
@ -128,10 +128,10 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
convert.SetUserAgent(wsOpts.Headers) convert.SetUserAgent(wsOpts.Headers)
} }
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
c, err = v.streamTLSOrXTLSConn(c, false) c, err = v.streamTLSConn(ctx, c, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -146,7 +146,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = vmess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
c, err = v.streamTLSOrXTLSConn(c, true) c, err = v.streamTLSConn(ctx, c, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,8 +161,8 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
// default tcp network // default tcp network
// handle TLS And XTLS // handle TLS
c, err = v.streamTLSOrXTLSConn(c, false) c, err = v.streamTLSConn(ctx, c, false)
} }
if err != nil { if err != nil {
@ -178,7 +178,7 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
metadata = &C.Metadata{ metadata = &C.Metadata{
NetWork: C.UDP, NetWork: C.UDP,
Host: packetaddr.SeqPacketMagicAddress, Host: packetaddr.SeqPacketMagicAddress,
DstPort: "443", DstPort: 443,
} }
} else { } else {
metadata = &C.Metadata{ // a clear metadata only contains ip metadata = &C.Metadata{ // a clear metadata only contains ip
@ -200,29 +200,17 @@ func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
return return
} }
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) 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(conn, &xtlsOpts)
} else if v.option.TLS {
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if isH2 { if isH2 {
@ -233,16 +221,12 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
return vmess.StreamTLSConn(conn, &tlsOpts) return vmess.StreamTLSConn(ctx, conn, &tlsOpts)
} }
return conn, nil return conn, nil
} }
func (v *Vless) isLegacyXTLSEnabled() bool {
return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport // gun transport
@ -277,12 +261,12 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -342,12 +326,12 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
@ -372,15 +356,21 @@ func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada
} }
if v.option.XUDP { if v.option.XUDP {
return newPacketConn(&threadSafePacketConn{ var globalID [8]byte
PacketConn: vmessSing.NewXUDPConn(c, M.SocksaddrFromNet(metadata.UDPAddr())), if metadata.SourceValid() {
}, v), nil globalID = utils.GlobalID(metadata.SourceAddress())
}
return newPacketConn(N.NewThreadSafePacketConn(
vmessSing.NewXUDPConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
} else if v.option.PacketAddr { } else if v.option.PacketAddr {
return newPacketConn(&threadSafePacketConn{ return newPacketConn(N.NewThreadSafePacketConn(
PacketConn: packetaddr.NewConn(&vlessPacketConn{ packetaddr.NewConn(&vlessPacketConn{
Conn: c, rAddr: metadata.UDPAddr(), Conn: c, rAddr: metadata.UDPAddr(),
}, M.SocksaddrFromNet(metadata.UDPAddr())), }, M.SocksaddrFromNet(metadata.UDPAddr())),
}, v), nil ), v), nil
} }
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
@ -409,12 +399,11 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
copy(addr[1:], metadata.Host) copy(addr[1:], metadata.Host)
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{ return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
Addr: addr, Addr: addr,
Port: uint16(port), Port: metadata.DstPort,
Mux: metadata.NetWork == C.UDP && xudp, Mux: metadata.NetWork == C.UDP && xudp,
} }
} }
@ -518,11 +507,11 @@ func NewVless(option VlessOption) (*Vless, error) {
switch option.Flow { switch option.Flow {
case vless.XRV: case vless.XRV:
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", 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{ addons = &vless.Addons{
Flow: option.Flow, 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: default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
@ -541,7 +530,7 @@ func NewVless(option VlessOption) (*Vless, error) {
option.PacketAddr = false option.PacketAddr = false
} }
client, err := vless.NewClient(option.UUID, addons, option.FlowShow) client, err := vless.NewClient(option.UUID, addons)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -554,6 +543,7 @@ func NewVless(option VlessOption) (*Vless, error) {
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, xudp: option.XUDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -586,7 +576,7 @@ func NewVless(option VlessOption) (*Vless, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
return c, nil return c, nil
} }
@ -595,15 +585,19 @@ func NewVless(option VlessOption) (*Vless, error) {
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
} }
tlsConfig := tlsC.GetGlobalTLSConfig(&tls.Config{ if option.ServerName == "" {
gunConfig.Host = v.addr
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) })
if option.ServerName == "" {
if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host
gunConfig.Host = host }
} }
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig

View File

@ -12,16 +12,19 @@ import (
"sync" "sync"
N "github.com/Dreamacro/clash/common/net" 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/dialer"
"github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls" tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/ntp"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
clashVMess "github.com/Dreamacro/clash/transport/vmess" clashVMess "github.com/Dreamacro/clash/transport/vmess"
vmess "github.com/sagernet/sing-vmess" vmess "github.com/metacubex/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr" "github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
) )
@ -51,6 +54,7 @@ type VmessOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
@ -87,10 +91,11 @@ type WSOptions struct {
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"` MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
V2rayHttpUpgrade bool `proxy:"v2ray-http-upgrade,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) { if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
@ -106,6 +111,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -124,13 +130,10 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if len(v.option.Fingerprint) == 0 { wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig) if err != nil {
} else {
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
return nil, err return nil, err
} }
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
@ -138,7 +141,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} }
c, err = clashVMess.StreamWebsocketConn(c, wsOpts) c, err = clashVMess.StreamWebsocketConn(ctx, c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
if v.option.TLS { if v.option.TLS {
@ -148,12 +151,13 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(ctx, c, tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -182,7 +186,7 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, &tlsOpts) c, err = clashVMess.StreamTLSConn(ctx, c, &tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -204,13 +208,14 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = clashVMess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(ctx, c, tlsOpts)
} }
} }
@ -223,30 +228,44 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
if v.option.XUDP { if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
conn = v.client.DialEarlyXUDPPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr())) conn = v.client.DialEarlyXUDPPacketConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else { } 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 { } else if v.option.PacketAddr {
if N.NeedHandshake(c) { 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 { } 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) conn = packetaddr.NewBindConn(conn)
} else { } else {
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
conn = v.client.DialEarlyPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr())) conn = v.client.DialEarlyPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else { } else {
conn, err = v.client.DialPacketConn(c, M.SocksaddrFromNet(metadata.UDPAddr())) conn, err = v.client.DialPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
} }
} }
} else { } else {
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
conn = v.client.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) conn = v.client.DialEarlyConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
} else { } else {
conn, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) conn, err = v.client.DialConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
} }
} }
if err != nil { if err != nil {
@ -267,7 +286,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -289,12 +308,12 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConnContext(ctx, c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
} }
@ -350,12 +369,12 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
@ -379,7 +398,7 @@ func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada
} }
if pc, ok := c.(net.PacketConn); ok { if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
} }
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
@ -398,6 +417,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if option.AuthenticatedLength { if option.AuthenticatedLength {
options = append(options, vmess.ClientWithAuthenticatedLength()) options = append(options, vmess.ClientWithAuthenticatedLength())
} }
options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...) client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -413,13 +433,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
option.PacketAddr = false option.PacketAddr = false
} }
switch option.Network {
case "h2", "grpc":
if !option.TLS {
option.TLS = true
}
}
v := &Vmess{ v := &Vmess{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
@ -428,6 +441,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, xudp: option.XUDP,
tfo: option.TFO, tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
@ -455,7 +469,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) N.TCPKeepAlive(c)
return c, nil return c, nil
} }
@ -464,15 +478,19 @@ func NewVmess(option VmessOption) (*Vmess, error) {
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
} }
tlsConfig := &tls.Config{ if option.ServerName == "" {
gunConfig.Host = v.addr
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
} })
if option.ServerName == "" {
if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host
gunConfig.Host = host }
} }
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
@ -489,17 +507,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return v, nil return v, nil
} }
type threadSafePacketConn struct {
net.PacketConn
access sync.Mutex
}
func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
c.access.Lock()
defer c.access.Unlock()
return c.PacketConn.WriteTo(b, addr)
}
type vmessPacketConn struct { type vmessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr

View File

@ -27,7 +27,6 @@ import (
"github.com/sagernet/sing/common/debug" "github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/device" "github.com/sagernet/wireguard-go/device"
) )
@ -36,7 +35,7 @@ type WireGuard struct {
bind *wireguard.ClientBind bind *wireguard.ClientBind
device *device.Device device *device.Device
tunDevice wireguard.Device tunDevice wireguard.Device
dialer *wgSingDialer dialer proxydialer.SingDialer
startOnce sync.Once startOnce sync.Once
startErr error startErr error
resolver *dns.Resolver resolver *dns.Resolver
@ -67,38 +66,7 @@ type WireGuardPeerOption struct {
PublicKey string `proxy:"public-key,omitempty"` PublicKey string `proxy:"public-key,omitempty"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"` PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"` Reserved []uint8 `proxy:"reserved,omitempty"`
AllowedIPs []string `proxy:"allowed_ips,omitempty"` 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 { type wgSingErrorHandler struct {
@ -168,7 +136,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
dialer: &wgSingDialer{dialer: dialer.NewDialer(), proxyName: option.DialerProxy}, dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()),
} }
runtime.SetFinalizer(outbound, closeWireGuard) runtime.SetFinalizer(outbound, closeWireGuard)
@ -302,7 +270,7 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
if err != nil { if err != nil {
return nil, E.Cause(err, "create WireGuard device") 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{}) { Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...))) 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) { func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := w.Base.DialOptions(opts...) options := w.Base.DialOptions(opts...)
w.dialer.dialer = dialer.NewDialer(options...) w.dialer.SetDialer(dialer.NewDialer(options...))
var conn net.Conn var conn net.Conn
w.startOnce.Do(func() { w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start() 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})) options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
} else { } else {
port, _ := strconv.Atoi(metadata.DstPort) conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
} }
if err != nil { if err != nil {
return nil, err 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) { func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := w.Base.DialOptions(opts...) options := w.Base.DialOptions(opts...)
w.dialer.dialer = dialer.NewDialer(options...) w.dialer.SetDialer(dialer.NewDialer(options...))
var pc net.PacketConn var pc net.PacketConn
w.startOnce.Do(func() { w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start() w.startErr = w.tunDevice.Start()
@ -412,8 +379,7 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
port, _ := strconv.Atoi(metadata.DstPort) pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)).Unwrap())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -499,9 +465,9 @@ func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
return nil, C.ErrNotSupport return nil, C.ErrNotSupport
} }
func (r *refProxyAdapter) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if r.proxyAdapter != nil { if r.proxyAdapter != nil {
return r.proxyAdapter.StreamConn(c, metadata) return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
} }
return nil, C.ErrNotSupport return nil, C.ErrNotSupport
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/callback" "github.com/Dreamacro/clash/common/callback"
N "github.com/Dreamacro/clash/common/net" N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
@ -19,6 +20,7 @@ type Fallback struct {
disableUDP bool disableUDP bool
testUrl string testUrl string
selected string selected string
expectedStatus string
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
@ -85,6 +87,8 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
"type": f.Type().String(), "type": f.Type().String(),
"now": f.Now(), "now": f.Now(),
"all": all, "all": all,
"testUrl": f.testUrl,
"expected": f.expectedStatus,
}) })
} }
@ -98,12 +102,14 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch) proxies := f.GetProxies(touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if len(f.selected) == 0 { if len(f.selected) == 0 {
if proxy.Alive() { // if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy return proxy
} }
} else { } else {
if proxy.Name() == f.selected { if proxy.Name() == f.selected {
if proxy.Alive() { // if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy return proxy
} else { } else {
f.selected = "" f.selected = ""
@ -129,10 +135,12 @@ func (f *Fallback) Set(name string) error {
} }
f.selected = name 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)) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel() defer cancel()
_, _ = p.URLTest(ctx, f.testUrl) expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus)
_, _ = p.URLTest(ctx, f.testUrl, expectedStatus, C.ExtraHistory)
} }
return nil return nil
@ -158,5 +166,6 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
}), }),
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL, testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
} }
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/atomic" "github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
@ -27,7 +28,7 @@ type GroupBase struct {
failedTestMux sync.Mutex failedTestMux sync.Mutex
failedTimes int failedTimes int
failedTime time.Time failedTime time.Time
failedTesting *atomic.Bool failedTesting atomic.Bool
proxies [][]C.Proxy proxies [][]C.Proxy
versions []atomic.Uint32 versions []atomic.Uint32
} }
@ -130,10 +131,6 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
} }
} }
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
var newProxies []C.Proxy var newProxies []C.Proxy
proxiesSet := map[string]struct{}{} proxiesSet := map[string]struct{}{}
@ -189,10 +186,14 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
proxies = newProxies proxies = newProxies
} }
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
return proxies 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 wg sync.WaitGroup
var lock sync.Mutex var lock sync.Mutex
mp := map[string]uint16{} mp := map[string]uint16{}
@ -201,7 +202,7 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
proxy := proxy proxy := proxy
wg.Add(1) wg.Add(1)
go func() { go func() {
delay, err := proxy.URLTest(ctx, url) delay, err := proxy.URLTest(ctx, url, expectedStatus, C.DropHistory)
if err == nil { if err == nil {
lock.Lock() lock.Lock()
mp[proxy.Name()] = delay mp[proxy.Name()] = delay

View File

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

View File

@ -3,19 +3,20 @@ package outboundgroup
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
) )
var ( var (
errFormat = errors.New("format error") errFormat = errors.New("format error")
errType = errors.New("unsupport type") errType = errors.New("unsupported type")
errMissProxy = errors.New("`use` or `proxies` missing") errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("duplicate provider name") errDuplicateProvider = errors.New("duplicate provider name")
) )
@ -32,6 +33,7 @@ type GroupCommonOption struct {
Filter string `group:"filter,omitempty"` Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"` ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,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) { 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{} providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 { if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, errMissProxy return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
} }
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 { if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies) ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s: %w", groupName, err)
} }
if _, ok := providersMap[groupName]; ok { if _, ok := providersMap[groupName]; ok {
return nil, errDuplicateProvider return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
} }
var url string
var interval uint
// select don't need health check // select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" { if groupOption.Type != "select" && groupOption.Type != "relay" {
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" { if groupOption.URL == "" {
groupOption.URL = "https://cp.cloudflare.com/generate_204" 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 groupOption.Interval = 300
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) url = groupOption.URL
interval = uint(groupOption.Interval)
}
hc := provider.NewHealthCheck(ps, url, interval, true, expectedStatus)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s: %w", groupName, err)
} }
providers = append(providers, pd) providers = append(providers, pd)
providersMap[groupName] = pd providersMap[groupName] = pd
} }
}
if len(groupOption.Use) != 0 { if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use) list, err := getProviders(providersMap, groupOption.Use)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s: %w", groupName, err)
} }
// different proxy groups use different test URL
addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
providers = append(providers, list...) providers = append(providers, list...)
} else { } else {
groupOption.Filter = "" groupOption.Filter = ""
@ -154,3 +169,13 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
} }
return ps, nil 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

@ -27,6 +27,7 @@ type URLTest struct {
*GroupBase *GroupBase
selected string selected string
testUrl string testUrl string
expectedStatus string
tolerance uint16 tolerance uint16
disableUDP bool disableUDP bool
fastNode C.Proxy fastNode C.Proxy
@ -96,44 +97,49 @@ func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
var s C.Proxy
proxies := u.GetProxies(touch) proxies := u.GetProxies(touch)
fast := proxies[0] if u.selected != "" {
if fast.Name() == u.selected { for _, proxy := range proxies {
s = fast if !proxy.Alive() {
continue
} }
min := fast.LastDelay() if proxy.Name() == u.selected {
u.fastNode = proxy
return proxy
}
}
}
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
fast := proxies[0]
// min := fast.LastDelay()
min := fast.LastDelayForTestUrl(u.testUrl)
fastNotExist := true fastNotExist := true
for _, proxy := range proxies[1:] { for _, proxy := range proxies[1:] {
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() { if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
fastNotExist = false fastNotExist = false
} }
if proxy.Name() == u.selected { // if !proxy.Alive() {
s = proxy if !proxy.AliveForTestUrl(u.testUrl) {
}
if !proxy.Alive() {
continue continue
} }
delay := proxy.LastDelay() // delay := proxy.LastDelay()
delay := proxy.LastDelayForTestUrl(u.testUrl)
if delay < min { if delay < min {
fast = proxy fast = proxy
min = delay min = delay
} }
} }
// tolerance // 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 u.fastNode = fast
} }
if s != nil {
if s.Alive() && s.LastDelay() < fast.LastDelay()+u.tolerance {
u.fastNode = s
}
}
return u.fastNode, nil return u.fastNode, nil
}) })
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
@ -166,6 +172,8 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
"type": u.Type().String(), "type": u.Type().String(),
"now": u.Now(), "now": u.Now(),
"all": all, "all": all,
"testUrl": u.testUrl,
"expected": u.expectedStatus,
}) })
} }
@ -200,6 +208,7 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL, testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
} }
for _, option := range options { for _, option := range options {

View File

@ -1,17 +1,5 @@
package outboundgroup 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 { type SelectAble interface {
Set(string) error Set(string) error
ForceSet(name string) ForceSet(name string)

View File

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

View File

@ -2,6 +2,8 @@ package provider
import ( import (
"context" "context"
"strings"
"sync"
"time" "time"
"github.com/Dreamacro/clash/common/atomic" "github.com/Dreamacro/clash/common/atomic"
@ -10,6 +12,8 @@ import (
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/dlclark/regexp2"
) )
const ( const (
@ -21,29 +25,46 @@ type HealthCheckOption struct {
Interval uint Interval uint
} }
type extraOption struct {
expectedStatus utils.IntRanges[uint16]
filters map[string]struct{}
}
type HealthCheck struct { type HealthCheck struct {
url string url string
extra map[string]*extraOption
mu sync.Mutex
started atomic.Bool
proxies []C.Proxy proxies []C.Proxy
interval uint interval time.Duration
lazy bool lazy bool
lastTouch *atomic.Int64 expectedStatus utils.IntRanges[uint16]
lastTouch atomic.TypedValue[time.Time]
done chan struct{} done chan struct{}
singleDo *singledo.Single[struct{}] singleDo *singledo.Single[struct{}]
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(hc.interval)
hc.start()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
now := time.Now().Unix() lastTouch := hc.lastTouch.Load()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) { since := time.Since(lastTouch)
if !hc.lazy || since < hc.interval {
hc.check() hc.check()
} else { } else {
log.Debugln("Skip once health check because we are lazy") log.Debugln("Skip once health check because we are lazy")
} }
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
hc.stop()
return return
} }
} }
@ -53,12 +74,77 @@ func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies 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 = time.Duration(interval) * time.Second
}
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 { func (hc *HealthCheck) auto() bool {
return hc.interval != 0 return hc.interval != 0
} }
func (hc *HealthCheck) touch() { func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix()) hc.lastTouch.Store(time.Now())
}
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
@ -66,35 +152,86 @@ func (hc *HealthCheck) check() {
id := utils.NewUUIDV4().String() id := utils.NewUUIDV4().String()
log.Debugln("Start New Health Checking {%s}", id) log.Debugln("Start New Health Checking {%s}", id)
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) 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() b.Wait()
log.Debugln("Finish A Health Checking {%s}", id) log.Debugln("Finish A Health Checking {%s}", id)
return struct{}{}, nil 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() { func (hc *HealthCheck) close() {
hc.done <- struct{}{} 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{ return &HealthCheck{
proxies: proxies, proxies: proxies,
url: url, url: url,
interval: interval, extra: map[string]*extraOption{},
interval: time.Duration(interval) * time.Second,
lazy: lazy, lazy: lazy,
lastTouch: atomic.NewInt64(0), expectedStatus: expectedStatus,
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second), singleDo: singledo.NewSingle[struct{}](time.Second),
} }

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package provider package provider
import ( import (
"github.com/dlclark/regexp2"
"strconv" "strconv"
"strings" "strings"
) )
@ -13,45 +12,24 @@ type SubscriptionInfo struct {
Expire int64 Expire int64
} }
func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) { func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) {
si = &SubscriptionInfo{} userinfo = strings.ToLower(userinfo)
str = strings.ToLower(str) userinfo = strings.ReplaceAll(userinfo, " ", "")
reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0) si = new(SubscriptionInfo)
reExpire := regexp2.MustCompile("expire=(\\d+)", 0) for _, field := range strings.Split(userinfo, ";") {
switch name, value, _ := strings.Cut(field, "="); name {
match, err := reTraffic.FindStringMatch(str) case "upload":
if err != nil || match == nil { si.Upload, err = strconv.ParseInt(value, 10, 64)
return nil, err case "download":
si.Download, err = strconv.ParseInt(value, 10, 64)
case "total":
si.Total, err = strconv.ParseInt(value, 10, 64)
case "expire":
si.Expire, err = strconv.ParseInt(value, 10, 64)
} }
group := match.Groups()
si.Upload, err = str2uint64(group[1].String())
if err != nil { if err != nil {
return nil, err return
}
si.Download, err = str2uint64(group[2].String())
if err != nil {
return nil, err
}
si.Total, err = str2uint64(group[3].String())
if err != nil {
return nil, err
}
match, _ = reExpire.FindStringMatch(str)
if match != nil {
group = match.Groups()
si.Expire, err = str2uint64(group[1].String())
if err != nil {
return nil, err
} }
} }
return return
} }
func str2uint64(str string) (int64, error) {
i, err := strconv.ParseInt(str, 10, 64)
return i, err
}

View File

@ -11,10 +11,9 @@ type Bool struct {
atomic.Bool atomic.Bool
} }
func NewBool(val bool) *Bool { func NewBool(val bool) (i Bool) {
i := &Bool{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Bool) MarshalJSON() ([]byte, error) { func (i *Bool) MarshalJSON() ([]byte, error) {
@ -39,12 +38,11 @@ type Pointer[T any] struct {
atomic.Pointer[T] atomic.Pointer[T]
} }
func NewPointer[T any](v *T) *Pointer[T] { func NewPointer[T any](v *T) (p Pointer[T]) {
var p Pointer[T]
if v != nil { if v != nil {
p.Store(v) p.Store(v)
} }
return &p return
} }
func (p *Pointer[T]) MarshalJSON() ([]byte, error) { func (p *Pointer[T]) MarshalJSON() ([]byte, error) {
@ -68,10 +66,9 @@ type Int32 struct {
atomic.Int32 atomic.Int32
} }
func NewInt32(val int32) *Int32 { func NewInt32(val int32) (i Int32) {
i := &Int32{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Int32) MarshalJSON() ([]byte, error) { func (i *Int32) MarshalJSON() ([]byte, error) {
@ -96,10 +93,9 @@ type Int64 struct {
atomic.Int64 atomic.Int64
} }
func NewInt64(val int64) *Int64 { func NewInt64(val int64) (i Int64) {
i := &Int64{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Int64) MarshalJSON() ([]byte, error) { func (i *Int64) MarshalJSON() ([]byte, error) {
@ -124,10 +120,9 @@ type Uint32 struct {
atomic.Uint32 atomic.Uint32
} }
func NewUint32(val uint32) *Uint32 { func NewUint32(val uint32) (i Uint32) {
i := &Uint32{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Uint32) MarshalJSON() ([]byte, error) { func (i *Uint32) MarshalJSON() ([]byte, error) {
@ -152,10 +147,9 @@ type Uint64 struct {
atomic.Uint64 atomic.Uint64
} }
func NewUint64(val uint64) *Uint64 { func NewUint64(val uint64) (i Uint64) {
i := &Uint64{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Uint64) MarshalJSON() ([]byte, error) { func (i *Uint64) MarshalJSON() ([]byte, error) {
@ -180,10 +174,9 @@ type Uintptr struct {
atomic.Uintptr atomic.Uintptr
} }
func NewUintptr(val uintptr) *Uintptr { func NewUintptr(val uintptr) (i Uintptr) {
i := &Uintptr{}
i.Store(val) i.Store(val)
return i return
} }
func (i *Uintptr) MarshalJSON() ([]byte, error) { func (i *Uintptr) MarshalJSON() ([]byte, error) {

View File

@ -12,6 +12,7 @@ func DefaultValue[T any]() T {
type TypedValue[T any] struct { type TypedValue[T any] struct {
value atomic.Value value atomic.Value
_ noCopy
} }
func (t *TypedValue[T]) Load() T { func (t *TypedValue[T]) Load() T {
@ -51,8 +52,13 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func NewTypedValue[T any](t T) *TypedValue[T] { func NewTypedValue[T any](t T) (v TypedValue[T]) {
v := &TypedValue[T]{}
v.Store(t) v.Store(t)
return v return
} }
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

View File

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

View File

@ -7,6 +7,8 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/generics/list" "github.com/Dreamacro/clash/common/generics/list"
"github.com/samber/lo"
) )
// Option is part of Functional Options Pattern // 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 // Get returns the any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache[K, V]) Get(key K) (V, bool) { func (c *LruCache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { 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 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. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) { func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { 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 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. // Set stores the any representation of a response for a given key.
func (c *LruCache[K, V]) Set(key K, value V) { 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) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge 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. // 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() c.mu.Lock()
defer c.mu.Unlock() 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 { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value 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] { func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
le, ok := c.cache[key] le, ok := c.cache[key]
if !ok { if !ok {
return nil 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. // Delete removes the value associated with a key.
func (c *LruCache[K, V]) Delete(key K) { func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.deleteElement(le) c.deleteElement(le)
} }
c.mu.Unlock()
} }
func (c *LruCache[K, V]) maybeDeleteOldest() { 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 { func (c *LruCache[K, V]) Clear() error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[K]*list.Element[*entry[K, V]]) c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil return nil
} }
@ -231,8 +261,3 @@ type entry[K comparable, V any] struct {
value V value V
expires int64 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) { func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
_, err := ExecCmd("dir") _, err := ExecCmd("cmd -c 'dir'")
assert.Nil(t, err) assert.Nil(t, err)
return return
} }

View File

@ -50,7 +50,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port() hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer") hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs") 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["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol") hysteria["protocol"] = query.Get("protocol")
up := query.Get("up") 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")) hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria) 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": case "trojan":
urlTrojan, err := url.Parse(line) urlTrojan, err := url.Parse(line)
@ -86,10 +161,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["udp"] = true trojan["udp"] = true
trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure"))
sni := query.Get("sni") if sni := query.Get("sni"); sni != "" {
if sni != "" {
trojan["sni"] = sni trojan["sni"] = sni
} }
if alpn := query.Get("alpn"); alpn != "" {
trojan["alpn"] = strings.Split(alpn, ",")
}
network := strings.ToLower(query.Get("type")) network := strings.ToLower(query.Get("type"))
if network != "" { if network != "" {
@ -217,6 +294,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if strings.HasSuffix(tls, "tls") { if strings.HasSuffix(tls, "tls") {
vmess["tls"] = true vmess["tls"] = true
} }
if alpn, ok := values["alpn"].(string); ok {
vmess["alpn"] = strings.Split(alpn, ",")
}
} }
switch network { switch network {
@ -332,6 +412,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
} }
} }
proxies = append(proxies, ss) proxies = append(proxies, ss)
case "ssr": case "ssr":
dcBuf, err := encRaw.DecodeString(body) dcBuf, err := encRaw.DecodeString(body)
if err != nil { 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["port"] = url.Port()
proxy["uuid"] = url.User.Username() proxy["uuid"] = url.User.Username()
proxy["udp"] = true proxy["udp"] = true
proxy["skip-cert-verify"] = false
proxy["tls"] = false
tls := strings.ToLower(query.Get("security")) tls := strings.ToLower(query.Get("security"))
if strings.HasSuffix(tls, "tls") || tls == "reality" { if strings.HasSuffix(tls, "tls") || tls == "reality" {
proxy["tls"] = true proxy["tls"] = true
@ -34,6 +32,9 @@ func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy m
} else { } else {
proxy["client-fingerprint"] = fingerprint proxy["client-fingerprint"] = fingerprint
} }
if alpn := query.Get("alpn"); alpn != "" {
proxy["alpn"] = strings.Split(alpn, ",")
}
} }
if sni := query.Get("sni"); sni != "" { if sni := query.Get("sni"); sni != "" {
proxy["servername"] = sni proxy["servername"] = sni

View File

@ -3,34 +3,43 @@ package net
import "net" import "net"
type bindPacketConn struct { type bindPacketConn struct {
net.PacketConn EnhancePacketConn
rAddr net.Addr rAddr net.Addr
} }
func (wpc *bindPacketConn) Read(b []byte) (n int, err error) { func (c *bindPacketConn) Read(b []byte) (n int, err error) {
n, _, err = wpc.PacketConn.ReadFrom(b) n, _, err = c.EnhancePacketConn.ReadFrom(b)
return n, err return n, err
} }
func (wpc *bindPacketConn) Write(b []byte) (n int, err error) { func (c *bindPacketConn) WaitRead() (data []byte, put func(), err error) {
return wpc.PacketConn.WriteTo(b, wpc.rAddr) data, put, _, err = c.EnhancePacketConn.WaitReadFrom()
return
} }
func (wpc *bindPacketConn) RemoteAddr() net.Addr { func (c *bindPacketConn) Write(b []byte) (n int, err error) {
return wpc.rAddr return c.EnhancePacketConn.WriteTo(b, c.rAddr)
} }
func (wpc *bindPacketConn) LocalAddr() net.Addr { func (c *bindPacketConn) RemoteAddr() net.Addr {
if wpc.PacketConn.LocalAddr() == nil { return c.rAddr
}
func (c *bindPacketConn) LocalAddr() net.Addr {
if c.EnhancePacketConn.LocalAddr() == nil {
return &net.UDPAddr{IP: net.IPv4zero, Port: 0} return &net.UDPAddr{IP: net.IPv4zero, Port: 0}
} else { } else {
return wpc.PacketConn.LocalAddr() return c.EnhancePacketConn.LocalAddr()
} }
} }
func (c *bindPacketConn) Upstream() any {
return c.EnhancePacketConn
}
func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn { func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn {
return &bindPacketConn{ return &bindPacketConn{
PacketConn: pc, EnhancePacketConn: NewEnhancePacketConn(pc),
rAddr: rAddr, rAddr: rAddr,
} }
} }

View File

@ -22,6 +22,16 @@ func NewBufferedConn(c net.Conn) *BufferedConn {
return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false} return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false}
} }
func WarpConnWithBioReader(c net.Conn, br *bufio.Reader) net.Conn {
if br != nil && br.Buffered() > 0 {
if bc, ok := c.(*BufferedConn); ok && bc.r == br {
return bc
}
return &BufferedConn{br, NewExtendedConn(c), true}
}
return c
}
// Reader returns the internal bufio.Reader. // Reader returns the internal bufio.Reader.
func (c *BufferedConn) Reader() *bufio.Reader { func (c *BufferedConn) Reader() *bufio.Reader {
return c.r return c.r
@ -62,7 +72,7 @@ func (c *BufferedConn) Buffered() int {
} }
func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) { func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) {
if c.r.Buffered() > 0 { if c.r != nil && c.r.Buffered() > 0 {
_, err = buffer.ReadOnceFrom(c.r) _, err = buffer.ReadOnceFrom(c.r)
return return
} }
@ -70,10 +80,11 @@ func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) {
} }
func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy
if c.r.Buffered() > 0 { if c.r != nil && c.r.Buffered() > 0 {
length := c.r.Buffered() length := c.r.Buffered()
b, _ := c.r.Peek(length) b, _ := c.r.Peek(length)
_, _ = c.r.Discard(length) _, _ = c.r.Discard(length)
c.r = nil // drop bufio.Reader to let gc can clean up its internal buf
return buf.As(b) return buf.As(b)
} }
return nil return nil
@ -84,7 +95,7 @@ func (c *BufferedConn) Upstream() any {
} }
func (c *BufferedConn) ReaderReplaceable() bool { func (c *BufferedConn) ReaderReplaceable() bool {
if c.r.Buffered() > 0 { if c.r != nil && c.r.Buffered() > 0 {
return false return false
} }
return true return true

49
common/net/cached.go Normal file
View File

@ -0,0 +1,49 @@
package net
import (
"net"
"github.com/Dreamacro/clash/common/buf"
)
var _ ExtendedConn = (*CachedConn)(nil)
type CachedConn struct {
ExtendedConn
data []byte
}
func NewCachedConn(c net.Conn, data []byte) *CachedConn {
return &CachedConn{NewExtendedConn(c), data}
}
func (c *CachedConn) Read(b []byte) (n int, err error) {
if len(c.data) > 0 {
n = copy(b, c.data)
c.data = c.data[n:]
return
}
return c.ExtendedConn.Read(b)
}
func (c *CachedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy
if len(c.data) > 0 {
return buf.As(c.data)
}
return nil
}
func (c *CachedConn) Upstream() any {
return c.ExtendedConn
}
func (c *CachedConn) ReaderReplaceable() bool {
if len(c.data) > 0 {
return false
}
return true
}
func (c *CachedConn) WriterReplaceable() bool {
return true
}

31
common/net/context.go Normal file
View File

@ -0,0 +1,31 @@
package net
import (
"context"
"net"
)
// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine.
func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) {
var (
quit = make(chan struct{})
interrupt = make(chan error, 1)
)
go func() {
select {
case <-quit:
interrupt <- nil
case <-ctx.Done():
// Close the connection, discarding the error
_ = conn.Close()
interrupt <- ctx.Err()
}
}()
return func(inputErr *error) {
close(quit)
if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil {
// Return context error to user.
inputErr = &ctxErr
}
}
}

View File

@ -0,0 +1,154 @@
package deadline
import (
"net"
"os"
"runtime"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/net/packet"
)
type readResult struct {
data []byte
addr net.Addr
err error
}
type NetPacketConn struct {
net.PacketConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan any
}
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
npc := &NetPacketConn{
PacketConn: pc,
pipeDeadline: makePipeDeadline(),
resultCh: make(chan any, 1),
}
npc.resultCh <- nil
if enhancePC, isEnhance := pc.(packet.EnhancePacketConn); isEnhance {
epc := &EnhancePacketConn{
NetPacketConn: npc,
enhancePacketConn: enhancePacketConn{
netPacketConn: npc,
enhancePacketConn: enhancePC,
},
}
if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC {
return &EnhanceSingPacketConn{
EnhancePacketConn: epc,
singPacketConn: singPacketConn{
netPacketConn: npc,
singPacketConn: singPC,
},
}
}
return epc
}
if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC {
return &SingPacketConn{
NetPacketConn: npc,
singPacketConn: singPacketConn{
netPacketConn: npc,
singPacketConn: singPC,
},
}
}
return npc
}
func (c *NetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
FOR:
for {
select {
case result := <-c.resultCh:
if result != nil {
if result, ok := result.(*readResult); ok {
n = copy(p, result.data)
addr = result.addr
err = result.err
c.resultCh <- nil // finish cache read
return
}
c.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.resultCh <- nil
break FOR
}
case <-c.pipeDeadline.wait():
return 0, nil, os.ErrDeadlineExceeded
}
}
if c.disablePipe.Load() {
return c.PacketConn.ReadFrom(p)
} else if c.deadline.Load().IsZero() {
c.inRead.Store(true)
defer c.inRead.Store(false)
n, addr, err = c.PacketConn.ReadFrom(p)
return
}
<-c.resultCh
go c.pipeReadFrom(len(p))
return c.ReadFrom(p)
}
func (c *NetPacketConn) pipeReadFrom(size int) {
buffer := make([]byte, size)
n, addr, err := c.PacketConn.ReadFrom(buffer)
buffer = buffer[:n]
result := &readResult{}
result.data = buffer
result.addr = addr
result.err = err
c.resultCh <- result
}
func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
if c.disablePipe.Load() {
return c.PacketConn.SetReadDeadline(t)
} else if c.inRead.Load() {
c.disablePipe.Store(true)
return c.PacketConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
return nil
}
func (c *NetPacketConn) ReaderReplaceable() bool {
select {
case result := <-c.resultCh:
c.resultCh <- result
if result != nil {
return false // cache reading
} else {
break
}
default:
return false // pipe reading
}
return c.disablePipe.Load() || c.deadline.Load().IsZero()
}
func (c *NetPacketConn) WriterReplaceable() bool {
return true
}
func (c *NetPacketConn) Upstream() any {
return c.PacketConn
}
func (c *NetPacketConn) NeedAdditionalReadDeadline() bool {
return false
}

View File

@ -0,0 +1,83 @@
package deadline
import (
"net"
"os"
"runtime"
"github.com/Dreamacro/clash/common/net/packet"
)
type EnhancePacketConn struct {
*NetPacketConn
enhancePacketConn
}
var _ packet.EnhancePacketConn = (*EnhancePacketConn)(nil)
func NewEnhancePacketConn(pc packet.EnhancePacketConn) packet.EnhancePacketConn {
return NewNetPacketConn(pc).(packet.EnhancePacketConn)
}
type enhanceReadResult struct {
data []byte
put func()
addr net.Addr
err error
}
type enhancePacketConn struct {
netPacketConn *NetPacketConn
enhancePacketConn packet.EnhancePacketConn
}
func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*enhanceReadResult); ok {
data = result.data
put = result.put
addr = result.addr
err = result.err
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return nil, nil, nil, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.enhancePacketConn.WaitReadFrom()
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
data, put, addr, err = c.enhancePacketConn.WaitReadFrom()
return
}
<-c.netPacketConn.resultCh
go c.pipeWaitReadFrom()
return c.WaitReadFrom()
}
func (c *enhancePacketConn) pipeWaitReadFrom() {
data, put, addr, err := c.enhancePacketConn.WaitReadFrom()
result := &enhanceReadResult{}
result.data = data
result.put = put
result.addr = addr
result.err = err
c.netPacketConn.resultCh <- result
}

View File

@ -0,0 +1,177 @@
package deadline
import (
"os"
"runtime"
"github.com/Dreamacro/clash/common/net/packet"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingPacketConn struct {
*NetPacketConn
singPacketConn
}
var _ packet.SingPacketConn = (*SingPacketConn)(nil)
func NewSingPacketConn(pc packet.SingPacketConn) packet.SingPacketConn {
return NewNetPacketConn(pc).(packet.SingPacketConn)
}
type EnhanceSingPacketConn struct {
*EnhancePacketConn
singPacketConn
}
func NewEnhanceSingPacketConn(pc packet.EnhanceSingPacketConn) packet.EnhanceSingPacketConn {
return NewNetPacketConn(pc).(packet.EnhanceSingPacketConn)
}
var _ packet.EnhanceSingPacketConn = (*EnhanceSingPacketConn)(nil)
type singReadResult struct {
buffer *buf.Buffer
destination M.Socksaddr
err error
}
type singPacketConn struct {
netPacketConn *NetPacketConn
singPacketConn packet.SingPacketConn
}
func (c *singPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*singReadResult); ok {
destination = result.destination
err = result.err
n, _ := buffer.Write(result.buffer.Bytes())
result.buffer.Advance(n)
if result.buffer.IsEmpty() {
result.buffer.Release()
}
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.singPacketConn.ReadPacket(buffer)
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
destination, err = c.singPacketConn.ReadPacket(buffer)
return
}
<-c.netPacketConn.resultCh
go c.pipeReadPacket(buffer.FreeLen())
return c.ReadPacket(buffer)
}
func (c *singPacketConn) pipeReadPacket(pLen int) {
buffer := buf.NewSize(pLen)
destination, err := c.singPacketConn.ReadPacket(buffer)
result := &singReadResult{}
result.destination = destination
result.err = err
c.netPacketConn.resultCh <- result
}
func (c *singPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *singPacketConn) CreateReadWaiter() (N.PacketReadWaiter, bool) {
prw, isReadWaiter := bufio.CreatePacketReadWaiter(c.singPacketConn)
if isReadWaiter {
return &singPacketReadWaiter{
netPacketConn: c.netPacketConn,
packetReadWaiter: prw,
}, true
}
return nil, false
}
var _ N.PacketReadWaiter = (*singPacketReadWaiter)(nil)
type singPacketReadWaiter struct {
netPacketConn *NetPacketConn
packetReadWaiter N.PacketReadWaiter
}
type singWaitReadResult singReadResult
func (c *singPacketReadWaiter) InitializeReadWaiter(newBuffer func() *buf.Buffer) {
c.packetReadWaiter.InitializeReadWaiter(newBuffer)
}
func (c *singPacketReadWaiter) WaitReadPacket() (destination M.Socksaddr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*singWaitReadResult); ok {
destination = result.destination
err = result.err
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.packetReadWaiter.WaitReadPacket()
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
destination, err = c.packetReadWaiter.WaitReadPacket()
return
}
<-c.netPacketConn.resultCh
go c.pipeWaitReadPacket()
return c.WaitReadPacket()
}
func (c *singPacketReadWaiter) pipeWaitReadPacket() {
destination, err := c.packetReadWaiter.WaitReadPacket()
result := &singWaitReadResult{}
result.destination = destination
result.err = err
c.netPacketConn.resultCh <- result
}
func (c *singPacketReadWaiter) Upstream() any {
return c.packetReadWaiter
}

View File

@ -0,0 +1,84 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package deadline
import (
"sync"
"time"
)
// pipeDeadline is an abstraction for handling timeouts.
type pipeDeadline struct {
mu sync.Mutex // Guards timer and cancel
timer *time.Timer
cancel chan struct{} // Must be non-nil
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
}
// set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future.
//
// A zero value for t prevents timeout.
func (d *pipeDeadline) set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil && !d.timer.Stop() {
<-d.cancel // Wait for the timer callback to finish and close cancel
}
d.timer = nil
// Time is zero, then there is no deadline.
closed := isClosedChan(d.cancel)
if t.IsZero() {
if closed {
d.cancel = make(chan struct{})
}
return
}
// Time in the future, setup a timer to cancel in the future.
if dur := time.Until(t); dur > 0 {
if closed {
d.cancel = make(chan struct{})
}
d.timer = time.AfterFunc(dur, func() {
close(d.cancel)
})
return
}
// Time in the past, so close immediately.
if !closed {
close(d.cancel)
}
}
// wait returns a channel that is closed when the deadline is exceeded.
func (d *pipeDeadline) wait() chan struct{} {
d.mu.Lock()
defer d.mu.Unlock()
return d.cancel
}
func isClosedChan(c <-chan struct{}) bool {
select {
case <-c:
return true
default:
return false
}
}
func makeFilledChan() chan struct{} {
ch := make(chan struct{}, 1)
ch <- struct{}{}
return ch
}

18
common/net/packet.go Normal file
View File

@ -0,0 +1,18 @@
package net
import (
"github.com/Dreamacro/clash/common/net/deadline"
"github.com/Dreamacro/clash/common/net/packet"
)
type EnhancePacketConn = packet.EnhancePacketConn
type WaitReadFrom = packet.WaitReadFrom
var NewEnhancePacketConn = packet.NewEnhancePacketConn
var NewThreadSafePacketConn = packet.NewThreadSafePacketConn
var NewRefPacketConn = packet.NewRefPacketConn
var NewDeadlineNetPacketConn = deadline.NewNetPacketConn
var NewDeadlineEnhancePacketConn = deadline.NewEnhancePacketConn
var NewDeadlineSingPacketConn = deadline.NewSingPacketConn
var NewDeadlineEnhanceSingPacketConn = deadline.NewEnhanceSingPacketConn

View File

@ -0,0 +1,77 @@
package packet
import (
"net"
"github.com/Dreamacro/clash/common/pool"
)
type WaitReadFrom interface {
WaitReadFrom() (data []byte, put func(), addr net.Addr, err error)
}
type EnhancePacketConn interface {
net.PacketConn
WaitReadFrom
}
func NewEnhancePacketConn(pc net.PacketConn) EnhancePacketConn {
if udpConn, isUDPConn := pc.(*net.UDPConn); isUDPConn {
return &enhanceUDPConn{UDPConn: udpConn}
}
if enhancePC, isEnhancePC := pc.(EnhancePacketConn); isEnhancePC {
return enhancePC
}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return newEnhanceSingPacketConn(singPC)
}
return &enhancePacketConn{PacketConn: pc}
}
type enhancePacketConn struct {
net.PacketConn
}
func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
return waitReadFrom(c.PacketConn)
}
func (c *enhancePacketConn) Upstream() any {
return c.PacketConn
}
func (c *enhancePacketConn) WriterReplaceable() bool {
return true
}
func (c *enhancePacketConn) ReaderReplaceable() bool {
return true
}
func (c *enhanceUDPConn) Upstream() any {
return c.UDPConn
}
func (c *enhanceUDPConn) WriterReplaceable() bool {
return true
}
func (c *enhanceUDPConn) ReaderReplaceable() bool {
return true
}
func waitReadFrom(pc net.PacketConn) (data []byte, put func(), addr net.Addr, err error) {
readBuf := pool.Get(pool.UDPBufferSize)
put = func() {
_ = pool.Put(readBuf)
}
var readN int
readN, addr, err = pc.ReadFrom(readBuf)
if readN > 0 {
data = readBuf[:readN]
} else {
put()
put = nil
}
return
}

View File

@ -0,0 +1,65 @@
//go:build !windows
package packet
import (
"net"
"strconv"
"syscall"
"github.com/Dreamacro/clash/common/pool"
)
type enhanceUDPConn struct {
*net.UDPConn
rawConn syscall.RawConn
}
func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
if c.rawConn == nil {
c.rawConn, _ = c.UDPConn.SyscallConn()
}
var readErr error
err = c.rawConn.Read(func(fd uintptr) (done bool) {
readBuf := pool.Get(pool.UDPBufferSize)
put = func() {
_ = pool.Put(readBuf)
}
var readFrom syscall.Sockaddr
var readN int
readN, _, _, readFrom, readErr = syscall.Recvmsg(int(fd), readBuf, nil, 0)
if readN > 0 {
data = readBuf[:readN]
} else {
put()
put = nil
data = nil
}
if readErr == syscall.EAGAIN {
return false
}
if readFrom != nil {
switch from := readFrom.(type) {
case *syscall.SockaddrInet4:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
case *syscall.SockaddrInet6:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
}
}
// udp should not convert readN == 0 to io.EOF
//if readN == 0 {
// readErr = io.EOF
//}
return true
})
if err != nil {
return
}
if readErr != nil {
err = readErr
return
}
return
}

View File

@ -0,0 +1,79 @@
package packet
import (
"net"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingPacketConn = N.NetPacketConn
type EnhanceSingPacketConn interface {
SingPacketConn
EnhancePacketConn
}
type enhanceSingPacketConn struct {
SingPacketConn
packetReadWaiter N.PacketReadWaiter
}
func (c *enhanceSingPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
var buff *buf.Buffer
var dest M.Socksaddr
newBuffer := func() *buf.Buffer {
buff = buf.NewPacket() // do not use stack buffer
return buff
}
if c.packetReadWaiter != nil {
c.packetReadWaiter.InitializeReadWaiter(newBuffer)
defer c.packetReadWaiter.InitializeReadWaiter(nil)
dest, err = c.packetReadWaiter.WaitReadPacket()
} else {
dest, err = c.SingPacketConn.ReadPacket(newBuffer())
}
if dest.IsFqdn() {
addr = dest
} else {
addr = dest.UDPAddr()
}
if err != nil {
if buff != nil {
buff.Release()
}
return
}
if buff == nil {
return
}
if buff.IsEmpty() {
buff.Release()
return
}
data = buff.Bytes()
put = buff.Release
return
}
func (c *enhanceSingPacketConn) Upstream() any {
return c.SingPacketConn
}
func (c *enhanceSingPacketConn) WriterReplaceable() bool {
return true
}
func (c *enhanceSingPacketConn) ReaderReplaceable() bool {
return true
}
func newEnhanceSingPacketConn(conn SingPacketConn) *enhanceSingPacketConn {
epc := &enhanceSingPacketConn{SingPacketConn: conn}
if readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn); isReadWaiter {
epc.packetReadWaiter = readWaiter
}
return epc
}

View File

@ -0,0 +1,15 @@
//go:build windows
package packet
import (
"net"
)
type enhanceUDPConn struct {
*net.UDPConn
}
func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
return waitReadFrom(c.UDPConn)
}

75
common/net/packet/ref.go Normal file
View File

@ -0,0 +1,75 @@
package packet
import (
"net"
"runtime"
"time"
)
type refPacketConn struct {
pc EnhancePacketConn
ref any
}
func (c *refPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.WaitReadFrom()
}
func (c *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.ReadFrom(p)
}
func (c *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.WriteTo(p, addr)
}
func (c *refPacketConn) Close() error {
defer runtime.KeepAlive(c.ref)
return c.pc.Close()
}
func (c *refPacketConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.pc.LocalAddr()
}
func (c *refPacketConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetDeadline(t)
}
func (c *refPacketConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetReadDeadline(t)
}
func (c *refPacketConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetWriteDeadline(t)
}
func (c *refPacketConn) Upstream() any {
return c.pc
}
func (c *refPacketConn) ReaderReplaceable() bool { // Relay() will handle reference
return true
}
func (c *refPacketConn) WriterReplaceable() bool { // Relay() will handle reference
return true
}
func NewRefPacketConn(pc net.PacketConn, ref any) EnhancePacketConn {
rPC := &refPacketConn{pc: NewEnhancePacketConn(pc), ref: ref}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return &refSingPacketConn{
refPacketConn: rPC,
singPacketConn: singPC,
}
}
return rPC
}

View File

@ -0,0 +1,26 @@
package packet
import (
"runtime"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type refSingPacketConn struct {
*refPacketConn
singPacketConn SingPacketConn
}
var _ N.NetPacketConn = (*refSingPacketConn)(nil)
func (c *refSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer runtime.KeepAlive(c.ref)
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *refSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
defer runtime.KeepAlive(c.ref)
return c.singPacketConn.ReadPacket(buffer)
}

View File

@ -0,0 +1,36 @@
package packet
import (
"net"
"sync"
)
type threadSafePacketConn struct {
EnhancePacketConn
access sync.Mutex
}
func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
c.access.Lock()
defer c.access.Unlock()
return c.EnhancePacketConn.WriteTo(b, addr)
}
func (c *threadSafePacketConn) Upstream() any {
return c.EnhancePacketConn
}
func (c *threadSafePacketConn) ReaderReplaceable() bool {
return true
}
func NewThreadSafePacketConn(pc net.PacketConn) EnhancePacketConn {
tsPC := &threadSafePacketConn{EnhancePacketConn: NewEnhancePacketConn(pc)}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return &threadSafeSingPacketConn{
threadSafePacketConn: tsPC,
singPacketConn: singPC,
}
}
return tsPC
}

View File

@ -0,0 +1,24 @@
package packet
import (
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type threadSafeSingPacketConn struct {
*threadSafePacketConn
singPacketConn SingPacketConn
}
var _ N.NetPacketConn = (*threadSafeSingPacketConn)(nil)
func (c *threadSafeSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
c.access.Lock()
defer c.access.Unlock()
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *threadSafeSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
return c.singPacketConn.ReadPacket(buffer)
}

View File

@ -80,47 +80,3 @@ var _ ExtendedConn = (*refConn)(nil)
func NewRefConn(conn net.Conn, ref any) net.Conn { func NewRefConn(conn net.Conn, ref any) net.Conn {
return &refConn{conn: NewExtendedConn(conn), ref: ref} return &refConn{conn: NewExtendedConn(conn), ref: ref}
} }
type refPacketConn struct {
pc net.PacketConn
ref any
}
func (pc *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
defer runtime.KeepAlive(pc.ref)
return pc.pc.ReadFrom(p)
}
func (pc *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
defer runtime.KeepAlive(pc.ref)
return pc.pc.WriteTo(p, addr)
}
func (pc *refPacketConn) Close() error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.Close()
}
func (pc *refPacketConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(pc.ref)
return pc.pc.LocalAddr()
}
func (pc *refPacketConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetDeadline(t)
}
func (pc *refPacketConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetReadDeadline(t)
}
func (pc *refPacketConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetWriteDeadline(t)
}
func NewRefPacketConn(pc net.PacketConn, ref any) net.PacketConn {
return &refPacketConn{pc: pc, ref: ref}
}

View File

@ -23,10 +23,6 @@ func NewDeadlineConn(conn net.Conn) ExtendedConn {
return deadline.NewFallbackConn(conn) return deadline.NewFallbackConn(conn)
} }
func NewDeadlinePacketConn(pc net.PacketConn) net.PacketConn {
return deadline.NewFallbackPacketConn(bufio.NewPacketConn(pc))
}
func NeedHandshake(conn any) bool { func NeedHandshake(conn any) bool {
if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() { if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() {
return true return true

View File

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

View File

@ -10,8 +10,12 @@ import (
"math/big" "math/big"
) )
func ParseCert(certificate, privateKey string) (tls.Certificate, error) { type Path interface {
if certificate == "" || privateKey == "" { Resolve(path string) string
}
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair() return newRandomTLSKeyPair()
} }
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
@ -19,6 +23,8 @@ func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
return cert, nil return cert, nil
} }
certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey)
cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey) cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey)
if loadErr != nil { if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@ -47,6 +47,7 @@ func (p *Picker[T]) Wait() T {
p.wg.Wait() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
p.cancel = nil
} }
return p.result return p.result
} }
@ -69,6 +70,7 @@ func (p *Picker[T]) Go(f func() (T, error)) {
p.result = ret p.result = ret
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
p.cancel = nil
} }
}) })
} else { } 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" "testing"
"time" "time"
"github.com/samber/lo"
"github.com/stretchr/testify/assert" "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: case <-timer.C:
return input, nil return input, nil
case <-ctx.Done(): 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)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.Equal(t, number, getZero[int]()) assert.Equal(t, number, lo.Empty[int]())
assert.NotNil(t, picker.Error()) 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 // Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte { func (alloc *Allocator) Get(size int) []byte {
if size <= 0 || size > 65536 { switch {
case size < 0:
panic("alloc.Get: len out of range")
case size == 0:
return nil return nil
} case size > 65536:
return make([]byte, size)
default:
bits := msb(size) bits := msb(size)
if size == 1<<bits { if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size] return alloc.buffers[bits].Get().([]byte)[:size]
} }
return alloc.buffers[bits+1].Get().([]byte)[:size] return alloc.buffers[bits+1].Get().([]byte)[:size]
}
} }
// Put returns a []byte to pool for future use, // Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n // which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error { func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}
bits := msb(cap(buf)) bits := msb(cap(buf))
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits { if cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }

View File

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

View File

@ -2,6 +2,8 @@ package queue
import ( import (
"sync" "sync"
"github.com/samber/lo"
) )
// Queue is a simple concurrent safe queue // Queue is a simple concurrent safe queue
@ -24,7 +26,7 @@ func (q *Queue[T]) Put(items ...T) {
// Pop returns the head of items. // Pop returns the head of items.
func (q *Queue[T]) Pop() T { func (q *Queue[T]) Pop() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return GetZero[T]() return lo.Empty[T]()
} }
q.lock.Lock() q.lock.Lock()
@ -37,7 +39,7 @@ func (q *Queue[T]) Pop() T {
// Last returns the last of item. // Last returns the last of item.
func (q *Queue[T]) Last() T { func (q *Queue[T]) Last() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return GetZero[T]() return lo.Empty[T]()
} }
q.lock.RLock() q.lock.RLock()
@ -69,8 +71,3 @@ func New[T any](hint int64) *Queue[T] {
items: make([]T, 0, hint), 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) return d.decodeFloat(name, data, val)
} }
switch kind { 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: case reflect.String:
return d.decodeString(name, data, val) return d.decodeString(name, data, val)
case reflect.Bool: case reflect.Bool:
@ -282,6 +287,9 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
} }
valSlice := val 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++ { for i := 0; i < dataVal.Len(); i++ {
currentData := dataVal.Index(i).Interface() currentData := dataVal.Index(i).Interface()
for valSlice.Len() <= i { for valSlice.Len() <= i {

View File

@ -0,0 +1,8 @@
package util
import "github.com/samber/lo"
func EmptyOr[T comparable](v T, def T) T {
ret, _ := lo.Coalesce(v, def)
return ret
}

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 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 { if start > end {
return &Range[T]{ return Range[T]{
start: end, start: end,
end: start, end: start,
} }
} }
return &Range[T]{ return Range[T]{
start: start, start: start,
end: end, 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 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 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 return t > r.start && t <= r.end
} }
func (r *Range[T]) Start() T { func (r Range[T]) Start() T {
return r.start return r.start
} }
func (r *Range[T]) End() T { func (r Range[T]) End() T {
return r.end 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,9 +1,5 @@
package auth package auth
import (
"sync"
)
type Authenticator interface { type Authenticator interface {
Verify(user string, pass string) bool Verify(user string, pass string) bool
Users() []string Users() []string
@ -15,12 +11,12 @@ type AuthUser struct {
} }
type inMemoryAuthenticator struct { type inMemoryAuthenticator struct {
storage *sync.Map storage map[string]string
usernames []string usernames []string
} }
func (au *inMemoryAuthenticator) Verify(user string, pass string) bool { func (au *inMemoryAuthenticator) Verify(user string, pass string) bool {
realPass, ok := au.storage.Load(user) realPass, ok := au.storage[user]
return ok && realPass == pass return ok && realPass == pass
} }
@ -30,17 +26,13 @@ func NewAuthenticator(users []AuthUser) Authenticator {
if len(users) == 0 { if len(users) == 0 {
return nil return nil
} }
au := &inMemoryAuthenticator{
au := &inMemoryAuthenticator{storage: &sync.Map{}} storage: make(map[string]string),
for _, user := range users { usernames: make([]string, 0, len(users)),
au.storage.Store(user.User, user.Pass) }
for _, user := range users {
au.storage[user.User] = user.Pass
au.usernames = append(au.usernames, user.User)
} }
usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value any) bool {
usernames = append(usernames, key.(string))
return true
})
au.usernames = usernames
return au return au
} }

View File

@ -1,4 +1,4 @@
package tls package ca
import ( import (
"bytes" "bytes"
@ -8,16 +8,15 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"sync" "sync"
xtls "github.com/xtls/go"
) )
var trustCerts []*x509.Certificate var trustCerts []*x509.Certificate
var certPool *x509.CertPool var globalCertPool *x509.CertPool
var mutex sync.RWMutex 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 { func AddCertificate(certificate string) error {
mutex.Lock() mutex.Lock()
@ -33,30 +32,37 @@ func AddCertificate(certificate string) error {
} }
} }
func initializeCertPool() {
var err error
globalCertPool, err = x509.SystemCertPool()
if err != nil {
globalCertPool = x509.NewCertPool()
}
for _, cert := range trustCerts {
globalCertPool.AddCert(cert)
}
}
func ResetCertificate() { func ResetCertificate() {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
trustCerts = nil trustCerts = nil
initializeCertPool()
} }
func getCertPool() *x509.CertPool { func getCertPool() *x509.CertPool {
if len(trustCerts) == 0 { if len(trustCerts) == 0 {
return nil return nil
} }
if certPool == nil { if globalCertPool == nil {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
if certPool != nil { if globalCertPool != nil {
return certPool return globalCertPool
} }
certPool, err := x509.SystemCertPool() initializeCertPool()
if err == nil {
for _, cert := range trustCerts {
certPool.AddCert(cert)
} }
} return globalCertPool
}
return certPool
} }
func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@ -72,7 +78,7 @@ func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedCh
} }
} }
} }
return errNotMacth return errNotMatch
} }
} }
@ -89,53 +95,49 @@ func convertFingerprint(fingerprint string) (*[32]byte, error) {
return (*[32]byte)(fpByte), nil return (*[32]byte)(fpByte), nil
} }
func GetDefaultTLSConfig() *tls.Config { // GetTLSConfig specified fingerprint, customCA and customCAString
return GetGlobalTLSConfig(nil) 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 // GetSpecifiedFingerprintTLSConfig specified fingerprint
func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) { func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) {
if fingerprintBytes, err := convertFingerprint(fingerprint); err != nil { return GetTLSConfig(tlsConfig, fingerprint, "", "")
return nil, err
} else {
tlsConfig = GetGlobalTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true
return tlsConfig, nil
}
} }
func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config { func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config {
certPool := getCertPool() tlsConfig, _ = GetTLSConfig(tlsConfig, "", "", "")
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
return tlsConfig return tlsConfig
} }

View File

@ -14,5 +14,15 @@ func ListenDHCPClient(ctx context.Context, ifaceName string) (net.PacketConn, er
listenAddr = "255.255.255.255:68" listenAddr = "255.255.255.255:68"
} }
return dialer.ListenPacket(ctx, "udp4", listenAddr, dialer.WithInterface(ifaceName), dialer.WithAddrReuse(true)) options := []dialer.Option{
dialer.WithInterface(ifaceName),
dialer.WithAddrReuse(true),
}
// fallback bind on windows, because syscall bind can not receive broadcast
if runtime.GOOS == "windows" {
options = append(options, dialer.WithFallbackBind(true))
}
return dialer.ListenPacket(ctx, "udp4", listenAddr, options...)
} }

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

@ -0,0 +1,101 @@
package dialer
import (
"net"
"net/netip"
"strconv"
"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
}
func fallbackBindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() {
return nil
}
local := uint64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
}
}
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
}
func fallbackBindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
}
func fallbackParseNetwork(network string, addr netip.Addr) string {
// fix fallbackBindIfaceToListenConfig() force bind to an ipv4 address
if !strings.HasSuffix(network, "4") &&
!strings.HasSuffix(network, "6") &&
addr.Unmap().Is6() {
network += "6"
}
return network
}

View File

@ -5,99 +5,16 @@ package dialer
import ( import (
"net" "net"
"net/netip" "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 { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() { return fallbackBindIfaceToDialer(ifaceName, dialer, network, destination)
return nil
}
local := uint64(0)
if dialer.LocalAddr != nil {
_, port, err := net.SplitHostPort(dialer.LocalAddr.String())
if err == nil {
local, _ = strconv.ParseUint(port, 10, 16)
}
}
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local))
if err != nil {
return err
}
dialer.LocalAddr = addr
return nil
} }
func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, address string) (string, error) { func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, network, address string) (string, error) {
_, port, err := net.SplitHostPort(address) return fallbackBindIfaceToListenConfig(ifaceName, lc, network, address)
if err != nil {
port = "0"
}
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
if err != nil {
return "", err
}
return addr.String(), nil
} }
func ParseNetwork(network string, addr netip.Addr) string { func ParseNetwork(network string, addr netip.Addr) string {
// fix bindIfaceToListenConfig() force bind to an ipv4 address return fallbackParseNetwork(network, addr)
if !strings.HasSuffix(network, "4") &&
!strings.HasSuffix(network, "6") &&
addr.Unmap().Is6() {
network += "6"
}
return network
} }

View File

@ -20,3 +20,20 @@ func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) {
return fn(context.Background(), network, address, c) 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 ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@ -73,7 +74,11 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.interfaceName != "" { if cfg.interfaceName != "" {
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address) bind := bindIfaceToListenConfig
if cfg.fallbackBind {
bind = fallbackBindIfaceToListenConfig
}
addr, err := bind(cfg.interfaceName, lc, network, address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -124,13 +129,20 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
dialer := netDialer.(*net.Dialer) dialer := netDialer.(*net.Dialer)
if opt.interfaceName != "" { if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { bind := bindIfaceToDialer
if opt.fallbackBind {
bind = fallbackBindIfaceToDialer
}
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err return nil, err
} }
} }
if opt.routingMark != 0 { if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination) bindMarkToDialer(opt.routingMark, dialer, network, destination)
} }
if opt.mpTcp {
setMultiPathTCP(dialer)
}
if opt.tfo { if opt.tfo {
return dialTFO(ctx, *dialer, network, address) return dialTFO(ctx, *dialer, network, address)
} }
@ -158,14 +170,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) { func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
ipv4s, ipv6s := resolver.SortationAddr(ips) 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) fallbackTicker := time.NewTicker(fallbackTimeout)
defer fallbackTicker.Stop() defer fallbackTicker.Stop()
results := make(chan dialResult) results := make(chan dialResult)
returned := make(chan struct{}) returned := make(chan struct{})
defer close(returned) defer close(returned)
var wg sync.WaitGroup
racer := func(ips []netip.Addr, isPrimary bool) { racer := func(ips []netip.Addr, isPrimary bool) {
defer wg.Done()
result := dialResult{isPrimary: isPrimary} result := dialResult{isPrimary: isPrimary}
defer func() { defer func() {
select { select {
@ -178,18 +198,36 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
}() }()
result.Conn, result.error = dialFn(ctx, network, ips, port, opt) result.Conn, result.error = dialFn(ctx, network, ips, port, opt)
} }
if len(ipv4s) != 0 {
wg.Add(1)
go racer(ipv4s, preferIPVersion != 6) go racer(ipv4s, preferIPVersion != 6)
}
if len(ipv6s) != 0 {
wg.Add(1)
go racer(ipv6s, preferIPVersion != 4) go racer(ipv6s, preferIPVersion != 4)
}
go func() {
wg.Wait()
close(results)
}()
var fallback dialResult var fallback dialResult
var errs []error var errs []error
for i := 0; i < 2; {
loop:
for {
select { select {
case <-fallbackTicker.C: case <-fallbackTicker.C:
if fallback.error == nil && fallback.Conn != nil { if fallback.error == nil && fallback.Conn != nil {
return fallback.Conn, nil return fallback.Conn, nil
} }
case res := <-results: case res, ok := <-results:
i++ if !ok {
break loop
}
if res.error == nil { if res.error == nil {
if res.isPrimary { if res.isPrimary {
return res.Conn, nil return res.Conn, nil
@ -204,10 +242,11 @@ func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string,
} }
} }
} }
if fallback.error == nil && fallback.Conn != nil { if fallback.error == nil && fallback.Conn != nil {
return 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) { func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
@ -244,7 +283,7 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr,
} }
if len(errs) > 0 { if len(errs) > 0 {
return nil, errorsJoin(errs...) return nil, errors.Join(errs...)
} }
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
@ -261,7 +300,7 @@ func serialDialContext(ctx context.Context, network string, ips []netip.Addr, po
errs = append(errs, err) errs = append(errs, err)
} }
} }
return nil, errorsJoin(errs...) return nil, errors.Join(errs...)
} }
type dialResult struct { type dialResult struct {

View File

@ -2,17 +2,9 @@ package dialer
import ( import (
"errors" "errors"
E "github.com/sagernet/sing/common/exceptions"
) )
var ( var (
ErrorNoIpAddress = errors.New("no ip address") ErrorNoIpAddress = errors.New("no ip address")
ErrorInvalidedNetworkStack = errors.New("invalided network stack") 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...)
}

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