Compare commits

...

158 Commits

Author SHA1 Message Date
riolu.rs
454dbb1772 feat: ntp service 2023-09-01 08:02:27 +08:00
Larvan2
44d1d2e0c2 fix: concurrent map writes #707 2023-09-01 02:31:07 +08:00
YanceyChiew
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
Larvan2
414d8f2162 chore: use WaitGroup in dualStackDialContext 2023-08-30 17:28:36 +08:00
Mitt
86cf1dd54b fix: dualStack confusing error on ipv4 failed connect 2023-08-30 17:28:36 +08:00
Larvan2
d099375200 chore: rename func name 2023-08-30 15:52:41 +08:00
Alpha
9536372cfb fix: call shutdown before restart (#709) 2023-08-30 15:49:28 +08:00
Larvan2
630a17cf90 chore: cleanup codes 2023-08-26 21:20:20 +08:00
wwqgtxx
0a7b7894bd feat: proxies support direct type 2023-08-24 23:33:03 +08:00
wwqgtxx
3a9fc39cd9 chore: update quic-go to 0.38.0 2023-08-21 16:18:56 +08:00
wwqgtxx
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
Larvan2
b8a60261ef chore: restore unselected
clear selected node in outboundgoup/URLtest when getGroupDelay triggered
2023-08-18 22:17:07 +08:00
wwqgtxx
db68d55a0e fix: sing-vmess panic 2023-08-17 22:33:07 +08:00
wwqgtxx
574efb4526 chore: Update dependencies 2023-08-16 21:30:12 +08:00
3andne
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
H1JK
ed09df4e13 fix: TLS ALPN support 2023-08-14 15:48:13 +08:00
H1JK
f89ecd97d6 feat: Converter unofficial TUIC share link support 2023-08-14 15:11:33 +08:00
wwqgtxx
3093fc4f33 chore: update go1.21.0 release 2023-08-09 17:26:24 +08:00
wwqgtxx
984fca4726 feat: add inbound-mptcp for listeners 2023-08-09 17:09:03 +08:00
wwqgtxx
cc42d787d4 feat: add mptcp for all proxy 2023-08-09 16:57:39 +08:00
wwqgtxx
e2e0fd4eba chore: using uint16 for ports in Metadata 2023-08-09 13:51:02 +08:00
xishang0128
bad9f2e6dc fix geodata-mode 2023-08-07 01:43:23 +08:00
H1JK
68bf6f16ac refactor: Geodata initialization 2023-08-06 23:34:10 +08:00
H1JK
cca701c641 chore: Update dependencies 2023-08-06 18:38:50 +08:00
wwqgtxx
09ec7c8a62 chore: update quic-go to 0.37.3 2023-08-06 09:45:51 +08:00
wwqgtxx
68f312288d chore: update quic-go to 0.37.2 and go1.21rc4 2023-08-05 12:53:49 +08:00
wwqgtxx
191243a1d2 chore: better tuicV5 deFragger 2023-08-03 23:07:30 +08:00
YuSaki丶Kanade
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
wwqgtxx
f125e1ce9e chore: Update dependencies 2023-08-01 13:54:22 +08:00
wwqgtxx
e2216b7824 chore: update quic-go to 0.37.1 2023-08-01 09:55:55 +08:00
H1JK
7632827177 chore: Use Meta-geoip for default 2023-07-20 23:24:48 +08:00
H1JK
b0e76ec791 feat: Add Meta-geoip V0 database support 2023-07-17 10:33:20 +08:00
Hellojack
a82745f544 chore: Remove legacy XTLS support (#645)
* chore: Remove legacy XTLS support

* chore: Rename function
2023-07-16 23:26:07 +08:00
Skyxim
cbb8ef5dfe fix: discard http unsuccessful status 2023-07-16 11:43:55 +08:00
wwqgtxx
a181e35865 chore: structure support decode pointer 2023-07-16 11:11:30 +08:00
Skyxim
014537e1ea fix: discard http unsuccessful status 2023-07-16 11:10:07 +08:00
wwqgtxx
9b50f56e7c fix: tunnel's handleUDPToLocal panic 2023-07-16 10:35:10 +08:00
wwqgtxx
9cbca162a0 feat: tuic outbound allow set an empty ALPN array 2023-07-16 10:29:43 +08:00
Skyxim
f73f32e41c fix: parse nested sub-rules failed 2023-07-16 10:15:43 +08:00
wwqgtxx
cfc30753af chore: Update go1.21rc3 2023-07-15 16:52:44 +08:00
H1JK
081e94c738 feat: Add sing-geoip database support 2023-07-14 22:28:24 +08:00
H1JK
5dd57bab67 chore: Update dependencies 2023-07-14 11:37:15 +08:00
H1JK
492a731ec1 fix: DNS cache 2023-07-14 09:55:43 +08:00
wwqgtxx
0b1aff5759 chore: Update dependencies 2023-07-02 10:41:02 +08:00
wwqgtxx
8f1475d5d0 chore: update to go1.21rc2, drop support for go1.19 2023-07-02 09:59:18 +08:00
wwqgtxx
c6b84b0f20 chore: update quic-go to 0.36.1 2023-07-02 09:05:16 +08:00
moranno
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
wwqgtxx
8e16738465 chore: better env parsing 2023-06-29 16:40:08 +08:00
wwqgtxx
db6b2b7702 chore: better resolv.conf parsing 2023-06-28 09:17:54 +08:00
Skyxim
603d0809b4 fix: panic when add 4in6 ipcidr 2023-06-26 21:04:54 +08:00
wwqgtxx
614cc93cac chore: better close single connection in restful api 2023-06-26 18:25:36 +08:00
wwqgtxx
1cb75350e2 chore: statistic's Snapshot only contains TrackerInfo 2023-06-26 18:13:17 +08:00
wwqgtxx
42ef4fedfa chore: avoid unneeded map copy when close connection in restful api 2023-06-26 17:46:14 +08:00
wwqgtxx
2284acce94 chore: update quic-go to 0.36.0 2023-06-26 12:08:38 +08:00
wwqgtxx
919daf0dbb fix: tuic server cwnd parsing 2023-06-21 14:00:49 +08:00
wwqgtxx
6d824c8745 chore: tuic server can handle V4 and V5 in same port 2023-06-21 13:53:37 +08:00
Larvan2
1d94546902 chore: fix TUIC cwnd parsing 2023-06-21 00:47:05 +08:00
wwqgtxx
ad7508f203 Revert "chore: Refine adapter type name"
This reverts commit 61734e5cac.
2023-06-19 14:28:06 +08:00
wwqgtxx
d391fda051 chore: function rename 2023-06-19 08:32:11 +08:00
wwqgtxx
fe0f2d9ef9 chore: Update dependencies 2023-06-19 08:23:48 +08:00
xishang0128
b9110c164d update docs 2023-06-18 01:50:32 +08:00
Larvan2
6c8631d5cc chore: adjustable cwnd for cc in quic 2023-06-18 00:47:26 +08:00
H1JK
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
H1JK
af28b99b2a Add REALITY ChaCha20-Poly1305 auth mode support 2023-06-14 17:17:46 +08:00
wwqgtxx
4f79bb7931 fix: singmux return wrong supportUDP value 2023-06-14 15:51:13 +08:00
wwqgtxx
644abcf071 fix: tuicV5's heartbeat should be a datagram packet 2023-06-13 17:50:10 +08:00
Skyxim
183f2d974c fix: dns concurrent not work 2023-06-12 18:42:46 +08:00
wwqgtxx
e914317bef feat: support tuicV5 2023-06-12 18:42:46 +08:00
wwqgtxx
5e20fedf5f chore: Update dependencies 2023-06-11 23:57:25 +08:00
H1JK
54337ecdf3 chore: Disable cache for RCode client 2023-06-11 23:01:51 +08:00
H1JK
c7de0e0253 feat: Add RCode DNS client 2023-06-11 23:01:45 +08:00
Skyxim
b72219c06a chore: allow unsafe path for provider by environment variable 2023-06-11 01:55:49 +00:00
H1JK
64b23257db chore: Replace murmur3 with maphash 2023-06-10 17:35:19 +08:00
Skyxim
c57f17d094 chore: reduce process lookup attempts when process not exist #613 2023-06-08 18:07:56 +08:00
H1JK
cd44901e90 fix: Disable XUDP global ID if source address invalid 2023-06-08 15:57:51 +08:00
wwqgtxx
766d08a8eb chore: init gopacket only when dial fake-tcp to decrease memory using 2023-06-08 11:58:51 +08:00
H1JK
c3ef05b257 feat: Add XUDP migration support 2023-06-07 23:03:36 +08:00
Larvan2
093453582f fix: Resolve delay omission in the presence of nested proxy-groups 2023-06-07 13:20:45 +08:00
wzdnzd
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
wwqgtxx
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
wzdnzd
e7174866e5 fix: nil pointer in urltest (#603) 2023-06-05 12:40:46 +08:00
Mars160
fdaa6a22a4 fix hysteria faketcp lookback in TUN mode (#601) 2023-06-04 23:43:54 +08:00
Larvan2
fd0c71a485 chore: Ignore PR in Docker build 2023-06-04 15:51:25 +08:00
wzdnzd
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
wzdnzd
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
wwqgtxx
03d0c8620e fix: hysteria faketcp loopback in tun mode 2023-06-03 22:15:09 +08:00
wwqgtxx
63b5387164 chore: update proxy's udpConn when received a new packet 2023-06-03 21:40:09 +08:00
Skyxim
2af758e5f1 chore: Random only if the certificate and private-key are empty 2023-06-03 17:45:47 +08:00
wwqgtxx
2c44b4e170 chore: update quic-go to 0.35.1 2023-06-03 16:45:35 +08:00
H1JK
7906fbfee6 chore: Update dependencies 2023-06-03 00:24:51 +08:00
H1JK
17565ec93b chore: Reject packet conn implement wait read 2023-06-02 22:58:33 +08:00
Larvan2
26acaee424 fix: handle manually select in url-test 2023-06-02 18:26:51 +08:00
wwqgtxx
9b6e56a65e chore: update quic-go to 0.34.0 2023-06-01 16:25:02 +08:00
wwqgtxx
7fa3d3aa0b chore: cleanup system dns code 2023-06-01 12:36:53 +08:00
Larvan2
1120c8185d chore: Use API to create windows firewall rule 2023-05-31 15:54:36 +08:00
wwqgtxx
41af94ea66 fix: deadline reader cause panic 2023-05-30 20:21:51 +08:00
wwqgtxx
36539bb670 fix: sing-ss2's Reader not set buffer end 2023-05-30 07:59:55 +08:00
wwqgtxx
8e88e0b9f5 chore: add WaitReadFrom support in ssr 2023-05-28 22:51:44 +08:00
Larvan2
097f3e250c chore: slightly improve quic-bbr performance 2023-05-28 20:15:11 +08:00
wwqgtxx
9c2972afb0 chore: add IN-USER and IN-NAME rules 2023-05-28 17:19:57 +08:00
wwqgtxx
7aae781569 chore: add WaitReadFrom support in quicStreamPacketConn 2023-05-28 15:22:08 +08:00
wwqgtxx
92f71fd25f chore: add WaitReadFrom support in hyPacketConn 2023-05-28 09:33:42 +08:00
wwqgtxx
f44ba26f0c chore: switch ss uot default back to version 1 2023-05-28 08:50:02 +08:00
wwqgtxx
73140ab826 fix: udp panic when server return a domain name 2023-05-27 13:43:41 +08:00
H1JK
4971b9d804 chore: Add vision splice support 2023-05-27 11:26:13 +08:00
H1JK
654e76d91e refactor: Move vision implementation to a new package 2023-05-26 20:11:06 +08:00
wwqgtxx
984bf27d9b chore: using internal socks5.ReadAddr0 in trojan 2023-05-20 18:35:04 +08:00
H1JK
546b2bc24b chore: Decrease UoT read memory 2023-05-20 17:01:52 +08:00
wwqgtxx
d4e4f6d2d7 chore: rebuild ref and threadSafe packetConn 2023-05-20 16:57:42 +08:00
wwqgtxx
b047ca0294 chore: packet deadline support CreateReadWaiter interface 2023-05-20 11:44:11 +08:00
wwqgtxx
2b1e69153b chore: better packet deadline 2023-05-19 23:29:59 +08:00
8Mi_Yile
ae8d42fb82 Fix: update action to support Node 16 (#565) 2023-05-19 21:00:00 +08:00
wwqgtxx
89ae640487 fix: ensure group not empty 2023-05-19 19:57:55 +08:00
wwqgtxx
6e0c3a368f chore: upgrade dependencies 2023-05-19 11:08:14 +08:00
wwqgtxx
033f902ace chore: more context passing in outbounds 2023-05-18 13:15:08 +08:00
Larvan2
6b1a4385b2 chore: better updater 2023-05-17 00:33:59 +08:00
wwqgtxx
e552b5475f fix: tfoConn panic 2023-05-16 14:55:50 +08:00
wwqgtxx
8b631f11b8 chore: better sing's udp api support 2023-05-15 22:45:08 +08:00
wwqgtxx
1a9104c003 fix: UDP packet should not return io.EOF 2023-05-15 19:06:58 +08:00
H1JK
872a28a5eb Fix: deprecated action commands (#556)
Co-authored-by: 8Mi_Yile <admin@8mi.tech>
2023-05-14 13:43:25 +08:00
H1JK
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
H1JK
c6fed3e97f fix: TLS certificate pool initialize
Co-authored-by: Skyxim <noreply@skyxim.dev>
2023-05-14 00:21:59 +08:00
H1JK
ed17478961 feat: Support insecure gRPC 2023-05-13 09:38:14 +08:00
wwqgtxx
b674983034 chore: improve read waiter interface 2023-05-12 12:12:22 +08:00
wwqgtxx
a22b1cd69e fix: sing-based listener panic 2023-05-12 09:14:27 +08:00
wwqgtxx
f1be9b3f4a fix: tuic server return error udp address 2023-05-11 22:45:27 +08:00
wwqgtxx
534282839c chore: better tproxy error logging 2023-05-11 21:31:29 +08:00
cubemaze
8dd7632d0a chore: update docs 2023-05-11 21:24:38 +08:00
wwqgtxx
51e9f3598e fix: shadowsocks rc4-md5 not working 2023-05-11 20:42:36 +08:00
wwqgtxx
76caab19bf fix: Deadline not apply on EnhancePacketConn 2023-05-11 19:58:50 +08:00
wwqgtxx
234f7dbd3b chore: decrease shadowsocks udp read memory used for no-windows platform 2023-05-11 19:01:41 +08:00
wwqgtxx
e404695a0d fix: mux's udp should add write lock 2023-05-11 15:34:28 +08:00
wwqgtxx
75cd72385a chore: decrease direct udp read memory used for no-windows platform 2023-05-11 13:47:51 +08:00
wwqgtxx
d9fa051dd8 chore: drop bufio.Reader in BufferedConn to let gc can clean up its internal buf 2023-05-11 11:30:20 +08:00
wwqgtxx
98394095e4 fix: udp can't auto close 2023-05-11 00:03:40 +08:00
wwqgtxx
c58400572c chore: sing inbound support WaitReadPacket 2023-05-10 22:35:50 +08:00
wwqgtxx
3b291d3fbf fix: sing inbound should check needAdditionReadDeadline on udp too 2023-05-10 16:03:28 +08:00
wwqgtxx
15a8d7c473 chore: better tuic earlyConn impl 2023-05-10 09:36:06 +08:00
wwqgtxx
67b9314693 fix: tuic can't work with proxy-dialer 2023-05-10 09:03:49 +08:00
wwqgtxx
99f7c4f821 fix: ss aead udp problem 2023-05-10 08:31:16 +08:00
wwqgtxx
0cb594dd5d chore: upgrade dependencies 2023-05-10 07:23:49 +08:00
H1JK
bd431fbf49 fix: Update unsafe pointer add usage 2023-05-06 15:49:10 +08:00
Skyxim
8c0168d3a8 chore: upgrade dependencies 2023-05-05 07:27:36 +00:00
PuerNya
7ae3e78b15 Feat: rewrite http outbound 2023-05-03 22:00:06 +08:00
Larvan2
d61a5af335 chore: update release note 2023-05-02 22:06:07 +08:00
H1JK
f90066f286 chore: Update dependencies 2023-05-01 23:38:02 +08:00
sleshep
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
H1JK
1eefa71e1f chore: Make slash optional for system resolver 2023-05-01 12:58:02 +08:00
H1JK
969c235490 chore: Remove default DNS in system resolver 2023-05-01 12:41:36 +08:00
Larvan2
f35ff24d0c docs: update config.yaml 2023-04-30 17:10:57 +00:00
Larvan2
94f990da31 feat: support system dns for windows 2023-05-01 00:46:57 +08:00
sleshep
d6931ec491 feat: support system dns 2023-04-30 23:59:54 +08:00
H1JK
19b403da86 refactor: Switch to sing-shadowsocks2 client 2023-04-30 18:57:16 +08:00
Skyxim
6ecd1c31e5 fix: tuic connection error using fast_open 2023-04-29 15:44:07 +00:00
Skyxim
a7233f6036 fix: wildcard matching problem 2023-04-28 16:55:35 +00:00
214 changed files with 7569 additions and 3490 deletions

View File

@@ -126,9 +126,9 @@ jobs:
shell: bash shell: bash
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: "1.20" go-version: "1.21"
check-latest: true check-latest: true
- name: Test - name: Test
@@ -223,7 +223,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 +246,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 +280,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 +305,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 +316,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 +325,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 +335,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

@@ -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 \
@@ -103,6 +105,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)-$@

View File

@@ -3,25 +3,41 @@ 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"
"sync"
"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"
) )
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 sync.Map
} }
// Alive implements C.Proxy // Alive implements C.Proxy
@@ -29,6 +45,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.(*extraProxyState).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 +87,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.(*extraProxyState).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, v interface{}) bool {
testUrl := k.(string)
state := v.(*extraProxyState)
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 +147,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.(*extraProxyState).alive.Load()
history = state.(*extraProxyState).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 +179,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,16 +190,49 @@ 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
record := C.DelayHistory{Time: time.Now()} store = p.determineFinalStoreType(store, url)
if err == nil {
record.Delay = t switch store {
} case C.OriginalHistory:
p.history.Put(record) p.alive.Store(alive)
if p.history.Len() > 10 { record := C.DelayHistory{Time: time.Now()}
p.history.Pop() if alive {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum {
p.history.Pop()
}
// test URL configured by the proxy provider
if len(p.url) == 0 {
p.url = url
}
case C.ExtraHistory:
record := C.DelayHistory{Time: time.Now()}
if alive {
record.Delay = t
}
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.(*extraProxyState).alive.Store(alive)
state.(*extraProxyState).history.Put(record)
if state.(*extraProxyState).history.Len() > defaultHistoriesNum {
state.(*extraProxyState).history.Pop()
}
default:
log.Debugln("health check result will be discarded, url: %s alive: %t, delay: %d", url, alive, t)
} }
}() }()
@@ -172,12 +296,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: sync.Map{}}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@@ -198,11 +332,46 @@ 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
}
length := 0
p.extra.Range(func(_, _ interface{}) bool {
length++
return length < 2*C.DefaultMaxHealthCheckUrlNum
})
if length == 0 {
return C.ExtraHistory
}
_, ok := p.extra.Load(url)
if ok {
return C.ExtraHistory
}
if length < 2*C.DefaultMaxHealthCheckUrlNum {
return C.ExtraHistory
}
return store
}

