Compare commits

...

188 Commits

Author SHA1 Message Date
63917aa020 fix: uuid-map return failed error 2022-05-14 23:45:10 +08:00
5016f529af revert: yaml v2 2022-05-14 23:36:19 +08:00
5bd5f1bfda chore: remove Script mode residual code. 2022-05-14 13:00:33 +08:00
d4dcbce9cb chore: log show all ips when all ips shake hands failed 2022-05-13 21:43:42 +08:00
df8196a68c fix: print process path logic 2022-05-12 18:57:30 +08:00
c1631759fc feat: add strategy:sticky-sessions for LoadBalance
Signed-off-by: Meta <maze.y2b@gmail.com>
2022-05-09 18:56:36 +08:00
9e9c3c810f fixed: make log api unblocked 2022-05-09 18:54:00 +08:00
463101aec1 fix: limit load provider concurrent size 2022-05-08 22:52:46 +08:00
2072964701 revert: tls handshake timeout recovery 10s 2022-05-08 21:56:59 +08:00
aded1b78b5 chore: sniffer give the err to the caller 2022-05-08 09:09:39 +08:00
ca9c859059 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-05-08 07:59:17 +08:00
55811dae32 fix: Adjust the timing of loading proxy selection 2022-05-08 07:58:26 +08:00
7136d145f8 chore: update dependencies 2022-05-08 00:47:01 +08:00
2fbbf7519f fix: provider auto update 2022-05-08 00:04:16 +08:00
663bf4fbb0 fix: remove misjudgment 2022-05-07 12:53:13 +08:00
f0a22a4a4c chore: modify sniff error log 2022-05-07 12:44:28 +08:00
4ab91520bd refactor: reuse uuid namespace 2022-05-07 12:35:14 +08:00
980d8a2641 refactor: string map to uuidv5 2022-05-06 14:02:34 +08:00
a95d439852 chore: the uuid-map is transferred to the protocol 2022-05-06 13:28:09 +08:00
a08e39faec fix uuid match 2022-05-06 13:08:27 +08:00
b3295262c1 chore: Initialize provider ahead of time 2022-05-05 21:14:46 +08:00
27aa026568 fix: use actual metadata 2022-05-04 20:13:12 +08:00
9969e1706e fix: loadbalance group npe 2022-05-04 19:52:48 +08:00
fb58595d44 feat: Expose remote destination (udp proxy maybe domain of node) 2022-05-04 16:57:08 +08:00
bdfa16ca6f fix: wrong parameters 2022-05-04 01:04:43 +08:00
b307bcb4a9 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	listener/tun/ipstack/commons/router_linux.go
2022-05-04 01:04:11 +08:00
f26941091b fix: default router with fakeIP when tun enable 2022-05-04 00:59:04 +08:00
6cd5769ed7 fix: default router with fakeIP when tun enable 2022-05-04 00:36:44 +08:00
41adfa65b3 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	listener/tun/ipstack/commons/router_linux.go
2022-05-03 23:59:41 +08:00
3fbb7c7a2d chore: add default router when tun enable 2022-05-03 23:58:11 +08:00
0aa82c04ea chore: add default router 198.18.0.0/16 when tun enable 2022-05-03 23:28:02 +08:00
5c6f2694c7 chore: sniffer param skip-sni renamed to ship-domain, old param will be removed in the release version 2022-05-03 23:10:59 +08:00
eca7615f08 fix: patch update support tun 2022-05-03 19:31:00 +08:00
52d559bb38 feat: rule-provider support NetWork rule 2022-05-03 01:36:03 +08:00
259736390a feat: rule-provider support rules field 2022-05-03 00:53:22 +08:00
7db07630a7 fix: DNS mapping error when sniffing result is ip, Discard sniffs that result in ip 2022-05-02 22:24:14 +08:00
d617b0f447 style: uid log tidy 2022-05-02 19:52:34 +08:00
80ff5917f7 fix: The sniffer does not clean up the original address 2022-05-02 17:09:24 +08:00
b401da5eba refactor: provider init order 2022-05-02 16:47:48 +08:00
05b25c334f Merge branch 'makefile' into Alpha 2022-05-02 14:43:51 +08:00
2c5a47a275 fix: Failed to get version tag 2022-05-02 14:43:01 +08:00
b2605a9012 fix: tun dns 2022-05-02 14:21:37 +08:00
b929a19f48 refactor: Unified active health detection, supported by load balancing policy group 2022-05-02 13:50:10 +08:00
4b04faa88b fix: http sniffer return host that was handled correctly 2022-05-02 09:51:26 +08:00
5fee0b5bf1 chore: adjust pass to reject.go 2022-05-02 09:16:47 +08:00
27120fb0f5 Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-05-02 08:49:23 +08:00
0cf539fb82 chore: adjust sniffer constant 2022-05-02 08:49:18 +08:00
26a38bd8de chore: adjust sniffer constant 2022-05-02 08:46:24 +08:00
ebbce4d061 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-05-02 08:28:00 +08:00
5acd2f6c3a chore: workflow 2022-05-02 08:27:17 +08:00
fe2bc903b8 fix trojan and snell's normal udp 2022-05-02 06:28:27 +08:00
658f1f5cda fix code mistake 2022-05-02 05:34:20 +08:00
5ccc047fe4 chore: adjust sniffer err info 2022-05-02 05:17:13 +08:00
6d704b9cd1 feat: sniffer support http 2022-05-02 05:10:18 +08:00
5f957b5cf9 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	.github/workflows/prerelease.yml
#	.github/workflows/release.yaml
2022-05-02 05:01:33 +08:00
8681728b8d chore: doq parameters 2022-05-02 05:01:07 +08:00
bb1eb5979b chore: Merge alpha and beta 2022-05-02 01:14:30 +08:00
032b6a2cc5 chore: workflow 2022-05-02 00:59:41 +08:00
45a02e3439 chore: workflow 2022-05-02 00:19:28 +08:00
9d8cd036ff refactor: remove dns and tun relationship, the enabled of dns module should be decided by user 2022-05-01 09:41:27 +08:00
2aad9818e8 fix trojan and snell's udp over tcp 2022-04-30 22:26:38 +08:00
4d5c0d2bf4 fix: auto-route priority wlan0 in Android 2022-04-30 17:43:37 +08:00
861205dbbe support udp in relay if last proxy could udp-over-tcp 2022-04-30 11:36:42 +08:00
9dbe20f2c5 fix: npe when with resolver is nil 2022-04-29 13:03:55 +08:00
8dea62cd63 refactor: del useless file 2022-04-28 23:49:24 +08:00
2d99f86780 fix: dhcp ifacename type 2022-04-28 23:44:37 +08:00
fac63625fc fix: replace with sync.map for GroupBase 2022-04-28 23:43:10 +08:00
b9f270162a refactor: field name 2022-04-28 23:10:08 +08:00
bbbe371ea9 fix: dns specified interface does not change 2022-04-28 22:40:06 +08:00
e2409f9639 Merge branch 'doq' into Alpha 2022-04-28 22:24:02 +08:00
4f634edaa9 refactor: doq dialer 2022-04-28 22:21:48 +08:00
f1dab9e9ce refactor: optimize the performance of filter in proxy-group 2022-04-28 19:01:13 +08:00
8217db82ce Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-28 17:47:29 +08:00
f9dd0a6bfc [Skip CI] chore: add docker workflow 2022-04-28 14:24:38 +08:00
b9b25be68d Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	.github/workflows/docker.yaml
2022-04-28 14:19:25 +08:00
179e5aad58 chore: add docker workflow 2022-04-28 14:18:54 +08:00
fc1eaf9a9d chore: add docker workflow 2022-04-28 14:11:35 +08:00
1756730693 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-04-28 12:44:51 +08:00
c7bc67e44c fix: handle metadata when dst is ip:port 2022-04-28 12:44:27 +08:00
949c1551f8 fixup! chore: system err log 2022-04-28 12:37:53 +08:00
b23a333acf Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-28 11:51:57 +08:00
47568051bf fix: problems caused when uid is 0 2022-04-28 11:51:40 +08:00
22c1e05e1c fix: rule provider http api crash 2022-04-28 09:44:29 +08:00
c161d5e6be fix: inner request error 2022-04-28 09:24:40 +08:00
573be855f7 fix: file not change modify time when updated rule 2022-04-28 09:07:58 +08:00
a38b2bcb6d Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-04-28 08:56:00 +08:00
2e74986fe4 refactor: adjust provider loading order, remove meaningless pointers 2022-04-28 08:55:45 +08:00
30eaa8add6 fix: count error 2022-04-28 08:54:33 +08:00
b498d2dda2 chore: system err log 2022-04-27 22:54:12 +08:00
96a32f5038 refactor: tcp concurrent 2022-04-27 21:37:20 +08:00
e11f6f84df chore: adjust workflows 2022-04-27 18:05:03 +08:00
5a1e1050b7 chore: adjust sniffer log 2022-04-27 18:04:02 +08:00
73aa8c7be7 feat: support uuid with custom string 2022-04-27 18:02:29 +08:00
183973e823 chore: Adjust the tcp-concurrent and sniffer log 2022-04-27 15:22:42 +08:00
2e08a4b4e1 fix: undefined parameter 2022-04-27 14:42:58 +08:00
564a6fdf35 Chore: http 2022-04-27 12:52:11 +08:00
cca3a1a934 Fix: http proxy Upgrade behavior (#2097) 2022-04-27 12:38:31 +08:00
ffb49ba4c5 fix: gvisor panic 2022-04-25 18:39:45 +08:00
e4fb10faf9 Chore: update dependencies 2022-04-25 13:23:56 +08:00
16a35527b7 Chore: increase nattable capacity 2022-04-25 13:18:41 +08:00
4fd7d0f707 Chore: use generics as possible 2022-04-25 13:18:30 +08:00
dee1aeb6c3 fix: logic of auto-detect-interface 2022-04-23 23:42:42 +08:00
2f95d56a12 pref: uid style in log 2022-04-23 17:37:50 +08:00
c77993eed3 refactor: tidy auto-route code 2022-04-23 17:31:16 +08:00
ce663a7b96 fix: ipv6 enable logic 2022-04-23 14:21:58 +08:00
9c2ca0feb0 fix: uid match 2022-04-23 13:37:37 +08:00
b8d5321615 feat: cache uid 2022-04-23 12:11:26 +08:00
eb6f7e3158 fix: relay conn error when addr is domain 2022-04-23 10:26:22 +08:00
0947cb4a5a fix: whitelist 2022-04-23 09:52:23 +08:00
0368bb4180 fix: sniffer port whitelist error 2022-04-23 09:36:11 +08:00
4aeac0e227 chore: Adjust the connection IP log 2022-04-23 08:53:51 +08:00
bd3c493c9f fix: ipv6 enable logic 2022-04-23 02:03:10 +08:00
0a99fc4d74 fix: wrong parameter name 2022-04-23 00:45:43 +08:00
19fc70b2c4 fix: general ipv6 is false should be broke ipv6 conn 2022-04-23 00:30:25 +08:00
81b5543b0d feat: support tcp concurrent, Separate dialing and dns resolver ipv6
tcp-concurrent:true
2022-04-23 00:27:22 +08:00
2e1d9a4f2e fix: hotspot for android 2022-04-22 22:25:45 +08:00
de4341c8cd Revert: "fix: proxy-groups filter logic"
This reverts commit 8a85c63b08.
2022-04-22 18:56:35 +08:00
8a85c63b08 fix: proxy-groups filter logic 2022-04-22 17:27:55 +08:00
b0dd74e74e fix: sniffer 2022-04-22 17:00:39 +08:00
4dd9e199b7 fix: uid rule only support linux and android 2022-04-22 16:51:01 +08:00
3d6aea4c1e feat: support uid rule
eg. UID,1000/5000-6000,Proxy
2022-04-22 16:27:51 +08:00
0cb5270452 Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-22 15:58:57 +08:00
3f6d2e5f91 feat: dnsHijack support "any"
chore: adjust process debug display logic
2022-04-22 13:30:04 +08:00
3b16fcef92 Chore: wait for system stack to close 2022-04-22 12:44:51 +08:00
f91d106cdf Chore: fix typos 2022-04-22 12:42:20 +08:00
9e6ba64940 fix: add wait timeout, and log 2022-04-21 08:08:37 -07:00
bee1bddceb feat: add sniffer port whitelist, when empty will add all ports 2022-04-21 07:06:08 -07:00
e98dcc4267 [fix] logic 2022-04-21 18:56:33 +08:00
4b79f8de93 [fix] auto-route for android 2022-04-21 17:47:04 +08:00
f40c2eb71d Chore: update dependencies 2022-04-20 22:55:30 +08:00
7ca1a03d73 Refactor: metadata use netip.Addr 2022-04-20 22:52:05 +08:00
6c4791480e Chore: IpToAddr 2022-04-20 22:09:16 +08:00
42d853a7e6 chore: upgrade dependencies 2022-04-20 01:31:33 +08:00
5d36d8b139 Improve: replace bootstrap dns (#2080) 2022-04-19 22:49:39 +08:00
5a4441c47f Chore: add none alias to dummy on ShadowsocksR (#2056) 2022-04-19 22:49:22 +08:00
0ca10798ea Chore: fix typo 2022-04-19 22:38:20 +08:00
3ea3653d7a Chore: persistence fakeip pool state 2022-04-19 22:37:47 +08:00
58cd8f9ac1 fix:force-domain invalid 2022-04-17 21:17:21 +08:00
ea0d236259 chore: change comments 2022-04-17 20:03:53 +08:00
48a01adb7a refactor: sniffer param force and reverses deprecated, will be removed when release version, replace force-domain and skip-sni,
force-domain add '+' equivalent to force is true
sniffer:
  enable: true
  force-domain:
    - "google.com"
  skip-sni:
    - www.baidu.com
  sniffing:
    - tls
2022-04-17 20:02:13 +08:00
f8d7f29856 fix: PASS policy inconsistent names 2022-04-17 14:11:58 +08:00
1cf9321aa0 fix: domain tree match failed 2022-04-16 11:55:49 +08:00
71a1f5dfbd fix: domain type fix Mapping 2022-04-16 09:51:31 +08:00
25426cba33 chore: log style 2022-04-16 09:04:43 +08:00
9d364f66e9 fix: reverse error when force is false 2022-04-16 08:53:31 +08:00
7c23fa2bd4 fix: sniffer npe 2022-04-16 08:45:18 +08:00
0658ecadd3 fix: adjust loading timing 2022-04-16 08:29:38 +08:00
efbc334b3b Merge branch 'logic-rule' into Alpha 2022-04-16 08:22:08 +08:00
80764217c2 feat: add domain list for sniffer, reverse force logic
when force is false, if domain in the list, will force replace
when force is true, if sniff domain in the list, will skip it
2022-04-16 08:21:31 +08:00
45fe6e996b fix: npe when parse rule 2022-04-16 00:21:08 +08:00
36a719e2f8 feat: support http headers 2022-04-14 13:07:39 +08:00
1b6b0052c2 chore:adjust sniffer debuglog info 2022-04-13 08:38:55 +08:00
c981ef0f28 chore: update dependencies 2022-04-13 02:32:55 +08:00
1bf291d240 chore: update dependencies 2022-04-13 02:24:21 +08:00
b179d09efb Chore: adjust ipstack 2022-04-13 02:20:53 +08:00
4be17653e0 Fix: fakeip pool cycle used 2022-04-13 02:19:42 +08:00
21446ba5d4 chore: adjust code 2022-04-12 21:39:31 +08:00
75ce6b59bf Refactor: fakeip pool use netip.Prefix, supports ipv6 range 2022-04-12 20:32:08 +08:00
ce96ac35fb chore:merge & adjust code 2022-04-12 20:20:04 +08:00
173e10abe6 Chore: fix typos 2022-04-12 19:08:13 +08:00
a6eb11ce18 Refactor: DomainTrie use generics 2022-04-12 18:45:47 +08:00
0c65f6962a Refactor: queue use generics 2022-04-12 18:44:13 +08:00
baa9e02af6 Refactor: cache use generics 2022-04-12 18:44:10 +08:00
673541e2a8 Refactor: lrucache use generics 2022-04-12 18:44:07 +08:00
14878b37f6 fix: trojan fail may panic 2022-04-12 18:43:55 +08:00
83e0abaa8c chore: adjust code 2022-04-11 13:23:59 +08:00
7166db2ac9 fix: code logic error 2022-04-10 20:01:35 +08:00
815a060309 Update metadata.go
revet commit 13012a9
2022-04-10 00:47:22 +08:00
544e0f137d feat: sniffer support
sniffer:
  enable: true
  force: false # Overwrite domain
  sniffing:
    - tls
2022-04-09 22:30:36 +08:00
07906c0aa5 fix: parse logic rule error 2022-04-09 22:25:39 +08:00
b2981f921c chore: reduce a little memory 2022-04-09 22:24:48 +08:00
7be3e617ab disable process name on android 2022-04-09 17:54:01 +08:00
9a3bc8ef9e fix: auto detect interface add param[auto-detect-interface], default is true, only use it when tun is enabled 2022-04-07 21:36:19 +08:00
e083d1c57d Revert "Add docker workflow"
This reverts commit ce4902e5e6.
2022-04-07 10:24:23 +08:00
ce4902e5e6 Add docker workflow 2022-04-06 08:31:34 +08:00
4b9edc3b66 revert:the name of tun device on mac 2022-04-05 23:04:59 +08:00
91e48b707b Merge remote-tracking branch 'yaling888/with-tun' into Alpha 2022-04-05 14:44:40 +08:00
7a8af90b86 feat: add SMTPS/POP3S/IMAPS port to sni detect 2022-04-05 03:26:23 +08:00
93d2cfa091 fix: when ssh connect to a ip, if this ip map to a domain in clash, change ip to host may redirect to a diffrent ip 2022-04-05 03:26:23 +08:00
5719b9d22f fix: npe panic 2022-04-04 22:28:47 +08:00
b553dd749b refactor: Some adjustments 2022-04-03 19:15:16 +08:00
9461bcd44e fix: default-nameserver allow DOT and DOH with host is ip 2022-04-03 19:14:21 +08:00
6548dc90fa Merge remote-tracking branch 'Plus/with-tun' into Alpha 2022-04-02 20:48:11 +08:00
13012a9f89 fix: dns over proxy may due to cancel request, but proxy live status is fine 2022-04-02 17:32:37 +08:00
afdcb6cfc7 fix: log level ajust and lint fix 2022-03-31 21:27:25 +08:00
c495d314d4 feat: 添加tls sni 嗅探
# Conflicts:
#	tunnel/statistic/tracker.go
#	tunnel/tunnel.go
2022-03-31 21:27:25 +08:00
e877b68179 Chore: revert "Feature: add tls SNI sniffing (#68)"
This reverts commit 24ce6622a2.
2022-03-31 21:20:46 +08:00
24ce6622a2 Feature: add tls SNI sniffing (#68) 2022-03-31 19:34:40 +08:00
178 changed files with 4759 additions and 2218 deletions

61
.github/workflows/docker.yaml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Docker
on:
push:
branches:
- Beta
tags:
- "v*"
env:
REGISTRY: docker.io
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
- name: Log into registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: |
linux/386
linux/amd64
linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,8 +1,15 @@
name: Alpha
on: [push]
name: Prerelease
on:
push:
branches:
- Alpha
- Beta
pull_request:
branches:
- Alpha
- Beta
jobs:
Feature-build:
if: ${{ !contains(github.event.head_commit.message, '[Skip CI]') }}
Build:
runs-on: ubuntu-latest
steps:
- name: Get latest go version
@ -24,9 +31,13 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# - name: Get dependencies, run test
# run: |
# go test ./...
- name: Test
if: ${{github.ref_name=='Beta'}}
run: |
go test ./...
- name: Build
if: success()
env:
@ -38,34 +49,22 @@ jobs:
uses: andreaswilli/delete-release-assets-action@v2.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: alpha
tag: Prerelease-${{ github.ref_name }}
deleteOnlyFromDrafts: false
- name: Tag Repo
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
tag_name: Prerelease-${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Alpha
uses: softprops/action-gh-release@v1
if: ${{ env.GIT_BRANCH != 'Meta' && success() }}
if: ${{ success() }}
with:
tag: ${{ github.ref }}
tag_name: alpha
tag: ${{ github.ref_name }}
tag_name: Prerelease-${{ github.ref_name }}
files: bin/*
prerelease: true
- name: send telegram message on push
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TTELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
${{ github.actor }} created commit:
Commit message: ${{ github.event.commits[0].message }}
Repository: ${{ github.repository }}
See changes: https://github.com/${{ github.repository }}/commit/${{github.sha}}
generate_release_notes: true

44
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
Build:
runs-on: ubuntu-latest
steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Cache go module
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test
run: |
go test ./...
- name: Build
if: success()
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j releases
- name: Upload Release
uses: softprops/action-gh-release@v1
if: ${{ success() && startsWith(github.ref, 'refs/tags/')}}
with:
tag: ${{ github.ref }}
files: bin/*
generate_release_notes: true

View File

@ -1,18 +1,26 @@
FROM golang:alpine as builder
RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb
WORKDIR /clash-src
COPY --from=tonistiigi/xx:golang / /
mkdir /clash-config && \
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
COPY . /clash-src
RUN go mod download && \
make docker && \
mv ./bin/clash-docker /clash
WORKDIR /clash-src
RUN go mod download &&\
make docker &&\
mv ./bin/Clash.Meta-docker /clash
FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta"
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash /
ENTRYPOINT ["/clash"]
VOLUME ["/root/.config/clash/"]
COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /clash /clash
RUN chmod +x /clash
ENTRYPOINT [ "/clash" ]

View File

@ -1,7 +1,16 @@
NAME=Clash.Meta
BINDIR=bin
BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
BRANCH=$(shell git branch --show-current)
ifeq ($(BRANCH),Alpha)
VERSION=alpha-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),Beta)
VERSION=beta-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),)
VERSION=$(shell git describe --tags)
else
VERSION=unknown
endif
BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
@ -43,7 +52,7 @@ all:linux-amd64 linux-arm64\
windows-amd64 windows-arm64\
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -150,4 +159,4 @@ lint:
golangci-lint run ./...
clean:
rm $(BINDIR)/*
rm $(BINDIR)/*

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"time"
@ -21,7 +22,7 @@ var UnifiedDelay = atomic.NewBool(false)
type Proxy struct {
C.ProxyAdapter
history *queue.Queue
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
}
@ -64,10 +65,10 @@ func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
// DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy()
queueM := p.history.Copy()
histories := []C.DelayHistory{}
for _, item := range queue {
histories = append(histories, item.(C.DelayHistory))
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
@ -80,11 +81,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
return max
}
last := p.history.Last()
if last == nil {
return max
}
history := last.(C.DelayHistory)
history := p.history.Last()
if history.Delay == 0 {
return max
}
@ -99,7 +96,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
}
mapping := map[string]any{}
json.Unmarshal(inner, &mapping)
_ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
@ -133,7 +130,9 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
if err != nil {
return
}
defer instance.Close()
defer func() {
_ = instance.Close()
}()
req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil {
@ -142,7 +141,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
req = req.WithContext(ctx)
transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) {
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
@ -171,14 +170,13 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
return
}
}
resp.Body.Close()
_ = resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -203,7 +201,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
return

View File

@ -2,6 +2,7 @@ package inbound
import (
"net"
"net/netip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -13,9 +14,13 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = source
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
remoteAddr := conn.RemoteAddr()
// Filter when net.Addr interface is nil
if remoteAddr != nil {
if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
}
return context.NewConnContext(conn, metadata)
@ -29,14 +34,15 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
metadata.Host = host
metadata.AddrType = C.AtypDomainName
metadata.Process = C.ClashName
if ip, port, err := parseAddr(dst); err == nil {
if h, port, err := net.SplitHostPort(dst); err == nil {
metadata.DstPort = port
if host == "" {
metadata.DstIP = ip
if ip.To4() == nil {
metadata.AddrType = C.AtypIPv6
} else {
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
metadata.AddrType = C.AtypIPv4
if ip.Is6() {
metadata.AddrType = C.AtypIPv6
}
}
}
}

View File

@ -3,9 +3,11 @@ package inbound
import (
"net"
"net/http"
"net/netip"
"strconv"
"strings"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
)
@ -21,12 +23,10 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
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]))
case socks5.AtypIPv4:
ip := net.IP(target[1 : 1+net.IPv4len])
metadata.DstIP = ip
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]))
case socks5.AtypIPv6:
ip := net.IP(target[1 : 1+net.IPv6len])
metadata.DstIP = ip
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv6len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
@ -47,14 +47,14 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
ip := net.ParseIP(host)
if ip != nil {
ip, err := netip.ParseAddr(host)
if err == nil {
switch {
case ip.To4() == nil:
case ip.Is6():
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
@ -65,12 +65,12 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata
}
func parseAddr(addr string) (net.IP, string, error) {
func parseAddr(addr string) (netip.Addr, string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, "", err
return netip.Addr{}, "", err
}
ip := net.ParseIP(host)
return ip, port, nil
ip, err := netip.ParseAddr(host)
return ip, port, err
}

View File

@ -39,6 +39,16 @@ func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
return nil, errors.New("no support")
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, errors.New("no support")
}
// SupportUOT implements C.ProxyAdapter
func (b *Base) SupportUOT() bool {
return false
}
// SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool {
return b.udp

View File

@ -56,13 +56,3 @@ func NewCompatible() *Direct {
},
}
}
func NewPass() *Direct {
return &Direct{
Base: &Base{
name: "Pass",
tp: C.Pass,
udp: true,
},
}
}

View File

@ -22,18 +22,20 @@ type Http struct {
user string
pass string
tlsConfig *tls.Config
option *HttpOption
}
type HttpOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
}
// StreamConn implements C.ProxyAdapter
@ -84,6 +86,13 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
},
}
//增加headers
if len(h.option.Headers) != 0 {
for key, value := range h.option.Headers {
req.Header.Add(key, value)
}
}
if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
@ -141,5 +150,6 @@ func NewHttp(option HttpOption) *Http {
user: option.UserName,
pass: option.Password,
tlsConfig: tlsConfig,
option: &option,
}
}

View File

@ -34,6 +34,16 @@ func NewReject() *Reject {
}
}
func NewPass() *Reject {
return &Reject{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
},
}
}
type nopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) {

View File

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

View File

@ -53,6 +53,10 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP {
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
@ -104,6 +108,17 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return newPacketConn(pc, s), nil
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (s *Snell) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
}
// SupportUOT implements C.ProxyAdapter
func (s *Snell) SupportUOT() bool {
return true
}
func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk)

View File

@ -13,8 +13,6 @@ import (
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
"golang.org/x/net/http2"
)
type Trojan struct {
@ -25,7 +23,7 @@ type Trojan struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
transport *gun.TransportWrap
}
type TrojanOption struct {
@ -90,6 +88,10 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
return nil, err
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err
}
@ -166,6 +168,17 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return newPacketConn(pc, t), err
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
// SupportUOT implements C.ProxyAdapter
func (t *Trojan) SupportUOT() bool {
return true
}
func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))

View File

@ -22,8 +22,8 @@ var (
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
@ -48,14 +48,14 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType {
case socks5.AtypDomainName:
len := uint8(len(metadata.Host))
lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port}
buf = [][]byte{{aType, lenM}, host, port}
case socks5.AtypIPv4:
host := metadata.DstIP.To4()
host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6:
host := metadata.DstIP.To16()
host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port}
}
return bytes.Join(buf, nil)
@ -76,6 +76,6 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
func safeConnClose(c net.Conn, err error) {
if err != nil {
c.Close()
_ = c.Close()
}
}

View File

@ -18,8 +18,6 @@ import (
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
const (
@ -35,7 +33,7 @@ type Vless struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
transport *gun.TransportWrap
}
type VlessOption struct {
@ -249,26 +247,36 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, fmt.Errorf("new vless client error: %v", err)
}
return v.ListenPacketOnStreamConn(c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
// SupportUOT implements C.ProxyAdapter
func (v *Vless) SupportUOT() bool {
return true
}
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vless.AtypIPv4)
addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vless.AtypIPv6)
addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vless.AtypDomainName)
addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
copy(addr[1:], metadata.Host)
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)

View File

@ -15,8 +15,6 @@ import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
)
type Vmess struct {
@ -27,7 +25,7 @@ type Vmess struct {
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *http2.Transport
transport *gun.TransportWrap
}
type VmessOption struct {
@ -260,9 +258,19 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return v.ListenPacketOnStreamConn(c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
// SupportUOT implements C.ProxyAdapter
func (v *Vmess) SupportUOT() bool {
return true
}
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{
@ -342,11 +350,11 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)

View File

@ -1,55 +0,0 @@
package outboundgroup
import (
"github.com/Dreamacro/clash/tunnel"
"github.com/dlclark/regexp2"
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
const (
defaultGetProxiesDuration = time.Second * 5
)
func getProvidersProxies(providers []provider.ProxyProvider, touch bool, filter string) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
}
var filterReg *regexp2.Regexp = nil
var matchedProxies []C.Proxy
if len(filter) > 0 {
//filterReg = regexp.MustCompile(filter)
filterReg = regexp2.MustCompile(filter, 0)
for _, p := range proxies {
if p.Type() < 8 {
matchedProxies = append(matchedProxies, p)
}
//if filterReg.MatchString(p.Name()) {
if mat, _ := filterReg.FindStringMatch(p.Name()); mat != nil {
matchedProxies = append(matchedProxies, p)
}
}
if len(matchedProxies) > 0 {
return matchedProxies
} else {
return append([]C.Proxy{}, tunnel.Proxies()["COMPATIBLE"])
}
} else {
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
} else {
return proxies
}
}
}

View File

@ -3,25 +3,15 @@ package outboundgroup
import (
"context"
"encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Fallback struct {
*outbound.Base
disableUDP bool
filter string
single *singledo.Single
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
*GroupBase
disableUDP bool
}
func (f *Fallback) Now() string {
@ -35,8 +25,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
f.onDialSuccess()
} else {
f.onDialFailed()
}
@ -50,8 +39,7 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
f.onDialSuccess()
} else {
f.onDialFailed()
}
@ -59,32 +47,6 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
return pc, err
}
func (f *Fallback) onDialFailed() {
if f.failedTime.Load() == -1 {
log.Warnln("%s first failed", f.Name())
now := time.Now().UnixMilli()
f.failedTime.Store(now)
f.failedTimes.Store(1)
} else {
if f.failedTime.Load()-time.Now().UnixMilli() > 5*time.Second.Milliseconds() {
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
failedCount := f.failedTimes.Inc()
log.Warnln("%s failed count: %d", f.Name(), failedCount)
if failedCount >= 5 {
log.Warnln("because %s failed multiple times, active health check", f.Name())
for _, proxyProvider := range f.providers {
go proxyProvider.HealthCheck()
}
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
}
}
}
}
// SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool {
if f.disableUDP {
@ -98,7 +60,7 @@ func (f *Fallback) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range f.proxies(false) {
for _, proxy := range f.GetProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -114,16 +76,8 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
return proxy
}
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) {
return getProvidersProxies(f.providers, touch, f.filter), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch)
proxies := f.GetProxies(touch)
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
@ -135,17 +89,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Fallback,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
disableUDP: option.DisableUDP,
}
}

View File

@ -0,0 +1,144 @@
package outboundgroup
import (
"github.com/Dreamacro/clash/adapter/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/dlclark/regexp2"
"go.uber.org/atomic"
"sync"
"time"
)
type GroupBase struct {
*outbound.Base
filter *regexp2.Regexp
providers []provider.ProxyProvider
versions sync.Map // map[string]uint
proxies sync.Map // map[string][]C.Proxy
failedTimes *atomic.Int32
failedTime *atomic.Int64
}
type GroupBaseOption struct {
outbound.BaseOption
filter string
providers []provider.ProxyProvider
}
func NewGroupBase(opt GroupBaseOption) *GroupBase {
var filter *regexp2.Regexp = nil
if opt.filter != "" {
filter = regexp2.MustCompile(opt.filter, 0)
}
return &GroupBase{
Base: outbound.NewBase(opt.BaseOption),
filter: filter,
providers: opt.providers,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
}
}
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
if gb.filter == nil {
var proxies []C.Proxy
for _, pd := range gb.providers {
if touch {
proxies = append(proxies, pd.ProxiesWithTouch()...)
} else {
proxies = append(proxies, pd.Proxies()...)
}
}
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
return proxies
}
for _, pd := range gb.providers {
if pd.VehicleType() == types.Compatible {
if touch {
gb.proxies.Store(pd.Name(), pd.ProxiesWithTouch())
} else {
gb.proxies.Store(pd.Name(), pd.Proxies())
}
gb.versions.Store(pd.Name(), pd.Version())
continue
}
if version, ok := gb.versions.Load(pd.Name()); !ok || version != pd.Version() {
var (
proxies []C.Proxy
newProxies []C.Proxy
)
if touch {
proxies = pd.ProxiesWithTouch()
} else {
proxies = pd.Proxies()
}
for _, p := range proxies {
if mat, _ := gb.filter.FindStringMatch(p.Name()); mat != nil {
newProxies = append(newProxies, p)
}
}
gb.proxies.Store(pd.Name(), newProxies)
gb.versions.Store(pd.Name(), pd.Version())
}
}
var proxies []C.Proxy
gb.proxies.Range(func(key, value any) bool {
proxies = append(proxies, value.([]C.Proxy)...)
return true
})
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
return proxies
}
func (gb *GroupBase) onDialFailed() {
if gb.failedTime.Load() == -1 {
log.Warnln("%s first failed", gb.Name())
now := time.Now().UnixMilli()
gb.failedTime.Store(now)
gb.failedTimes.Store(1)
} else {
if gb.failedTime.Load()-time.Now().UnixMilli() > gb.failedIntervalTime() {
gb.failedTimes.Store(-1)
gb.failedTime.Store(-1)
} else {
failedCount := gb.failedTimes.Inc()
log.Warnln("%s failed count: %d", gb.Name(), failedCount)
if failedCount >= gb.maxFailedTimes() {
log.Warnln("because %s failed multiple times, active health check", gb.Name())
for _, proxyProvider := range gb.providers {
go proxyProvider.HealthCheck()
}
gb.failedTimes.Store(-1)
gb.failedTime.Store(-1)
}
}
}
}
func (gb *GroupBase) failedIntervalTime() int64 {
return 5 * time.Second.Milliseconds()
}
func (gb *GroupBase) onDialSuccess() {
gb.failedTimes.Store(-1)
gb.failedTime.Store(-1)
}
func (gb *GroupBase) maxFailedTimes() int32 {
return 5
}

View File

@ -5,11 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -20,11 +21,8 @@ import (
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct {
*outbound.Base
*GroupBase
disableUDP bool
single *singledo.Single
filter string
providers []provider.ProxyProvider
strategyFn strategyFn
}
@ -40,6 +38,10 @@ func parseStrategy(config map[string]any) string {
}
func getKey(metadata *C.Metadata) string {
if metadata == nil {
return ""
}
if metadata.Host != "" {
// ip host
if ip := net.ParseIP(metadata.Host); ip != nil {
@ -51,7 +53,7 @@ func getKey(metadata *C.Metadata) string {
}
}
if metadata.DstIP == nil {
if !metadata.DstIP.IsValid() {
return ""
}
@ -75,6 +77,9 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
defer func() {
if err == nil {
c.AppendToChains(lb)
lb.onDialSuccess()
} else {
lb.onDialFailed()
}
}()
@ -134,24 +139,70 @@ func strategyConsistentHashing() strategyFn {
}
}
// Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata)
func strategyStickySessions() strategyFn {
timeout := int64(600)
type Session struct {
idx int
time time.Time
}
Sessions := make(map[string]map[string]Session)
go func() {
for true {
time.Sleep(time.Second * 60)
now := time.Now().Unix()
for _, subMap := range Sessions {
for dest, session := range subMap {
if now-session.time.Unix() > timeout {
delete(subMap, dest)
}
}
}
}
}()
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
src := metadata.SrcIP.String()
dest := getKey(metadata)
now := time.Now()
length := len(proxies)
if Sessions[src] == nil {
Sessions[src] = make(map[string]Session)
}
session, ok := Sessions[src][dest]
if !ok || now.Unix()-session.time.Unix() > timeout {
session.idx = rand.Intn(length)
}
session.time = now
var i int
var res C.Proxy
for i := 0; i < length; i++ {
idx := (session.idx + i) % length
proxy := proxies[idx]
if proxy.Alive() {
session.idx = idx
res = proxy
break
}
}
if i == length {
session.idx = 0
res = proxies[0]
}
Sessions[src][dest] = session
return res
}
}
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (any, error) {
return getProvidersProxies(lb.providers, touch, lb.filter), nil
})
return elm.([]C.Proxy)
// Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.GetProxies(true)
return lb.strategyFn(proxies, metadata)
}
// MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range lb.proxies(false) {
var all []string
for _, proxy := range lb.GetProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -167,20 +218,23 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
strategyFn = strategyConsistentHashing()
case "round-robin":
strategyFn = strategyRoundRobin()
case "sticky-sessions":
strategyFn = strategyStickySessions()
default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
return &LoadBalance{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.LoadBalance,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
filter: option.Filter,
}, nil
}

View File

@ -6,27 +6,18 @@ import (
"fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Relay struct {
*outbound.Base
single *singledo.Single
providers []provider.ProxyProvider
filter string
*GroupBase
}
// DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
var proxies []C.Proxy
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
proxies = append(proxies, proxy)
}
}
proxies, chainProxies := r.proxies(metadata, true)
switch len(proxies) {
case 0:
@ -64,13 +55,86 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
return outbound.NewConn(c, r), nil
conn := outbound.NewConn(c, last)
for i := len(chainProxies) - 2; i >= 0; i-- {
conn.AppendToChains(chainProxies[i])
}
conn.AppendToChains(r)
return conn, nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
proxies, chainProxies := r.proxies(metadata, true)
switch len(proxies) {
case 0:
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
first := proxies[0]
last := proxies[len(proxies)-1]
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
tcpKeepAlive(c)
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
}
c, err = last.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
var pc C.PacketConn
pc, err = last.ListenPacketOnStreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
for i := len(chainProxies) - 2; i >= 0; i-- {
pc.AppendToChains(chainProxies[i])
}
pc.AppendToChains(r)
return pc, nil
}
// SupportUDP implements C.ProxyAdapter
func (r *Relay) SupportUDP() bool {
proxies, _ := r.proxies(nil, false)
if len(proxies) == 0 { // C.Direct
return true
}
last := proxies[len(proxies)-1]
return last.SupportUDP() && last.SupportUOT()
}
// MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range r.rawProxies(false) {
for _, proxy := range r.GetProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -79,38 +143,49 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
})
}
func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (any, error) {
return getProvidersProxies(r.providers, touch, r.filter), nil
})
func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
rawProxies := r.GetProxies(touch)
return elm.([]C.Proxy)
}
var proxies []C.Proxy
var chainProxies []C.Proxy
var targetProxies []C.Proxy
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies(touch)
for n, proxy := range proxies {
for n, proxy := range rawProxies {
proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy)
subproxy := proxy.Unwrap(metadata)
for subproxy != nil {
chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata)
}
}
return proxies
for _, proxy := range proxies {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
targetProxies = append(targetProxies, proxy)
}
}
return targetProxies, chainProxies
}
func (r *Relay) Addr() string {
proxies, _ := r.proxies(nil, true)
return proxies[len(proxies)-1].Addr()
}
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
"",
providers,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
filter: option.Filter,
}
}

View File

@ -6,19 +6,15 @@ import (
"errors"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
type Selector struct {
*outbound.Base
*GroupBase
disableUDP bool
single *singledo.Single
selected string
filter string
providers []provider.ProxyProvider
}
// DialContext implements C.ProxyAdapter
@ -51,7 +47,7 @@ func (s *Selector) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range getProvidersProxies(s.providers, false, s.filter) {
for _, proxy := range s.GetProxies(false) {
all = append(all, proxy.Name())
}
@ -67,10 +63,9 @@ func (s *Selector) Now() string {
}
func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers, false, s.filter) {
for _, proxy := range s.GetProxies(false) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
return nil
}
}
@ -84,32 +79,29 @@ func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (any, error) {
proxies := getProvidersProxies(s.providers, touch, s.filter)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil
}
proxies := s.GetProxies(touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy
}
}
return proxies[0], nil
})
return elm.(C.Proxy)
return proxies[0]
}
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
return &Selector{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.Selector,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
selected: "COMPATIBLE",
disableUDP: option.DisableUDP,
filter: option.Filter,
}
}

View File

@ -3,8 +3,6 @@ package outboundgroup
import (
"context"
"encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
@ -23,16 +21,11 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
}
type URLTest struct {
*outbound.Base
tolerance uint16
disableUDP bool
fastNode C.Proxy
filter string
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
*GroupBase
tolerance uint16
disableUDP bool
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
}
func (u *URLTest) Now() string {
@ -57,11 +50,11 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
pc.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
u.onDialSuccess()
} else {
u.onDialFailed()
}
return pc, err
}
@ -70,17 +63,9 @@ func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(true)
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (any, error) {
return getProvidersProxies(u.providers, touch, u.filter), nil
})
return elm.([]C.Proxy)
}
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (any, error) {
proxies := u.proxies(touch)
elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch)
fast := proxies[0]
min := fast.LastDelay()
fastNotExist := true
@ -109,7 +94,7 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil
})
return elm.(C.Proxy)
return elm
}
// SupportUDP implements C.ProxyAdapter
@ -124,7 +109,7 @@ func (u *URLTest) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) {
all := []string{}
for _, proxy := range u.proxies(false) {
for _, proxy := range u.GetProxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
@ -134,32 +119,6 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
})
}
func (u *URLTest) onDialFailed() {
if u.failedTime.Load() == -1 {
log.Warnln("%s first failed", u.Name())
now := time.Now().UnixMilli()
u.failedTime.Store(now)
u.failedTimes.Store(1)
} else {
if u.failedTime.Load()-time.Now().UnixMilli() > 5*1000 {
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
failedCount := u.failedTimes.Inc()
log.Warnln("%s failed count: %d", u.Name(), failedCount)
if failedCount >= 5 {
log.Warnln("because %s failed multiple times, active health check", u.Name())
for _, proxyProvider := range u.providers {
go proxyProvider.HealthCheck()
}
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
}
}
}
}
func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{}
@ -175,19 +134,19 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{
Base: outbound.NewBase(outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name,
Type: C.URLTest,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}),
single: singledo.NewSingle(defaultGetProxiesDuration),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP,
}
for _, option := range options {

View File

@ -3,6 +3,7 @@ package outboundgroup
import (
"fmt"
"net"
"net/netip"
"time"
C "github.com/Dreamacro/clash/constant"
@ -15,20 +16,21 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
return
}
ip := net.ParseIP(host)
if ip == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
addr = &C.Metadata{
AddrType: C.AtypDomainName,
Host: host,
DstIP: nil,
DstIP: netip.Addr{},
DstPort: port,
}
err = nil
return
} else if ip4 := ip.To4(); ip4 != nil {
} else if ip.Is4() {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip4,
DstIP: ip,
DstPort: port,
}
return
@ -45,7 +47,7 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}

View File

@ -43,6 +43,14 @@ func (f *fetcher) Initial() (any, error) {
err error
isLocal bool
)
defer func() {
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
}
}()
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime()
@ -84,11 +92,6 @@ func (f *fetcher) Initial() (any, error) {
f.hash = md5.Sum(buf)
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
}
return proxies, nil
}

View File

@ -65,14 +65,14 @@ func (hc *HealthCheck) touch() {
}
func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range hc.proxies {
p := proxy
b.Go(p.Name(), func() (any, error) {
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)
return nil, nil
_, _ = p.URLTest(ctx, hc.url)
return false, nil
})
}
b.Wait()

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"github.com/dlclark/regexp2"
"math"
"runtime"
"time"
@ -32,6 +33,11 @@ type proxySetProvider struct {
*fetcher
proxies []C.Proxy
healthCheck *HealthCheck
version uint
}
func (pp *proxySetProvider) Version() uint {
return pp.version
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@ -94,6 +100,7 @@ func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
if pp.healthCheck.auto() {
go pp.healthCheck.check()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
@ -116,6 +123,11 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
onUpdate := func(elm any) {
ret := elm.([]C.Proxy)
pd.setProxies(ret)
if pd.version == math.MaxUint {
pd.version = 0
} else {
pd.version++
}
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
@ -170,6 +182,11 @@ type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint
}
func (cp *compatibleProvider) Version() uint {
return cp.version
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {

View File

@ -2,7 +2,6 @@ package provider
import (
"context"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/listener/inner"
"io"
"net"
@ -58,7 +57,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
}
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
req.Header.Set("user-agent", netHttp.UA)
req.Header.Set("User-Agent", netHttp.UA)
if err != nil {
return nil, err
@ -74,7 +73,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
transport := &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
@ -86,10 +85,6 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
client := http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
}
resp, err = client.Do(req)
if err != nil {
return nil, err
}

28
check_amd64.sh Normal file
View File

@ -0,0 +1,28 @@
#!/bin/sh
flags=$(grep '^flags\b' </proc/cpuinfo | head -n 1)
flags=" ${flags#*:} "
has_flags () {
for flag; do
case "$flags" in
*" $flag "*) :;;
*) return 1;;
esac
done
}
determine_level () {
level=0
has_flags lm cmov cx8 fpu fxsr mmx syscall sse2 || return 0
level=1
has_flags cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 || return 0
level=2
has_flags avx avx2 bmi1 bmi2 f16c fma abm movbe xsave || return 0
level=3
has_flags avx512f avx512bw avx512cd avx512dq avx512vl || return 0
level=4
}
determine_level
echo "Your CPU supports amd64-v$level"
return $level

View File

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

View File

@ -11,14 +11,14 @@ import (
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
b, _ := New[string](context.Background())
now := time.Now()
b.Go("foo", func() (any, error) {
b.Go("foo", func() (string, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (any, error) {
b.Go("bar", func() (string, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
@ -32,20 +32,20 @@ func TestBatch(t *testing.T) {
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
assert.Equal(t, k, v.Value)
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
b, _ := New[string](
context.Background(),
WithConcurrencyNum(3),
WithConcurrencyNum[string](3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (any, error) {
b.Go(strconv.Itoa(idx), func() (string, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
@ -57,21 +57,21 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
assert.Equal(t, k, v.Value)
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b, ctx := New[string](context.Background())
b.Go("error", func() (any, error) {
b.Go("error", func() (string, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
return "", errors.New("test error")
})
b.Go("ctx", func() (any, error) {
b.Go("ctx", func() (string, error) {
<-ctx.Done()
return nil, ctx.Err()
return "", ctx.Err()
})
result, err := b.WaitAndGetResult()

48
common/cache/cache.go vendored
View File

@ -7,50 +7,50 @@ import (
)
// Cache store element with a expired time
type Cache struct {
*cache
type Cache[K comparable, V any] struct {
*cache[K, V]
}
type cache struct {
type cache[K comparable, V any] struct {
mapping sync.Map
janitor *janitor
janitor *janitor[K, V]
}
type element struct {
type element[V any] struct {
Expired time.Time
Payload any
Payload V
}
// Put element in Cache with its ttl
func (c *cache) Put(key any, payload any, ttl time.Duration) {
c.mapping.Store(key, &element{
func (c *cache[K, V]) Put(key K, payload V, ttl time.Duration) {
c.mapping.Store(key, &element[V]{
Payload: payload,
Expired: time.Now().Add(ttl),
})
}
// Get element in Cache, and drop when it expired
func (c *cache) Get(key any) any {
func (c *cache[K, V]) Get(key K) V {
item, exist := c.mapping.Load(key)
if !exist {
return nil
return getZero[V]()
}
elm := item.(*element)
elm := item.(*element[V])
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
return nil
return getZero[V]()
}
return elm.Payload
}
// GetWithExpire element in Cache with Expire Time
func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
func (c *cache[K, V]) GetWithExpire(key K) (payload V, expired time.Time) {
item, exist := c.mapping.Load(key)
if !exist {
return
}
elm := item.(*element)
elm := item.(*element[V])
// expired
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
@ -59,10 +59,10 @@ func (c *cache) GetWithExpire(key any) (payload any, expired time.Time) {
return elm.Payload, elm.Expired
}
func (c *cache) cleanup() {
func (c *cache[K, V]) cleanup() {
c.mapping.Range(func(k, v any) bool {
key := k.(string)
elm := v.(*element)
elm := v.(*element[V])
if time.Since(elm.Expired) > 0 {
c.mapping.Delete(key)
}
@ -70,12 +70,12 @@ func (c *cache) cleanup() {
})
}
type janitor struct {
type janitor[K comparable, V any] struct {
interval time.Duration
stop chan struct{}
}
func (j *janitor) process(c *cache) {
func (j *janitor[K, V]) process(c *cache[K, V]) {
ticker := time.NewTicker(j.interval)
for {
select {
@ -88,19 +88,19 @@ func (j *janitor) process(c *cache) {
}
}
func stopJanitor(c *Cache) {
func stopJanitor[K comparable, V any](c *Cache[K, V]) {
c.janitor.stop <- struct{}{}
}
// New return *Cache
func New(interval time.Duration) *Cache {
j := &janitor{
func New[K comparable, V any](interval time.Duration) *Cache[K, V] {
j := &janitor[K, V]{
interval: interval,
stop: make(chan struct{}),
}
c := &cache{janitor: j}
c := &cache[K, V]{janitor: j}
go j.process(c)
C := &Cache{c}
runtime.SetFinalizer(C, stopJanitor)
C := &Cache[K, V]{c}
runtime.SetFinalizer(C, stopJanitor[K, V])
return C
}

View File

@ -11,48 +11,50 @@ import (
func TestCache_Basic(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
c.Put("string", "a", ttl)
d := New[string, string](interval)
d.Put("string", "a", ttl)
i := c.Get("int")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.Equal(t, i, 1, "should recv 1")
s := c.Get("string")
assert.Equal(t, s.(string), "a", "should recv 'a'")
s := d.Get("string")
assert.Equal(t, s, "a", "should recv 'a'")
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
now := time.Now()
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
c.Put("int2", 2, ttl)
i := c.Get("int")
_, expired := c.GetWithExpire("int2")
assert.Equal(t, i.(int), 1, "should recv 1")
assert.Equal(t, i, 1, "should recv 1")
assert.True(t, now.Before(expired))
time.Sleep(ttl * 2)
i = c.Get("int")
j, _ := c.GetWithExpire("int2")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoCleanup(t *testing.T) {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
time.Sleep(ttl * 2)
i := c.Get("int")
j, _ := c.GetWithExpire("int")
assert.Nil(t, i, "should recv nil")
assert.Nil(t, j, "should recv nil")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoGC(t *testing.T) {
@ -60,7 +62,7 @@ func TestCache_AutoGC(t *testing.T) {
go func() {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New(interval)
c := New[string, int](interval)
c.Put("int", 1, ttl)
sign <- struct{}{}
}()

View File

@ -3,49 +3,50 @@ package cache
// Modified by https://github.com/die-net/lrucache
import (
"container/list"
"sync"
"time"
"github.com/Dreamacro/clash/common/generics/list"
)
// Option is part of Functional Options Pattern
type Option func(*LruCache)
type Option[K comparable, V any] func(*LruCache[K, V])
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key any, value any)
type EvictCallback[K comparable, V any] func(key K, value V)
// WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option {
return func(l *LruCache) {
func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
return func(l *LruCache[K, V]) {
l.onEvict = cb
}
}
// WithUpdateAgeOnGet update expires when Get element
func WithUpdateAgeOnGet() Option {
return func(l *LruCache) {
func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
return func(l *LruCache[K, V]) {
l.updateAgeOnGet = true
}
}
// WithAge defined element max age (second)
func WithAge(maxAge int64) Option {
return func(l *LruCache) {
func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
return func(l *LruCache[K, V]) {
l.maxAge = maxAge
}
}
// WithSize defined max length of LruCache
func WithSize(maxSize int) Option {
return func(l *LruCache) {
func WithSize[K comparable, V any](maxSize int) Option[K, V] {
return func(l *LruCache[K, V]) {
l.maxSize = maxSize
}
}
// WithStale decide whether Stale return is enabled.
// If this feature is enabled, element will not get Evicted according to `WithAge`.
func WithStale(stale bool) Option {
return func(l *LruCache) {
func WithStale[K comparable, V any](stale bool) Option[K, V] {
return func(l *LruCache[K, V]) {
l.staleReturn = stale
}
}
@ -53,22 +54,22 @@ func WithStale(stale bool) Option {
// LruCache is a thread-safe, in-memory lru-cache that evicts the
// least recently used entries from memory when (if set) the entries are
// older than maxAge (in seconds). Use the New constructor to create one.
type LruCache struct {
type LruCache[K comparable, V any] struct {
maxAge int64
maxSize int
mu sync.Mutex
cache map[any]*list.Element
lru *list.List // Front is least-recent
cache map[K]*list.Element[*entry[K, V]]
lru *list.List[*entry[K, V]] // Front is least-recent
updateAgeOnGet bool
staleReturn bool
onEvict EvictCallback
onEvict EvictCallback[K, V]
}
// NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache {
lc := &LruCache{
lru: list.New(),
cache: make(map[any]*list.Element),
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{
lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]),
}
for _, option := range options {
@ -80,12 +81,12 @@ func NewLRUCache(options ...Option) *LruCache {
// Get returns the any representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache) Get(key any) (any, bool) {
entry := c.get(key)
if entry == nil {
return nil, false
func (c *LruCache[K, V]) Get(key K) (V, bool) {
el := c.get(key)
if el == nil {
return getZero[V](), false
}
value := entry.value
value := el.value
return value, true
}
@ -94,17 +95,17 @@ func (c *LruCache) Get(key any) (any, bool) {
// a time.Time Give expected expires,
// and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) {
entry := c.get(key)
if entry == nil {
return nil, time.Time{}, false
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
el := c.get(key)
if el == nil {
return getZero[V](), time.Time{}, false
}
return entry.value, time.Unix(entry.expires, 0), true
return el.value, time.Unix(el.expires, 0), true
}
// Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key any) bool {
func (c *LruCache[K, V]) Exist(key K) bool {
c.mu.Lock()
defer c.mu.Unlock()
@ -113,7 +114,7 @@ func (c *LruCache) Exist(key any) bool {
}
// Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key any, value any) {
func (c *LruCache[K, V]) Set(key K, value V) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
@ -123,21 +124,21 @@ func (c *LruCache) Set(key any, value any) {
// SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second.
func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le)
e := le.Value.(*entry)
e := le.Value
e.value = value
e.expires = expires.Unix()
} else {
e := &entry{key: key, value: value, expires: expires.Unix()}
e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
c.cache[key] = c.lru.PushBack(e)
if c.maxSize > 0 {
if len := c.lru.Len(); len > c.maxSize {
if elLen := c.lru.Len(); elLen > c.maxSize {
c.deleteElement(c.lru.Front())
}
}
@ -147,23 +148,23 @@ func (c *LruCache) SetWithExpire(key any, value any, expires time.Time) {
}
// CloneTo clone and overwrite elements to another LruCache
func (c *LruCache) CloneTo(n *LruCache) {
func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
c.mu.Lock()
defer c.mu.Unlock()
n.mu.Lock()
defer n.mu.Unlock()
n.lru = list.New()
n.cache = make(map[any]*list.Element)
n.lru = list.New[*entry[K, V]]()
n.cache = make(map[K]*list.Element[*entry[K, V]])
for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry)
elm := e.Value
n.cache[elm.key] = n.lru.PushBack(elm)
}
}
func (c *LruCache) get(key any) *entry {
func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
@ -172,7 +173,7 @@ func (c *LruCache) get(key any) *entry {
return nil
}
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() {
if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() {
c.deleteElement(le)
c.maybeDeleteOldest()
@ -180,15 +181,15 @@ func (c *LruCache) get(key any) *entry {
}
c.lru.MoveToBack(le)
entry := le.Value.(*entry)
el := le.Value
if c.maxAge > 0 && c.updateAgeOnGet {
entry.expires = time.Now().Unix() + c.maxAge
el.expires = time.Now().Unix() + c.maxAge
}
return entry
return el
}
// Delete removes the value associated with a key.
func (c *LruCache) Delete(key any) {
func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock()
if le, ok := c.cache[key]; ok {
@ -198,35 +199,40 @@ func (c *LruCache) Delete(key any) {
c.mu.Unlock()
}
func (c *LruCache) maybeDeleteOldest() {
func (c *LruCache[K, V]) maybeDeleteOldest() {
if !c.staleReturn && c.maxAge > 0 {
now := time.Now().Unix()
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() {
for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() {
c.deleteElement(le)
}
}
}
func (c *LruCache) deleteElement(le *list.Element) {
func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
c.lru.Remove(le)
e := le.Value.(*entry)
e := le.Value
delete(c.cache, e.key)
if c.onEvict != nil {
c.onEvict(e.key, e.value)
}
}
func (c *LruCache) Clear() error {
func (c *LruCache[K, V]) Clear() error {
c.mu.Lock()
c.cache = make(map[any]*list.Element)
c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil
}
type entry struct {
key any
value any
type entry[K comparable, V any] struct {
key K
value V
expires int64
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -19,7 +19,7 @@ var entries = []struct {
}
func TestLRUCache(t *testing.T) {
c := NewLRUCache()
c := NewLRUCache[string, string]()
for _, e := range entries {
c.Set(e.key, e.value)
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
for _, e := range entries {
value, ok := c.Get(e.key)
if assert.True(t, ok) {
assert.Equal(t, e.value, value.(string))
assert.Equal(t, e.value, value)
}
}
@ -45,25 +45,25 @@ func TestLRUCache(t *testing.T) {
}
func TestLRUMaxAge(t *testing.T) {
c := NewLRUCache(WithAge(86400))
c := NewLRUCache[string, string](WithAge[string, string](86400))
now := time.Now().Unix()
expected := now + 86400
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = now
c.lru.Back().Value.expires = now
// Reset
c.Set("foo", "bar")
e := c.lru.Back().Value.(*entry)
e := c.lru.Back().Value
assert.True(t, e.expires >= now)
c.lru.Back().Value.(*entry).expires = now
c.lru.Back().Value.expires = now
// Set a few and verify expiration times
for _, s := range entries {
c.Set(s.key, s.value)
e := c.lru.Back().Value.(*entry)
e := c.lru.Back().Value
assert.True(t, e.expires >= expected && e.expires <= expected+10)
}
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
for _, s := range entries {
le, ok := c.cache[s.key]
if assert.True(t, ok) {
le.Value.(*entry).expires = now
le.Value.expires = now
}
}
@ -88,22 +88,22 @@ func TestLRUMaxAge(t *testing.T) {
}
func TestLRUpdateOnGet(t *testing.T) {
c := NewLRUCache(WithAge(86400), WithUpdateAgeOnGet())
c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
now := time.Now().Unix()
expires := now + 86400/2
// Add one expired entry
c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = expires
c.lru.Back().Value.expires = expires
_, ok := c.Get("foo")
assert.True(t, ok)
assert.True(t, c.lru.Back().Value.(*entry).expires > expires)
assert.True(t, c.lru.Back().Value.expires > expires)
}
func TestMaxSize(t *testing.T) {
c := NewLRUCache(WithSize(2))
c := NewLRUCache[string, string](WithSize[string, string](2))
// Add one expired entry
c.Set("foo", "bar")
_, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
}
func TestExist(t *testing.T) {
c := NewLRUCache(WithSize(1))
c := NewLRUCache[int, int](WithSize[int, int](1))
c.Set(1, 2)
assert.True(t, c.Exist(1))
c.Set(2, 3)
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) {
temp := 0
evict := func(key any, value any) {
temp = key.(int) + value.(int)
evict := func(key int, value int) {
temp = key + value
}
c := NewLRUCache(WithEvict(evict), WithSize(1))
c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
c.Set(1, 2)
c.Set(2, 3)
@ -138,21 +138,22 @@ func TestEvict(t *testing.T) {
}
func TestSetWithExpire(t *testing.T) {
c := NewLRUCache(WithAge(1))
c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
c.SetWithExpire(1, 2, tenSecBefore)
c.SetWithExpire(1, &struct{}{}, tenSecBefore)
// res is expected not to exist, and expires should be empty time.Time
res, expires, exist := c.GetWithExpire(1)
assert.Equal(t, nil, res)
assert.True(t, nil == res)
assert.Equal(t, time.Time{}, expires)
assert.Equal(t, false, exist)
}
func TestStale(t *testing.T) {
c := NewLRUCache(WithAge(1), WithStale(true))
c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0)
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
}
func TestCloneTo(t *testing.T) {
o := NewLRUCache(WithSize(10))
o := NewLRUCache[string, int](WithSize[string, int](10))
o.Set("1", 1)
o.Set("2", 2)
n := NewLRUCache(WithSize(2))
n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3)
n.Set("4", 4)

View File

@ -0,0 +1,235 @@
// Copyright 2009 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 list implements a doubly linked list.
//
// To iterate over a list (where l is a *List):
// for e := l.Front(); e != nil; e = e.Next() {
// // do something with e.Value
// }
//
package list
// Element is an element of a linked list.
type Element[T any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element[T]
// The list to which this element belongs.
list *List[T]
// The value stored with this element.
Value T
}
// Next returns the next list element or nil.
func (e *Element[T]) Next() *Element[T] {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element[T]) Prev() *Element[T] {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List[T any] struct {
root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List[T]) Init() *List[T] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New[T any]() *List[T] { return new(List[T]).Init() }
// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List[T]) Len() int { return l.len }
// Front returns the first element of list l or nil if the list is empty.
func (l *List[T]) Front() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.next
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List[T]) Back() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.prev
}
// lazyInit lazily initializes a zero List value.
func (l *List[T]) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
// insert inserts e after at, increments l.len, and returns e.
func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
return l.insert(&Element[T]{Value: v}, at)
}
// remove removes e from its list, decrements l.len
func (l *List[T]) remove(e *Element[T]) {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
}
// move moves e to next to at.
func (l *List[T]) move(e, at *Element[T]) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List[T]) Remove(e *Element[T]) T {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List[T]) PushFront(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, &l.root)
}
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List[T]) PushBack(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark.prev)
}
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToFront(e *Element[T]) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToBack(e *Element[T]) {
if e.list != l || l.root.prev == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, l.root.prev)
}
// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveBefore(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveAfter(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
// PushBackList inserts a copy of another list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushBackList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
l.insertValue(e.Value, l.root.prev)
}
}
// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushFrontList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
l.insertValue(e.Value, &l.root)
}
}

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

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

53
common/nnip/netip.go Normal file
View File

@ -0,0 +1,53 @@
package nnip
import (
"encoding/binary"
"net"
"net/netip"
)
// IpToAddr converts the net.IP to netip.Addr.
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
func IpToAddr(slice net.IP) netip.Addr {
ip := slice
if len(ip) != 4 {
if ip = slice.To4(); ip == nil {
ip = slice
}
}
if addr, ok := netip.AddrFromSlice(ip); ok {
return addr
}
return netip.Addr{}
}
// UnMasked returns p's last IP address.
// If p is invalid, UnMasked returns netip.Addr{}
func UnMasked(p netip.Prefix) netip.Addr {
if !p.IsValid() {
return netip.Addr{}
}
buf := p.Addr().As16()
hi := binary.BigEndian.Uint64(buf[:8])
lo := binary.BigEndian.Uint64(buf[8:])
bits := p.Bits()
if bits <= 32 {
bits += 96
}
hi = hi | ^uint64(0)>>bits
lo = lo | ^(^uint64(0) << (128 - bits))
binary.BigEndian.PutUint64(buf[:8], hi)
binary.BigEndian.PutUint64(buf[8:], lo)
addr := netip.AddrFrom16(buf)
if p.Addr().Is4() {
return addr.Unmap()
}
return addr
}

View File

@ -1,3 +1,3 @@
package observable
type Iterable <-chan any
type Iterable[T any] <-chan T

View File

@ -5,14 +5,14 @@ import (
"sync"
)
type Observable struct {
iterable Iterable
listener map[Subscription]*Subscriber
type Observable[T any] struct {
iterable Iterable[T]
listener map[Subscription[T]]*Subscriber[T]
mux sync.Mutex
done bool
}
func (o *Observable) process() {
func (o *Observable[T]) process() {
for item := range o.iterable {
o.mux.Lock()
for _, sub := range o.listener {
@ -23,7 +23,7 @@ func (o *Observable) process() {
o.close()
}
func (o *Observable) close() {
func (o *Observable[T]) close() {
o.mux.Lock()
defer o.mux.Unlock()
@ -33,18 +33,18 @@ func (o *Observable) close() {
}
}
func (o *Observable) Subscribe() (Subscription, error) {
func (o *Observable[T]) Subscribe() (Subscription[T], error) {
o.mux.Lock()
defer o.mux.Unlock()
if o.done {
return nil, errors.New("Observable is closed")
return nil, errors.New("observable is closed")
}
subscriber := newSubscriber()
subscriber := newSubscriber[T]()
o.listener[subscriber.Out()] = subscriber
return subscriber.Out(), nil
}
func (o *Observable) UnSubscribe(sub Subscription) {
func (o *Observable[T]) UnSubscribe(sub Subscription[T]) {
o.mux.Lock()
defer o.mux.Unlock()
subscriber, exist := o.listener[sub]
@ -55,10 +55,10 @@ func (o *Observable) UnSubscribe(sub Subscription) {
subscriber.Close()
}
func NewObservable(any Iterable) *Observable {
observable := &Observable{
iterable: any,
listener: map[Subscription]*Subscriber{},
func NewObservable[T any](iter Iterable[T]) *Observable[T] {
observable := &Observable[T]{
iterable: iter,
listener: map[Subscription[T]]*Subscriber[T]{},
}
go observable.process()
return observable

View File

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

View File

@ -4,30 +4,30 @@ import (
"sync"
)
type Subscription <-chan any
type Subscription[T any] <-chan T
type Subscriber struct {
buffer chan any
type Subscriber[T any] struct {
buffer chan T
once sync.Once
}
func (s *Subscriber) Emit(item any) {
func (s *Subscriber[T]) Emit(item T) {
s.buffer <- item
}
func (s *Subscriber) Out() Subscription {
func (s *Subscriber[T]) Out() Subscription[T] {
return s.buffer
}
func (s *Subscriber) Close() {
func (s *Subscriber[T]) Close() {
s.once.Do(func() {
close(s.buffer)
})
}
func newSubscriber() *Subscriber {
sub := &Subscriber{
buffer: make(chan any, 200),
func newSubscriber[T any]() *Subscriber[T] {
sub := &Subscriber[T]{
buffer: make(chan T, 200),
}
return sub
}

View File

@ -9,7 +9,7 @@ import (
// Picker provides synchronization, and Context cancelation
// for groups of goroutines working on subtasks of a common task.
// Inspired by errGroup
type Picker struct {
type Picker[T any] struct {
ctx context.Context
cancel func()
@ -17,12 +17,12 @@ type Picker struct {
once sync.Once
errOnce sync.Once
result any
result T
err error
}
func newPicker(ctx context.Context, cancel func()) *Picker {
return &Picker{
func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
return &Picker[T]{
ctx: ctx,
cancel: cancel,
}
@ -30,20 +30,20 @@ func newPicker(ctx context.Context, cancel func()) *Picker {
// WithContext returns a new Picker and an associated Context derived from ctx.
// and cancel when first element return.
func WithContext(ctx context.Context) (*Picker, context.Context) {
func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) {
ctx, cancel := context.WithCancel(ctx)
return newPicker(ctx, cancel), ctx
return newPicker[T](ctx, cancel), ctx
}
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) {
func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) {
ctx, cancel := context.WithTimeout(ctx, timeout)
return newPicker(ctx, cancel), ctx
return newPicker[T](ctx, cancel), ctx
}
// Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them.
func (p *Picker) Wait() any {
func (p *Picker[T]) Wait() T {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
@ -52,13 +52,13 @@ func (p *Picker) Wait() any {
}
// Error return the first error (if all success return nil)
func (p *Picker) Error() error {
func (p *Picker[T]) Error() error {
return p.err
}
// Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (any, error)) {
func (p *Picker[T]) Go(f func() (T, error)) {
p.wg.Add(1)
go func() {

View File

@ -8,33 +8,38 @@ import (
"github.com/stretchr/testify/assert"
)
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (any, error) {
func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) {
return func() (T, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select {
case <-timer.C:
return input, nil
case <-ctx.Done():
return nil, ctx.Err()
return getZero[T](), ctx.Err()
}
}
}
func TestPicker_Basic(t *testing.T) {
picker, ctx := WithContext(context.Background())
picker, ctx := WithContext[int](context.Background())
picker.Go(sleepAndSend(ctx, 30, 2))
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.NotNil(t, number)
assert.Equal(t, number.(int), 1)
assert.Equal(t, number, 1)
}
func TestPicker_Timeout(t *testing.T) {
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5)
picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait()
assert.Nil(t, number)
assert.Equal(t, number, getZero[int]())
assert.NotNil(t, picker.Error())
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -5,13 +5,13 @@ import (
)
// Queue is a simple concurrent safe queue
type Queue struct {
items []any
type Queue[T any] struct {
items []T
lock sync.RWMutex
}
// Put add the item to the queue.
func (q *Queue) Put(items ...any) {
func (q *Queue[T]) Put(items ...T) {
if len(items) == 0 {
return
}
@ -22,9 +22,9 @@ func (q *Queue) Put(items ...any) {
}
// Pop returns the head of items.
func (q *Queue) Pop() any {
func (q *Queue[T]) Pop() T {
if len(q.items) == 0 {
return nil
return GetZero[T]()
}
q.lock.Lock()
@ -35,9 +35,9 @@ func (q *Queue) Pop() any {
}
// Last returns the last of item.
func (q *Queue) Last() any {
func (q *Queue[T]) Last() T {
if len(q.items) == 0 {
return nil
return GetZero[T]()
}
q.lock.RLock()
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
}
// Copy get the copy of queue.
func (q *Queue) Copy() []any {
items := []any{}
func (q *Queue[T]) Copy() []T {
items := []T{}
q.lock.RLock()
items = append(items, q.items...)
q.lock.RUnlock()
@ -56,7 +56,7 @@ func (q *Queue) Copy() []any {
}
// Len returns the number of items in this queue.
func (q *Queue) Len() int64 {
func (q *Queue[T]) Len() int64 {
q.lock.Lock()
defer q.lock.Unlock()
@ -64,8 +64,13 @@ func (q *Queue) Len() int64 {
}
// New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue {
return &Queue{
items: make([]any, 0, hint),
func New[T any](hint int64) *Queue[T] {
return &Queue[T]{
items: make([]T, 0, hint),
}
}
func GetZero[T any]() T {
var result T
return result
}

View File

@ -5,28 +5,28 @@ import (
"time"
)
type call struct {
type call[T any] struct {
wg sync.WaitGroup
val any
val T
err error
}
type Single struct {
type Single[T any] struct {
mux sync.Mutex
last time.Time
wait time.Duration
call *call
result *Result
call *call[T]
result *Result[T]
}
type Result struct {
Val any
type Result[T any] struct {
Val T
Err error
}
// Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight
func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
s.mux.Lock()
now := time.Now()
if now.Before(s.last.Add(s.wait)) {
@ -34,31 +34,31 @@ func (s *Single) Do(fn func() (any, error)) (v any, err error, shared bool) {
return s.result.Val, s.result.Err, true
}
if call := s.call; call != nil {
if callM := s.call; callM != nil {
s.mux.Unlock()
call.wg.Wait()
return call.val, call.err, true
callM.wg.Wait()
return callM.val, callM.err, true
}
call := &call{}
call.wg.Add(1)
s.call = call
callM := &call[T]{}
callM.wg.Add(1)
s.call = callM
s.mux.Unlock()
call.val, call.err = fn()
call.wg.Done()
callM.val, callM.err = fn()
callM.wg.Done()
s.mux.Lock()
s.call = nil
s.result = &Result{call.val, call.err}
s.result = &Result[T]{callM.val, callM.err}
s.last = now
s.mux.Unlock()
return call.val, call.err, false
return callM.val, callM.err, false
}
func (s *Single) Reset() {
func (s *Single[T]) Reset() {
s.last = time.Time{}
}
func NewSingle(wait time.Duration) *Single {
return &Single{wait: wait}
func NewSingle[T any](wait time.Duration) *Single[T] {
return &Single[T]{wait: wait}
}

View File

@ -10,13 +10,13 @@ import (
)
func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
shardCount := atomic.NewInt32(0)
call := func() (any, error) {
call := func() (int, error) {
foo++
time.Sleep(time.Millisecond * 5)
return nil, nil
return 0, nil
}
var wg sync.WaitGroup
@ -38,32 +38,32 @@ func TestBasic(t *testing.T) {
}
func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
call := func() (any, error) {
callM := func() (int, error) {
foo++
return nil, nil
return 0, nil
}
single.Do(call)
_, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond)
_, _, shard := single.Do(call)
_, _, shard := single.Do(callM)
assert.Equal(t, 1, foo)
assert.True(t, shard)
}
func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30)
single := NewSingle[int](time.Millisecond * 30)
foo := 0
call := func() (any, error) {
callM := func() (int, error) {
foo++
return nil, nil
return 0, nil
}
single.Do(call)
_, _, _ = single.Do(callM)
single.Reset()
single.Do(call)
_, _, _ = single.Do(callM)
assert.Equal(t, 2, foo)
}

View File

@ -31,7 +31,7 @@ func NewDecoder(option Option) *Decoder {
// Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct")
return fmt.Errorf("decode must recive a ptr struct")
}
t := reflect.TypeOf(dst).Elem()
v := reflect.ValueOf(dst).Elem()
@ -159,9 +159,19 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
}
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil {
return err
}
@ -291,7 +301,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField
val reflect.Value
}
fields := []field{}
var fields []field
for len(structs) > 0 {
structVal := structs[0]
structs = structs[1:]

44
common/utils/range.go Normal file
View File

@ -0,0 +1,44 @@
package utils
import (
"golang.org/x/exp/constraints"
)
type Range[T constraints.Ordered] struct {
start T
end T
}
func NewRange[T constraints.Ordered](start, end T) *Range[T] {
if start > end {
return &Range[T]{
start: end,
end: start,
}
}
return &Range[T]{
start: start,
end: end,
}
}
func (r *Range[T]) Contains(t T) bool {
return t >= r.start && t <= r.end
}
func (r *Range[T]) LeftContains(t T) bool {
return t >= r.start && t < r.end
}
func (r *Range[T]) RightContains(t T) bool {
return t > r.start && t <= r.end
}
func (r *Range[T]) Start() T {
return r.start
}
func (r *Range[T]) End() T {
return r.end
}

16
common/utils/uuid.go Normal file
View File

@ -0,0 +1,16 @@
package utils
import (
"github.com/gofrs/uuid"
)
var uuidNamespace, _ = uuid.FromString("00000000-0000-0000-0000-000000000000")
// UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090
func UUIDMap(str string) (uuid.UUID, error) {
u, err := uuid.FromString(str)
if err != nil {
return uuid.NewV5(uuidNamespace, str), nil
}
return u, nil
}

74
common/utils/uuid_test.go Normal file
View File

@ -0,0 +1,74 @@
package utils
import (
"github.com/gofrs/uuid"
"reflect"
"testing"
)
func TestUUIDMap(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want uuid.UUID
wantErr bool
}{
{
name: "uuid-test-1",
args: args{
str: "82410302-039e-41b6-98b0-d964084b4170",
},
want: uuid.FromStringOrNil("82410302-039e-41b6-98b0-d964084b4170"),
wantErr: false,
},
{
name: "uuid-test-2",
args: args{
str: "88c502e6-d7eb-4c8e-8259-94cb13d83c77",
},
want: uuid.FromStringOrNil("88c502e6-d7eb-4c8e-8259-94cb13d83c77"),
wantErr: false,
},
{
name: "uuid-map-1",
args: args{
str: "123456",
},
want: uuid.FromStringOrNil("f8598425-92f2-5508-a071-4fc67f9040ac"),
wantErr: false,
},
// GENERATED BY 'xray uuid -i'
{
name: "uuid-map-2",
args: args{
str: "a9dk23bz0",
},
want: uuid.FromStringOrNil("c91481b6-fc0f-5d9e-b166-5ddf07b9c3c5"),
wantErr: false,
},
{
name: "uuid-map-2",
args: args{
str: "中文123",
},
want: uuid.FromStringOrNil("145c544c-2229-59e5-8dbb-3f33b7610d26"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := UUIDMap(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("UUIDMap() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UUIDMap() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -4,7 +4,9 @@ import (
"context"
"errors"
"net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4"
@ -15,14 +17,16 @@ var (
ErrNotFound = errors.New("DNS option not found")
)
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) {
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr, error) {
conn, err := ListenDHCPClient(context, ifaceName)
if err != nil {
return nil, err
}
defer conn.Close()
defer func() {
_ = conn.Close()
}()
result := make(chan []net.IP, 1)
result := make(chan []netip.Addr, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
@ -52,7 +56,7 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, er
}
}
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) {
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []netip.Addr) {
defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize)
@ -77,11 +81,17 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
}
dns := pkt.DNS()
if len(dns) == 0 {
l := len(dns)
if l == 0 {
return
}
result <- dns
dnsAddr := make([]netip.Addr, l)
for i := 0; i < l; i++ {
dnsAddr[i] = nnip.IpToAddr(dns[i])
}
result <- dnsAddr
return
}

View File

@ -2,6 +2,7 @@ package dialer
import (
"net"
"net/netip"
"syscall"
"github.com/Dreamacro/clash/component/iface"
@ -19,12 +20,9 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
var innerErr error
@ -45,7 +43,7 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err

View File

@ -2,6 +2,7 @@ package dialer
import (
"net"
"net/netip"
"syscall"
"golang.org/x/sys/unix"
@ -17,12 +18,9 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
var innerErr error
@ -38,7 +36,7 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
dialer.Control = bindControl(ifaceName, dialer.Control)
return nil

View File

@ -4,27 +4,28 @@ package dialer
import (
"net"
"net/netip"
"strconv"
"strings"
"github.com/Dreamacro/clash/component/iface"
)
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) {
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 *net.IPNet
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 != nil {
if destination.To4() != nil {
if destination.IsValid() {
if destination.Is4() {
addr, err = ifaceObj.PickIPv4Addr(destination)
} else {
addr, err = ifaceObj.PickIPv6Addr(destination)
@ -39,12 +40,12 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{
IP: addr.IP,
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
} else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{
IP: addr.IP,
IP: addr.Addr().AsSlice(),
Port: port,
}, nil
}
@ -52,7 +53,7 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
return nil, iface.ErrAddrNotFound
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error {
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() {
return nil
}
@ -83,7 +84,7 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local))
addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
if err != nil {
return "", err
}

View File

@ -3,11 +3,21 @@ package dialer
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/component/resolver"
)
var (
dialMux sync.Mutex
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
DisableIPv6 = false
)
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := &option{
interfaceName: DefaultInterface.Load(),
@ -24,33 +34,9 @@ func DialContext(ctx context.Context, network, address string, options ...Option
switch network {
case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip net.IP
switch network {
case "tcp4", "udp4":
if !opt.direct {
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv4(host)
}
default:
if !opt.direct {
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv6(host)
}
}
if err != nil {
return nil, err
}
return dialContext(ctx, network, ip, port, opt)
return actualSingleDialContext(ctx, network, address, opt)
case "tcp", "udp":
return dualStackDialContext(ctx, network, address, opt)
return actualDualStackDialContext(ctx, network, address, opt)
default:
return nil, errors.New("network invalid")
}
@ -88,7 +74,20 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
return lc.ListenPacket(ctx, network, address)
}
func dialContext(ctx context.Context, network string, destination net.IP, port string, opt *option) (net.Conn, error) {
func SetDial(concurrent bool) {
dialMux.Lock()
if concurrent {
actualSingleDialContext = concurrentSingleDialContext
actualDualStackDialContext = concurrentDualStackDialContext
} else {
actualSingleDialContext = singleDialContext
actualDualStackDialContext = concurrentDualStackDialContext
}
dialMux.Unlock()
}
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
dialer := &net.Dialer{}
if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
@ -99,6 +98,10 @@ func dialContext(ctx context.Context, network string, destination net.IP, port s
bindMarkToDialer(opt.routingMark, dialer, network, destination)
}
if DisableIPv6 && destination.Is6() {
return nil, fmt.Errorf("IPv6 is diabled, dialer cancel")
}
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
}
@ -128,12 +131,12 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
case results <- result:
case <-returned:
if result.Conn != nil {
result.Conn.Close()
_ = result.Conn.Close()
}
}
}()
var ip net.IP
var ip netip.Addr
if ipv6 {
if !direct {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)
@ -182,3 +185,130 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
return nil, errors.New("never touched")
}
func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
if opt.direct {
ips, err = resolver.ResolveAllIP(host)
} else {
ips, err = resolver.ResolveAllIPProxyServerHost(host)
}
return concurrentDialContext(ctx, network, ips, port, opt)
}
func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
returned := make(chan struct{})
defer close(returned)
type dialResult struct {
ip netip.Addr
net.Conn
error
resolved bool
}
results := make(chan dialResult)
tcpRacer := func(ctx context.Context, ip netip.Addr) {
result := dialResult{ip: ip}
defer func() {
select {
case results <- result:
case <-returned:
if result.Conn != nil {
result.Conn.Close()
}
}
}()
v := "4"
if ip.Is6() {
v = "6"
}
result.Conn, result.error = dialContext(ctx, network+v, ip, port, opt)
}
for _, ip := range ips {
go tcpRacer(ctx, ip)
}
connCount := len(ips)
for res := range results {
connCount--
if res.error == nil {
return res.Conn, nil
}
if connCount == 0 {
break
}
}
return nil, fmt.Errorf("all ips %v tcp shake hands failed", ips)
}
func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip netip.Addr
switch network {
case "tcp4", "udp4":
if !opt.direct {
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv4(host)
}
default:
if !opt.direct {
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv6(host)
}
}
if err != nil {
return nil, err
}
return dialContext(ctx, network, ip, port, opt)
}
func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
switch network {
case "tcp4", "udp4":
if !opt.direct {
ips, err = resolver.ResolveAllIPv4ProxyServerHost(host)
} else {
ips, err = resolver.ResolveAllIPv4(host)
}
default:
if !opt.direct {
ips, err = resolver.ResolveAllIPv6ProxyServerHost(host)
} else {
ips, err = resolver.ResolveAllIPv6(host)
}
}
if err != nil {
return nil, err
}
return concurrentDialContext(ctx, network, ips, port, opt)
}

View File

@ -4,14 +4,15 @@ package dialer
import (
"net"
"net/netip"
"syscall"
)
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
dialer.Control = bindMarkToControl(mark, dialer.Control)
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
lc.Control = bindMarkToControl(mark, lc.Control)
}
@ -23,20 +24,17 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return
}
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
}
})
}

View File

@ -4,6 +4,7 @@ package dialer
import (
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/log"
@ -17,10 +18,10 @@ func printMarkWarn() {
})
}
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) {
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
printMarkWarn()
}
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) {
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
printMarkWarn()
}

View File

@ -3,6 +3,7 @@ package dialer
import (
"context"
"net"
"net/netip"
)
func init() {
@ -18,9 +19,9 @@ func resolverDialContext(ctx context.Context, network, address string) (net.Conn
interfaceName := DefaultInterface.Load()
if interfaceName != "" {
dstIP := net.ParseIP(address)
if dstIP != nil {
bindIfaceToDialer(interfaceName, d, network, dstIP)
dstIP, err := netip.ParseAddr(address)
if err == nil {
_ = bindIfaceToDialer(interfaceName, d, network, dstIP)
}
}

View File

@ -1,7 +1,7 @@
package fakeip
import (
"net"
"net/netip"
"github.com/Dreamacro/clash/component/profile/cachefile"
)
@ -11,22 +11,27 @@ type cachefileStore struct {
}
// GetByHost implements store.GetByHost
func (c *cachefileStore) GetByHost(host string) (net.IP, bool) {
func (c *cachefileStore) GetByHost(host string) (netip.Addr, bool) {
elm := c.cache.GetFakeip([]byte(host))
if elm == nil {
return nil, false
return netip.Addr{}, false
}
if len(elm) == 4 {
return netip.AddrFrom4(*(*[4]byte)(elm)), true
} else {
return netip.AddrFrom16(*(*[16]byte)(elm)), true
}
return net.IP(elm), true
}
// PutByHost implements store.PutByHost
func (c *cachefileStore) PutByHost(host string, ip net.IP) {
c.cache.PutFakeip([]byte(host), ip)
func (c *cachefileStore) PutByHost(host string, ip netip.Addr) {
c.cache.PutFakeip([]byte(host), ip.AsSlice())
}
// GetByIP implements store.GetByIP
func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
elm := c.cache.GetFakeip(ip.To4())
func (c *cachefileStore) GetByIP(ip netip.Addr) (string, bool) {
elm := c.cache.GetFakeip(ip.AsSlice())
if elm == nil {
return "", false
}
@ -34,18 +39,18 @@ func (c *cachefileStore) GetByIP(ip net.IP) (string, bool) {
}
// PutByIP implements store.PutByIP
func (c *cachefileStore) PutByIP(ip net.IP, host string) {
c.cache.PutFakeip(ip.To4(), []byte(host))
func (c *cachefileStore) PutByIP(ip netip.Addr, host string) {
c.cache.PutFakeip(ip.AsSlice(), []byte(host))
}
// DelByIP implements store.DelByIP
func (c *cachefileStore) DelByIP(ip net.IP) {
ip = ip.To4()
c.cache.DelFakeipPair(ip, c.cache.GetFakeip(ip.To4()))
func (c *cachefileStore) DelByIP(ip netip.Addr) {
addr := ip.AsSlice()
c.cache.DelFakeipPair(addr, c.cache.GetFakeip(addr))
}
// Exist implements store.Exist
func (c *cachefileStore) Exist(ip net.IP) bool {
func (c *cachefileStore) Exist(ip netip.Addr) bool {
_, exist := c.GetByIP(ip)
return exist
}

View File

@ -1,40 +1,37 @@
package fakeip
import (
"net"
"net/netip"
"github.com/Dreamacro/clash/common/cache"
)
type memoryStore struct {
cache *cache.LruCache
cacheIP *cache.LruCache[string, netip.Addr]
cacheHost *cache.LruCache[netip.Addr, string]
}
// GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) {
if elm, exist := m.cache.Get(host); exist {
ip := elm.(net.IP)
func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) {
if ip, exist := m.cacheIP.Get(host); exist {
// ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4()))
m.cacheHost.Get(ip)
return ip, true
}
return nil, false
return netip.Addr{}, false
}
// PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) {
m.cache.Set(host, ip)
func (m *memoryStore) PutByHost(host string, ip netip.Addr) {
m.cacheIP.Set(host, ip)
}
// GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist {
host := elm.(string)
func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) {
if host, exist := m.cacheHost.Get(ip); exist {
// ensure host --> ip on head of linked list
m.cache.Get(host)
m.cacheIP.Get(host)
return host, true
}
@ -42,33 +39,41 @@ func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
}
// PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) {
m.cache.Set(ipToUint(ip.To4()), host)
func (m *memoryStore) PutByIP(ip netip.Addr, host string) {
m.cacheHost.Set(ip, host)
}
// DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) {
ipNum := ipToUint(ip.To4())
if elm, exist := m.cache.Get(ipNum); exist {
m.cache.Delete(elm.(string))
func (m *memoryStore) DelByIP(ip netip.Addr) {
if host, exist := m.cacheHost.Get(ip); exist {
m.cacheIP.Delete(host)
}
m.cache.Delete(ipNum)
m.cacheHost.Delete(ip)
}
// Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool {
return m.cache.Exist(ipToUint(ip.To4()))
func (m *memoryStore) Exist(ip netip.Addr) bool {
return m.cacheHost.Exist(ip)
}
// CloneTo implements store.CloneTo
// only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok {
m.cache.CloneTo(ms.cache)
m.cacheIP.CloneTo(ms.cacheIP)
m.cacheHost.CloneTo(ms.cacheHost)
}
}
// FlushFakeIP implements store.FlushFakeIP
func (m *memoryStore) FlushFakeIP() error {
return m.cache.Clear()
_ = m.cacheIP.Clear()
return m.cacheHost.Clear()
}
func newMemoryStore(size int) *memoryStore {
return &memoryStore{
cacheIP: cache.NewLRUCache[string, netip.Addr](cache.WithSize[string, netip.Addr](size)),
cacheHost: cache.NewLRUCache[netip.Addr, string](cache.WithSize[netip.Addr, string](size)),
}
}

View File

@ -2,40 +2,45 @@ package fakeip
import (
"errors"
"net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/profile/cachefile"
"github.com/Dreamacro/clash/component/trie"
)
const (
offsetKey = "key-offset-fake-ip"
cycleKey = "key-cycle-fake-ip"
)
type store interface {
GetByHost(host string) (net.IP, bool)
PutByHost(host string, ip net.IP)
GetByIP(ip net.IP) (string, bool)
PutByIP(ip net.IP, host string)
DelByIP(ip net.IP)
Exist(ip net.IP) bool
GetByHost(host string) (netip.Addr, bool)
PutByHost(host string, ip netip.Addr)
GetByIP(ip netip.Addr) (string, bool)
PutByIP(ip netip.Addr, host string)
DelByIP(ip netip.Addr)
Exist(ip netip.Addr) bool
CloneTo(store)
FlushFakeIP() error
}
// Pool is a implementation about fake ip generator without storage
type Pool struct {
max uint32
min uint32
gateway uint32
broadcast uint32
offset uint32
mux sync.Mutex
host *trie.DomainTrie
ipnet *net.IPNet
store store
gateway netip.Addr
first netip.Addr
last netip.Addr
offset netip.Addr
cycle bool
mux sync.Mutex
host *trie.DomainTrie[bool]
ipnet *netip.Prefix
store store
}
// Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP {
func (p *Pool) Lookup(host string) netip.Addr {
p.mux.Lock()
defer p.mux.Unlock()
if ip, exist := p.store.GetByHost(host); exist {
@ -48,14 +53,10 @@ func (p *Pool) Lookup(host string) net.IP {
}
// LookBack return host with the fake ip
func (p *Pool) LookBack(ip net.IP) (string, bool) {
func (p *Pool) LookBack(ip netip.Addr) (string, bool) {
p.mux.Lock()
defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
return p.store.GetByIP(ip)
}
@ -68,29 +69,25 @@ func (p *Pool) ShouldSkipped(domain string) bool {
}
// Exist returns if given ip exists in fake-ip pool
func (p *Pool) Exist(ip net.IP) bool {
func (p *Pool) Exist(ip netip.Addr) bool {
p.mux.Lock()
defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return false
}
return p.store.Exist(ip)
}
// Gateway return gateway ip
func (p *Pool) Gateway() net.IP {
return uintToIP(p.gateway)
func (p *Pool) Gateway() netip.Addr {
return p.gateway
}
// Broadcast return broadcast ip
func (p *Pool) Broadcast() net.IP {
return uintToIP(p.broadcast)
// Broadcast return the last ip
func (p *Pool) Broadcast() netip.Addr {
return p.last
}
// IPNet return raw ipnet
func (p *Pool) IPNet() *net.IPNet {
func (p *Pool) IPNet() *netip.Prefix {
return p.ipnet
}
@ -99,47 +96,61 @@ func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store)
}
func (p *Pool) get(host string) net.IP {
current := p.offset
for {
p.offset = (p.offset + 1) % (p.max - p.min)
// Avoid infinite loops
if p.offset == current {
p.offset = (p.offset + 1) % (p.max - p.min)
ip := uintToIP(p.min + p.offset - 1)
p.store.DelByIP(ip)
break
}
func (p *Pool) get(host string) netip.Addr {
p.offset = p.offset.Next()
ip := uintToIP(p.min + p.offset - 1)
if !p.store.Exist(ip) {
break
}
if !p.offset.Less(p.last) {
p.cycle = true
p.offset = p.first
}
ip := uintToIP(p.min + p.offset - 1)
p.store.PutByIP(ip, host)
return ip
if p.cycle || p.store.Exist(p.offset) {
p.store.DelByIP(p.offset)
}
p.store.PutByIP(p.offset, host)
return p.offset
}
func (p *Pool) FlushFakeIP() error {
return p.store.FlushFakeIP()
err := p.store.FlushFakeIP()
if err == nil {
p.cycle = false
p.offset = p.first.Prev()
}
return err
}
func ipToUint(ip net.IP) uint32 {
v := uint32(ip[0]) << 24
v += uint32(ip[1]) << 16
v += uint32(ip[2]) << 8
v += uint32(ip[3])
return v
func (p *Pool) StoreState() {
if s, ok := p.store.(*cachefileStore); ok {
s.PutByHost(offsetKey, p.offset)
if p.cycle {
s.PutByHost(cycleKey, p.offset)
}
}
}
func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
func (p *Pool) restoreState() {
if s, ok := p.store.(*cachefileStore); ok {
if _, exist := s.GetByHost(cycleKey); exist {
p.cycle = true
}
if offset, exist := s.GetByHost(offsetKey); exist {
if p.ipnet.Contains(offset) {
p.offset = offset
} else {
_ = p.FlushFakeIP()
}
} else if s.Exist(p.first) {
_ = p.FlushFakeIP()
}
}
}
type Options struct {
IPNet *net.IPNet
Host *trie.DomainTrie
IPNet *netip.Prefix
Host *trie.DomainTrie[bool]
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
@ -152,33 +163,35 @@ type Options struct {
// New return Pool instance
func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 3
var (
hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next()
first = gateway.Next().Next()
last = nnip.UnMasked(*options.IPNet)
)
ones, bits := options.IPNet.Mask.Size()
total := 1<<uint(bits-ones) - 4
if total <= 0 {
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {
return nil, errors.New("ipnet don't have valid ip")
}
max := min + uint32(total) - 1
pool := &Pool{
min: min,
max: max,
gateway: min - 2,
broadcast: max + 1,
host: options.Host,
ipnet: options.IPNet,
gateway: gateway,
first: first,
last: last,
offset: first.Prev(),
cycle: false,
host: options.Host,
ipnet: options.IPNet,
}
if options.Persistence {
pool.store = &cachefileStore{
cache: cachefile.Cache(),
}
} else {
pool.store = &memoryStore{
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
pool.store = newMemoryStore(options.Size)
}
pool.restoreState()
return pool, nil
}

View File

@ -2,7 +2,7 @@ package fakeip
import (
"fmt"
"net"
"net/netip"
"os"
"testing"
"time"
@ -49,9 +49,9 @@ func createCachefileStore(options Options) (*Pool, string, error) {
}
func TestPool_Basic(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.0/28")
ipnet := netip.MustParsePrefix("192.168.0.0/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -62,24 +62,52 @@ func TestPool_Basic(t *testing.T) {
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first.Equal(net.IP{192, 168, 0, 3}))
assert.Equal(t, pool.Lookup("foo.com"), net.IP{192, 168, 0, 3})
assert.True(t, last.Equal(net.IP{192, 168, 0, 4}))
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, pool.Lookup("foo.com") == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.Equal(t, pool.Gateway(), net.IP{192, 168, 0, 1})
assert.Equal(t, pool.Broadcast(), net.IP{192, 168, 0, 15})
assert.True(t, pool.Gateway() == netip.AddrFrom4([4]byte{192, 168, 0, 1}))
assert.True(t, pool.Broadcast() == netip.AddrFrom4([4]byte{192, 168, 0, 15}))
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(net.IP{192, 168, 0, 4}))
assert.False(t, pool.Exist(net.IP{192, 168, 0, 5}))
assert.False(t, pool.Exist(net.ParseIP("::1")))
assert.True(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 4})))
assert.False(t, pool.Exist(netip.AddrFrom4([4]byte{192, 168, 0, 5})))
assert.False(t, pool.Exist(netip.MustParseAddr("::1")))
}
}
func TestPool_BasicV6(t *testing.T) {
ipnet := netip.MustParsePrefix("2001:4860:4860::8888/118")
pools, tempfile, err := createPools(Options{
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
bar, exist := pool.LookBack(last)
assert.True(t, first == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, pool.Lookup("foo.com") == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8803"))
assert.True(t, last == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804"))
assert.True(t, exist)
assert.Equal(t, bar, "bar.com")
assert.True(t, pool.Gateway() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8801"))
assert.True(t, pool.Broadcast() == netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8bff"))
assert.Equal(t, pool.IPNet().String(), ipnet.String())
assert.True(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8804")))
assert.False(t, pool.Exist(netip.MustParseAddr("2001:4860:4860:0000:0000:0000:0000:8805")))
assert.False(t, pool.Exist(netip.MustParseAddr("127.0.0.1")))
}
}
func TestPool_CycleUsed(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.16/28")
ipnet := netip.MustParsePrefix("192.168.0.16/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -88,22 +116,22 @@ func TestPool_CycleUsed(t *testing.T) {
for _, pool := range pools {
foo := pool.Lookup("foo.com")
bar := pool.Lookup("bar.com")
for i := 0; i < 9; i++ {
for i := 0; i < 10; i++ {
pool.Lookup(fmt.Sprintf("%d.com", i))
}
baz := pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.True(t, foo.Equal(baz))
assert.True(t, next.Equal(bar))
assert.True(t, foo == baz)
assert.True(t, next == bar)
}
}
func TestPool_Skip(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/29")
tree := trie.New()
tree.Insert("example.com", tree)
ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[bool]()
tree.Insert("example.com", true)
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
Host: tree,
})
@ -117,9 +145,9 @@ func TestPool_Skip(t *testing.T) {
}
func TestPool_MaxCacheSize(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
@ -128,13 +156,13 @@ func TestPool_MaxCacheSize(t *testing.T) {
pool.Lookup("baz.com")
next := pool.Lookup("foo.com")
assert.False(t, first.Equal(next))
assert.False(t, first == next)
}
func TestPool_DoubleMapping(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
@ -158,23 +186,23 @@ func TestPool_DoubleMapping(t *testing.T) {
assert.False(t, bazExist)
assert.True(t, barExist)
assert.False(t, bazIP.Equal(newBazIP))
assert.False(t, bazIP == newBazIP)
}
func TestPool_Clone(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/24")
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
first := pool.Lookup("foo.com")
last := pool.Lookup("bar.com")
assert.True(t, first.Equal(net.IP{192, 168, 0, 3}))
assert.True(t, last.Equal(net.IP{192, 168, 0, 4}))
assert.True(t, first == netip.AddrFrom4([4]byte{192, 168, 0, 3}))
assert.True(t, last == netip.AddrFrom4([4]byte{192, 168, 0, 4}))
newPool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 2,
})
newPool.CloneFrom(pool)
@ -185,9 +213,9 @@ func TestPool_Clone(t *testing.T) {
}
func TestPool_Error(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/31")
ipnet := netip.MustParsePrefix("192.168.0.1/31")
_, err := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
@ -195,9 +223,9 @@ func TestPool_Error(t *testing.T) {
}
func TestPool_FlushFileCache(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/28")
ipnet := netip.MustParsePrefix("192.168.0.1/28")
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
assert.Nil(t, err)
@ -212,22 +240,24 @@ func TestPool_FlushFileCache(t *testing.T) {
err = pool.FlushFakeIP()
assert.Nil(t, err)
baz := pool.Lookup("foo.com")
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz)
assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next)
assert.True(t, baz == nero)
}
}
func TestPool_FlushMemoryCache(t *testing.T) {
_, ipnet, _ := net.ParseCIDR("192.168.0.1/28")
ipnet := netip.MustParsePrefix("192.168.0.1/28")
pool, _ := New(Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 10,
})
@ -239,13 +269,15 @@ func TestPool_FlushMemoryCache(t *testing.T) {
err := pool.FlushFakeIP()
assert.Nil(t, err)
baz := pool.Lookup("foo.com")
next := pool.Lookup("baz.com")
baz := pool.Lookup("foo.com")
nero := pool.Lookup("foo.com")
assert.Equal(t, foo, fox)
assert.NotEqual(t, foo, baz)
assert.Equal(t, bar, bax)
assert.NotEqual(t, bar, next)
assert.Equal(t, baz, nero)
assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz)
assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next)
assert.True(t, baz == nero)
}

View File

@ -1,7 +1,7 @@
package strmatcher
import (
"container/list"
"github.com/Dreamacro/clash/common/generics/list"
)
const validCharCount = 53
@ -190,7 +190,7 @@ func (ac *ACAutomaton) Add(domain string, t Type) {
}
func (ac *ACAutomaton) Build() {
queue := list.New()
queue := list.New[Edge]()
for i := 0; i < validCharCount; i++ {
if ac.trie[0][i].nextNode != 0 {
queue.PushBack(ac.trie[0][i])
@ -201,7 +201,7 @@ func (ac *ACAutomaton) Build() {
if front == nil {
break
} else {
node := front.Value.(Edge).nextNode
node := front.Value.nextNode
queue.Remove(front)
for i := 0; i < validCharCount; i++ {
if ac.trie[node][i].nextNode != 0 {

View File

@ -15,6 +15,9 @@ func LoaderName() string {
}
func SetLoader(newLoader string) {
if newLoader == "memc" {
newLoader = "memconservative"
}
geoLoaderName = newLoader
}

View File

@ -3,6 +3,7 @@ package iface
import (
"errors"
"net"
"net/netip"
"time"
"github.com/Dreamacro/clash/common/singledo"
@ -11,7 +12,7 @@ import (
type Interface struct {
Index int
Name string
Addrs []*net.IPNet
Addrs []*netip.Prefix
HardwareAddr net.HardwareAddr
}
@ -20,10 +21,10 @@ var (
ErrAddrNotFound = errors.New("addr not found")
)
var interfaces = singledo.NewSingle(time.Second * 20)
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (any, error) {
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
@ -37,14 +38,18 @@ func ResolveInterface(name string) (*Interface, error) {
continue
}
ipNets := make([]*net.IPNet, 0, len(addrs))
ipNets := make([]*netip.Prefix, 0, len(addrs))
for _, addr := range addrs {
ipNet := addr.(*net.IPNet)
if v4 := ipNet.IP.To4(); v4 != nil {
ipNet.IP = v4
ip, _ := netip.AddrFromSlice(ipNet.IP)
ones, bits := ipNet.Mask.Size()
if bits == 32 {
ip = ip.Unmap()
}
ipNets = append(ipNets, ipNet)
pf := netip.PrefixFrom(ip, ones)
ipNets = append(ipNets, &pf)
}
r[iface.Name] = &Interface{
@ -61,7 +66,7 @@ func ResolveInterface(name string) (*Interface, error) {
return nil, err
}
ifaces := value.(map[string]*Interface)
ifaces := value
iface, ok := ifaces[name]
if !ok {
return nil, ErrIfaceNotFound
@ -74,35 +79,35 @@ func FlushCache() {
interfaces.Reset()
}
func (iface *Interface) PickIPv4Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() != nil
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is4()
})
}
func (iface *Interface) PickIPv6Addr(destination net.IP) (*net.IPNet, error) {
return iface.pickIPAddr(destination, func(addr *net.IPNet) bool {
return addr.IP.To4() == nil
func (iface *Interface) PickIPv6Addr(destination netip.Addr) (*netip.Prefix, error) {
return iface.pickIPAddr(destination, func(addr *netip.Prefix) bool {
return addr.Addr().Is6()
})
}
func (iface *Interface) pickIPAddr(destination net.IP, accept func(addr *net.IPNet) bool) (*net.IPNet, error) {
var fallback *net.IPNet
func (iface *Interface) pickIPAddr(destination netip.Addr, accept func(addr *netip.Prefix) bool) (*netip.Prefix, error) {
var fallback *netip.Prefix
for _, addr := range iface.Addrs {
if !accept(addr) {
continue
}
if fallback == nil && !addr.IP.IsLinkLocalUnicast() {
if fallback == nil && !addr.Addr().IsLinkLocalUnicast() {
fallback = addr
if destination == nil {
if !destination.IsValid() {
break
}
}
if destination != nil && addr.Contains(destination) {
if destination.IsValid() && addr.Contains(destination) {
return addr, nil
}
}

View File

@ -6,55 +6,55 @@ import (
"time"
)
type Factory = func(context.Context) (any, error)
type Factory[T any] func(context.Context) (T, error)
type entry struct {
elm any
type entry[T any] struct {
elm T
time time.Time
}
type Option func(*pool)
type Option[T any] func(*pool[T])
// WithEvict set the evict callback
func WithEvict(cb func(any)) Option {
return func(p *pool) {
func WithEvict[T any](cb func(T)) Option[T] {
return func(p *pool[T]) {
p.evict = cb
}
}
// WithAge defined element max age (millisecond)
func WithAge(maxAge int64) Option {
return func(p *pool) {
func WithAge[T any](maxAge int64) Option[T] {
return func(p *pool[T]) {
p.maxAge = maxAge
}
}
// WithSize defined max size of Pool
func WithSize(maxSize int) Option {
return func(p *pool) {
p.ch = make(chan any, maxSize)
func WithSize[T any](maxSize int) Option[T] {
return func(p *pool[T]) {
p.ch = make(chan *entry[T], maxSize)
}
}
// Pool is for GC, see New for detail
type Pool struct {
*pool
type Pool[T any] struct {
*pool[T]
}
type pool struct {
ch chan any
factory Factory
evict func(any)
type pool[T any] struct {
ch chan *entry[T]
factory Factory[T]
evict func(T)
maxAge int64
}
func (p *pool) GetContext(ctx context.Context) (any, error) {
func (p *pool[T]) GetContext(ctx context.Context) (T, error) {
now := time.Now()
for {
select {
case item := <-p.ch:
elm := item.(*entry)
if p.maxAge != 0 && now.Sub(item.(*entry).time).Milliseconds() > p.maxAge {
elm := item
if p.maxAge != 0 && now.Sub(item.time).Milliseconds() > p.maxAge {
if p.evict != nil {
p.evict(elm.elm)
}
@ -68,12 +68,12 @@ func (p *pool) GetContext(ctx context.Context) (any, error) {
}
}
func (p *pool) Get() (any, error) {
func (p *pool[T]) Get() (T, error) {
return p.GetContext(context.Background())
}
func (p *pool) Put(item any) {
e := &entry{
func (p *pool[T]) Put(item T) {
e := &entry[T]{
elm: item,
time: time.Now(),
}
@ -90,17 +90,17 @@ func (p *pool) Put(item any) {
}
}
func recycle(p *Pool) {
func recycle[T any](p *Pool[T]) {
for item := range p.pool.ch {
if p.pool.evict != nil {
p.pool.evict(item.(*entry).elm)
p.pool.evict(item.elm)
}
}
}
func New(factory Factory, options ...Option) *Pool {
p := &pool{
ch: make(chan any, 10),
func New[T any](factory Factory[T], options ...Option[T]) *Pool[T] {
p := &pool[T]{
ch: make(chan *entry[T], 10),
factory: factory,
}
@ -108,7 +108,7 @@ func New(factory Factory, options ...Option) *Pool {
option(p)
}
P := &Pool{p}
runtime.SetFinalizer(P, recycle)
P := &Pool[T]{p}
runtime.SetFinalizer(P, recycle[T])
return P
}

View File

@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert"
)
func lg() Factory {
func lg() Factory[int] {
initial := -1
return func(context.Context) (any, error) {
return func(context.Context) (int, error) {
initial++
return initial, nil
}
@ -18,23 +18,23 @@ func lg() Factory {
func TestPool_Basic(t *testing.T) {
g := lg()
pool := New(g)
pool := New[int](g)
elm, _ := pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
assert.Equal(t, 1, elm)
}
func TestPool_MaxSize(t *testing.T) {
g := lg()
size := 5
pool := New(g, WithSize(size))
pool := New[int](g, WithSize[int](size))
var items []any
var items []int
for i := 0; i < size; i++ {
item, _ := pool.Get()
@ -42,7 +42,7 @@ func TestPool_MaxSize(t *testing.T) {
}
extra, _ := pool.Get()
assert.Equal(t, size, extra.(int))
assert.Equal(t, size, extra)
for _, item := range items {
pool.Put(item)
@ -52,22 +52,22 @@ func TestPool_MaxSize(t *testing.T) {
for _, item := range items {
elm, _ := pool.Get()
assert.Equal(t, item.(int), elm.(int))
assert.Equal(t, item, elm)
}
}
func TestPool_MaxAge(t *testing.T) {
g := lg()
pool := New(g, WithAge(20))
pool := New[int](g, WithAge[int](20))
elm, _ := pool.Get()
pool.Put(elm)
elm, _ = pool.Get()
assert.Equal(t, 0, elm.(int))
assert.Equal(t, 0, elm)
pool.Put(elm)
time.Sleep(time.Millisecond * 22)
elm, _ = pool.Get()
assert.Equal(t, 1, elm.(int))
assert.Equal(t, 1, elm)
}

View File

@ -2,9 +2,11 @@ package process
import (
"errors"
"net"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant"
"net"
"net/netip"
"runtime"
)
var (
@ -18,32 +20,43 @@ const (
UDP = "udp"
)
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort)
}
func FindUid(network string, srcIP netip.Addr, srcPort int) (int32, error) {
_, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
if err != nil {
return -1, err
}
return uid, nil
}
func ShouldFindProcess(metadata *C.Metadata) bool {
if metadata.Process != "" {
if runtime.GOOS == "android" {
return false
}
if metadata.Process != "" || metadata.ProcessPath != "" {
return false
}
for _, ip := range localIPs {
if ip.Equal(metadata.SrcIP) {
if ip == metadata.SrcIP {
return true
}
}
return false
}
func AppendLocalIPs(ip ...net.IP) {
func AppendLocalIPs(ip ...netip.Addr) {
localIPs = append(ip, localIPs...)
}
func getLocalIPs() []net.IP {
ips := []net.IP{net.IPv4zero, net.IPv6zero}
func getLocalIPs() []netip.Addr {
ips := []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified()}
netInterfaces, err := net.Interfaces()
if err != nil {
ips = append(ips, net.IPv4(127, 0, 0, 1), net.IPv6loopback)
ips = append(ips, netip.AddrFrom4([4]byte{127, 0, 0, 1}), nnip.IpToAddr(net.IPv6loopback))
return ips
}
@ -53,7 +66,7 @@ func getLocalIPs() []net.IP {
for _, address := range adds {
if ipNet, ok := address.(*net.IPNet); ok {
ips = append(ips, ipNet.IP)
ips = append(ips, nnip.IpToAddr(ipNet.IP))
}
}
}
@ -62,7 +75,7 @@ func getLocalIPs() []net.IP {
return ips
}
var localIPs []net.IP
var localIPs []netip.Addr
func init() {
localIPs = getLocalIPs()

View File

@ -1,230 +0,0 @@
package process
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"unicode"
"unsafe"
"github.com/Dreamacro/clash/common/pool"
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
return binary.BigEndian
}
return binary.LittleEndian
}()
const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20
pathProc = "/proc"
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil {
return "", err
}
return resolveProcessNameByProcSearch(inode, uid)
}
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int32, error) {
var family byte
var protocol byte
switch network {
case TCP:
protocol = syscall.IPPROTO_TCP
case UDP:
protocol = syscall.IPPROTO_UDP
default:
return 0, 0, ErrInvalidNetwork
}
if ip.To4() != nil {
family = syscall.AF_INET
} else {
family = syscall.AF_INET6
}
req := packSocketDiagRequest(family, protocol, ip, uint16(srcPort))
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil {
return 0, 0, fmt.Errorf("dial netlink: %w", err)
}
defer syscall.Close(socket)
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pad: 0,
Pid: 0,
Groups: 0,
}); err != nil {
return 0, 0, err
}
if _, err := syscall.Write(socket, req); err != nil {
return 0, 0, fmt.Errorf("write request: %w", err)
}
rb := pool.Get(pool.RelayBufferSize)
defer pool.Put(rb)
n, err := syscall.Read(socket, rb)
if err != nil {
return 0, 0, fmt.Errorf("read response: %w", err)
}
messages, err := syscall.ParseNetlinkMessage(rb[:n])
if err != nil {
return 0, 0, fmt.Errorf("parse netlink message: %w", err)
} else if len(messages) == 0 {
return 0, 0, fmt.Errorf("unexcepted netlink response")
}
message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR")
}
uid, inode := unpackSocketDiagResponse(&messages[0])
if uid < 0 || inode < 0 {
return 0, 0, fmt.Errorf("invalid uid(%d) or inode(%d)", uid, inode)
}
return uid, inode, nil
}
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
s := make([]byte, 16)
if v4 := source.To4(); v4 != nil {
copy(s, v4)
} else {
copy(s, source)
}
buf := make([]byte, sizeOfSocketDiagRequest)
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
nativeEndian.PutUint32(buf[8:12], 0)
nativeEndian.PutUint32(buf[12:16], 0)
buf[16] = family
buf[17] = protocol
buf[18] = 0
buf[19] = 0
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
binary.BigEndian.PutUint16(buf[24:26], sourcePort)
binary.BigEndian.PutUint16(buf[26:28], 0)
copy(buf[28:44], s)
copy(buf[44:60], net.IPv6zero)
nativeEndian.PutUint32(buf[60:64], 0)
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
return buf
}
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) {
if len(msg.Data) < 72 {
return 0, 0
}
data := msg.Data
uid = int32(nativeEndian.Uint32(data[64:68]))
inode = int32(nativeEndian.Uint32(data[68:72]))
return
}
func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
files, err := os.ReadDir(pathProc)
if err != nil {
return "", err
}
buffer := make([]byte, syscall.PathMax)
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
for _, f := range files {
if !f.IsDir() || !isPid(f.Name()) {
continue
}
info, err := f.Info()
if err != nil {
return "", err
}
if info.Sys().(*syscall.Stat_t).Uid != uint32(uid) {
continue
}
processPath := path.Join(pathProc, f.Name())
fdPath := path.Join(processPath, "fd")
fds, err := os.ReadDir(fdPath)
if err != nil {
continue
}
for _, fd := range fds {
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
if err != nil {
continue
}
if bytes.Equal(buffer[:n], socket) {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
}
}
}
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
func splitCmdline(cmdline []byte) string {
cmdline = bytes.Trim(cmdline, " ")
idx := bytes.IndexFunc(cmdline, func(r rune) bool {
return unicode.IsControl(r) || unicode.IsSpace(r)
})
if idx == -1 {
return filepath.Base(string(cmdline))
}
return filepath.Base(string(cmdline[:idx]))
}
func isPid(s string) bool {
return strings.IndexFunc(s, func(r rune) bool {
return !unicode.IsDigit(r)
}) == -1
}

View File

@ -2,10 +2,12 @@ package process
import (
"encoding/binary"
"net"
"net/netip"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"golang.org/x/sys/unix"
)
@ -15,7 +17,11 @@ const (
proccallnumpidinfo = 0x2
)
func findProcessName(network string, ip net.IP, port int) (string, error) {
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
case TCP:
@ -26,7 +32,7 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
return "", ErrInvalidNetwork
}
isIPv4 := ip.To4() != nil
isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath)
if err != nil {
@ -57,19 +63,19 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
// xinpcb_n.inp_vflag
flag := buf[inp+44]
var srcIP net.IP
var srcIP netip.Addr
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+76 : inp+80])
srcIP = nnip.IpToAddr(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+64 : inp+80])
srcIP = nnip.IpToAddr(buf[inp+64 : inp+80])
default:
continue
}
if !ip.Equal(srcIP) && (network == TCP || !srcIP.IsUnspecified()) {
if ip != srcIP && (network == TCP || !srcIP.IsUnspecified()) {
continue
}

View File

@ -3,13 +3,14 @@ package process
import (
"encoding/binary"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/log"
)
@ -20,7 +21,11 @@ var (
once sync.Once
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
once.Do(func() {
if err := initSearcher(); err != nil {
log.Errorln("Initialize PROCESS-NAME failed: %s", err.Error())
@ -102,7 +107,7 @@ type searcher struct {
pid int
}
func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint32, error) {
func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (uint32, error) {
var itemSize int
var inpOffset int
@ -116,7 +121,7 @@ func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint3
inpOffset = s.udpInpOffset
}
isIPv4 := ip.To4() != nil
isIPv4 := ip.Is4()
// skip the first xinpgen block
for i := s.headSize; i+itemSize <= len(buf); i += itemSize {
inp := i + inpOffset
@ -130,19 +135,19 @@ func (s *searcher) Search(buf []byte, ip net.IP, port uint16, isTCP bool) (uint3
// xinpcb.inp_vflag
flag := buf[inp+s.vflag]
var srcIP net.IP
var srcIP netip.Addr
switch {
case flag&0x1 > 0 && isIPv4:
// ipv4
srcIP = net.IP(buf[inp+s.ip : inp+s.ip+4])
srcIP = nnip.IpToAddr(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4:
// ipv6
srcIP = net.IP(buf[inp+s.ip-12 : inp+s.ip+4])
srcIP = nnip.IpToAddr(buf[inp+s.ip-12 : inp+s.ip+4])
default:
continue
}
if !ip.Equal(srcIP) {
if ip != srcIP {
continue
}

View File

@ -1,5 +1,3 @@
//go:build !android
package process
import (
@ -7,6 +5,7 @@ import (
"encoding/binary"
"fmt"
"net"
"net/netip"
"os"
"path"
"strings"
@ -33,16 +32,15 @@ const (
pathProc = "/proc"
)
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil {
return "", err
}
return resolveProcessNameByProcSearch(inode, uid)
}
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int32, error) {
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
var family byte
var protocol byte
@ -55,7 +53,7 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
return 0, 0, ErrInvalidNetwork
}
if ip.To4() != nil {
if ip.Is4() {
family = syscall.AF_INET
} else {
family = syscall.AF_INET6
@ -67,10 +65,12 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
if err != nil {
return 0, 0, fmt.Errorf("dial netlink: %w", err)
}
defer syscall.Close(socket)
defer func() {
_ = syscall.Close(socket)
}()
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
_ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
_ = syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
@ -86,7 +86,9 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
}
rb := pool.Get(pool.RelayBufferSize)
defer pool.Put(rb)
defer func() {
_ = pool.Put(rb)
}()
n, err := syscall.Read(socket, rb)
if err != nil {
@ -105,7 +107,7 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR")
}
inode, uid := unpackSocketDiagResponse(&messages[0])
inode, uid := unpackSocketDiagResponse(&message)
if inode < 0 || uid < 0 {
return 0, 0, fmt.Errorf("invalid inode(%d) or uid(%d)", inode, uid)
}
@ -113,14 +115,10 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int3
return inode, uid, nil
}
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
func packSocketDiagRequest(family, protocol byte, source netip.Addr, sourcePort uint16) []byte {
s := make([]byte, 16)
if v4 := source.To4(); v4 != nil {
copy(s, v4)
} else {
copy(s, source)
}
copy(s, source.AsSlice())
buf := make([]byte, sizeOfSocketDiagRequest)

View File

@ -2,8 +2,12 @@
package process
import "net"
import "net/netip"
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
return "", ErrPlatformNotSupport
}
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}

View File

@ -2,11 +2,12 @@ package process
import (
"fmt"
"net"
"net/netip"
"sync"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows"
@ -28,6 +29,10 @@ var (
once sync.Once
)
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func initWin32API() error {
h, err := windows.LoadLibrary("iphlpapi.dll")
if err != nil {
@ -57,7 +62,7 @@ func initWin32API() error {
return nil
}
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
func findProcessName(network string, ip netip.Addr, srcPort int) (string, error) {
once.Do(func() {
err := initWin32API()
if err != nil {
@ -67,7 +72,7 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
}
})
family := windows.AF_INET
if ip.To4() == nil {
if ip.Is6() {
family = windows.AF_INET6
}
@ -107,7 +112,7 @@ type searcher struct {
tcpState int
}
func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error) {
n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize
for i := 0; i < n; i++ {
@ -131,9 +136,9 @@ func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
continue
}
srcIP := net.IP(row[s.ip : s.ip+s.ipSize])
srcIP := nnip.IpToAddr(row[s.ip : s.ip+s.ipSize])
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if !ip.Equal(srcIP) && (!srcIP.IsUnspecified() || s.tcpState != -1) {
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue
}
@ -214,8 +219,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
uintptr(h),
uintptr(1),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
0, 0)
uintptr(unsafe.Pointer(&size)))
if r1 == 0 {
return "", err
}

View File

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

View File

@ -1,19 +1,19 @@
package resolver
import (
"net"
)
import "net/netip"
var DefaultHostMapper Enhancer
type Enhancer interface {
FakeIPEnabled() bool
MappingEnabled() bool
IsFakeIP(net.IP) bool
IsFakeBroadcastIP(net.IP) bool
IsExistFakeIP(net.IP) bool
FindHostByIP(net.IP) (string, bool)
IsFakeIP(netip.Addr) bool
IsFakeBroadcastIP(netip.Addr) bool
IsExistFakeIP(netip.Addr) bool
FindHostByIP(netip.Addr) (string, bool)
FlushFakeIP() error
InsertHostByIP(netip.Addr, string)
StoreFakePoolState()
}
func FakeIPEnabled() bool {
@ -32,7 +32,7 @@ func MappingEnabled() bool {
return false
}
func IsFakeIP(ip net.IP) bool {
func IsFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeIP(ip)
}
@ -40,7 +40,7 @@ func IsFakeIP(ip net.IP) bool {
return false
}
func IsFakeBroadcastIP(ip net.IP) bool {
func IsFakeBroadcastIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeBroadcastIP(ip)
}
@ -48,7 +48,7 @@ func IsFakeBroadcastIP(ip net.IP) bool {
return false
}
func IsExistFakeIP(ip net.IP) bool {
func IsExistFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsExistFakeIP(ip)
}
@ -56,7 +56,13 @@ func IsExistFakeIP(ip net.IP) bool {
return false
}
func FindHostByIP(ip net.IP) (string, bool) {
func InsertHostByIP(ip netip.Addr, host string) {
if mapper := DefaultHostMapper; mapper != nil {
mapper.InsertHostByIP(ip, host)
}
}
func FindHostByIP(ip netip.Addr) (string, bool) {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.FindHostByIP(ip)
}
@ -70,3 +76,9 @@ func FlushFakeIP() error {
}
return nil
}
func StoreFakePoolState() {
if mapper := DefaultHostMapper; mapper != nil {
mapper.StoreFakePoolState()
}
}

View File

@ -5,9 +5,10 @@ import (
"errors"
"math/rand"
"net"
"strings"
"net/netip"
"time"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/trie"
)
@ -23,7 +24,7 @@ var (
DisableIPv6 = true
// DefaultHosts aim to resolve hosts
DefaultHosts = trie.New()
DefaultHosts = trie.New[netip.Addr]()
// DefaultDNSTimeout defined the default dns request timeout
DefaultDNSTimeout = time.Second * 5
@ -36,134 +37,57 @@ var (
)
type Resolver interface {
ResolveIP(host string) (ip net.IP, err error)
ResolveIPv4(host string) (ip net.IP, err error)
ResolveIPv6(host string) (ip net.IP, err error)
ResolveIP(host string) (ip netip.Addr, err error)
ResolveIPv4(host string) (ip netip.Addr, err error)
ResolveIPv6(host string) (ip netip.Addr, err error)
ResolveAllIP(host string) (ip []netip.Addr, err error)
ResolveAllIPPrimaryIPv4(host string) (ips []netip.Addr, err error)
ResolveAllIPv4(host string) (ips []netip.Addr, err error)
ResolveAllIPv6(host string) (ips []netip.Addr, err error)
}
// ResolveIPv4 with a host, return ipv4
func ResolveIPv4(host string) (net.IP, error) {
func ResolveIPv4(host string) (netip.Addr, error) {
return ResolveIPv4WithResolver(host, DefaultResolver)
}
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To4(); ip != nil {
return ip, nil
}
func ResolveIPv4WithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPv4WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, nil
}
ip := net.ParseIP(host)
if ip != nil {
if !strings.Contains(host, ":") {
return ip, nil
}
return nil, ErrIPVersion
}
if r != nil {
return r.ResolveIPv4(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return nil, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
}
return nil, ErrIPNotFound
}
// ResolveIPv6 with a host, return ipv6
func ResolveIPv6(host string) (net.IP, error) {
func ResolveIPv6(host string) (netip.Addr, error) {
return ResolveIPv6WithResolver(host, DefaultResolver)
}
func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
if DisableIPv6 {
return nil, ErrIPv6Disabled
func ResolveIPv6WithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPv6WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To16(); ip != nil {
return ip, nil
}
}
ip := net.ParseIP(host)
if ip != nil {
if strings.Contains(host, ":") {
return ip, nil
}
return nil, ErrIPVersion
}
if r != nil {
return r.ResolveIPv6(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil {
return nil, err
} else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
}
return nil, ErrIPNotFound
}
// ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil {
return node.Data.(net.IP), nil
func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPPrimaryIPv4WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
}
if r != nil {
if DisableIPv6 {
return r.ResolveIPv4(host)
}
return r.ResolveIP(host)
} else if DisableIPv6 {
return ResolveIPv4(host)
}
ip := net.ParseIP(host)
if ip != nil {
return ip, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return nil, err
}
return ipAddr.IP, nil
}
return nil, ErrIPNotFound
}
// ResolveIP with a host, return ip
func ResolveIP(host string) (net.IP, error) {
func ResolveIP(host string) (netip.Addr, error) {
return ResolveIPWithResolver(host, DefaultResolver)
}
// ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPv4WithResolver(host, ProxyServerHostResolver)
}
@ -171,7 +95,7 @@ func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
}
// ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPv6WithResolver(host, ProxyServerHostResolver)
}
@ -179,9 +103,188 @@ func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
}
// ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (net.IP, error) {
func ResolveProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveIPWithResolver(host, ProxyServerHostResolver)
}
return ResolveIP(host)
}
func ResolveAllIPv6WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if DisableIPv6 {
return []netip.Addr{}, ErrIPv6Disabled
}
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is6() {
return []netip.Addr{ip}, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is6() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveAllIPv6(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil {
return []netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return []netip.Addr{}, ErrIPNotFound
}
return []netip.Addr{netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))]))}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is4() {
return []netip.Addr{node.Data}, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
}
if r != nil {
return r.ResolveAllIPv4(host)
}
if DefaultResolver == nil {
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
if err != nil {
return []netip.Addr{}, err
} else if len(ipAddrs) == 0 {
return []netip.Addr{}, ErrIPNotFound
}
ip := ipAddrs[rand.Intn(len(ipAddrs))].To4()
if ip == nil {
return []netip.Addr{}, ErrIPVersion
}
return []netip.Addr{netip.AddrFrom4(*(*[4]byte)(ip))}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPWithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
return []netip.Addr{node.Data}, nil
}
if r != nil {
if DisableIPv6 {
return r.ResolveAllIPv4(host)
}
return r.ResolveAllIP(host)
} else if DisableIPv6 {
return ResolveAllIPv4(host)
}
ip, err := netip.ParseAddr(host)
if err == nil {
return []netip.Addr{ip}, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return []netip.Addr{}, err
}
return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIPPrimaryIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil {
return []netip.Addr{node.Data}, nil
}
if r != nil {
if DisableIPv6 {
return r.ResolveAllIPv4(host)
}
return r.ResolveAllIPPrimaryIPv4(host)
} else if DisableIPv6 {
return ResolveAllIPv4(host)
}
ip, err := netip.ParseAddr(host)
if err == nil {
return []netip.Addr{ip}, nil
}
if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
return []netip.Addr{}, err
}
return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil
}
return []netip.Addr{}, ErrIPNotFound
}
func ResolveAllIP(host string) ([]netip.Addr, error) {
return ResolveAllIPWithResolver(host, DefaultResolver)
}
func ResolveAllIPv4(host string) ([]netip.Addr, error) {
return ResolveAllIPv4WithResolver(host, DefaultResolver)
}
func ResolveAllIPv6(host string) ([]netip.Addr, error) {
return ResolveAllIPv6WithResolver(host, DefaultResolver)
}
func ResolveAllIPv6ProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPv6WithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIPv6(host)
}
func ResolveAllIPv4ProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPv4WithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIPv4(host)
}
func ResolveAllIPProxyServerHost(host string) ([]netip.Addr, error) {
if ProxyServerHostResolver != nil {
return ResolveAllIPWithResolver(host, ProxyServerHostResolver)
}
return ResolveAllIP(host)
}

View File

@ -0,0 +1,176 @@
package sniffer
import (
"errors"
"github.com/Dreamacro/clash/constant/sniffer"
"net"
"net/netip"
"strconv"
"time"
"github.com/Dreamacro/clash/component/trie"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
)
var (
ErrorUnsupportedSniffer = errors.New("unsupported sniffer")
ErrorSniffFailed = errors.New("all sniffer failed")
ErrNoClue = errors.New("not enough information for making a decision")
)
var Dispatcher SnifferDispatcher
type (
SnifferDispatcher struct {
enable bool
sniffers []sniffer.Sniffer
foreDomain *trie.DomainTrie[bool]
skipSNI *trie.DomainTrie[bool]
portRanges *[]utils.Range[uint16]
}
)
func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) {
bufConn, ok := conn.(*CN.BufferedConn)
if !ok {
return
}
if metadata.Host == "" || sd.foreDomain.Search(metadata.Host) != nil {
port, err := strconv.ParseUint(metadata.DstPort, 10, 16)
if err != nil {
log.Debugln("[Sniffer] Dst port is error")
return
}
inWhitelist := false
for _, portRange := range *sd.portRanges {
if portRange.Contains(uint16(port)) {
inWhitelist = true
break
}
}
if !inWhitelist {
return
}
if host, err := sd.sniffDomain(bufConn, metadata); err != nil {
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return
} else {
if sd.skipSNI.Search(host) != nil {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return
}
sd.replaceDomain(metadata, host)
}
}
}
func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string) {
log.Debugln("[Sniffer] Sniff TCP [%s:%s]-->[%s:%s] success, replace domain [%s]-->[%s]",
metadata.SrcIP, metadata.SrcPort,
metadata.DstIP, metadata.DstPort,
metadata.Host, host)
metadata.AddrType = C.AtypDomainName
metadata.Host = host
metadata.DNSMode = C.DNSMapping
resolver.InsertHostByIP(metadata.DstIP, host)
}
func (sd *SnifferDispatcher) Enable() bool {
return sd.enable
}
func (sd *SnifferDispatcher) sniffDomain(conn *CN.BufferedConn, metadata *C.Metadata) (string, error) {
for _, sniffer := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP {
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
_, err := conn.Peek(1)
_ = conn.SetReadDeadline(time.Time{})
if err != nil {
_, ok := err.(*net.OpError)
if ok {
log.Errorln("[Sniffer] [%s] may not have any sent data, Consider adding skip", metadata.DstIP.String())
_ = conn.Close()
}
return "", err
}
bufferedLen := conn.Buffered()
bytes, err := conn.Peek(bufferedLen)
if err != nil {
log.Debugln("[Sniffer] the data length not enough")
continue
}
host, err := sniffer.SniffTCP(bytes)
if err != nil {
log.Debugln("[Sniffer] [%s] Sniff data failed %s", sniffer.Protocol(), metadata.DstIP)
continue
}
_, err = netip.ParseAddr(host)
if err == nil {
log.Debugln("[Sniffer] [%s] Sniff data failed %s", sniffer.Protocol(), metadata.DstIP)
continue
}
return host, nil
}
}
return "", ErrorSniffFailed
}
func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) {
dispatcher := SnifferDispatcher{
enable: false,
}
return &dispatcher, nil
}
func NewSnifferDispatcher(needSniffer []sniffer.Type, forceDomain *trie.DomainTrie[bool],
skipSNI *trie.DomainTrie[bool], ports *[]utils.Range[uint16]) (*SnifferDispatcher, error) {
dispatcher := SnifferDispatcher{
enable: true,
foreDomain: forceDomain,
skipSNI: skipSNI,
portRanges: ports,
}
for _, snifferName := range needSniffer {
sniffer, err := NewSniffer(snifferName)
if err != nil {
log.Errorln("Sniffer name[%s] is error", snifferName)
return &SnifferDispatcher{enable: false}, err
}
dispatcher.sniffers = append(dispatcher.sniffers, sniffer)
}
return &dispatcher, nil
}
func NewSniffer(name sniffer.Type) (sniffer.Sniffer, error) {
switch name {
case sniffer.TLS:
return &TLSSniffer{}, nil
case sniffer.HTTP:
return &HTTPSniffer{}, nil
default:
return nil, ErrorUnsupportedSniffer
}
}

View File

@ -0,0 +1,100 @@
package sniffer
import (
"bytes"
"errors"
C "github.com/Dreamacro/clash/constant"
"net"
"strings"
)
var (
// refer to https://pkg.go.dev/net/http@master#pkg-constants
methods = [...]string{"get", "post", "head", "put", "delete", "options", "connect", "patch", "trace"}
errNotHTTPMethod = errors.New("not an HTTP method")
)
type version byte
const (
HTTP1 version = iota
HTTP2
)
type HTTPSniffer struct {
version version
host string
}
func (http *HTTPSniffer) Protocol() string {
switch http.version {
case HTTP1:
return "http1"
case HTTP2:
return "http2"
default:
return "unknown"
}
}
func (http *HTTPSniffer) SupportNetwork() C.NetWork {
return C.TCP
}
func (http *HTTPSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffHTTP(bytes)
if err == nil {
return *domain, nil
} else {
return "", err
}
}
func beginWithHTTPMethod(b []byte) error {
for _, m := range &methods {
if len(b) >= len(m) && strings.EqualFold(string(b[:len(m)]), m) {
return nil
}
if len(b) < len(m) {
return ErrNoClue
}
}
return errNotHTTPMethod
}
func SniffHTTP(b []byte) (*string, error) {
if err := beginWithHTTPMethod(b); err != nil {
return nil, err
}
_ = &HTTPSniffer{
version: HTTP1,
}
headers := bytes.Split(b, []byte{'\n'})
for i := 1; i < len(headers); i++ {
header := headers[i]
if len(header) == 0 {
break
}
parts := bytes.SplitN(header, []byte{':'}, 2)
if len(parts) != 2 {
continue
}
key := strings.ToLower(string(parts[0]))
if key == "host" {
rawHost := strings.ToLower(string(bytes.TrimSpace(parts[1])))
host, _, err := net.SplitHostPort(rawHost)
if err != nil {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
host = rawHost
} else {
return nil, err
}
}
return &host, nil
}
}
return nil, ErrNoClue
}

View File

@ -0,0 +1,3 @@
package sniffer
//TODO

View File

@ -0,0 +1,159 @@
package sniffer
import (
"testing"
)
func TestTLSHeaders(t *testing.T) {
cases := []struct {
input []byte
domain string
err bool
}{
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xc8, 0x01, 0x00, 0x00,
0xc4, 0x03, 0x03, 0x1a, 0xac, 0xb2, 0xa8, 0xfe,
0xb4, 0x96, 0x04, 0x5b, 0xca, 0xf7, 0xc1, 0xf4,
0x2e, 0x53, 0x24, 0x6e, 0x34, 0x0c, 0x58, 0x36,
0x71, 0x97, 0x59, 0xe9, 0x41, 0x66, 0xe2, 0x43,
0xa0, 0x13, 0xb6, 0x00, 0x00, 0x20, 0x1a, 0x1a,
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
0x00, 0x7b, 0xba, 0xba, 0x00, 0x00, 0xff, 0x01,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
0x14, 0x00, 0x00, 0x11, 0x63, 0x2e, 0x73, 0x2d,
0x6d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66,
0x74, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x17, 0x00,
0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d, 0x00,
0x14, 0x00, 0x12, 0x04, 0x03, 0x08, 0x04, 0x04,
0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08,
0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x05, 0x00,
0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12,
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c,
0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70,
0x2f, 0x31, 0x2e, 0x31, 0x00, 0x0b, 0x00, 0x02,
0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
0xaa, 0xaa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
0xaa, 0xaa, 0x00, 0x01, 0x00,
},
domain: "c.s-microsoft.com",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xee, 0x01, 0x00, 0x00,
0xea, 0x03, 0x03, 0xe7, 0x91, 0x9e, 0x93, 0xca,
0x78, 0x1b, 0x3c, 0xe0, 0x65, 0x25, 0x58, 0xb5,
0x93, 0xe1, 0x0f, 0x85, 0xec, 0x9a, 0x66, 0x8e,
0x61, 0x82, 0x88, 0xc8, 0xfc, 0xae, 0x1e, 0xca,
0xd7, 0xa5, 0x63, 0x20, 0xbd, 0x1c, 0x00, 0x00,
0x8b, 0xee, 0x09, 0xe3, 0x47, 0x6a, 0x0e, 0x74,
0xb0, 0xbc, 0xa3, 0x02, 0xa7, 0x35, 0xe8, 0x85,
0x70, 0x7c, 0x7a, 0xf0, 0x00, 0xdf, 0x4a, 0xea,
0x87, 0x01, 0x14, 0x91, 0x00, 0x20, 0xea, 0xea,
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0x14, 0xcc, 0x13,
0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d,
0x00, 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00,
0x00, 0x81, 0x9a, 0x9a, 0x00, 0x00, 0xff, 0x01,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00,
0x16, 0x00, 0x00, 0x13, 0x77, 0x77, 0x77, 0x30,
0x37, 0x2e, 0x63, 0x6c, 0x69, 0x63, 0x6b, 0x74,
0x61, 0x6c, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x00,
0x17, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00,
0x0d, 0x00, 0x14, 0x00, 0x12, 0x04, 0x03, 0x08,
0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05,
0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00,
0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x12, 0x00, 0x00, 0x00, 0x10, 0x00, 0x0e,
0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74,
0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 0x75, 0x50,
0x00, 0x00, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x9a, 0x9a,
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x8a, 0x8a,
0x00, 0x01, 0x00,
},
domain: "www07.clicktale.net",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x00, 0xe6, 0x01, 0x00, 0x00, 0xe2, 0x03, 0x03, 0x81, 0x47, 0xc1,
0x66, 0xd5, 0x1b, 0xfa, 0x4b, 0xb5, 0xe0, 0x2a, 0xe1, 0xa7, 0x87, 0x13, 0x1d, 0x11, 0xaa, 0xc6,
0xce, 0xfc, 0x7f, 0xab, 0x94, 0xc8, 0x62, 0xad, 0xc8, 0xab, 0x0c, 0xdd, 0xcb, 0x20, 0x6f, 0x9d,
0x07, 0xf1, 0x95, 0x3e, 0x99, 0xd8, 0xf3, 0x6d, 0x97, 0xee, 0x19, 0x0b, 0x06, 0x1b, 0xf4, 0x84,
0x0b, 0xb6, 0x8f, 0xcc, 0xde, 0xe2, 0xd0, 0x2d, 0x6b, 0x0c, 0x1f, 0x52, 0x53, 0x13, 0x00, 0x08,
0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00, 0x91, 0x00, 0x00, 0x00, 0x0c,
0x00, 0x0a, 0x00, 0x00, 0x07, 0x64, 0x6f, 0x67, 0x66, 0x69, 0x73, 0x68, 0x00, 0x0b, 0x00, 0x04,
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0c, 0x00, 0x0a, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e,
0x00, 0x19, 0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00,
0x00, 0x0d, 0x00, 0x1e, 0x00, 0x1c, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08,
0x08, 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01,
0x06, 0x01, 0x00, 0x2b, 0x00, 0x07, 0x06, 0x7f, 0x1c, 0x7f, 0x1b, 0x7f, 0x1a, 0x00, 0x2d, 0x00,
0x02, 0x01, 0x01, 0x00, 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0x2f, 0x35, 0x0c,
0xb6, 0x90, 0x0a, 0xb7, 0xd5, 0xc4, 0x1b, 0x2f, 0x60, 0xaa, 0x56, 0x7b, 0x3f, 0x71, 0xc8, 0x01,
0x7e, 0x86, 0xd3, 0xb7, 0x0c, 0x29, 0x1a, 0x9e, 0x5b, 0x38, 0x3f, 0x01, 0x72,
},
domain: "dogfish",
err: false,
},
{
input: []byte{
0x16, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00,
0xff, 0x03, 0x03, 0x3d, 0x89, 0x52, 0x9e, 0xee,
0xbe, 0x17, 0x63, 0x75, 0xef, 0x29, 0xbd, 0x14,
0x6a, 0x49, 0xe0, 0x2c, 0x37, 0x57, 0x71, 0x62,
0x82, 0x44, 0x94, 0x8f, 0x6e, 0x94, 0x08, 0x45,
0x7f, 0xdb, 0xc1, 0x00, 0x00, 0x3e, 0xc0, 0x2c,
0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8,
0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e,
0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23,
0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14,
0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33,
0x00, 0x9d, 0x00, 0x9c, 0x13, 0x02, 0x13, 0x03,
0x13, 0x01, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35,
0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x00, 0x98,
0x00, 0x00, 0x00, 0x10, 0x00, 0x0e, 0x00, 0x00,
0x0b, 0x31, 0x30, 0x2e, 0x34, 0x32, 0x2e, 0x30,
0x2e, 0x32, 0x34, 0x33, 0x00, 0x0b, 0x00, 0x04,
0x03, 0x00, 0x01, 0x02, 0x00, 0x0a, 0x00, 0x0a,
0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19,
0x00, 0x18, 0x00, 0x23, 0x00, 0x00, 0x00, 0x0d,
0x00, 0x20, 0x00, 0x1e, 0x04, 0x03, 0x05, 0x03,
0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06,
0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03,
0x02, 0x01, 0x02, 0x02, 0x04, 0x02, 0x05, 0x02,
0x06, 0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17,
0x00, 0x00, 0x00, 0x2b, 0x00, 0x09, 0x08, 0x7f,
0x14, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00,
0x2d, 0x00, 0x03, 0x02, 0x01, 0x00, 0x00, 0x28,
0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20,
0x13, 0x7c, 0x6e, 0x97, 0xc4, 0xfd, 0x09, 0x2e,
0x70, 0x2f, 0x73, 0x5a, 0x9b, 0x57, 0x4d, 0x5f,
0x2b, 0x73, 0x2c, 0xa5, 0x4a, 0x98, 0x40, 0x3d,
0x75, 0x6e, 0xb4, 0x76, 0xf9, 0x48, 0x8f, 0x36,
},
domain: "10.42.0.243",
err: false,
},
}
for _, test := range cases {
domain, err := SniffTLS(test.input)
if test.err {
if err == nil {
t.Errorf("Exepct error but nil in test %v", test)
}
} else {
if err != nil {
t.Errorf("Expect no error but actually %s in test %v", err.Error(), test)
}
if *domain != test.domain {
t.Error("expect domain ", test.domain, " but got ", domain)
}
}
}
}

View File

@ -0,0 +1,156 @@
package sniffer
import (
"encoding/binary"
"errors"
"strings"
C "github.com/Dreamacro/clash/constant"
)
var (
errNotTLS = errors.New("not TLS header")
errNotClientHello = errors.New("not client hello")
)
type TLSSniffer struct {
}
func (tls *TLSSniffer) Protocol() string {
return "tls"
}
func (tls *TLSSniffer) SupportNetwork() C.NetWork {
return C.TCP
}
func (tls *TLSSniffer) SniffTCP(bytes []byte) (string, error) {
domain, err := SniffTLS(bytes)
if err == nil {
return *domain, nil
} else {
return "", err
}
}
func IsValidTLSVersion(major, minor byte) bool {
return major == 3
}
// ReadClientHello returns server name (if any) from TLS client hello message.
// https://github.com/golang/go/blob/master/src/crypto/tls/handshake_messages.go#L300
func ReadClientHello(data []byte) (*string, error) {
if len(data) < 42 {
return nil, ErrNoClue
}
sessionIDLen := int(data[38])
if sessionIDLen > 32 || len(data) < 39+sessionIDLen {
return nil, ErrNoClue
}
data = data[39+sessionIDLen:]
if len(data) < 2 {
return nil, ErrNoClue
}
// cipherSuiteLen is the number of bytes of cipher suite numbers. Since
// they are uint16s, the number must be even.
cipherSuiteLen := int(data[0])<<8 | int(data[1])
if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen {
return nil, errNotClientHello
}
data = data[2+cipherSuiteLen:]
if len(data) < 1 {
return nil, ErrNoClue
}
compressionMethodsLen := int(data[0])
if len(data) < 1+compressionMethodsLen {
return nil, ErrNoClue
}
data = data[1+compressionMethodsLen:]
if len(data) == 0 {
return nil, errNotClientHello
}
if len(data) < 2 {
return nil, errNotClientHello
}
extensionsLength := int(data[0])<<8 | int(data[1])
data = data[2:]
if extensionsLength != len(data) {
return nil, errNotClientHello
}
for len(data) != 0 {
if len(data) < 4 {
return nil, errNotClientHello
}
extension := uint16(data[0])<<8 | uint16(data[1])
length := int(data[2])<<8 | int(data[3])
data = data[4:]
if len(data) < length {
return nil, errNotClientHello
}
if extension == 0x00 { /* extensionServerName */
d := data[:length]
if len(d) < 2 {
return nil, errNotClientHello
}
namesLen := int(d[0])<<8 | int(d[1])
d = d[2:]
if len(d) != namesLen {
return nil, errNotClientHello
}
for len(d) > 0 {
if len(d) < 3 {
return nil, errNotClientHello
}
nameType := d[0]
nameLen := int(d[1])<<8 | int(d[2])
d = d[3:]
if len(d) < nameLen {
return nil, errNotClientHello
}
if nameType == 0 {
serverName := string(d[:nameLen])
// An SNI value may not include a
// trailing dot. See
// https://tools.ietf.org/html/rfc6066#section-3.
if strings.HasSuffix(serverName, ".") {
return nil, errNotClientHello
}
return &serverName, nil
}
d = d[nameLen:]
}
}
data = data[length:]
}
return nil, errNotTLS
}
func SniffTLS(b []byte) (*string, error) {
if len(b) < 5 {
return nil, ErrNoClue
}
if b[0] != 0x16 /* TLS Handshake */ {
return nil, errNotTLS
}
if !IsValidTLSVersion(b[1], b[2]) {
return nil, errNotTLS
}
headerLen := int(binary.BigEndian.Uint16(b[3:5]))
if 5+headerLen > len(b) {
return nil, ErrNoClue
}
domain, err := ReadClientHello(b[5 : 5+headerLen])
if err == nil {
return domain, nil
}
return nil, err
}

View File

@ -17,8 +17,8 @@ var ErrInvalidDomain = errors.New("invalid domain")
// DomainTrie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com)
type DomainTrie struct {
root *Node
type DomainTrie[T comparable] struct {
root *Node[T]
}
func ValidAndSplitDomain(domain string) ([]string, bool) {
@ -51,7 +51,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) {
// 3. subdomain.*.example.com
// 4. .example.com
// 5. +.example.com
func (t *DomainTrie) Insert(domain string, data any) error {
func (t *DomainTrie[T]) Insert(domain string, data T) error {
parts, valid := ValidAndSplitDomain(domain)
if !valid {
return ErrInvalidDomain
@ -68,13 +68,13 @@ func (t *DomainTrie) Insert(domain string, data any) error {
return nil
}
func (t *DomainTrie) insert(parts []string, data any) {
func (t *DomainTrie[T]) insert(parts []string, data T) {
node := t.root
// reverse storage domain part to save space
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
if !node.hasChild(part) {
node.addChild(part, newNode(nil))
node.addChild(part, newNode(getZero[T]()))
}
node = node.getChild(part)
@ -88,7 +88,7 @@ func (t *DomainTrie) insert(parts []string, data any) {
// 1. static part
// 2. wildcard domain
// 2. dot wildcard domain
func (t *DomainTrie) Search(domain string) *Node {
func (t *DomainTrie[T]) Search(domain string) *Node[T] {
parts, valid := ValidAndSplitDomain(domain)
if !valid || parts[0] == "" {
return nil
@ -96,26 +96,26 @@ func (t *DomainTrie) Search(domain string) *Node {
n := t.search(t.root, parts)
if n == nil || n.Data == nil {
if n == nil || n.Data == getZero[T]() {
return nil
}
return n
}
func (t *DomainTrie) search(node *Node, parts []string) *Node {
func (t *DomainTrie[T]) search(node *Node[T], parts []string) *Node[T] {
if len(parts) == 0 {
return node
}
if c := node.getChild(parts[len(parts)-1]); c != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
return n
}
}
if c := node.getChild(wildcard); c != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != nil {
if n := t.search(c, parts[:len(parts)-1]); n != nil && n.Data != getZero[T]() {
return n
}
}
@ -124,6 +124,6 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
}
// New returns a new, empty Trie.
func New() *DomainTrie {
return &DomainTrie{root: newNode(nil)}
func New[T comparable]() *DomainTrie[T] {
return &DomainTrie[T]{root: newNode[T](getZero[T]())}
}

View File

@ -1,16 +1,16 @@
package trie
import (
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
var localIP = net.IP{127, 0, 0, 1}
var localIP = netip.AddrFrom4([4]byte{127, 0, 0, 1})
func TestTrie_Basic(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
domains := []string{
"example.com",
"google.com",
@ -23,7 +23,7 @@ func TestTrie_Basic(t *testing.T) {
node := tree.Search("example.com")
assert.NotNil(t, node)
assert.True(t, node.Data.(net.IP).Equal(localIP))
assert.True(t, node.Data == localIP)
assert.NotNil(t, tree.Insert("", localIP))
assert.Nil(t, tree.Search(""))
assert.NotNil(t, tree.Search("localhost"))
@ -31,7 +31,7 @@ func TestTrie_Basic(t *testing.T) {
}
func TestTrie_Wildcard(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
domains := []string{
"*.example.com",
"sub.*.example.com",
@ -64,7 +64,7 @@ func TestTrie_Wildcard(t *testing.T) {
}
func TestTrie_Priority(t *testing.T) {
tree := New()
tree := New[int]()
domains := []string{
".dev",
"example.dev",
@ -79,18 +79,18 @@ func TestTrie_Priority(t *testing.T) {
}
for idx, domain := range domains {
tree.Insert(domain, idx)
tree.Insert(domain, idx+1)
}
assertFn("test.dev", 0)
assertFn("foo.bar.dev", 0)
assertFn("example.dev", 1)
assertFn("foo.example.dev", 2)
assertFn("test.example.dev", 3)
assertFn("test.dev", 1)
assertFn("foo.bar.dev", 1)
assertFn("example.dev", 2)
assertFn("foo.example.dev", 3)
assertFn("test.example.dev", 4)
}
func TestTrie_Boundary(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
tree.Insert("*.dev", localIP)
assert.NotNil(t, tree.Insert(".", localIP))
@ -99,7 +99,7 @@ func TestTrie_Boundary(t *testing.T) {
}
func TestTrie_WildcardBoundary(t *testing.T) {
tree := New()
tree := New[netip.Addr]()
tree.Insert("+.*", localIP)
tree.Insert("stun.*.*.*", localIP)

View File

@ -1,26 +1,31 @@
package trie
// Node is the trie's node
type Node struct {
children map[string]*Node
Data any
type Node[T comparable] struct {
children map[string]*Node[T]
Data T
}
func (n *Node) getChild(s string) *Node {
func (n *Node[T]) getChild(s string) *Node[T] {
return n.children[s]
}
func (n *Node) hasChild(s string) bool {
func (n *Node[T]) hasChild(s string) bool {
return n.getChild(s) != nil
}
func (n *Node) addChild(s string, child *Node) {
func (n *Node[T]) addChild(s string, child *Node[T]) {
n.children[s] = child
}
func newNode(data any) *Node {
return &Node{
func newNode[T comparable](data T) *Node[T] {
return &Node[T]{
Data: data,
children: map[string]*Node{},
children: map[string]*Node[T]{},
}
}
func getZero[T comparable]() T {
var result T
return result
}

View File

@ -4,16 +4,21 @@ import (
"container/list"
"errors"
"fmt"
R "github.com/Dreamacro/clash/rule"
RP "github.com/Dreamacro/clash/rule/provider"
"github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"net"
"net/netip"
"net/url"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/Dreamacro/clash/common/utils"
R "github.com/Dreamacro/clash/rule"
RP "github.com/Dreamacro/clash/rule/provider"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup"
@ -26,8 +31,8 @@ import (
"github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant"
providerTypes "github.com/Dreamacro/clash/constant/provider"
snifferTypes "github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log"
T "github.com/Dreamacro/clash/tunnel"
@ -46,6 +51,8 @@ type General struct {
RoutingMark int `json:"-"`
GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"`
Tun Tun `json:"tun"`
}
// Inbound config
@ -78,7 +85,7 @@ type DNS struct {
EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie
Hosts *trie.DomainTrie[netip.Addr]
NameServerPolicy map[string]dns.NameServer
ProxyServerNameserver []dns.NameServer
}
@ -87,7 +94,7 @@ type DNS struct {
type FallbackFilter struct {
GeoIP bool `yaml:"geoip"`
GeoIPCode string `yaml:"geoip-code"`
IPCIDR []*net.IPNet `yaml:"ipcidr"`
IPCIDR []*netip.Prefix `yaml:"ipcidr"`
Domain []string `yaml:"domain"`
GeoSite []*router.DomainMatcher `yaml:"geosite"`
}
@ -106,17 +113,12 @@ type Profile struct {
// Tun config
type Tun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
}
// Script config
type Script struct {
MainCode string `yaml:"code" json:"code"`
ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"`
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"`
}
// IPTables config
@ -126,6 +128,16 @@ type IPTables struct {
Bypass []string `yaml:"bypass" json:"bypass"`
}
type Sniffer struct {
Enable bool
Force bool
Sniffers []sniffer.Type
Reverses *trie.DomainTrie[bool]
ForceDomain *trie.DomainTrie[bool]
SkipDomain *trie.DomainTrie[bool]
Ports *[]utils.Range[uint16]
}
// Experimental config
type Experimental struct{}
@ -136,13 +148,14 @@ type Config struct {
IPTables *IPTables
DNS *DNS
Experimental *Experimental
Hosts *trie.DomainTrie
Hosts *trie.DomainTrie[netip.Addr]
Profile *Profile
Rules []C.Rule
Users []auth.AuthUser
Proxies map[string]C.Proxy
Providers map[string]providerTypes.ProxyProvider
RuleProviders map[string]*providerTypes.RuleProvider
RuleProviders map[string]providerTypes.RuleProvider
Sniffer *Sniffer
}
type RawDNS struct {
@ -170,11 +183,12 @@ type RawFallbackFilter struct {
}
type RawTun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface"`
}
type RawConfig struct {
@ -197,7 +211,9 @@ type RawConfig struct {
RoutingMark int `yaml:"routing-mark"`
GeodataMode bool `yaml:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader"`
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
Sniffer SnifferRaw `yaml:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]string `yaml:"hosts"`
@ -211,6 +227,17 @@ type RawConfig struct {
Rule []string `yaml:"rules"`
}
type SnifferRaw struct {
Enable bool `yaml:"enable" json:"enable"`
Sniffing []string `yaml:"sniffing" json:"sniffing"`
Force bool `yaml:"force" json:"force"`
Reverse []string `yaml:"reverses" json:"reverses"`
ForceDomain []string `yaml:"force-domain" json:"force-domain"`
SkipDomain []string `yaml:"skip-domain" json:"skip-domain"`
SkipSNI []string `yaml:"skip-sni" json:"skip-sni"`
Ports []string `yaml:"port-whitelist" json:"port-whitelist"`
}
// Parse config
func Parse(buf []byte) (*Config, error) {
rawCfg, err := UnmarshalRawConfig(buf)
@ -236,12 +263,14 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Rule: []string{},
Proxy: []map[string]any{},
ProxyGroup: []map[string]any{},
TCPConcurrent: false,
Tun: RawTun{
Enable: false,
Device: "",
Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true,
Enable: false,
Device: "",
Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: false,
AutoDetectInterface: false,
},
IPTables: IPTables{
Enable: false,
@ -275,6 +304,15 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"www.msftconnecttest.com",
},
},
Sniffer: SnifferRaw{
Enable: false,
Force: false,
Sniffing: []string{},
Reverse: []string{},
ForceDomain: []string{},
SkipDomain: []string{},
Ports: []string{},
},
Profile: Profile{
StoreSelected: true,
},
@ -337,6 +375,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Users = parseAuthentication(rawCfg.Authentication)
config.Sniffer, err = parseSniffer(rawCfg.Sniffer)
if err != nil {
return nil, err
}
elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm
return config, nil
@ -377,6 +420,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
RoutingMark: cfg.RoutingMark,
GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent,
}, nil
}
@ -483,8 +527,8 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return proxies, providersMap, nil
}
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]*providerTypes.RuleProvider, error) {
ruleProviders := map[string]*providerTypes.RuleProvider{}
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]providerTypes.RuleProvider, error) {
ruleProviders := map[string]providerTypes.RuleProvider{}
log.Infoln("Geodata Loader mode: %s", geodata.LoaderName())
// parse rule provider
for name, mapping := range cfg.RuleProvider {
@ -493,13 +537,12 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return nil, nil, err
}
ruleProviders[name] = &rp
ruleProviders[name] = rp
RP.SetRuleProvider(rp)
}
var rules []C.Rule
rulesConfig := cfg.Rule
mode := cfg.Mode
// parse rules
for idx, line := range rulesConfig {
@ -511,10 +554,6 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
ruleName = strings.ToUpper(rule[0])
)
if mode == T.Script && ruleName != "GEOSITE" {
continue
}
l := len(rule)
if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" {
@ -538,7 +577,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
params = rule[l:]
}
if _, ok := proxies[target]; mode != T.Script && !ok {
if _, ok := proxies[target]; !ok {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
}
@ -555,9 +594,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return nil, nil, fmt.Errorf("rules[%d] [%s] error: %s", idx, line, parseErr.Error())
}
if mode != T.Script {
rules = append(rules, parsed)
}
rules = append(rules, parsed)
}
runtime.GC()
@ -565,18 +602,18 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return rules, ruleProviders, nil
}
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) {
tree := trie.New()
func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
tree := trie.New[netip.Addr]()
// add default hosts
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil {
if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil {
log.Errorln("insert localhost to host error: %s", err.Error())
}
if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr)
if ip == nil {
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return nil, fmt.Errorf("%s is not a valid IP", ipStr)
}
_ = tree.Insert(domain, ip)
@ -651,7 +688,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
Net: dnsNetType,
Addr: addr,
ProxyAdapter: u.Fragment,
Interface: dialer.DefaultInterface.Load(),
Interface: dialer.DefaultInterface,
},
)
}
@ -675,15 +712,15 @@ func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServe
return policy, nil
}
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
var ipNets []*net.IPNet
func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
var ipNets []*netip.Prefix
for idx, ip := range ips {
_, ipnet, err := net.ParseCIDR(ip)
ipnet, err := netip.ParsePrefix(ip)
if err != nil {
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
}
ipNets = append(ipNets, ipnet)
ipNets = append(ipNets, &ipnet)
}
return ipNets, nil
@ -724,7 +761,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil
}
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, error) {
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) {
cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@ -736,7 +773,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
IPv6: cfg.IPv6,
EnhancedMode: cfg.EnhancedMode,
FallbackFilter: FallbackFilter{
IPCIDR: []*net.IPNet{},
IPCIDR: []*netip.Prefix{},
GeoSite: []*router.DomainMatcher{},
},
}
@ -767,20 +804,23 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
for _, ns := range dnsCfg.DefaultNameserver {
host, _, err := net.SplitHostPort(ns.Addr)
if err != nil || net.ParseIP(host) == nil {
return nil, errors.New("default nameserver should be pure IP")
u, err := url.Parse(ns.Addr)
if err != nil || net.ParseIP(u.Host) == nil {
return nil, errors.New("default nameserver should be pure IP")
}
}
}
if cfg.EnhancedMode == C.DNSFakeIP {
_, ipnet, err := net.ParseCIDR(cfg.FakeIPRange)
ipnet, err := netip.ParsePrefix(cfg.FakeIPRange)
if err != nil {
return nil, err
}
var host *trie.DomainTrie
var host *trie.DomainTrie[bool]
// fake ip skip host filter
if len(cfg.FakeIPFilter) != 0 {
host = trie.New()
host = trie.New[bool]()
for _, domain := range cfg.FakeIPFilter {
_ = host.Insert(domain, true)
}
@ -788,7 +828,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
if len(dnsCfg.Fallback) != 0 {
if host == nil {
host = trie.New()
host = trie.New[bool]()
}
for _, fb := range dnsCfg.Fallback {
if net.ParseIP(fb.Addr) != nil {
@ -799,7 +839,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
}
pool, err := fakeip.New(fakeip.Options{
IPNet: ipnet,
IPNet: &ipnet,
Size: 1000,
Host: host,
Persistence: rawCfg.Profile.StoreFakeIP,
@ -843,9 +883,9 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
}
func parseTun(rawTun RawTun, general *General) (*Tun, error) {
if (rawTun.Enable || general.TProxyPort != 0) && general.Interface == "" {
if rawTun.Enable && rawTun.AutoDetectInterface {
autoDetectInterfaceName, err := commons.GetAutoDetectInterface()
if err != nil || autoDetectInterfaceName == "" {
if err != nil {
log.Warnln("Can not find auto detect interface.[%s]", err)
} else {
log.Warnln("Auto detect interface: %s", autoDetectInterfaceName)
@ -860,7 +900,7 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
if _, after, ok := strings.Cut(d, "://"); ok {
d = after
}
d = strings.Replace(d, "any", "0.0.0.0", 1)
addrPort, err := netip.ParseAddrPort(d)
if err != nil {
return nil, fmt.Errorf("parse dns-hijack url error: %w", err)
@ -870,10 +910,108 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
}
return &Tun{
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
AutoDetectInterface: rawTun.AutoDetectInterface,
}, nil
}
func parseSniffer(snifferRaw SnifferRaw) (*Sniffer, error) {
sniffer := &Sniffer{
Enable: snifferRaw.Enable,
Force: snifferRaw.Force,
}
var ports []utils.Range[uint16]
if len(snifferRaw.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](0, 65535))
} else {
for _, portRange := range snifferRaw.Ports {
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))
}
}
}
sniffer.Ports = &ports
loadSniffer := make(map[snifferTypes.Type]struct{})
for _, snifferName := range snifferRaw.Sniffing {
find := false
for _, snifferType := range snifferTypes.List {
if snifferType.String() == strings.ToUpper(snifferName) {
find = true
loadSniffer[snifferType] = struct{}{}
}
}
if !find {
return nil, fmt.Errorf("not find the sniffer[%s]", snifferName)
}
}
for st := range loadSniffer {
sniffer.Sniffers = append(sniffer.Sniffers, st)
}
sniffer.ForceDomain = trie.New[bool]()
for _, domain := range snifferRaw.ForceDomain {
err := sniffer.ForceDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
}
if snifferRaw.SkipSNI != nil {
log.Warnln("Sniffer param skip-sni renamed to ship-domain, old param will be removed in the release version")
snifferRaw.SkipDomain = snifferRaw.SkipSNI
}
sniffer.SkipDomain = trie.New[bool]()
for _, domain := range snifferRaw.SkipDomain {
err := sniffer.SkipDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
}
// Compatibility, remove it when release
if strings.Contains(C.Version, "alpha") || strings.Contains(C.Version, "develop") || strings.Contains(C.Version, "1.10.0") {
log.Warnln("Sniffer param force and reverses deprecated, will be removed in the release version, see https://github.com/MetaCubeX/Clash.Meta/commit/48a01adb7a4f38974b9d9639f931d0d245aebf28")
if snifferRaw.Force {
// match all domain
sniffer.ForceDomain.Insert("+", true)
for _, domain := range snifferRaw.Reverse {
err := sniffer.SkipDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s], error:%v", domain, err)
}
}
} else {
for _, domain := range snifferRaw.Reverse {
err := sniffer.ForceDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s], error:%v", domain, err)
}
}
}
}
return sniffer, nil
}

View File

@ -99,6 +99,10 @@ type ProxyAdapter interface {
DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error)
ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, error)
// SupportUOT return UDP over TCP support
SupportUOT() bool
ListenPacketOnStreamConn(c net.Conn, metadata *Metadata) (PacketConn, error)
// Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract.
Unwrap(metadata *Metadata) Proxy
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net"
"net/netip"
"strconv"
)
@ -73,19 +74,24 @@ func (t Type) MarshalJSON() ([]byte, error) {
// Metadata is used to store connection address
type Metadata struct {
NetWork NetWork `json:"network"`
Type Type `json:"type"`
SrcIP net.IP `json:"sourceIP"`
DstIP net.IP `json:"destinationIP"`
SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"`
AddrType int `json:"-"`
Host string `json:"host"`
DNSMode DNSMode `json:"dnsMode"`
Process string `json:"process"`
ProcessPath string `json:"processPath"`
NetWork NetWork `json:"network"`
Type Type `json:"type"`
SrcIP netip.Addr `json:"sourceIP"`
DstIP netip.Addr `json:"destinationIP"`
SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"`
AddrType int `json:"-"`
Host string `json:"host"`
DNSMode DNSMode `json:"dnsMode"`
Uid *int32 `json:"uid"`
Process string `json:"process"`
ProcessPath string `json:"processPath"`
RemoteDst string `json:"remoteDestination"`
}
// avoid stack overflow
type jsonMetadata Metadata
func (m *Metadata) RemoteAddress() string {
return net.JoinHostPort(m.String(), m.DstPort)
}
@ -95,45 +101,49 @@ func (m *Metadata) SourceAddress() string {
}
func (m *Metadata) SourceDetail() string {
if m.Process != "" {
if m.Type == INNER {
return fmt.Sprintf("[%s]", ClashName)
}
if m.Process != "" && m.Uid != nil {
return fmt.Sprintf("%s(%s, uid=%d)", m.SourceAddress(), m.Process, *m.Uid)
} else if m.Uid != nil {
return fmt.Sprintf("%s(uid=%d)", m.SourceAddress(), *m.Uid)
} else if m.Process != "" {
return fmt.Sprintf("%s(%s)", m.SourceAddress(), m.Process)
} else {
if m.Type == INNER {
return fmt.Sprintf("[Clash]")
}
return fmt.Sprintf("%s", m.SourceAddress())
}
}
func (m *Metadata) Resolved() bool {
return m.DstIP != nil
return m.DstIP.IsValid()
}
// Pure is used to solve unexpected behavior
// when dialing proxy connection in DNSMapping mode.
func (m *Metadata) Pure() *Metadata {
if m.DNSMode == DNSMapping && m.DstIP != nil {
copy := *m
copy.Host = ""
if copy.DstIP.To4() != nil {
copy.AddrType = AtypIPv4
if m.DNSMode == DNSMapping && m.DstIP.IsValid() {
copyM := *m
copyM.Host = ""
if copyM.DstIP.Is4() {
copyM.AddrType = AtypIPv4
} else {
copy.AddrType = AtypIPv6
copyM.AddrType = AtypIPv6
}
return &copy
return &copyM
}
return m
}
func (m *Metadata) UDPAddr() *net.UDPAddr {
if m.NetWork != UDP || m.DstIP == nil {
if m.NetWork != UDP || !m.DstIP.IsValid() {
return nil
}
port, _ := strconv.ParseUint(m.DstPort, 10, 16)
return &net.UDPAddr{
IP: m.DstIP,
IP: m.DstIP.AsSlice(),
Port: int(port),
}
}
@ -141,7 +151,7 @@ func (m *Metadata) UDPAddr() *net.UDPAddr {
func (m *Metadata) String() string {
if m.Host != "" {
return m.Host
} else if m.DstIP != nil {
} else if m.DstIP.IsValid() {
return m.DstIP.String()
} else {
return "<nil>"
@ -149,5 +159,5 @@ func (m *Metadata) String() string {
}
func (m *Metadata) Valid() bool {
return m.Host != "" || m.DstIP != nil
return m.Host != "" || m.DstIP.IsValid()
}

View File

@ -29,7 +29,6 @@ var Path = func() *path {
type path struct {
homeDir string
configFile string
scriptDir string
}
// SetHomeDir is used to set the configuration path
@ -123,23 +122,6 @@ func (p *path) GeoSite() string {
return P.Join(p.homeDir, "GeoSite.dat")
}
func (p *path) ScriptDir() string {
if len(p.scriptDir) != 0 {
return p.scriptDir
}
if dir, err := os.MkdirTemp("", Name+"-"); err == nil {
p.scriptDir = dir
} else {
p.scriptDir = P.Join(os.TempDir(), Name)
_ = os.MkdirAll(p.scriptDir, 0o644)
}
return p.scriptDir
}
func (p *path) Script() string {
return P.Join(p.ScriptDir(), "clash_script.py")
}
func (p *path) GetAssetLocation(file string) string {
return P.Join(p.homeDir, file)
}

View File

@ -70,6 +70,7 @@ type ProxyProvider interface {
// Commonly used in DialContext and DialPacketConn
ProxiesWithTouch() []constant.Proxy
HealthCheck()
Version() uint
}
// Rule Type

View File

@ -13,9 +13,9 @@ const (
DstPort
Process
ProcessPath
Script
RuleSet
Network
Uid
MATCH
AND
OR
@ -48,14 +48,14 @@ func (rt RuleType) String() string {
return "Process"
case ProcessPath:
return "ProcessPath"
case Script:
return "Script"
case MATCH:
return "Match"
case RuleSet:
return "RuleSet"
case Network:
return "Network"
case Uid:
return "Uid"
case AND:
return "AND"
case OR:

View File

@ -1,7 +1,7 @@
package constant
import (
"net"
"net/netip"
"strings"
"github.com/Dreamacro/clash/component/geodata/router"
@ -9,7 +9,7 @@ import (
type RuleExtra struct {
Network NetWork
SourceIPs []*net.IPNet
SourceIPs []*netip.Prefix
ProcessNames []string
}
@ -17,7 +17,7 @@ func (re *RuleExtra) NotMatchNetwork(network NetWork) bool {
return re.Network != ALLNet && re.Network != network
}
func (re *RuleExtra) NotMatchSourceIP(srcIP net.IP) bool {
func (re *RuleExtra) NotMatchSourceIP(srcIP netip.Addr) bool {
if re.SourceIPs == nil {
return false
}

View File

@ -0,0 +1,31 @@
package sniffer
import "github.com/Dreamacro/clash/constant"
type Sniffer interface {
SupportNetwork() constant.NetWork
SniffTCP(bytes []byte) (string, error)
Protocol() string
}
const (
TLS Type = iota
HTTP
)
var (
List = []Type{TLS, HTTP}
)
type Type int
func (rt Type) String() string {
switch rt {
case TLS:
return "TLS"
case HTTP:
return "HTTP"
default:
return "Unknown"
}
}

View File

@ -3,8 +3,8 @@ package context
import (
"net"
CN "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
)
@ -19,7 +19,7 @@ func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext {
return &ConnContext{
id: id,
metadata: metadata,
conn: conn,
conn: CN.NewBufferedConn(conn),
}
}

View File

@ -4,7 +4,9 @@ import (
"context"
"crypto/tls"
"fmt"
"go.uber.org/atomic"
"net"
"net/netip"
"strings"
"github.com/Dreamacro/clash/component/dialer"
@ -18,7 +20,7 @@ type client struct {
r *Resolver
port string
host string
iface string
iface *atomic.String
proxyAdapter string
}
@ -28,10 +30,10 @@ func (c *client) Exchange(m *D.Msg) (*D.Msg, error) {
func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
var (
ip net.IP
ip netip.Addr
err error
)
if ip = net.ParseIP(c.host); ip == nil {
if ip, err = netip.ParseAddr(c.host); err != nil {
if c.r == nil {
return nil, fmt.Errorf("dns %s not a valid ip", c.host)
} else {
@ -48,8 +50,8 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
}
options := []dialer.Option{}
if c.iface != "" {
options = append(options, dialer.WithInterface(c.iface))
if c.iface != nil && c.iface.Load() != "" {
options = append(options, dialer.WithInterface(c.iface.Load()))
}
var conn net.Conn
@ -62,7 +64,9 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
if err != nil {
return nil, err
}
defer conn.Close()
defer func() {
_ = conn.Close()
}()
// miekg/dns ExchangeContext doesn't respond to context cancel.
// this is a workaround

View File

@ -1,9 +1,10 @@
package dns
import (
"bytes"
"context"
"go.uber.org/atomic"
"net"
"net/netip"
"sync"
"time"
@ -27,7 +28,7 @@ type dhcpClient struct {
ifaceInvalidate time.Time
dnsInvalidate time.Time
ifaceAddr *net.IPNet
ifaceAddr *netip.Prefix
done chan struct{}
resolver *Resolver
err error
@ -71,7 +72,7 @@ func (d *dhcpClient) resolve(ctx context.Context) (*Resolver, error) {
for _, item := range dns {
nameserver = append(nameserver, NameServer{
Addr: net.JoinHostPort(item.String(), "53"),
Interface: d.ifaceName,
Interface: atomic.NewString(d.ifaceName),
})
}
@ -127,12 +128,12 @@ func (d *dhcpClient) invalidate() (bool, error) {
return false, err
}
addr, err := ifaceObj.PickIPv4Addr(nil)
addr, err := ifaceObj.PickIPv4Addr(netip.Addr{})
if err != nil {
return false, err
}
if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr.IP.Equal(addr.IP) && bytes.Equal(d.ifaceAddr.Mask, addr.Mask) {
if time.Now().Before(d.dnsInvalidate) && d.ifaceAddr == addr {
return false, nil
}

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