View File

@@ -16,6 +16,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

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

@@ -3,6 +3,7 @@ package inbound
import ( import (
"net" "net"
"net/netip" "net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
@@ -30,21 +31,20 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Ad
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }
func NewInner(conn net.Conn, dst string, host string) *context.ConnContext { func NewInner(conn net.Conn, address string) *context.ConnContext {
metadata := &C.Metadata{} metadata := &C.Metadata{}
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.INNER metadata.Type = C.INNER
metadata.DNSMode = C.DNSNormal metadata.DNSMode = C.DNSNormal
metadata.Host = host
metadata.Process = C.ClashName metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(dst); err == nil { if h, port, err := net.SplitHostPort(address); err == nil {
metadata.DstPort = port if port, err := strconv.ParseUint(port, 10, 16); err == nil {
if host == "" { metadata.DstPort = uint16(port)
if ip, err := netip.ParseAddr(h); err == nil { }
metadata.DstIP = ip if ip, err := netip.ParseAddr(h); err == nil {
} else { metadata.DstIP = ip
metadata.Host = h } else {
} metadata.Host = h
} }
} }

View File

@@ -20,14 +20,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 +43,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)
@@ -58,10 +63,10 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata return metadata
} }
func parseAddr(addr net.Addr) (netip.Addr, string, error) { func parseAddr(addr net.Addr) (netip.Addr, uint16, error) {
// Filter when net.Addr interface is nil // Filter when net.Addr interface is nil
if addr == nil { if addr == nil {
return netip.Addr{}, "", errors.New("nil addr") return netip.Addr{}, 0, errors.New("nil addr")
} }
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok { if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
ip, port, err := parseAddr(rawAddr.RawAddr()) ip, port, err := parseAddr(rawAddr.RawAddr())
@@ -72,9 +77,14 @@ func parseAddr(addr net.Addr) (netip.Addr, string, error) {
addrStr := addr.String() addrStr := addr.String()
host, port, err := net.SplitHostPort(addrStr) host, port, err := net.SplitHostPort(addrStr)
if err != nil { if err != nil {
return netip.Addr{}, "", err return netip.Addr{}, 0, err
}
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
} }
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
return ip, port, err return ip, uint16Port, 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,6 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"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 +12,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))
@@ -39,11 +42,22 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
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

@@ -10,7 +10,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@@ -41,12 +40,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 {
@@ -83,7 +80,7 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
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 +95,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 for key, value := range h.option.Headers {
if len(h.option.Headers) != 0 { tempHeaders[key] = value
for key, value := range h.option.Headers {
req.Header.Add(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
} }
@@ -178,6 +177,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

@@ -318,6 +318,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

@@ -78,8 +78,11 @@ 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) Close() error { return nil } func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified } return nil, nil, nil, io.EOF
func (npc nopPacketConn) SetDeadline(time.Time) error { return nil } }
func (npc nopPacketConn) SetReadDeadline(time.Time) error { return nil } func (npc nopPacketConn) Close() error { return nil }
func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil } func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
func (npc nopPacketConn) SetDeadline(time.Time) error { return nil }
func (npc nopPacketConn) SetReadDeadline(time.Time) error { return nil }
func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil }

View File

@@ -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"
) )
@@ -87,14 +83,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 +94,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)
} }
@@ -196,7 +185,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 +223,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)
} }
@@ -303,7 +294,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 +302,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 +314,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 +329,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 (
@@ -83,7 +86,7 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
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

@@ -92,12 +92,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
} }

View File

@@ -52,15 +52,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 +71,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
} }
@@ -101,7 +99,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
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
} }
@@ -183,6 +181,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),

View File

@@ -39,12 +39,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 {
@@ -88,7 +86,7 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
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
} }
@@ -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

@@ -14,7 +14,6 @@ import (
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,12 +44,10 @@ 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{
@@ -71,14 +68,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 +85,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 +109,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
@@ -151,7 +137,7 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
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
} }
@@ -199,7 +185,7 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
tcpKeepAlive(c) 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 +223,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 +238,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),

View File

@@ -6,6 +6,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/hex" "encoding/hex"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"math" "math"
"net" "net"
@@ -13,13 +14,17 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/metacubex/quic-go"
"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"
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/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 +38,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 +53,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 +63,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 +89,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 +127,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 +139,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
} }
@@ -158,7 +195,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig) 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 +209,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 +222,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 +267,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 +304,40 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if clientMaxOpenStreams < 1 { if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1 clientMaxOpenStreams = 1
} }
clientOption := &tuic.ClientOption{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Host: host,
Token: tkn,
UdpRelayMode: option.UdpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
}
t.client = tuic.NewPoolClient(clientOption) if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV4(clientOption)
} else {
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV5(clientOption)
}
return t, nil return t, nil
} }

View File

@@ -4,10 +4,8 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/tls" "crypto/tls"
xtls "github.com/xtls/go"
"net" "net"
"net/netip" "net/netip"
"strconv"
"sync" "sync"
"time" "time"
@@ -17,9 +15,8 @@ import (
) )
var ( var (
globalClientSessionCache tls.ClientSessionCache globalClientSessionCache tls.ClientSessionCache
globalClientXSessionCache xtls.ClientSessionCache once sync.Once
once sync.Once
) )
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {
@@ -36,18 +33,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:

View File

@@ -13,6 +13,8 @@ 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/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 +26,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 +56,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 +76,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 {
@@ -128,10 +130,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 +148,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 +163,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 +180,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 +202,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) {
host, _, _ := net.SplitHostPort(v.addr) if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
if v.isLegacyXTLSEnabled() && !isH2 {
xtlsOpts := vless.XTLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
Fingerprint: v.option.Fingerprint,
}
if v.option.ServerName != "" {
xtlsOpts.Host = v.option.ServerName
}
return vless.StreamXTLSConn(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 +223,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
@@ -282,7 +268,7 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
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())
} }
@@ -347,7 +333,7 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
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 +358,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 +401,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 +509,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 +532,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 +545,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),
@@ -595,15 +587,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 == "" {
InsecureSkipVerify: v.option.SkipCertVerify, gunConfig.Host = v.addr
ServerName: v.option.ServerName, }
}) var tlsConfig *tls.Config
if option.TLS {
if v.option.ServerName == "" { tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
host, _, _ := net.SplitHostPort(v.addr) InsecureSkipVerify: v.option.SkipCertVerify,
tlsConfig.ServerName = host ServerName: v.option.ServerName,
gunConfig.Host = host })
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
}
} }
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig

View File

@@ -12,16 +12,18 @@ 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/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 +53,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"`
@@ -89,8 +92,8 @@ type WSOptions struct {
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"` EarlyDataHeaderName string `proxy:"early-data-header-name,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) {
@@ -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.ParseSocksaddr(metadata.RemoteAddress()))
} else { } else {
conn, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress())) conn, err = v.client.DialConn(c,
M.ParseSocksaddr(metadata.RemoteAddress()))
} }
} }
if err != nil { if err != nil {
@@ -294,7 +313,7 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
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
} }
@@ -355,7 +374,7 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
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),
@@ -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 == "" {
InsecureSkipVerify: v.option.SkipCertVerify, gunConfig.Host = v.addr
ServerName: v.option.ServerName,
} }
var tlsConfig *tls.Config
if v.option.ServerName == "" { if option.TLS {
host, _, _ := net.SplitHostPort(v.addr) tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
tlsConfig.ServerName = host InsecureSkipVerify: v.option.SkipCertVerify,
gunConfig.Host = host ServerName: v.option.ServerName,
})
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = 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

@@ -67,7 +67,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 { type wgSingDialer struct {
@@ -302,7 +302,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...)))
}, },
@@ -374,8 +374,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
@@ -412,8 +411,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 +497,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"
@@ -16,9 +17,10 @@ import (
type Fallback struct { type Fallback struct {
*GroupBase *GroupBase
disableUDP bool disableUDP bool
testUrl string testUrl string
selected string selected string
expectedStatus string
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
@@ -82,9 +84,11 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"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
@@ -156,7 +164,8 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
option.ExcludeType, option.ExcludeType,
providers, providers,
}), }),
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"
@@ -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"
@@ -25,8 +25,10 @@ type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Pr
type LoadBalance struct { 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)
@@ -230,8 +236,10 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
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)
} }
@@ -260,7 +268,9 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
option.ExcludeType, option.ExcludeType,
providers, providers,
}), }),
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
}, nil }, nil
} }

View File

@@ -3,35 +3,37 @@ 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")
) )
type GroupCommonOption struct { type GroupCommonOption struct {
outbound.BasicOption outbound.BasicOption
Name string `group:"name"` Name string `group:"name"`
Type string `group:"type"` Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"` Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"` Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"` URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"` Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"` 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)
} }
// select don't need health check var url string
if groupOption.Type == "select" || groupOption.Type == "relay" { var interval uint
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd) // select don't need health check
providersMap[groupName] = pd if groupOption.Type != "select" && groupOption.Type != "relay" {
} 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
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) interval = uint(groupOption.Interval)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} }
hc := provider.NewHealthCheck(ps, url, interval, true, expectedStatus)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
}
providers = append(providers, pd)
providersMap[groupName] = pd
} }
if len(groupOption.Use) != 0 { 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

@@ -25,12 +25,13 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct { type URLTest struct {
*GroupBase *GroupBase
selected string selected string
testUrl string testUrl string
tolerance uint16 expectedStatus string
disableUDP bool tolerance uint16
fastNode C.Proxy disableUDP bool
fastSingle *singledo.Single[C.Proxy] fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
@@ -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) if u.selected != "" {
fast := proxies[0] for _, proxy := range proxies {
if fast.Name() == u.selected { if !proxy.Alive() {
s = fast continue
}
if proxy.Name() == u.selected {
u.fastNode = proxy
return proxy
}
} }
min := fast.LastDelay() }
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
@@ -163,9 +169,11 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"type": u.Type().String(), "type": u.Type().String(),
"now": u.Now(), "now": u.Now(),
"all": all, "all": all,
"testUrl": u.testUrl,
"expected": u.expectedStatus,
}) })
} }
@@ -197,9 +205,10 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
option.ExcludeType, option.ExcludeType,
providers, providers,
}), }),
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

@@ -106,6 +106,13 @@ 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)
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,18 +25,33 @@ 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
proxies []C.Proxy extra map[string]*extraOption
interval uint mu sync.Mutex
lazy bool started *atomic.Bool
lastTouch *atomic.Int64 proxies []C.Proxy
done chan struct{} interval uint
singleDo *singledo.Single[struct{}] lazy bool
expectedStatus utils.IntRanges[uint16]
lastTouch *atomic.Int64
done chan struct{}
singleDo *singledo.Single[struct{}]
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
hc.start()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
@@ -44,6 +63,7 @@ func (hc *HealthCheck) process() {
} }
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
hc.stop()
return return
} }
} }
@@ -53,6 +73,63 @@ 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 = interval
}
if hc.extra == nil {
hc.extra = make(map[string]*extraOption)
}
// prioritize the use of previously registered configurations, especially those from provider
if _, ok := hc.extra[url]; ok {
// provider default health check does not set filter
if url != hc.url && len(filter) != 0 {
splitAndAddFiltersToExtra(filter, hc.extra[url])
}
log.Debugln("health check url: %s exists", url)
return
}
// due to the time-consuming nature of health checks, a maximum of defaultMaxTestURLNum URLs can be set for testing
if len(hc.extra) > C.DefaultMaxHealthCheckUrlNum {
log.Debugln("skip add url: %s to health check because it has reached the maximum limit: %d", url, C.DefaultMaxHealthCheckUrlNum)
return
}
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
filter = strings.TrimSpace(filter)
if len(filter) != 0 {
for _, regex := range strings.Split(filter, "`") {
regex = strings.TrimSpace(regex)
if len(regex) != 0 {
option.filters[regex] = struct{}{}
}
}
}
}
func (hc *HealthCheck) auto() bool { func (hc *HealthCheck) auto() bool {
return hc.interval != 0 return hc.interval != 0
} }
@@ -61,41 +138,102 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix()) hc.lastTouch.Store(time.Now().Unix())
} }
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
}
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
_, _, _ = hc.singleDo.Do(func() (struct{}, error) { _, _, _ = hc.singleDo.Do(func() (struct{}, error) {
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{},
lazy: lazy, started: atomic.NewBool(false),
lastTouch: atomic.NewInt64(0), interval: interval,
done: make(chan struct{}, 1), lazy: lazy,
singleDo: singledo.NewSingle[struct{}](time.Second), expectedStatus: expectedStatus,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
} }
} }

View File

@@ -6,23 +6,28 @@ import (
"time" "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":
vehicle = resource.NewHTTPVehicle(schema.URL, path) if schema.Path != "" {
path := C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
vehicle = resource.NewHTTPVehicle(schema.URL, path)
} else {
path := C.Path.GetPathByHash("proxies", schema.URL)
vehicle = resource.NewHTTPVehicle(schema.URL, path)
}
default: 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

@@ -11,18 +11,9 @@ type Buffer = buf.Buffer
var New = buf.New var New = buf.New
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

@@ -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")
@@ -67,6 +69,47 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
proxies = append(proxies, hysteria) proxies = append(proxies, hysteria)
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)
if err != nil { if err != nil {
@@ -86,10 +129,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 +262,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 +380,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

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

@@ -62,7 +62,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 +70,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 +85,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

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

@@ -11,7 +11,7 @@ import (
) )
func ParseCert(certificate, privateKey string) (tls.Certificate, error) { func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
if certificate == "" || privateKey == "" { if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair() return newRandomTLSKeyPair()
} }
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))

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)
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
}
bits := msb(size) return alloc.buffers[bits+1].Get().([]byte)[:size]
if size == 1<<bits {
return alloc.buffers[bits].Get().([]byte)[:size]
} }
return alloc.buffers[bits+1].Get().([]byte)[:size]
} }
// Put returns a []byte to pool for future use, // Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n // which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error { func (alloc *Allocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}
bits := msb(cap(buf)) bits := msb(cap(buf))
if cap(buf) == 0 || cap(buf) > 65536 || cap(buf) != 1<<bits { if cap(buf) != 1<<bits {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }

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 {

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

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

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

View File

@@ -7,52 +7,8 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
"strings" "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() { if !destination.IsGlobalUnicast() {
return nil return nil
@@ -66,7 +22,7 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, des
} }
} }
addr, err := lookupLocalAddr(ifaceName, network, destination, int(local)) addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, destination, int(local))
if err != nil { if err != nil {
return err return err
} }
@@ -84,7 +40,7 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
local, _ := strconv.ParseUint(port, 10, 16) local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local)) addr, err := LookupLocalAddrFromIfaceName(ifaceName, network, netip.Addr{}, int(local))
if err != nil { if err != nil {
return "", err return "", err
} }

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"
@@ -131,6 +132,9 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
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 +162,22 @@ func concurrentDualStackDialContext(ctx context.Context, network string, ips []n
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { 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 +190,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)
} }
go racer(ipv4s, preferIPVersion != 6)
go racer(ipv6s, preferIPVersion != 4) if len(ipv4s) != 0 {
wg.Add(1)
go racer(ipv4s, preferIPVersion != 6)
}
if len(ipv6s) != 0 {
wg.Add(1)
go racer(ipv6s, preferIPVersion != 4)
}
go func() {
wg.Wait()
close(results)
}()
var fallback dialResult var 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 +234,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 +275,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 +292,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...)
}

View File

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

View File

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

View File

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

View File

@@ -18,10 +18,11 @@ type tfoConn struct {
} }
func (c *tfoConn) Dial(earlyData []byte) (err error) { func (c *tfoConn) Dial(earlyData []byte) (err error) {
c.Conn, err = c.dialFn(c.ctx, earlyData) conn, err := c.dialFn(c.ctx, earlyData)
if err != nil { if err != nil {
return return
} }
c.Conn = conn
c.dialed <- true c.dialed <- true
return err return err
} }

View File

@@ -234,26 +234,26 @@ tail:
case s == 0: case s == 0:
case s < 4: case s < 4:
h ^= uint64(*(*byte)(p)) h ^= uint64(*(*byte)(p))
h ^= uint64(*(*byte)(add(p, s>>1))) << 8 h ^= uint64(*(*byte)(unsafe.Add(p, s>>1))) << 8
h ^= uint64(*(*byte)(add(p, s-1))) << 16 h ^= uint64(*(*byte)(unsafe.Add(p, s-1))) << 16
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
case s <= 8: case s <= 8:
h ^= uint64(readUnaligned32(p)) h ^= uint64(readUnaligned32(p))
h ^= uint64(readUnaligned32(add(p, s-4))) << 32 h ^= uint64(readUnaligned32(unsafe.Add(p, s-4))) << 32
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
case s <= 16: case s <= 16:
h ^= readUnaligned64(p) h ^= readUnaligned64(p)
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-8)) h ^= readUnaligned64(unsafe.Add(p, s-8))
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
case s <= 32: case s <= 32:
h ^= readUnaligned64(p) h ^= readUnaligned64(p)
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, 8)) h ^= readUnaligned64(unsafe.Add(p, 8))
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-16)) h ^= readUnaligned64(unsafe.Add(p, s-16))
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
h ^= readUnaligned64(add(p, s-8)) h ^= readUnaligned64(unsafe.Add(p, s-8))
h = rotl31(h*m1) * m2 h = rotl31(h*m1) * m2
default: default:
v1 := h v1 := h
@@ -263,16 +263,16 @@ tail:
for s >= 32 { for s >= 32 {
v1 ^= readUnaligned64(p) v1 ^= readUnaligned64(p)
v1 = rotl31(v1*m1) * m2 v1 = rotl31(v1*m1) * m2
p = add(p, 8) p = unsafe.Add(p, 8)
v2 ^= readUnaligned64(p) v2 ^= readUnaligned64(p)
v2 = rotl31(v2*m2) * m3 v2 = rotl31(v2*m2) * m3
p = add(p, 8) p = unsafe.Add(p, 8)
v3 ^= readUnaligned64(p) v3 ^= readUnaligned64(p)
v3 = rotl31(v3*m3) * m4 v3 = rotl31(v3*m3) * m4
p = add(p, 8) p = unsafe.Add(p, 8)
v4 ^= readUnaligned64(p) v4 ^= readUnaligned64(p)
v4 = rotl31(v4*m4) * m1 v4 = rotl31(v4*m4) * m1
p = add(p, 8) p = unsafe.Add(p, 8)
s -= 32 s -= 32
} }
h = v1 ^ v2 ^ v3 ^ v4 h = v1 ^ v2 ^ v3 ^ v4
@@ -285,10 +285,6 @@ tail:
return uintptr(h) return uintptr(h)
} }
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + x)
}
func readUnaligned32(p unsafe.Pointer) uint32 { func readUnaligned32(p unsafe.Pointer) uint32 {
q := (*[4]byte)(p) q := (*[4]byte)(p)
return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24 return uint32(q[0]) | uint32(q[1])<<8 | uint32(q[2])<<16 | uint32(q[3])<<24

View File

@@ -53,8 +53,12 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
conn := inner.HandleTcp(address, "") if conn, err := inner.HandleTcp(address); err == nil {
return conn, nil return conn, nil
} else {
d := net.Dialer{}
return d.DialContext(ctx, network, address)
}
}, },
TLSClientConfig: tls.GetDefaultTLSConfig(), TLSClientConfig: tls.GetDefaultTLSConfig(),
} }

View File

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

View File

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

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

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

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

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

View File

@@ -13,22 +13,24 @@ type Table struct {
type Entry struct { type Entry struct {
PacketConn C.PacketConn PacketConn C.PacketConn
WriteBackProxy C.WriteBackProxy
LocalUDPConnMap sync.Map LocalUDPConnMap sync.Map
} }
func (t *Table) Set(key string, e C.PacketConn) { func (t *Table) Set(key string, e C.PacketConn, w C.WriteBackProxy) {
t.mapping.Store(key, &Entry{ t.mapping.Store(key, &Entry{
PacketConn: e, PacketConn: e,
WriteBackProxy: w,
LocalUDPConnMap: sync.Map{}, LocalUDPConnMap: sync.Map{},
}) })
} }
func (t *Table) Get(key string) C.PacketConn { func (t *Table) Get(key string) (C.PacketConn, C.WriteBackProxy) {
entry, exist := t.getEntry(key) entry, exist := t.getEntry(key)
if !exist { if !exist {
return nil return nil, nil
} }
return entry.PacketConn return entry.PacketConn, entry.WriteBackProxy
} }
func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) { func (t *Table) GetOrCreateLock(key string) (*sync.Cond, bool) {

View File

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

View File

@@ -71,7 +71,7 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
currentMeta := &C.Metadata{Type: C.INNER} currentMeta := &C.Metadata{Type: C.INNER}
if err := currentMeta.SetRemoteAddress(address); err != nil { if err := currentMeta.SetRemoteAddress(rAddrPort.String()); err != nil {
return nil, err return nil, err
} }
return p.listenPacket(ctx, currentMeta) return p.listenPacket(ctx, currentMeta)

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"strconv"
"sync" "sync"
"time" "time"
@@ -26,29 +25,23 @@ var (
var Dispatcher *SnifferDispatcher var Dispatcher *SnifferDispatcher
type SnifferDispatcher struct { type SnifferDispatcher struct {
enable bool enable bool
sniffers map[sniffer.Sniffer]SnifferConfig sniffers map[sniffer.Sniffer]SnifferConfig
forceDomain *trie.DomainSet forceDomain *trie.DomainSet
skipSNI *trie.DomainSet skipSNI *trie.DomainSet
skipList *cache.LruCache[string, uint8] skipList *cache.LruCache[string, uint8]
rwMux sync.RWMutex rwMux sync.RWMutex
forceDnsMapping bool forceDnsMapping bool
parsePureIp bool parsePureIp bool
} }
func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) { func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) {
if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) { if (metadata.Host == "" && sd.parsePureIp) || sd.forceDomain.Has(metadata.Host) || (metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping) {
port, err := strconv.ParseUint(metadata.DstPort, 10, 16)
if err != nil {
log.Debugln("[Sniffer] Dst port is error")
return
}
inWhitelist := false inWhitelist := false
overrideDest := false overrideDest := false
for sniffer, config := range sd.sniffers { for sniffer, config := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet { if sniffer.SupportNetwork() == C.TCP || sniffer.SupportNetwork() == C.ALLNet {
inWhitelist = sniffer.SupportPort(uint16(port)) inWhitelist = sniffer.SupportPort(metadata.DstPort)
if inWhitelist { if inWhitelist {
overrideDest = config.OverrideDest overrideDest = config.OverrideDest
break break
@@ -61,7 +54,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
} }
sd.rwMux.RLock() sd.rwMux.RLock()
dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
if count, ok := sd.skipList.Get(dst); ok && count > 5 { if count, ok := sd.skipList.Get(dst); ok && count > 5 {
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
defer sd.rwMux.RUnlock() defer sd.rwMux.RUnlock()
@@ -71,7 +64,7 @@ func (sd *SnifferDispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata
if host, err := sd.sniffDomain(conn, metadata); err != nil { if host, err := sd.sniffDomain(conn, metadata); err != nil {
sd.cacheSniffFailed(metadata) sd.cacheSniffFailed(metadata)
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return return
} else { } else {
if sd.skipSNI.Has(host) { if sd.skipSNI.Has(host) {
@@ -149,7 +142,7 @@ func (sd *SnifferDispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metad
func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) { func (sd *SnifferDispatcher) cacheSniffFailed(metadata *C.Metadata) {
sd.rwMux.Lock() sd.rwMux.Lock()
dst := fmt.Sprintf("%s:%s", metadata.DstIP, metadata.DstPort) dst := fmt.Sprintf("%s:%d", metadata.DstIP, metadata.DstPort)
count, _ := sd.skipList.Get(dst) count, _ := sd.skipList.Get(dst)
if count <= 5 { if count <= 5 {
count++ count++

View File

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

View File

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

View File

@@ -10,14 +10,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
xtls "github.com/xtls/go"
) )
var trustCerts []*x509.Certificate var trustCerts []*x509.Certificate
var certPool *x509.CertPool var certPool *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,10 +31,22 @@ func AddCertificate(certificate string) error {
} }
} }
func initializeCertPool() {
var err error
certPool, err = x509.SystemCertPool()
if err != nil {
certPool = x509.NewCertPool()
}
for _, cert := range trustCerts {
certPool.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 {
@@ -49,12 +59,7 @@ func getCertPool() *x509.CertPool {
if certPool != nil { if certPool != nil {
return certPool return certPool
} }
certPool, err := x509.SystemCertPool() initializeCertPool()
if err == nil {
for _, cert := range trustCerts {
certPool.AddCert(cert)
}
}
} }
return certPool return certPool
} }
@@ -72,7 +77,7 @@ func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedCh
} }
} }
} }
return errNotMacth return errNotMatch
} }
} }
@@ -115,27 +120,3 @@ func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config {
tlsConfig.RootCAs = certPool tlsConfig.RootCAs = certPool
return tlsConfig 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
}

View File

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

View File

@@ -23,6 +23,8 @@ type DomainSet struct {
ranks, selects []int32 ranks, selects []int32
} }
type qElt struct{ s, e, col int }
// NewDomainSet creates a new *DomainSet struct, from a DomainTrie. // NewDomainSet creates a new *DomainSet struct, from a DomainTrie.
func (t *DomainTrie[T]) NewDomainSet() *DomainSet { func (t *DomainTrie[T]) NewDomainSet() *DomainSet {
reserveDomains := make([]string, 0) reserveDomains := make([]string, 0)
@@ -39,7 +41,6 @@ func (t *DomainTrie[T]) NewDomainSet() *DomainSet {
ss := &DomainSet{} ss := &DomainSet{}
lIdx := 0 lIdx := 0
type qElt struct{ s, e, col int }
queue := []qElt{{0, len(keys), 0}} queue := []qElt{{0, len(keys), 0}}
for i := 0; i < len(queue); i++ { for i := 0; i < len(queue); i++ {
elt := queue[i] elt := queue[i]
@@ -104,8 +105,8 @@ func (ss *DomainSet) Has(key string) bool {
goto RESTART goto RESTART
} }
} }
for ; ; nextBmIdx++ { for ; nextBmIdx-nextNodeId < len(ss.labels); nextBmIdx++ {
if nextBmIdx-nextNodeId < len(ss.labels) && ss.labels[nextBmIdx-nextNodeId] == domainStepByte { if ss.labels[nextBmIdx-nextNodeId] == domainStepByte {
bmIdx = nextBmIdx bmIdx = nextBmIdx
nodeId = nextNodeId nodeId = nextNodeId
i = j i = j

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import (
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@@ -52,6 +51,7 @@ type General struct {
IPv6 bool `json:"ipv6"` IPv6 bool `json:"ipv6"`
Interface string `json:"interface-name"` Interface string `json:"interface-name"`
RoutingMark int `json:"-"` RoutingMark int `json:"-"`
GeoXUrl GeoXUrl `json:"geox-url"`
GeodataMode bool `json:"geodata-mode"` GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"` GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"` TCPConcurrent bool `json:"tcp-concurrent"`
@@ -76,6 +76,7 @@ type Inbound struct {
AllowLan bool `json:"allow-lan"` AllowLan bool `json:"allow-lan"`
BindAddress string `json:"bind-address"` BindAddress string `json:"bind-address"`
InboundTfo bool `json:"inbound-tfo"` InboundTfo bool `json:"inbound-tfo"`
InboundMPTCP bool `json:"inbound-mptcp"`
} }
// Controller config // Controller config
@@ -86,6 +87,14 @@ type Controller struct {
Secret string `json:"-"` Secret string `json:"-"`
} }
// NTP config
type NTP struct {
Enable bool `yaml:"enable"`
Server string `yaml:"server"`
Port int `yaml:"port"`
Interval int `yaml:"interval"`
}
// DNS config // DNS config
type DNS struct { type DNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
@@ -150,6 +159,7 @@ type Experimental struct {
type Config struct { type Config struct {
General *General General *General
IPTables *IPTables IPTables *IPTables
NTP *NTP
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie[resolver.HostValue] Hosts *trie.DomainTrie[resolver.HostValue]
@@ -166,6 +176,13 @@ type Config struct {
TLS *TLS TLS *TLS
} }
type RawNTP struct {
Enable bool `yaml:"enable"`
Server string `yaml:"server"`
ServerPort int `yaml:"server-port"`
Interval int `yaml:"interval"`
}
type RawDNS struct { type RawDNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
PreferH3 bool `yaml:"prefer-h3"` PreferH3 bool `yaml:"prefer-h3"`
@@ -220,16 +237,18 @@ type RawTun struct {
} }
type RawTuicServer struct { type RawTuicServer struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Listen string `yaml:"listen" json:"listen"` Listen string `yaml:"listen" json:"listen"`
Token []string `yaml:"token" json:"token"` Token []string `yaml:"token" json:"token"`
Certificate string `yaml:"certificate" json:"certificate"` Users map[string]string `yaml:"users" json:"users,omitempty"`
PrivateKey string `yaml:"private-key" json:"private-key"` Certificate string `yaml:"certificate" json:"certificate"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` PrivateKey string `yaml:"private-key" json:"private-key"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size" json:"max-udp-relay-packet-size,omitempty"`
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
} }
type RawConfig struct { type RawConfig struct {
@@ -241,6 +260,7 @@ type RawConfig struct {
ShadowSocksConfig string `yaml:"ss-config"` ShadowSocksConfig string `yaml:"ss-config"`
VmessConfig string `yaml:"vmess-config"` VmessConfig string `yaml:"vmess-config"`
InboundTfo bool `yaml:"inbound-tfo"` InboundTfo bool `yaml:"inbound-tfo"`
InboundMPTCP bool `yaml:"inbound-mptcp"`
Authentication []string `yaml:"authentication"` Authentication []string `yaml:"authentication"`
AllowLan bool `yaml:"allow-lan"` AllowLan bool `yaml:"allow-lan"`
BindAddress string `yaml:"bind-address"` BindAddress string `yaml:"bind-address"`
@@ -265,6 +285,7 @@ type RawConfig struct {
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]any `yaml:"hosts"` Hosts map[string]any `yaml:"hosts"`
NTP RawNTP `yaml:"ntp"`
DNS RawDNS `yaml:"dns"` DNS RawDNS `yaml:"dns"`
Tun RawTun `yaml:"tun"` Tun RawTun `yaml:"tun"`
TuicServer RawTuicServer `yaml:"tuic-server"` TuicServer RawTuicServer `yaml:"tuic-server"`
@@ -272,7 +293,7 @@ type RawConfig struct {
IPTables IPTables `yaml:"iptables"` IPTables IPTables `yaml:"iptables"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
GeoXUrl RawGeoXUrl `yaml:"geox-url"` GeoXUrl GeoXUrl `yaml:"geox-url"`
Proxy []map[string]any `yaml:"proxies"` Proxy []map[string]any `yaml:"proxies"`
ProxyGroup []map[string]any `yaml:"proxy-groups"` ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"` Rule []string `yaml:"rules"`
@@ -281,7 +302,7 @@ type RawConfig struct {
Listeners []map[string]any `yaml:"listeners"` Listeners []map[string]any `yaml:"listeners"`
} }
type RawGeoXUrl struct { type GeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"` GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"` Mmdb string `yaml:"mmdb" json:"mmdb"`
GeoSite string `yaml:"geosite" json:"geosite"` GeoSite string `yaml:"geosite" json:"geosite"`
@@ -356,6 +377,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
TuicServer: RawTuicServer{ TuicServer: RawTuicServer{
Enable: false, Enable: false,
Token: nil, Token: nil,
Users: nil,
Certificate: "", Certificate: "",
PrivateKey: "", PrivateKey: "",
Listen: "", Listen: "",
@@ -416,10 +438,10 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
GeoXUrl: RawGeoXUrl{ GeoXUrl: GeoXUrl{
Mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb", Mmdb: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb",
GeoIp: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat", GeoIp: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat",
GeoSite: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat", GeoSite: "https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat",
}, },
} }
@@ -446,7 +468,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.General = general config.General = general
if len(config.General.GlobalClientFingerprint) != 0 { if len(config.General.GlobalClientFingerprint) != 0 {
log.Debugln("GlobalClientFingerprint:%s", config.General.GlobalClientFingerprint) log.Debugln("GlobalClientFingerprint: %s", config.General.GlobalClientFingerprint)
tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint) tlsC.SetGlobalUtlsClient(config.General.GlobalClientFingerprint)
} }
@@ -488,6 +510,9 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.Hosts = hosts config.Hosts = hosts
ntpCfg := paresNTP(rawCfg)
config.NTP = ntpCfg
dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders) dnsCfg, err := parseDNS(rawCfg, hosts, rules, ruleProviders)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -530,6 +555,10 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
func parseGeneral(cfg *RawConfig) (*General, error) { func parseGeneral(cfg *RawConfig) (*General, error) {
externalUI := cfg.ExternalUI externalUI := cfg.ExternalUI
geodata.SetLoader(cfg.GeodataLoader) geodata.SetLoader(cfg.GeodataLoader)
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
C.MmdbUrl = cfg.GeoXUrl.Mmdb
C.GeodataMode = cfg.GeodataMode
// checkout externalUI exist // checkout externalUI exist
if externalUI != "" { if externalUI != "" {
externalUI = C.Path.Resolve(externalUI) externalUI = C.Path.Resolve(externalUI)
@@ -550,6 +579,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
AllowLan: cfg.AllowLan, AllowLan: cfg.AllowLan,
BindAddress: cfg.BindAddress, BindAddress: cfg.BindAddress,
InboundTfo: cfg.InboundTfo, InboundTfo: cfg.InboundTfo,
InboundMPTCP: cfg.InboundMPTCP,
}, },
Controller: Controller{ Controller: Controller{
ExternalController: cfg.ExternalController, ExternalController: cfg.ExternalController,
@@ -563,6 +593,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
Interface: cfg.Interface, Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark, RoutingMark: cfg.RoutingMark,
GeoXUrl: cfg.GeoXUrl,
GeodataMode: cfg.GeodataMode, GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader, GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent, TCPConcurrent: cfg.TCPConcurrent,
@@ -655,7 +686,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
} }
ps = append(ps, proxies[v]) ps = append(ps, proxies[v])
} }
hc := provider.NewHealthCheck(ps, "", 0, true) hc := provider.NewHealthCheck(ps, "", 0, true, nil)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)
providersMap[provider.ReservedName] = pd providersMap[provider.ReservedName] = pd
@@ -710,6 +741,9 @@ func parseRuleProviders(cfg *RawConfig) (ruleProviders map[string]providerTypes.
func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) { func parseSubRules(cfg *RawConfig, proxies map[string]C.Proxy) (subRules map[string][]C.Rule, err error) {
subRules = map[string][]C.Rule{} subRules = map[string][]C.Rule{}
for name := range cfg.SubRules {
subRules[name] = make([]C.Rule, 0)
}
for name, rawRules := range cfg.SubRules { for name, rawRules := range cfg.SubRules {
if len(name) == 0 { if len(name) == 0 {
return nil, fmt.Errorf("sub-rule name is empty") return nil, fmt.Errorf("sub-rule name is empty")
@@ -841,7 +875,7 @@ func parseHosts(cfg *RawConfig) (*trie.DomainTrie[resolver.HostValue], error) {
} else { } else {
ips := make([]netip.Addr, 0) ips := make([]netip.Addr, 0)
for _, addr := range addrs { for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback()&&!ipnet.IP.IsLinkLocalUnicast() { if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && !ipnet.IP.IsLinkLocalUnicast() {
if ip, err := netip.ParseAddr(ipnet.IP.String()); err == nil { if ip, err := netip.ParseAddr(ipnet.IP.String()); err == nil {
ips = append(ips, ip) ips = append(ips, ip)
} }
@@ -914,7 +948,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
addr, err = hostWithDefaultPort(u.Host, "443") addr, err = hostWithDefaultPort(u.Host, "443")
if err == nil { if err == nil {
proxyName = "" proxyName = ""
clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path} clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path, User: u.User}
addr = clearURL.String() addr = clearURL.String()
dnsNetType = "https" // DNS over HTTPS dnsNetType = "https" // DNS over HTTPS
if len(u.Fragment) != 0 { if len(u.Fragment) != 0 {
@@ -938,6 +972,21 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error)
case "quic": case "quic":
addr, err = hostWithDefaultPort(u.Host, "853") addr, err = hostWithDefaultPort(u.Host, "853")
dnsNetType = "quic" // DNS over QUIC dnsNetType = "quic" // DNS over QUIC
case "system":
dnsNetType = "system" // System DNS
case "rcode":
dnsNetType = "rcode"
addr = u.Host
switch addr {
case "success",
"format_error",
"server_failure",
"name_error",
"not_implemented",
"refused":
default:
err = fmt.Errorf("unsupported RCode type: %s", addr)
}
default: default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
} }
@@ -972,6 +1021,10 @@ func parsePureDNSServer(server string) string {
return "udp://" + server return "udp://" + server
} }
if server == "system" {
return "system://"
}
if ip, err := netip.ParseAddr(server); err != nil { if ip, err := netip.ParseAddr(server); err != nil {
if strings.Contains(server, "://") { if strings.Contains(server, "://") {
return server return server
@@ -1099,6 +1152,29 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil return sites, nil
} }
func paresNTP(rawCfg *RawConfig) *NTP {
var server = "time.apple.com"
var port = 123
var interval = 30
cfg := rawCfg.NTP
if len(cfg.Server) != 0 {
server = cfg.Server
}
if cfg.ServerPort != 0 {
port = cfg.ServerPort
}
if cfg.Interval != 0 {
interval = cfg.Interval
}
ntpCfg := &NTP{
Enable: cfg.Enable,
Server: server,
Port: port,
Interval: interval,
}
return ntpCfg
}
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rules []C.Rule, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) {
cfg := rawCfg.DNS cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
@@ -1142,6 +1218,9 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
} }
// check default nameserver is pure ip addr // check default nameserver is pure ip addr
for _, ns := range dnsCfg.DefaultNameserver { for _, ns := range dnsCfg.DefaultNameserver {
if ns.Net == "system" {
continue
}
host, _, err := net.SplitHostPort(ns.Addr) host, _, err := net.SplitHostPort(ns.Addr)
if err != nil || net.ParseIP(host) == nil { if err != nil || net.ParseIP(host) == nil {
u, err := url.Parse(ns.Addr) u, err := url.Parse(ns.Addr)
@@ -1273,6 +1352,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error {
Enable: rawTuic.Enable, Enable: rawTuic.Enable,
Listen: rawTuic.Listen, Listen: rawTuic.Listen,
Token: rawTuic.Token, Token: rawTuic.Token,
Users: rawTuic.Users,
Certificate: rawTuic.Certificate, Certificate: rawTuic.Certificate,
PrivateKey: rawTuic.PrivateKey, PrivateKey: rawTuic.PrivateKey,
CongestionController: rawTuic.CongestionController, CongestionController: rawTuic.CongestionController,
@@ -1280,6 +1360,7 @@ func parseTuicServer(rawTuic RawTuicServer, general *General) error {
AuthenticationTimeout: rawTuic.AuthenticationTimeout, AuthenticationTimeout: rawTuic.AuthenticationTimeout,
ALPN: rawTuic.ALPN, ALPN: rawTuic.ALPN,
MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize, MaxUdpRelayPacketSize: rawTuic.MaxUdpRelayPacketSize,
CWND: rawTuic.CWND,
} }
return nil return nil
} }
@@ -1295,7 +1376,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
if len(snifferRaw.Sniff) != 0 { if len(snifferRaw.Sniff) != 0 {
for sniffType, sniffConfig := range snifferRaw.Sniff { for sniffType, sniffConfig := range snifferRaw.Sniff {
find := false find := false
ports, err := parsePortRange(sniffConfig.Ports) ports, err := utils.NewIntRangesFromList[uint16](sniffConfig.Ports)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1322,7 +1403,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
// Deprecated: Use Sniff instead // Deprecated: Use Sniff instead
log.Warnln("Deprecated: Use Sniff instead") log.Warnln("Deprecated: Use Sniff instead")
} }
globalPorts, err := parsePortRange(snifferRaw.Ports) globalPorts, err := utils.NewIntRangesFromList[uint16](snifferRaw.Ports)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1367,28 +1448,3 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
return sniffer, nil return sniffer, nil
} }
func parsePortRange(portRanges []string) ([]utils.Range[uint16], error) {
ports := make([]utils.Range[uint16], 0)
for _, portRange := range portRanges {
portRaws := strings.Split(portRange, "-")
p, err := strconv.ParseUint(portRaws[0], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
start := uint16(p)
if len(portRaws) > 1 {
p, err = strconv.ParseUint(portRaws[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
end := uint16(p)
ports = append(ports, *utils.NewRange(start, end))
} else {
ports = append(ports, *utils.NewRange(start, start))
}
}
return ports, nil
}

View File

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

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