Compare commits

..

204 Commits

Author SHA1 Message Date
8b09db5f7f fix: Rule-Set中不解析DNS
feat: RULE-SET支持no-resolve
2022-05-18 18:43:44 +08:00
b5623602f5 chore: Android auto-detect-interface plus 2022-05-18 12:00:57 +08:00
16b27b3a1f fix: doq过代理错误 2022-05-17 21:30:54 +08:00
8b00be9039 fix: 删除udp触发的错误逻辑 2022-05-17 21:23:28 +08:00
fa9e27c5e4 refactor: 重构失败主动健康检测 2022-05-17 21:15:14 +08:00
f4d9384603 chore: debug log print dns result 2022-05-17 18:21:18 +08:00
c4408612b3 chore: 暴露数据给前端 2022-05-17 16:47:21 +08:00
0742f7db26 refactor: 重构StickySessions 2022-05-17 13:28:54 +08:00
891c2fe899 fix: 当dns被禁用时,dns将根据general ipv6设置解析dns 2022-05-17 09:01:41 +08:00
b831eb178b chore: remove noisy log 2022-05-16 18:20:13 +08:00
962ceaa89e refactor: strategyStickySessions 2022-05-16 17:46:28 +08:00
d52b00bd34 refactor: remove useless code 2022-05-16 17:29:08 +08:00
aa0d174ccb fix: strategyStickySessions nil pointer 2022-05-16 17:06:44 +08:00
b8e9c3d55a fix: geoip ReverseMatch 2022-05-16 17:06:44 +08:00
0b4c498c93 refactor: new way to get interface for android 2022-05-16 17:06:44 +08:00
efc7c82cac feat: "!"(not) support for geosite
eg. GEOSITE,!CN,Proxy & dns.fallback-filter.geosite: ['!CN']
2022-05-15 13:16:45 +08:00
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
182 changed files with 4852 additions and 2295 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: Release name: Prerelease
on: [push] on:
push:
branches:
- Alpha
- Beta
pull_request:
branches:
- Alpha
- Beta
jobs: jobs:
Feature-build: Build:
if: ${{ !contains(github.event.head_commit.message, '[Skip CI]') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Get latest go version - name: Get latest go version
@ -24,9 +31,13 @@ jobs:
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: | restore-keys: |
${{ runner.os }}-go- ${{ runner.os }}-go-
# - name: Get dependencies, run test
# run: |
# go test ./... - name: Test
if: ${{github.ref_name=='Beta'}}
run: |
go test ./...
- name: Build - name: Build
if: success() if: success()
env: env:
@ -38,34 +49,22 @@ jobs:
uses: andreaswilli/delete-release-assets-action@v2.0.0 uses: andreaswilli/delete-release-assets-action@v2.0.0
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
tag: alpha tag: Prerelease-${{ github.ref_name }}
deleteOnlyFromDrafts: false deleteOnlyFromDrafts: false
- name: Tag Repo - name: Tag Repo
uses: richardsimko/update-tag@v1 uses: richardsimko/update-tag@v1
with: with:
tag_name: v1.10.0 tag_name: Prerelease-${{ github.ref_name }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release - name: Upload Alpha
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ env.GIT_BRANCH == 'Meta' && success() }} if: ${{ success() }}
with: with:
tag: ${{ github.ref }} tag: ${{ github.ref_name }}
tag_name: v1.10.0 tag_name: Prerelease-${{ github.ref_name }}
files: bin/* files: bin/*
prerelease: false prerelease: true
generate_release_notes: 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}}

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 FROM golang:alpine as builder
RUN apk add --no-cache make git && \ RUN apk add --no-cache make git && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb mkdir /clash-config && \
WORKDIR /clash-src wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
COPY --from=tonistiigi/xx:golang / / 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 COPY . /clash-src
RUN go mod download && \ WORKDIR /clash-src
make docker && \ RUN go mod download &&\
mv ./bin/clash-docker /clash make docker &&\
mv ./bin/Clash.Meta-docker /clash
FROM alpine:latest 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 RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash / VOLUME ["/root/.config/clash/"]
ENTRYPOINT ["/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 NAME=Clash.Meta
BINDIR=bin BINDIR=bin
BRANCH=$(shell git rev-parse --abbrev-ref HEAD) BRANCH=$(shell git branch --show-current)
VERSION=$(shell git describe --tags || echo "unknown version") 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) BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
@ -43,7 +52,7 @@ all:linux-amd64 linux-arm64\
windows-amd64 windows-arm64\ windows-amd64 windows-arm64\
docker: docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v3: darwin-amd64v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -150,4 +159,4 @@ lint:
golangci-lint run ./... golangci-lint run ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/gofrs/uuid"
"net" "net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
@ -17,6 +18,7 @@ type Base struct {
tp C.AdapterType tp C.AdapterType
udp bool udp bool
rmark int rmark int
id string
} }
// Name implements C.ProxyAdapter // Name implements C.ProxyAdapter
@ -24,6 +26,20 @@ func (b *Base) Name() string {
return b.name return b.name
} }
// Id implements C.ProxyAdapter
func (b *Base) Id() string {
if b.id == "" {
id, err := uuid.NewV6()
if err != nil {
b.id = b.name
} else {
b.id = id.String()
}
}
return b.id
}
// Type implements C.ProxyAdapter // Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType { func (b *Base) Type() C.AdapterType {
return b.tp return b.tp
@ -39,6 +55,16 @@ func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, op
return nil, errors.New("no support") 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 // SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool { func (b *Base) SupportUDP() bool {
return b.udp return b.udp
@ -48,6 +74,7 @@ func (b *Base) SupportUDP() bool {
func (b *Base) MarshalJSON() ([]byte, error) { func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{ return json.Marshal(map[string]string{
"type": b.Type().String(), "type": b.Type().String(),
"id": b.Id(),
}) })
} }

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 user string
pass string pass string
tlsConfig *tls.Config tlsConfig *tls.Config
option *HttpOption
} }
type HttpOption struct { type HttpOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UserName string `proxy:"username,omitempty"` UserName string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"` Password string `proxy:"password,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // 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 != "" { if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass auth := h.user + ":" + h.pass
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
@ -141,5 +150,6 @@ func NewHttp(option HttpOption) *Http {
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tlsConfig: tlsConfig, 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{} type nopConn struct{}
func (rw *nopConn) Read(b []byte) (int, error) { 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) { 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)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
password := option.Password password := option.Password
@ -103,13 +109,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int ivSize int
key []byte key []byte
) )
if option.Cipher == "dummy" { if option.Cipher == "dummy" {
ivSize = 0 ivSize = 0
key = core.Kdf(option.Password, 16) key = core.Kdf(option.Password, 16)
} else { } else {
ciph, ok := coreCiph.(*core.StreamCipher) ciph, ok := coreCiph.(*core.StreamCipher)
if !ok { 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() ivSize = ciph.IVSize()
key = ciph.Key key = ciph.Key

View File

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

View File

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

View File

@ -22,8 +22,8 @@ var (
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok { if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true) _ = tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second) _ = tcp.SetKeepAlivePeriod(30 * time.Second)
} }
} }
@ -48,14 +48,14 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType { switch metadata.AddrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
len := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port} buf = [][]byte{{aType, lenM}, host, port}
case socks5.AtypIPv4: case socks5.AtypIPv4:
host := metadata.DstIP.To4() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6: case socks5.AtypIPv6:
host := metadata.DstIP.To16() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{aType}, host, port}
} }
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
@ -76,6 +76,6 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
func safeConnClose(c net.Conn, err error) { func safeConnClose(c net.Conn, err error) {
if err != nil { 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/gun"
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
) )
const ( const (
@ -35,7 +33,7 @@ type Vless struct {
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *http2.Transport transport *gun.TransportWrap
} }
type VlessOption struct { 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 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 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 { func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
var addrType byte var addrType byte
var addr []byte var addr []byte
switch metadata.AddrType { switch metadata.AddrType {
case C.AtypIPv4: case C.AtypIPv4:
addrType = byte(vless.AtypIPv4) addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6: case C.AtypIPv6:
addrType = byte(vless.AtypIPv6) addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName: case C.AtypDomainName:
addrType = byte(vless.AtypDomainName) addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host)) addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host)) copy(addr[1:], metadata.Host)
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)

View File

@ -15,8 +15,6 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2"
) )
type Vmess struct { type Vmess struct {
@ -27,7 +25,7 @@ type Vmess struct {
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *http2.Transport transport *gun.TransportWrap
} }
type VmessOption struct { 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 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 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) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{ client, err := vmess.NewClient(vmess.Config{
@ -342,11 +350,11 @@ func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
case C.AtypIPv4: case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4) addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6: case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6) addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName: case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName) addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1) 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 ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
type Fallback struct { type Fallback struct {
*outbound.Base *GroupBase
disableUDP bool disableUDP bool
filter string
single *singledo.Single
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (f *Fallback) Now() string { 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...)...) c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
f.failedTimes.Store(-1) f.onDialSuccess()
f.failedTime.Store(-1)
} else { } else {
f.onDialFailed() f.onDialFailed()
} }
@ -50,41 +39,11 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...) pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
f.onDialFailed()
} }
return pc, err 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 // SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool { func (f *Fallback) SupportUDP() bool {
if f.disableUDP { if f.disableUDP {
@ -98,7 +57,7 @@ func (f *Fallback) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) { func (f *Fallback) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range f.proxies(false) { for _, proxy := range f.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -114,16 +73,8 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
return 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 { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch) proxies := f.GetProxies(touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Alive() { if proxy.Alive() {
return proxy return proxy
@ -135,17 +86,16 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{ return &Fallback{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.Fallback, Name: option.Name,
Interface: option.Interface, Type: C.Fallback,
RoutingMark: option.RoutingMark, Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration), disableUDP: option.DisableUDP,
providers: providers,
disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
} }

View File

@ -0,0 +1,164 @@
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
failedTestMux sync.Mutex
failedTimes int
failedTime time.Time
failedTesting *atomic.Bool
}
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,
failedTesting: atomic.NewBool(false),
}
}
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.failedTesting.Load() {
return
}
go func() {
gb.failedTestMux.Lock()
defer gb.failedTestMux.Unlock()
gb.failedTimes++
if gb.failedTimes == 1 {
log.Warnln("ProxyGroup: %s first failed", gb.Name())
gb.failedTime = time.Now()
} else {
if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
return
}
log.Warnln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes() {
gb.failedTesting.Store(true)
log.Warnln("because %s failed multiple times, active health check", gb.Name())
wg := sync.WaitGroup{}
for _, proxyProvider := range gb.providers {
wg.Add(1)
proxyProvider := proxyProvider
go func() {
defer wg.Done()
proxyProvider.HealthCheck()
}()
}
wg.Wait()
gb.failedTesting.Store(false)
gb.failedTimes = 0
}
}
}()
}
func (gb *GroupBase) failedIntervalTime() int64 {
return 5 * time.Second.Milliseconds()
}
func (gb *GroupBase) onDialSuccess() {
if !gb.failedTesting.Load() {
gb.failedTimes = 0
}
}
func (gb *GroupBase) maxFailedTimes() int {
return 5
}
func (gb *GroupBase) failedTimeoutInterval() time.Duration {
return 5 * time.Second
}

View File

@ -5,11 +5,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/common/cache"
"net" "net"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/murmur3" "github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
@ -20,11 +21,8 @@ import (
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct { type LoadBalance struct {
*outbound.Base *GroupBase
disableUDP bool disableUDP bool
single *singledo.Single
filter string
providers []provider.ProxyProvider
strategyFn strategyFn strategyFn strategyFn
} }
@ -40,6 +38,10 @@ func parseStrategy(config map[string]any) string {
} }
func getKey(metadata *C.Metadata) string { func getKey(metadata *C.Metadata) string {
if metadata == nil {
return ""
}
if metadata.Host != "" { if metadata.Host != "" {
// ip host // ip host
if ip := net.ParseIP(metadata.Host); ip != nil { if ip := net.ParseIP(metadata.Host); ip != nil {
@ -51,13 +53,23 @@ func getKey(metadata *C.Metadata) string {
} }
} }
if metadata.DstIP == nil { if !metadata.DstIP.IsValid() {
return "" return ""
} }
return metadata.DstIP.String() return metadata.DstIP.String()
} }
func getKeyWithSrcAndDst(metadata *C.Metadata) string {
dst := getKey(metadata)
src := ""
if metadata != nil {
src = metadata.SrcIP.String()
}
return fmt.Sprintf("%s%s", src, dst)
}
func jumpHash(key uint64, buckets int32) int32 { func jumpHash(key uint64, buckets int32) int32 {
var b, j int64 var b, j int64
@ -75,6 +87,9 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
defer func() { defer func() {
if err == nil { if err == nil {
c.AppendToChains(lb) c.AppendToChains(lb)
lb.onDialSuccess()
} else {
lb.onDialFailed()
} }
}() }()
@ -134,24 +149,45 @@ func strategyConsistentHashing() strategyFn {
} }
} }
// Unwrap implements C.ProxyAdapter func strategyStickySessions() strategyFn {
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy { ttl := time.Minute * 10
proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata) c := cache.New[uint64, int](1 * time.Second)
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
length := len(proxies)
idx, expireTime := c.GetWithExpire(key)
if expireTime.IsZero() {
idx = int(jumpHash(key+uint64(time.Now().UnixMilli()), int32(length)))
}
for i := 0; i < length; i++ {
nowIdx := (idx + 1) % length
proxy := proxies[nowIdx]
if proxy.Alive() {
if nowIdx != idx {
c.Put(key, idx, -1)
c.Put(key, nowIdx, ttl)
}
return proxy
}
}
return proxies[0]
}
} }
func (lb *LoadBalance) proxies(touch bool) []C.Proxy { // Unwrap implements C.ProxyAdapter
elm, _, _ := lb.single.Do(func() (any, error) { func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
return getProvidersProxies(lb.providers, touch, lb.filter), nil proxies := lb.GetProxies(true)
}) return lb.strategyFn(proxies, metadata)
return elm.([]C.Proxy)
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) { func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all := []string{} var all []string
for _, proxy := range lb.proxies(false) { for _, proxy := range lb.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -167,20 +203,23 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
strategyFn = strategyConsistentHashing() strategyFn = strategyConsistentHashing()
case "round-robin": case "round-robin":
strategyFn = strategyRoundRobin() strategyFn = strategyRoundRobin()
case "sticky-sessions":
strategyFn = strategyStickySessions()
default: default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy) return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
} }
return &LoadBalance{ return &LoadBalance{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.LoadBalance, Name: option.Name,
Interface: option.Interface, Type: C.LoadBalance,
RoutingMark: option.RoutingMark, Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
filter: option.Filter,
}, nil }, nil
} }

View File

@ -6,27 +6,18 @@ import (
"fmt" "fmt"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
type Relay struct { type Relay struct {
*outbound.Base *GroupBase
single *singledo.Single
providers []provider.ProxyProvider
filter string
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
var proxies []C.Proxy proxies, chainProxies := r.proxies(metadata, true)
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
proxies = append(proxies, proxy)
}
}
switch len(proxies) { switch len(proxies) {
case 0: 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 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 // MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range r.rawProxies(false) { for _, proxy := range r.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -79,38 +143,49 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
}) })
} }
func (r *Relay) rawProxies(touch bool) []C.Proxy { func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
elm, _, _ := r.single.Do(func() (any, error) { rawProxies := r.GetProxies(touch)
return getProvidersProxies(r.providers, touch, r.filter), nil
})
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 { for n, proxy := range rawProxies {
proxies := r.rawProxies(touch) proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy)
for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata) subproxy := proxy.Unwrap(metadata)
for subproxy != nil { for subproxy != nil {
chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata) 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 { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.Relay, Name: option.Name,
Interface: option.Interface, Type: C.Relay,
RoutingMark: option.RoutingMark, Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
"",
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
filter: option.Filter,
} }
} }

View File

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

View File

@ -3,8 +3,6 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
@ -23,16 +21,11 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
} }
type URLTest struct { type URLTest struct {
*outbound.Base *GroupBase
tolerance uint16 tolerance uint16
disableUDP bool disableUDP bool
fastNode C.Proxy fastNode C.Proxy
filter string fastSingle *singledo.Single[C.Proxy]
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
@ -44,8 +37,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
u.failedTimes.Store(-1) u.onDialSuccess()
u.failedTime.Store(-1)
} else { } else {
u.onDialFailed() u.onDialFailed()
} }
@ -57,11 +49,8 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...) pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
u.onDialFailed()
} }
return pc, err return pc, err
} }
@ -70,17 +59,9 @@ func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(true) 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 { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (any, error) { elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.proxies(touch) proxies := u.GetProxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
fastNotExist := true fastNotExist := true
@ -109,7 +90,7 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil return u.fastNode, nil
}) })
return elm.(C.Proxy) return elm
} }
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
@ -124,7 +105,7 @@ func (u *URLTest) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) { func (u *URLTest) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range u.proxies(false) { for _, proxy := range u.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -134,32 +115,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 { func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{} opts := []urlTestOption{}
@ -175,19 +130,19 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{ urlTest := &URLTest{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.URLTest, Name: option.Name,
Interface: option.Interface, Type: C.URLTest,
RoutingMark: option.RoutingMark, Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
option.Filter,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
fastSingle: singledo.NewSingle(time.Second * 10), disableUDP: option.DisableUDP,
providers: providers,
disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
for _, option := range options { for _, option := range options {

View File

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

View File

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

View File

@ -65,14 +65,14 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { 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 { for _, proxy := range hc.proxies {
p := proxy p := proxy
b.Go(p.Name(), func() (any, error) { b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel() defer cancel()
p.URLTest(ctx, hc.url) _, _ = p.URLTest(ctx, hc.url)
return nil, nil return false, nil
}) })
} }
b.Wait() b.Wait()

View File

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

View File

@ -2,7 +2,6 @@ package provider
import ( import (
"context" "context"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/listener/inner"
"io" "io"
"net" "net"
@ -58,7 +57,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
} }
req, err := http.NewRequest(http.MethodGet, uri.String(), nil) 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 { if err != nil {
return nil, err return nil, err
@ -74,7 +73,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
transport := &http.Transport{ transport := &http.Transport{
// from http.DefaultTransport // from http.DefaultTransport
MaxIdleConns: 100, MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
@ -86,10 +85,6 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
client := http.Client{Transport: transport} client := http.Client{Transport: transport}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { 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 { if err != nil {
return nil, err 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" "sync"
) )
type Option = func(b *Batch) type Option[T any] func(b *Batch[T])
type Result struct { type Result[T any] struct {
Value any Value T
Err error Err error
} }
@ -17,8 +17,8 @@ type Error struct {
Err error Err error
} }
func WithConcurrencyNum(n int) Option { func WithConcurrencyNum[T any](n int) Option[T] {
return func(b *Batch) { return func(b *Batch[T]) {
q := make(chan struct{}, n) q := make(chan struct{}, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
q <- struct{}{} q <- struct{}{}
@ -28,8 +28,8 @@ func WithConcurrencyNum(n int) Option {
} }
// Batch similar to errgroup, but can control the maximum number of concurrent // Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct { type Batch[T any] struct {
result map[string]Result result map[string]Result[T]
queue chan struct{} queue chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
mux sync.Mutex mux sync.Mutex
@ -38,7 +38,7 @@ type Batch struct {
cancel func() 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) b.wg.Add(1)
go func() { go func() {
defer b.wg.Done() 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() b.mux.Lock()
defer b.mux.Unlock() defer b.mux.Unlock()
b.result[key] = ret b.result[key] = ret
}() }()
} }
func (b *Batch) Wait() *Error { func (b *Batch[T]) Wait() *Error {
b.wg.Wait() b.wg.Wait()
if b.cancel != nil { if b.cancel != nil {
b.cancel() b.cancel()
@ -74,26 +74,26 @@ func (b *Batch) Wait() *Error {
return b.err return b.err
} }
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) { func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) {
err := b.Wait() err := b.Wait()
return b.Result(), err return b.Result(), err
} }
func (b *Batch) Result() map[string]Result { func (b *Batch[T]) Result() map[string]Result[T] {
b.mux.Lock() b.mux.Lock()
defer b.mux.Unlock() defer b.mux.Unlock()
copy := map[string]Result{} copyM := map[string]Result[T]{}
for k, v := range b.result { 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) ctx, cancel := context.WithCancel(ctx)
b := &Batch{ b := &Batch[T]{
result: map[string]Result{}, result: map[string]Result[T]{},
} }
for _, o := range opts { for _, o := range opts {

View File

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

48
common/cache/cache.go vendored
View File

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

View File

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

View File

@ -3,49 +3,50 @@ package cache
// Modified by https://github.com/die-net/lrucache // Modified by https://github.com/die-net/lrucache
import ( import (
"container/list"
"sync" "sync"
"time" "time"
"github.com/Dreamacro/clash/common/generics/list"
) )
// Option is part of Functional Options Pattern // 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 // 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 // WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option { func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.onEvict = cb l.onEvict = cb
} }
} }
// WithUpdateAgeOnGet update expires when Get element // WithUpdateAgeOnGet update expires when Get element
func WithUpdateAgeOnGet() Option { func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.updateAgeOnGet = true l.updateAgeOnGet = true
} }
} }
// WithAge defined element max age (second) // WithAge defined element max age (second)
func WithAge(maxAge int64) Option { func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.maxAge = maxAge l.maxAge = maxAge
} }
} }
// WithSize defined max length of LruCache // WithSize defined max length of LruCache
func WithSize(maxSize int) Option { func WithSize[K comparable, V any](maxSize int) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.maxSize = maxSize l.maxSize = maxSize
} }
} }
// WithStale decide whether Stale return is enabled. // WithStale decide whether Stale return is enabled.
// If this feature is enabled, element will not get Evicted according to `WithAge`. // If this feature is enabled, element will not get Evicted according to `WithAge`.
func WithStale(stale bool) Option { func WithStale[K comparable, V any](stale bool) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.staleReturn = stale l.staleReturn = stale
} }
} }
@ -53,22 +54,22 @@ func WithStale(stale bool) Option {
// LruCache is a thread-safe, in-memory lru-cache that evicts the // LruCache is a thread-safe, in-memory lru-cache that evicts the
// least recently used entries from memory when (if set) the entries are // least recently used entries from memory when (if set) the entries are
// older than maxAge (in seconds). Use the New constructor to create one. // older than maxAge (in seconds). Use the New constructor to create one.
type LruCache struct { type LruCache[K comparable, V any] struct {
maxAge int64 maxAge int64
maxSize int maxSize int
mu sync.Mutex mu sync.Mutex
cache map[any]*list.Element cache map[K]*list.Element[*entry[K, V]]
lru *list.List // Front is least-recent lru *list.List[*entry[K, V]] // Front is least-recent
updateAgeOnGet bool updateAgeOnGet bool
staleReturn bool staleReturn bool
onEvict EvictCallback onEvict EvictCallback[K, V]
} }
// NewLRUCache creates an LruCache // NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache { func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache{ lc := &LruCache[K, V]{
lru: list.New(), lru: list.New[*entry[K, V]](),
cache: make(map[any]*list.Element), cache: make(map[K]*list.Element[*entry[K, V]]),
} }
for _, option := range options { 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 // Get returns the any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache) Get(key any) (any, bool) { func (c *LruCache[K, V]) Get(key K) (V, bool) {
entry := c.get(key) el := c.get(key)
if entry == nil { if el == nil {
return nil, false return getZero[V](), false
} }
value := entry.value value := el.value
return value, true return value, true
} }
@ -94,17 +95,17 @@ func (c *LruCache) Get(key any) (any, bool) {
// a time.Time Give expected expires, // a time.Time Give expected expires,
// and a bool set to true if the key was found. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key any) (any, time.Time, bool) { func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
entry := c.get(key) el := c.get(key)
if entry == nil { if el == nil {
return nil, time.Time{}, false 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 // 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() c.mu.Lock()
defer c.mu.Unlock() 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. // 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) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge 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. // SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second. // 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() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value.(*entry) e := le.Value
e.value = value e.value = value
e.expires = expires.Unix() e.expires = expires.Unix()
} else { } 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) c.cache[key] = c.lru.PushBack(e)
if c.maxSize > 0 { 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()) 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 // 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() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
n.lru = list.New() n.lru = list.New[*entry[K, V]]()
n.cache = make(map[any]*list.Element) n.cache = make(map[K]*list.Element[*entry[K, V]])
for e := c.lru.Front(); e != nil; e = e.Next() { 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) 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() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -172,7 +173,7 @@ func (c *LruCache) get(key any) *entry {
return nil 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.deleteElement(le)
c.maybeDeleteOldest() c.maybeDeleteOldest()
@ -180,15 +181,15 @@ func (c *LruCache) get(key any) *entry {
} }
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
entry := le.Value.(*entry) el := le.Value
if c.maxAge > 0 && c.updateAgeOnGet { 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. // 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() c.mu.Lock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
@ -198,35 +199,40 @@ func (c *LruCache) Delete(key any) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *LruCache) maybeDeleteOldest() { func (c *LruCache[K, V]) maybeDeleteOldest() {
if !c.staleReturn && c.maxAge > 0 { if !c.staleReturn && c.maxAge > 0 {
now := time.Now().Unix() 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) 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) c.lru.Remove(le)
e := le.Value.(*entry) e := le.Value
delete(c.cache, e.key) delete(c.cache, e.key)
if c.onEvict != nil { if c.onEvict != nil {
c.onEvict(e.key, e.value) c.onEvict(e.key, e.value)
} }
} }
func (c *LruCache) Clear() error { func (c *LruCache[K, V]) Clear() error {
c.mu.Lock() c.mu.Lock()
c.cache = make(map[any]*list.Element) c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock() c.mu.Unlock()
return nil return nil
} }
type entry struct { type entry[K comparable, V any] struct {
key any key K
value any value V
expires int64 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) { func TestLRUCache(t *testing.T) {
c := NewLRUCache() c := NewLRUCache[string, string]()
for _, e := range entries { for _, e := range entries {
c.Set(e.key, e.value) c.Set(e.key, e.value)
@ -32,7 +32,7 @@ func TestLRUCache(t *testing.T) {
for _, e := range entries { for _, e := range entries {
value, ok := c.Get(e.key) value, ok := c.Get(e.key)
if assert.True(t, ok) { 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) { func TestLRUMaxAge(t *testing.T) {
c := NewLRUCache(WithAge(86400)) c := NewLRUCache[string, string](WithAge[string, string](86400))
now := time.Now().Unix() now := time.Now().Unix()
expected := now + 86400 expected := now + 86400
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = now c.lru.Back().Value.expires = now
// Reset // Reset
c.Set("foo", "bar") c.Set("foo", "bar")
e := c.lru.Back().Value.(*entry) e := c.lru.Back().Value
assert.True(t, e.expires >= now) 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 // Set a few and verify expiration times
for _, s := range entries { for _, s := range entries {
c.Set(s.key, s.value) 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) assert.True(t, e.expires >= expected && e.expires <= expected+10)
} }
@ -77,7 +77,7 @@ func TestLRUMaxAge(t *testing.T) {
for _, s := range entries { for _, s := range entries {
le, ok := c.cache[s.key] le, ok := c.cache[s.key]
if assert.True(t, ok) { 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) { 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() now := time.Now().Unix()
expires := now + 86400/2 expires := now + 86400/2
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
c.lru.Back().Value.(*entry).expires = expires c.lru.Back().Value.expires = expires
_, ok := c.Get("foo") _, ok := c.Get("foo")
assert.True(t, ok) 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) { func TestMaxSize(t *testing.T) {
c := NewLRUCache(WithSize(2)) c := NewLRUCache[string, string](WithSize[string, string](2))
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
_, ok := c.Get("foo") _, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
} }
func TestExist(t *testing.T) { func TestExist(t *testing.T) {
c := NewLRUCache(WithSize(1)) c := NewLRUCache[int, int](WithSize[int, int](1))
c.Set(1, 2) c.Set(1, 2)
assert.True(t, c.Exist(1)) assert.True(t, c.Exist(1))
c.Set(2, 3) c.Set(2, 3)
@ -126,11 +126,11 @@ func TestExist(t *testing.T) {
func TestEvict(t *testing.T) { func TestEvict(t *testing.T) {
temp := 0 temp := 0
evict := func(key any, value any) { evict := func(key int, value int) {
temp = 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(1, 2)
c.Set(2, 3) c.Set(2, 3)
@ -138,21 +138,22 @@ func TestEvict(t *testing.T) {
} }
func TestSetWithExpire(t *testing.T) { func TestSetWithExpire(t *testing.T) {
c := NewLRUCache(WithAge(1)) c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
now := time.Now().Unix() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) 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 is expected not to exist, and expires should be empty time.Time
res, expires, exist := c.GetWithExpire(1) 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, time.Time{}, expires)
assert.Equal(t, false, exist) assert.Equal(t, false, exist)
} }
func TestStale(t *testing.T) { 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() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) tenSecBefore := time.Unix(now-10, 0)
@ -165,11 +166,11 @@ func TestStale(t *testing.T) {
} }
func TestCloneTo(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("1", 1)
o.Set("2", 2) o.Set("2", 2)
n := NewLRUCache(WithSize(2)) n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3) n.Set("3", 3)
n.Set("4", 4) 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 package observable
type Iterable <-chan any type Iterable[T any] <-chan T

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import (
// Picker provides synchronization, and Context cancelation // Picker provides synchronization, and Context cancelation
// for groups of goroutines working on subtasks of a common task. // for groups of goroutines working on subtasks of a common task.
// Inspired by errGroup // Inspired by errGroup
type Picker struct { type Picker[T any] struct {
ctx context.Context ctx context.Context
cancel func() cancel func()
@ -17,12 +17,12 @@ type Picker struct {
once sync.Once once sync.Once
errOnce sync.Once errOnce sync.Once
result any result T
err error err error
} }
func newPicker(ctx context.Context, cancel func()) *Picker { func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
return &Picker{ return &Picker[T]{
ctx: ctx, ctx: ctx,
cancel: cancel, 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. // WithContext returns a new Picker and an associated Context derived from ctx.
// and cancel when first element return. // 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) 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. // 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) 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, // Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them. // 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() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
@ -52,13 +52,13 @@ func (p *Picker) Wait() any {
} }
// Error return the first error (if all success return nil) // Error return the first error (if all success return nil)
func (p *Picker) Error() error { func (p *Picker[T]) Error() error {
return p.err return p.err
} }
// Go calls the given function in a new goroutine. // 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. // 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) p.wg.Add(1)
go func() { go func() {

View File

@ -8,33 +8,38 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) { func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) {
return func() (any, error) { return func() (T, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay)) timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select { select {
case <-timer.C: case <-timer.C:
return input, nil return input, nil
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return getZero[T](), ctx.Err()
} }
} }
} }
func TestPicker_Basic(t *testing.T) { 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, 30, 2))
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.NotNil(t, number) assert.NotNil(t, number)
assert.Equal(t, number.(int), 1) assert.Equal(t, number, 1)
} }
func TestPicker_Timeout(t *testing.T) { 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)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.Nil(t, number) assert.Equal(t, number, getZero[int]())
assert.NotNil(t, picker.Error()) 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 // Queue is a simple concurrent safe queue
type Queue struct { type Queue[T any] struct {
items []any items []T
lock sync.RWMutex lock sync.RWMutex
} }
// Put add the item to the queue. // Put add the item to the queue.
func (q *Queue) Put(items ...any) { func (q *Queue[T]) Put(items ...T) {
if len(items) == 0 { if len(items) == 0 {
return return
} }
@ -22,9 +22,9 @@ func (q *Queue) Put(items ...any) {
} }
// Pop returns the head of items. // Pop returns the head of items.
func (q *Queue) Pop() any { func (q *Queue[T]) Pop() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return nil return GetZero[T]()
} }
q.lock.Lock() q.lock.Lock()
@ -35,9 +35,9 @@ func (q *Queue) Pop() any {
} }
// Last returns the last of item. // Last returns the last of item.
func (q *Queue) Last() any { func (q *Queue[T]) Last() T {
if len(q.items) == 0 { if len(q.items) == 0 {
return nil return GetZero[T]()
} }
q.lock.RLock() q.lock.RLock()
@ -47,8 +47,8 @@ func (q *Queue) Last() any {
} }
// Copy get the copy of queue. // Copy get the copy of queue.
func (q *Queue) Copy() []any { func (q *Queue[T]) Copy() []T {
items := []any{} items := []T{}
q.lock.RLock() q.lock.RLock()
items = append(items, q.items...) items = append(items, q.items...)
q.lock.RUnlock() q.lock.RUnlock()
@ -56,7 +56,7 @@ func (q *Queue) Copy() []any {
} }
// Len returns the number of items in this queue. // Len returns the number of items in this queue.
func (q *Queue) Len() int64 { func (q *Queue[T]) Len() int64 {
q.lock.Lock() q.lock.Lock()
defer q.lock.Unlock() defer q.lock.Unlock()
@ -64,8 +64,13 @@ func (q *Queue) Len() int64 {
} }
// New is a constructor for a new concurrent safe queue. // New is a constructor for a new concurrent safe queue.
func New(hint int64) *Queue { func New[T any](hint int64) *Queue[T] {
return &Queue{ return &Queue[T]{
items: make([]any, 0, hint), items: make([]T, 0, hint),
} }
} }
func GetZero[T any]() T {
var result T
return result
}

View File

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

View File

@ -10,13 +10,13 @@ import (
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
shardCount := atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (any, error) { call := func() (int, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
return nil, nil return 0, nil
} }
var wg sync.WaitGroup var wg sync.WaitGroup
@ -38,32 +38,32 @@ func TestBasic(t *testing.T) {
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
call := func() (any, error) { callM := func() (int, error) {
foo++ foo++
return nil, nil return 0, nil
} }
single.Do(call) _, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
_, _, shard := single.Do(call) _, _, shard := single.Do(callM)
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
assert.True(t, shard) assert.True(t, shard)
} }
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
call := func() (any, error) { callM := func() (int, error) {
foo++ foo++
return nil, nil return 0, nil
} }
single.Do(call) _, _, _ = single.Do(callM)
single.Reset() single.Reset()
single.Do(call) _, _, _ = single.Do(callM)
assert.Equal(t, 2, foo) 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 // Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error { func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr { 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() t := reflect.TypeOf(dst).Elem()
v := reflect.ValueOf(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 { for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
} }
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, 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 { if err := d.decode(fieldName, currentData, currentField); err != nil {
return err return err
} }
@ -291,7 +301,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField field reflect.StructField
val reflect.Value val reflect.Value
} }
fields := []field{} var fields []field
for len(structs) > 0 { for len(structs) > 0 {
structVal := structs[0] structVal := structs[0]
structs = structs[1:] 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" "context"
"errors" "errors"
"net" "net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -15,14 +17,16 @@ var (
ErrNotFound = errors.New("DNS option not found") 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) conn, err := ListenDHCPClient(context, ifaceName)
if err != nil { if err != nil {
return nil, err 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) ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil { 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) defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize) buf := make([]byte, dhcpv4.MaxMessageSize)
@ -77,11 +81,17 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
} }
dns := pkt.DNS() dns := pkt.DNS()
if len(dns) == 0 { l := len(dns)
if l == 0 {
return return
} }
result <- dns dnsAddr := make([]netip.Addr, l)
for i := 0; i < l; i++ {
dnsAddr[i] = nnip.IpToAddr(dns[i])
}
result <- dnsAddr
return return
} }

View File

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

View File

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

View File

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

View File

@ -3,11 +3,21 @@ package dialer
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"net/netip"
"sync"
"github.com/Dreamacro/clash/component/resolver" "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) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := &option{ opt := &option{
interfaceName: DefaultInterface.Load(), interfaceName: DefaultInterface.Load(),
@ -24,33 +34,9 @@ func DialContext(ctx context.Context, network, address string, options ...Option
switch network { switch network {
case "tcp4", "tcp6", "udp4", "udp6": case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address) return actualSingleDialContext(ctx, network, address, opt)
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)
case "tcp", "udp": case "tcp", "udp":
return dualStackDialContext(ctx, network, address, opt) return actualDualStackDialContext(ctx, network, address, opt)
default: default:
return nil, errors.New("network invalid") 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) 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{} dialer := &net.Dialer{}
if opt.interfaceName != "" { if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { 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) 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)) 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 results <- result:
case <-returned: case <-returned:
if result.Conn != nil { if result.Conn != nil {
result.Conn.Close() _ = result.Conn.Close()
} }
} }
}() }()
var ip net.IP var ip netip.Addr
if ipv6 { if ipv6 {
if !direct { if !direct {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host) 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") 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 ( import (
"net" "net"
"net/netip"
"syscall" "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) 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) lc.Control = bindMarkToControl(mark, lc.Control)
} }
@ -23,20 +24,17 @@ func bindMarkToControl(mark int, chain controlFn) controlFn {
} }
}() }()
ipStr, _, err := net.SplitHostPort(address) addrPort, err := netip.ParseAddrPort(address)
if err == nil { if err == nil && !addrPort.Addr().IsGlobalUnicast() {
ip := net.ParseIP(ipStr) return
if ip != nil && !ip.IsGlobalUnicast() {
return
}
} }
return c.Control(func(fd uintptr) { return c.Control(func(fd uintptr) {
switch network { switch network {
case "tcp4", "udp4": 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": 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 ( import (
"net" "net"
"net/netip"
"sync" "sync"
"github.com/Dreamacro/clash/log" "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() printMarkWarn()
} }
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) { func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
printMarkWarn() printMarkWarn()
} }

View File

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

View File

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

View File

@ -1,40 +1,37 @@
package fakeip package fakeip
import ( import (
"net" "net/netip"
"github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/common/cache"
) )
type memoryStore struct { type memoryStore struct {
cache *cache.LruCache cacheIP *cache.LruCache[string, netip.Addr]
cacheHost *cache.LruCache[netip.Addr, string]
} }
// GetByHost implements store.GetByHost // GetByHost implements store.GetByHost
func (m *memoryStore) GetByHost(host string) (net.IP, bool) { func (m *memoryStore) GetByHost(host string) (netip.Addr, bool) {
if elm, exist := m.cache.Get(host); exist { if ip, exist := m.cacheIP.Get(host); exist {
ip := elm.(net.IP)
// ensure ip --> host on head of linked list // ensure ip --> host on head of linked list
m.cache.Get(ipToUint(ip.To4())) m.cacheHost.Get(ip)
return ip, true return ip, true
} }
return nil, false return netip.Addr{}, false
} }
// PutByHost implements store.PutByHost // PutByHost implements store.PutByHost
func (m *memoryStore) PutByHost(host string, ip net.IP) { func (m *memoryStore) PutByHost(host string, ip netip.Addr) {
m.cache.Set(host, ip) m.cacheIP.Set(host, ip)
} }
// GetByIP implements store.GetByIP // GetByIP implements store.GetByIP
func (m *memoryStore) GetByIP(ip net.IP) (string, bool) { func (m *memoryStore) GetByIP(ip netip.Addr) (string, bool) {
if elm, exist := m.cache.Get(ipToUint(ip.To4())); exist { if host, exist := m.cacheHost.Get(ip); exist {
host := elm.(string)
// ensure host --> ip on head of linked list // ensure host --> ip on head of linked list
m.cache.Get(host) m.cacheIP.Get(host)
return host, true return host, true
} }
@ -42,33 +39,41 @@ func (m *memoryStore) GetByIP(ip net.IP) (string, bool) {
} }
// PutByIP implements store.PutByIP // PutByIP implements store.PutByIP
func (m *memoryStore) PutByIP(ip net.IP, host string) { func (m *memoryStore) PutByIP(ip netip.Addr, host string) {
m.cache.Set(ipToUint(ip.To4()), host) m.cacheHost.Set(ip, host)
} }
// DelByIP implements store.DelByIP // DelByIP implements store.DelByIP
func (m *memoryStore) DelByIP(ip net.IP) { func (m *memoryStore) DelByIP(ip netip.Addr) {
ipNum := ipToUint(ip.To4()) if host, exist := m.cacheHost.Get(ip); exist {
if elm, exist := m.cache.Get(ipNum); exist { m.cacheIP.Delete(host)
m.cache.Delete(elm.(string))
} }
m.cache.Delete(ipNum) m.cacheHost.Delete(ip)
} }
// Exist implements store.Exist // Exist implements store.Exist
func (m *memoryStore) Exist(ip net.IP) bool { func (m *memoryStore) Exist(ip netip.Addr) bool {
return m.cache.Exist(ipToUint(ip.To4())) return m.cacheHost.Exist(ip)
} }
// CloneTo implements store.CloneTo // CloneTo implements store.CloneTo
// only for memoryStore to memoryStore // only for memoryStore to memoryStore
func (m *memoryStore) CloneTo(store store) { func (m *memoryStore) CloneTo(store store) {
if ms, ok := store.(*memoryStore); ok { 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 // FlushFakeIP implements store.FlushFakeIP
func (m *memoryStore) FlushFakeIP() error { 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 ( import (
"errors" "errors"
"net" "net/netip"
"sync" "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/profile/cachefile"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
const (
offsetKey = "key-offset-fake-ip"
cycleKey = "key-cycle-fake-ip"
)
type store interface { type store interface {
GetByHost(host string) (net.IP, bool) GetByHost(host string) (netip.Addr, bool)
PutByHost(host string, ip net.IP) PutByHost(host string, ip netip.Addr)
GetByIP(ip net.IP) (string, bool) GetByIP(ip netip.Addr) (string, bool)
PutByIP(ip net.IP, host string) PutByIP(ip netip.Addr, host string)
DelByIP(ip net.IP) DelByIP(ip netip.Addr)
Exist(ip net.IP) bool Exist(ip netip.Addr) bool
CloneTo(store) CloneTo(store)
FlushFakeIP() error FlushFakeIP() error
} }
// Pool is a implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
max uint32 gateway netip.Addr
min uint32 first netip.Addr
gateway uint32 last netip.Addr
broadcast uint32 offset netip.Addr
offset uint32 cycle bool
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie host *trie.DomainTrie[bool]
ipnet *net.IPNet ipnet *netip.Prefix
store store store store
} }
// Lookup return a fake ip with host // 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() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip, exist := p.store.GetByHost(host); exist { 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 // 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() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
return p.store.GetByIP(ip) 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 // 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() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return false
}
return p.store.Exist(ip) return p.store.Exist(ip)
} }
// Gateway return gateway ip // Gateway return gateway ip
func (p *Pool) Gateway() net.IP { func (p *Pool) Gateway() netip.Addr {
return uintToIP(p.gateway) return p.gateway
} }
// Broadcast return broadcast ip // Broadcast return the last ip
func (p *Pool) Broadcast() net.IP { func (p *Pool) Broadcast() netip.Addr {
return uintToIP(p.broadcast) return p.last
} }
// IPNet return raw ipnet // IPNet return raw ipnet
func (p *Pool) IPNet() *net.IPNet { func (p *Pool) IPNet() *netip.Prefix {
return p.ipnet return p.ipnet
} }
@ -99,47 +96,61 @@ func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store) o.store.CloneTo(p.store)
} }
func (p *Pool) get(host string) net.IP { func (p *Pool) get(host string) netip.Addr {
current := p.offset p.offset = p.offset.Next()
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
}
ip := uintToIP(p.min + p.offset - 1) if !p.offset.Less(p.last) {
if !p.store.Exist(ip) { p.cycle = true
break p.offset = p.first
}
} }
ip := uintToIP(p.min + p.offset - 1)
p.store.PutByIP(ip, host) if p.cycle || p.store.Exist(p.offset) {
return ip p.store.DelByIP(p.offset)
}
p.store.PutByIP(p.offset, host)
return p.offset
} }
func (p *Pool) FlushFakeIP() error { 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 { func (p *Pool) StoreState() {
v := uint32(ip[0]) << 24 if s, ok := p.store.(*cachefileStore); ok {
v += uint32(ip[1]) << 16 s.PutByHost(offsetKey, p.offset)
v += uint32(ip[2]) << 8 if p.cycle {
v += uint32(ip[3]) s.PutByHost(cycleKey, p.offset)
return v }
}
} }
func uintToIP(v uint32) net.IP { func (p *Pool) restoreState() {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)} 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 { type Options struct {
IPNet *net.IPNet IPNet *netip.Prefix
Host *trie.DomainTrie Host *trie.DomainTrie[bool]
// Size sets the maximum number of entries in memory // Size sets the maximum number of entries in memory
// and does not work if Persistence is true // and does not work if Persistence is true
@ -152,33 +163,35 @@ type Options struct {
// New return Pool instance // New return Pool instance
func New(options Options) (*Pool, error) { 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() if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {
total := 1<<uint(bits-ones) - 4
if total <= 0 {
return nil, errors.New("ipnet don't have valid ip") return nil, errors.New("ipnet don't have valid ip")
} }
max := min + uint32(total) - 1
pool := &Pool{ pool := &Pool{
min: min, gateway: gateway,
max: max, first: first,
gateway: min - 2, last: last,
broadcast: max + 1, offset: first.Prev(),
host: options.Host, cycle: false,
ipnet: options.IPNet, host: options.Host,
ipnet: options.IPNet,
} }
if options.Persistence { if options.Persistence {
pool.store = &cachefileStore{ pool.store = &cachefileStore{
cache: cachefile.Cache(), cache: cachefile.Cache(),
} }
} else { } else {
pool.store = &memoryStore{ pool.store = newMemoryStore(options.Size)
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
} }
pool.restoreState()
return pool, nil return pool, nil
} }

View File

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

View File

@ -33,9 +33,10 @@ func domainToMatcher(domain *Domain) (strmatcher.Matcher, error) {
type DomainMatcher struct { type DomainMatcher struct {
matchers strmatcher.IndexMatcher matchers strmatcher.IndexMatcher
not bool
} }
func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) { func NewMphMatcherGroup(domains []*Domain, not bool) (*DomainMatcher, error) {
g := strmatcher.NewMphMatcherGroup() g := strmatcher.NewMphMatcherGroup()
for _, d := range domains { for _, d := range domains {
matcherType, f := matcherTypeMap[d.Type] matcherType, f := matcherTypeMap[d.Type]
@ -50,11 +51,12 @@ func NewMphMatcherGroup(domains []*Domain) (*DomainMatcher, error) {
g.Build() g.Build()
return &DomainMatcher{ return &DomainMatcher{
matchers: g, matchers: g,
not: not,
}, nil }, nil
} }
// NewDomainMatcher new domain matcher. // NewDomainMatcher new domain matcher.
func NewDomainMatcher(domains []*Domain) (*DomainMatcher, error) { func NewDomainMatcher(domains []*Domain, not bool) (*DomainMatcher, error) {
g := new(strmatcher.MatcherGroup) g := new(strmatcher.MatcherGroup)
for _, d := range domains { for _, d := range domains {
m, err := domainToMatcher(d) m, err := domainToMatcher(d)
@ -66,11 +68,16 @@ func NewDomainMatcher(domains []*Domain) (*DomainMatcher, error) {
return &DomainMatcher{ return &DomainMatcher{
matchers: g, matchers: g,
not: not,
}, nil }, nil
} }
func (m *DomainMatcher) ApplyDomain(domain string) bool { func (m *DomainMatcher) ApplyDomain(domain string) bool {
return len(m.matchers.Match(strings.ToLower(domain))) > 0 isMatched := len(m.matchers.Match(strings.ToLower(domain))) > 0
if m.not {
isMatched = !isMatched
}
return isMatched
} }
// CIDRList is an alias of []*CIDR to provide sort.Interface. // CIDRList is an alias of []*CIDR to provide sort.Interface.

View File

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

View File

@ -1,9 +1,9 @@
package geodata package geodata
import ( import (
"fmt"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"strings"
) )
var geoLoaderName = "memconservative" var geoLoaderName = "memconservative"
@ -15,6 +15,9 @@ func LoaderName() string {
} }
func SetLoader(newLoader string) { func SetLoader(newLoader string) {
if newLoader == "memc" {
newLoader = "memconservative"
}
geoLoaderName = newLoader geoLoaderName = newLoader
} }
@ -32,6 +35,16 @@ func Verify(name string) bool {
} }
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
if len(countryCode) == 0 {
return nil, 0, fmt.Errorf("country code could not be empty")
}
not := false
if countryCode[0] == '!' {
not = true
countryCode = countryCode[1:]
}
geoLoader, err := GetGeoDataLoader(geoLoaderName) geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@ -47,7 +60,7 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
matcher, err := router.NewDomainMatcher(domains) matcher, err := router.NewDomainMatcher(domains)
mphminimal perfect hash algorithm mphminimal perfect hash algorithm
*/ */
matcher, err := router.NewMphMatcherGroup(domains) matcher, err := router.NewMphMatcherGroup(domains, not)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -56,12 +69,21 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
} }
func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) { func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) {
if len(country) == 0 {
return nil, 0, fmt.Errorf("country code could not be empty")
}
geoLoader, err := GetGeoDataLoader(geoLoaderName) geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
records, err := geoLoader.LoadGeoIP(strings.ReplaceAll(country, "!", "")) not := false
if country[0] == '!' {
not = true
country = country[1:]
}
records, err := geoLoader.LoadGeoIP(country)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -69,7 +91,7 @@ func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) {
geoIP := &router.GeoIP{ geoIP := &router.GeoIP{
CountryCode: country, CountryCode: country,
Cidr: records, Cidr: records,
ReverseMatch: strings.Contains(country, "!"), ReverseMatch: not,
} }
matcher, err := router.NewGeoIPMatcher(geoIP) matcher, err := router.NewGeoIPMatcher(geoIP)

View File

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

View File

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

View File

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

View File

@ -2,9 +2,11 @@ package process
import ( import (
"errors" "errors"
"net" "github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"net"
"net/netip"
"runtime"
) )
var ( var (
@ -18,32 +20,43 @@ const (
UDP = "udp" 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) 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 { func ShouldFindProcess(metadata *C.Metadata) bool {
if metadata.Process != "" { if runtime.GOOS == "android" {
return false
}
if metadata.Process != "" || metadata.ProcessPath != "" {
return false return false
} }
for _, ip := range localIPs { for _, ip := range localIPs {
if ip.Equal(metadata.SrcIP) { if ip == metadata.SrcIP {
return true return true
} }
} }
return false return false
} }
func AppendLocalIPs(ip ...net.IP) { func AppendLocalIPs(ip ...netip.Addr) {
localIPs = append(ip, localIPs...) localIPs = append(ip, localIPs...)
} }
func getLocalIPs() []net.IP { func getLocalIPs() []netip.Addr {
ips := []net.IP{net.IPv4zero, net.IPv6zero} ips := []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified()}
netInterfaces, err := net.Interfaces() netInterfaces, err := net.Interfaces()
if err != nil { 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 return ips
} }
@ -53,7 +66,7 @@ func getLocalIPs() []net.IP {
for _, address := range adds { for _, address := range adds {
if ipNet, ok := address.(*net.IPNet); ok { 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 return ips
} }
var localIPs []net.IP var localIPs []netip.Addr
func init() { func init() {
localIPs = getLocalIPs() 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 ( import (
"encoding/binary" "encoding/binary"
"net" "net/netip"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/Dreamacro/clash/common/nnip"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -15,7 +17,11 @@ const (
proccallnumpidinfo = 0x2 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 var spath string
switch network { switch network {
case TCP: case TCP:
@ -26,7 +32,7 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
return "", ErrInvalidNetwork return "", ErrInvalidNetwork
} }
isIPv4 := ip.To4() != nil isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath) value, err := syscall.Sysctl(spath)
if err != nil { if err != nil {
@ -57,19 +63,19 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
// xinpcb_n.inp_vflag // xinpcb_n.inp_vflag
flag := buf[inp+44] flag := buf[inp+44]
var srcIP net.IP var srcIP netip.Addr
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = net.IP(buf[inp+76 : inp+80]) srcIP = nnip.IpToAddr(buf[inp+76 : inp+80])
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = net.IP(buf[inp+64 : inp+80]) srcIP = nnip.IpToAddr(buf[inp+64 : inp+80])
default: default:
continue continue
} }
if !ip.Equal(srcIP) && (network == TCP || !srcIP.IsUnspecified()) { if ip != srcIP && (network == TCP || !srcIP.IsUnspecified()) {
continue continue
} }

View File

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

View File

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

View File

@ -2,8 +2,12 @@
package process 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 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 ( import (
"fmt" "fmt"
"net" "net/netip"
"sync" "sync"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@ -28,6 +29,10 @@ var (
once sync.Once once sync.Once
) )
func resolveSocketByNetlink(network string, ip netip.Addr, srcPort int) (int32, int32, error) {
return 0, 0, ErrPlatformNotSupport
}
func initWin32API() error { func initWin32API() error {
h, err := windows.LoadLibrary("iphlpapi.dll") h, err := windows.LoadLibrary("iphlpapi.dll")
if err != nil { if err != nil {
@ -57,7 +62,7 @@ func initWin32API() error {
return nil 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() { once.Do(func() {
err := initWin32API() err := initWin32API()
if err != nil { if err != nil {
@ -67,7 +72,7 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
} }
}) })
family := windows.AF_INET family := windows.AF_INET
if ip.To4() == nil { if ip.Is6() {
family = windows.AF_INET6 family = windows.AF_INET6
} }
@ -107,7 +112,7 @@ type searcher struct {
tcpState int 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])) n := int(readNativeUint32(b[:4]))
itemSize := s.itemSize itemSize := s.itemSize
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
@ -131,9 +136,9 @@ func (s *searcher) Search(b []byte, ip net.IP, port uint16) (uint32, error) {
continue 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 // 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 continue
} }
@ -214,8 +219,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
uintptr(h), uintptr(h),
uintptr(1), uintptr(1),
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&size)))
0, 0)
if r1 == 0 { if r1 == 0 {
return "", err 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 package resolver
import ( import "net/netip"
"net"
)
var DefaultHostMapper Enhancer var DefaultHostMapper Enhancer
type Enhancer interface { type Enhancer interface {
FakeIPEnabled() bool FakeIPEnabled() bool
MappingEnabled() bool MappingEnabled() bool
IsFakeIP(net.IP) bool IsFakeIP(netip.Addr) bool
IsFakeBroadcastIP(net.IP) bool IsFakeBroadcastIP(netip.Addr) bool
IsExistFakeIP(net.IP) bool IsExistFakeIP(netip.Addr) bool
FindHostByIP(net.IP) (string, bool) FindHostByIP(netip.Addr) (string, bool)
FlushFakeIP() error FlushFakeIP() error
InsertHostByIP(netip.Addr, string)
StoreFakePoolState()
} }
func FakeIPEnabled() bool { func FakeIPEnabled() bool {
@ -32,7 +32,7 @@ func MappingEnabled() bool {
return false return false
} }
func IsFakeIP(ip net.IP) bool { func IsFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeIP(ip) return mapper.IsFakeIP(ip)
} }
@ -40,7 +40,7 @@ func IsFakeIP(ip net.IP) bool {
return false return false
} }
func IsFakeBroadcastIP(ip net.IP) bool { func IsFakeBroadcastIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeBroadcastIP(ip) return mapper.IsFakeBroadcastIP(ip)
} }
@ -48,7 +48,7 @@ func IsFakeBroadcastIP(ip net.IP) bool {
return false return false
} }
func IsExistFakeIP(ip net.IP) bool { func IsExistFakeIP(ip netip.Addr) bool {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsExistFakeIP(ip) return mapper.IsExistFakeIP(ip)
} }
@ -56,7 +56,13 @@ func IsExistFakeIP(ip net.IP) bool {
return false 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 { if mapper := DefaultHostMapper; mapper != nil {
return mapper.FindHostByIP(ip) return mapper.FindHostByIP(ip)
} }
@ -70,3 +76,9 @@ func FlushFakeIP() error {
} }
return nil return nil
} }
func StoreFakePoolState() {
if mapper := DefaultHostMapper; mapper != nil {
mapper.StoreFakePoolState()
}
}

View File

@ -5,9 +5,10 @@ import (
"errors" "errors"
"math/rand" "math/rand"
"net" "net"
"strings" "net/netip"
"time" "time"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
@ -23,7 +24,7 @@ var (
DisableIPv6 = true DisableIPv6 = true
// DefaultHosts aim to resolve hosts // DefaultHosts aim to resolve hosts
DefaultHosts = trie.New() DefaultHosts = trie.New[netip.Addr]()
// DefaultDNSTimeout defined the default dns request timeout // DefaultDNSTimeout defined the default dns request timeout
DefaultDNSTimeout = time.Second * 5 DefaultDNSTimeout = time.Second * 5
@ -36,134 +37,57 @@ var (
) )
type Resolver interface { type Resolver interface {
ResolveIP(host string) (ip net.IP, err error) ResolveIP(host string) (ip netip.Addr, err error)
ResolveIPv4(host string) (ip net.IP, err error) ResolveIPv4(host string) (ip netip.Addr, err error)
ResolveIPv6(host string) (ip net.IP, 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 // ResolveIPv4 with a host, return ipv4
func ResolveIPv4(host string) (net.IP, error) { func ResolveIPv4(host string) (netip.Addr, error) {
return ResolveIPv4WithResolver(host, DefaultResolver) return ResolveIPv4WithResolver(host, DefaultResolver)
} }
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPv4WithResolver(host string, r Resolver) (netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil { if ips, err := ResolveAllIPv4WithResolver(host, r); err == nil {
if ip := node.Data.(net.IP).To4(); ip != nil { return ips[rand.Intn(len(ips))], nil
return ip, 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 // ResolveIPv6 with a host, return ipv6
func ResolveIPv6(host string) (net.IP, error) { func ResolveIPv6(host string) (netip.Addr, error) {
return ResolveIPv6WithResolver(host, DefaultResolver) return ResolveIPv6WithResolver(host, DefaultResolver)
} }
func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPv6WithResolver(host string, r Resolver) (netip.Addr, error) {
if DisableIPv6 { if ips, err := ResolveAllIPv6WithResolver(host, r); err == nil {
return nil, ErrIPv6Disabled 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 // ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) {
if node := DefaultHosts.Search(host); node != nil { if ips, err := ResolveAllIPPrimaryIPv4WithResolver(host, r); err == nil {
return node.Data.(net.IP), 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 // ResolveIP with a host, return ip
func ResolveIP(host string) (net.IP, error) { func ResolveIP(host string) (netip.Addr, error) {
return ResolveIPWithResolver(host, DefaultResolver) return ResolveIPWithResolver(host, DefaultResolver)
} }
// ResolveIPv4ProxyServerHost proxies server host only // ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPv4ProxyServerHost(host string) (net.IP, error) { func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil { if ProxyServerHostResolver != nil {
return ResolveIPv4WithResolver(host, ProxyServerHostResolver) return ResolveIPv4WithResolver(host, ProxyServerHostResolver)
} }
@ -171,7 +95,7 @@ func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
} }
// ResolveIPv6ProxyServerHost proxies server host only // ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (net.IP, error) { func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil { if ProxyServerHostResolver != nil {
return ResolveIPv6WithResolver(host, ProxyServerHostResolver) return ResolveIPv6WithResolver(host, ProxyServerHostResolver)
} }
@ -179,9 +103,188 @@ func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
} }
// ResolveProxyServerHost proxies server host only // ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (net.IP, error) { func ResolveProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil { if ProxyServerHostResolver != nil {
return ResolveIPWithResolver(host, ProxyServerHostResolver) return ResolveIPWithResolver(host, ProxyServerHostResolver)
} }
return ResolveIP(host) 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. // DomainTrie contains the main logic for adding and searching nodes for domain segments.
// support wildcard domain (e.g *.google.com) // support wildcard domain (e.g *.google.com)
type DomainTrie struct { type DomainTrie[T comparable] struct {
root *Node root *Node[T]
} }
func ValidAndSplitDomain(domain string) ([]string, bool) { func ValidAndSplitDomain(domain string) ([]string, bool) {
@ -51,7 +51,7 @@ func ValidAndSplitDomain(domain string) ([]string, bool) {
// 3. subdomain.*.example.com // 3. subdomain.*.example.com
// 4. .example.com // 4. .example.com
// 5. +.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) parts, valid := ValidAndSplitDomain(domain)
if !valid { if !valid {
return ErrInvalidDomain return ErrInvalidDomain
@ -68,13 +68,13 @@ func (t *DomainTrie) Insert(domain string, data any) error {
return nil return nil
} }
func (t *DomainTrie) insert(parts []string, data any) { func (t *DomainTrie[T]) insert(parts []string, data T) {
node := t.root node := t.root
// reverse storage domain part to save space // reverse storage domain part to save space
for i := len(parts) - 1; i >= 0; i-- { for i := len(parts) - 1; i >= 0; i-- {
part := parts[i] part := parts[i]
if !node.hasChild(part) { if !node.hasChild(part) {
node.addChild(part, newNode(nil)) node.addChild(part, newNode(getZero[T]()))
} }
node = node.getChild(part) node = node.getChild(part)
@ -88,7 +88,7 @@ func (t *DomainTrie) insert(parts []string, data any) {
// 1. static part // 1. static part
// 2. wildcard domain // 2. wildcard domain
// 2. dot 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) parts, valid := ValidAndSplitDomain(domain)
if !valid || parts[0] == "" { if !valid || parts[0] == "" {
return nil return nil
@ -96,26 +96,26 @@ func (t *DomainTrie) Search(domain string) *Node {
n := t.search(t.root, parts) n := t.search(t.root, parts)
if n == nil || n.Data == nil { if n == nil || n.Data == getZero[T]() {
return nil return nil
} }
return n 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 { if len(parts) == 0 {
return node return node
} }
if c := node.getChild(parts[len(parts)-1]); c != nil { 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 return n
} }
} }
if c := node.getChild(wildcard); c != nil { 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 return n
} }
} }
@ -124,6 +124,6 @@ func (t *DomainTrie) search(node *Node, parts []string) *Node {
} }
// New returns a new, empty Trie. // New returns a new, empty Trie.
func New() *DomainTrie { func New[T comparable]() *DomainTrie[T] {
return &DomainTrie{root: newNode(nil)} return &DomainTrie[T]{root: newNode[T](getZero[T]())}
} }

View File

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

View File

@ -1,26 +1,31 @@
package trie package trie
// Node is the trie's node // Node is the trie's node
type Node struct { type Node[T comparable] struct {
children map[string]*Node children map[string]*Node[T]
Data any Data T
} }
func (n *Node) getChild(s string) *Node { func (n *Node[T]) getChild(s string) *Node[T] {
return n.children[s] return n.children[s]
} }
func (n *Node) hasChild(s string) bool { func (n *Node[T]) hasChild(s string) bool {
return n.getChild(s) != nil 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 n.children[s] = child
} }
func newNode(data any) *Node { func newNode[T comparable](data T) *Node[T] {
return &Node{ return &Node[T]{
Data: data, 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" "container/list"
"errors" "errors"
"fmt" "fmt"
R "github.com/Dreamacro/clash/rule" "github.com/Dreamacro/clash/constant/sniffer"
RP "github.com/Dreamacro/clash/rule/provider" "github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "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"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
@ -26,8 +31,8 @@ import (
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
providerTypes "github.com/Dreamacro/clash/constant/provider" providerTypes "github.com/Dreamacro/clash/constant/provider"
snifferTypes "github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
T "github.com/Dreamacro/clash/tunnel" T "github.com/Dreamacro/clash/tunnel"
@ -46,6 +51,8 @@ type General struct {
RoutingMark int `json:"-"` RoutingMark int `json:"-"`
GeodataMode bool `json:"geodata-mode"` GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"` GeodataLoader string `json:"geodata-loader"`
TCPConcurrent bool `json:"tcp-concurrent"`
Tun Tun `json:"tun"`
} }
// Inbound config // Inbound config
@ -78,7 +85,7 @@ type DNS struct {
EnhancedMode C.DNSMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
NameServerPolicy map[string]dns.NameServer NameServerPolicy map[string]dns.NameServer
ProxyServerNameserver []dns.NameServer ProxyServerNameserver []dns.NameServer
} }
@ -87,17 +94,11 @@ type DNS struct {
type FallbackFilter struct { type FallbackFilter struct {
GeoIP bool `yaml:"geoip"` GeoIP bool `yaml:"geoip"`
GeoIPCode string `yaml:"geoip-code"` GeoIPCode string `yaml:"geoip-code"`
IPCIDR []*net.IPNet `yaml:"ipcidr"` IPCIDR []*netip.Prefix `yaml:"ipcidr"`
Domain []string `yaml:"domain"` Domain []string `yaml:"domain"`
GeoSite []*router.DomainMatcher `yaml:"geosite"` GeoSite []*router.DomainMatcher `yaml:"geosite"`
} }
var (
GroupsList = list.New()
ProxiesList = list.New()
ParsingProxiesCallback func(groupsList *list.List, proxiesList *list.List)
)
// Profile config // Profile config
type Profile struct { type Profile struct {
StoreSelected bool `yaml:"store-selected"` StoreSelected bool `yaml:"store-selected"`
@ -106,17 +107,12 @@ type Profile struct {
// Tun config // Tun config
type Tun struct { type Tun struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"` Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"` Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"` DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoRoute bool `yaml:"auto-route" json:"auto-route"`
} AutoDetectInterface bool `yaml:"auto-detect-interface" json:"auto-detect-interface"`
// Script config
type Script struct {
MainCode string `yaml:"code" json:"code"`
ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"`
} }
// IPTables config // IPTables config
@ -126,6 +122,15 @@ type IPTables struct {
Bypass []string `yaml:"bypass" json:"bypass"` Bypass []string `yaml:"bypass" json:"bypass"`
} }
type Sniffer struct {
Enable bool
Sniffers []sniffer.Type
Reverses *trie.DomainTrie[bool]
ForceDomain *trie.DomainTrie[bool]
SkipDomain *trie.DomainTrie[bool]
Ports *[]utils.Range[uint16]
}
// Experimental config // Experimental config
type Experimental struct{} type Experimental struct{}
@ -136,13 +141,14 @@ type Config struct {
IPTables *IPTables IPTables *IPTables
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
Profile *Profile Profile *Profile
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
Providers map[string]providerTypes.ProxyProvider Providers map[string]providerTypes.ProxyProvider
RuleProviders map[string]*providerTypes.RuleProvider RuleProviders map[string]providerTypes.RuleProvider
Sniffer *Sniffer
} }
type RawDNS struct { type RawDNS struct {
@ -170,11 +176,12 @@ type RawFallbackFilter struct {
} }
type RawTun struct { type RawTun struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"` Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"` Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"` DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"` AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface"`
} }
type RawConfig struct { type RawConfig struct {
@ -197,7 +204,9 @@ type RawConfig struct {
RoutingMark int `yaml:"routing-mark"` RoutingMark int `yaml:"routing-mark"`
GeodataMode bool `yaml:"geodata-mode"` GeodataMode bool `yaml:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader"` GeodataLoader string `yaml:"geodata-loader"`
TCPConcurrent bool `yaml:"tcp-concurrent" json:"tcp-concurrent"`
Sniffer RawSniffer `yaml:"sniffer"`
ProxyProvider map[string]map[string]any `yaml:"proxy-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
RuleProvider map[string]map[string]any `yaml:"rule-providers"` RuleProvider map[string]map[string]any `yaml:"rule-providers"`
Hosts map[string]string `yaml:"hosts"` Hosts map[string]string `yaml:"hosts"`
@ -211,6 +220,14 @@ type RawConfig struct {
Rule []string `yaml:"rules"` Rule []string `yaml:"rules"`
} }
type RawSniffer struct {
Enable bool `yaml:"enable" json:"enable"`
Sniffing []string `yaml:"sniffing" json:"sniffing"`
ForceDomain []string `yaml:"force-domain" json:"force-domain"`
SkipDomain []string `yaml:"skip-domain" json:"skip-domain"`
Ports []string `yaml:"port-whitelist" json:"port-whitelist"`
}
// Parse config // Parse config
func Parse(buf []byte) (*Config, error) { func Parse(buf []byte) (*Config, error) {
rawCfg, err := UnmarshalRawConfig(buf) rawCfg, err := UnmarshalRawConfig(buf)
@ -236,12 +253,14 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
Rule: []string{}, Rule: []string{},
Proxy: []map[string]any{}, Proxy: []map[string]any{},
ProxyGroup: []map[string]any{}, ProxyGroup: []map[string]any{},
TCPConcurrent: false,
Tun: RawTun{ Tun: RawTun{
Enable: false, Enable: false,
Device: "", Device: "",
Stack: C.TunGvisor, Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true, AutoRoute: false,
AutoDetectInterface: false,
}, },
IPTables: IPTables{ IPTables: IPTables{
Enable: false, Enable: false,
@ -275,6 +294,13 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"www.msftconnecttest.com", "www.msftconnecttest.com",
}, },
}, },
Sniffer: RawSniffer{
Enable: false,
Sniffing: []string{},
ForceDomain: []string{},
SkipDomain: []string{},
Ports: []string{},
},
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
@ -337,6 +363,11 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Users = parseAuthentication(rawCfg.Authentication) 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 elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm
return config, nil return config, nil
@ -377,6 +408,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
RoutingMark: cfg.RoutingMark, RoutingMark: cfg.RoutingMark,
GeodataMode: cfg.GeodataMode, GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader, GeodataLoader: cfg.GeodataLoader,
TCPConcurrent: cfg.TCPConcurrent,
}, nil }, nil
} }
@ -388,8 +420,8 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
providersConfig := cfg.ProxyProvider providersConfig := cfg.ProxyProvider
var proxyList []string var proxyList []string
_proxiesList := list.New() proxiesList := list.New()
_groupsList := list.New() groupsList := list.New()
proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect()) proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect())
proxies["REJECT"] = adapter.NewProxy(outbound.NewReject()) proxies["REJECT"] = adapter.NewProxy(outbound.NewReject())
@ -409,7 +441,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
} }
proxies[proxy.Name()] = proxy proxies[proxy.Name()] = proxy
proxyList = append(proxyList, proxy.Name()) proxyList = append(proxyList, proxy.Name())
_proxiesList.PushBack(mapping) proxiesList.PushBack(mapping)
} }
// keep the original order of ProxyGroups in config file // keep the original order of ProxyGroups in config file
@ -419,7 +451,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return nil, nil, fmt.Errorf("proxy group %d: missing name", idx) return nil, nil, fmt.Errorf("proxy group %d: missing name", idx)
} }
proxyList = append(proxyList, groupName) proxyList = append(proxyList, groupName)
_groupsList.PushBack(mapping) groupsList.PushBack(mapping)
} }
// check if any loop exists and sort the ProxyGroups // check if any loop exists and sort the ProxyGroups
@ -474,17 +506,12 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
[]providerTypes.ProxyProvider{pd}, []providerTypes.ProxyProvider{pd},
) )
proxies["GLOBAL"] = adapter.NewProxy(global) proxies["GLOBAL"] = adapter.NewProxy(global)
ProxiesList = _proxiesList
GroupsList = _groupsList
if ParsingProxiesCallback != nil {
// refresh tray menu
go ParsingProxiesCallback(GroupsList, ProxiesList)
}
return proxies, providersMap, nil return proxies, providersMap, nil
} }
func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]*providerTypes.RuleProvider, error) { func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[string]providerTypes.RuleProvider, error) {
ruleProviders := map[string]*providerTypes.RuleProvider{} ruleProviders := map[string]providerTypes.RuleProvider{}
log.Infoln("Geodata Loader mode: %s", geodata.LoaderName()) log.Infoln("Geodata Loader mode: %s", geodata.LoaderName())
// parse rule provider // parse rule provider
for name, mapping := range cfg.RuleProvider { for name, mapping := range cfg.RuleProvider {
@ -493,13 +520,12 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return nil, nil, err return nil, nil, err
} }
ruleProviders[name] = &rp ruleProviders[name] = rp
RP.SetRuleProvider(rp) RP.SetRuleProvider(rp)
} }
var rules []C.Rule var rules []C.Rule
rulesConfig := cfg.Rule rulesConfig := cfg.Rule
mode := cfg.Mode
// parse rules // parse rules
for idx, line := range rulesConfig { for idx, line := range rulesConfig {
@ -511,10 +537,6 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
ruleName = strings.ToUpper(rule[0]) ruleName = strings.ToUpper(rule[0])
) )
if mode == T.Script && ruleName != "GEOSITE" {
continue
}
l := len(rule) l := len(rule)
if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" { if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" {
@ -538,7 +560,7 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
params = rule[l:] 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) return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
} }
@ -555,9 +577,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()) 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() runtime.GC()
@ -565,18 +585,18 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
return rules, ruleProviders, nil return rules, ruleProviders, nil
} }
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
tree := trie.New() tree := trie.New[netip.Addr]()
// add default hosts // 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()) log.Errorln("insert localhost to host error: %s", err.Error())
} }
if len(cfg.Hosts) != 0 { if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts { for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr) ip, err := netip.ParseAddr(ipStr)
if ip == nil { if err != nil {
return nil, fmt.Errorf("%s is not a valid IP", ipStr) return nil, fmt.Errorf("%s is not a valid IP", ipStr)
} }
_ = tree.Insert(domain, ip) _ = tree.Insert(domain, ip)
@ -651,7 +671,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
Net: dnsNetType, Net: dnsNetType,
Addr: addr, Addr: addr,
ProxyAdapter: u.Fragment, ProxyAdapter: u.Fragment,
Interface: dialer.DefaultInterface.Load(), Interface: dialer.DefaultInterface,
}, },
) )
} }
@ -675,15 +695,15 @@ func parseNameServerPolicy(nsPolicy map[string]string) (map[string]dns.NameServe
return policy, nil return policy, nil
} }
func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) { func parseFallbackIPCIDR(ips []string) ([]*netip.Prefix, error) {
var ipNets []*net.IPNet var ipNets []*netip.Prefix
for idx, ip := range ips { for idx, ip := range ips {
_, ipnet, err := net.ParseCIDR(ip) ipnet, err := netip.ParsePrefix(ip)
if err != nil { if err != nil {
return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error()) return nil, fmt.Errorf("DNS FallbackIP[%d] format error: %s", idx, err.Error())
} }
ipNets = append(ipNets, ipnet) ipNets = append(ipNets, &ipnet)
} }
return ipNets, nil return ipNets, nil
@ -724,7 +744,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil 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 cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@ -736,7 +756,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
EnhancedMode: cfg.EnhancedMode, EnhancedMode: cfg.EnhancedMode,
FallbackFilter: FallbackFilter{ FallbackFilter: FallbackFilter{
IPCIDR: []*net.IPNet{}, IPCIDR: []*netip.Prefix{},
GeoSite: []*router.DomainMatcher{}, GeoSite: []*router.DomainMatcher{},
}, },
} }
@ -767,20 +787,23 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
for _, ns := range dnsCfg.DefaultNameserver { for _, ns := range dnsCfg.DefaultNameserver {
host, _, err := net.SplitHostPort(ns.Addr) host, _, err := net.SplitHostPort(ns.Addr)
if err != nil || net.ParseIP(host) == nil { if err != nil || net.ParseIP(host) == nil {
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 { if cfg.EnhancedMode == C.DNSFakeIP {
_, ipnet, err := net.ParseCIDR(cfg.FakeIPRange) ipnet, err := netip.ParsePrefix(cfg.FakeIPRange)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var host *trie.DomainTrie var host *trie.DomainTrie[bool]
// fake ip skip host filter // fake ip skip host filter
if len(cfg.FakeIPFilter) != 0 { if len(cfg.FakeIPFilter) != 0 {
host = trie.New() host = trie.New[bool]()
for _, domain := range cfg.FakeIPFilter { for _, domain := range cfg.FakeIPFilter {
_ = host.Insert(domain, true) _ = host.Insert(domain, true)
} }
@ -788,7 +811,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
if len(dnsCfg.Fallback) != 0 { if len(dnsCfg.Fallback) != 0 {
if host == nil { if host == nil {
host = trie.New() host = trie.New[bool]()
} }
for _, fb := range dnsCfg.Fallback { for _, fb := range dnsCfg.Fallback {
if net.ParseIP(fb.Addr) != nil { if net.ParseIP(fb.Addr) != nil {
@ -799,7 +822,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
} }
pool, err := fakeip.New(fakeip.Options{ pool, err := fakeip.New(fakeip.Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 1000, Size: 1000,
Host: host, Host: host,
Persistence: rawCfg.Profile.StoreFakeIP, Persistence: rawCfg.Profile.StoreFakeIP,
@ -843,9 +866,9 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
} }
func parseTun(rawTun RawTun, general *General) (*Tun, error) { 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() autoDetectInterfaceName, err := commons.GetAutoDetectInterface()
if err != nil || autoDetectInterfaceName == "" { if err != nil {
log.Warnln("Can not find auto detect interface.[%s]", err) log.Warnln("Can not find auto detect interface.[%s]", err)
} else { } else {
log.Warnln("Auto detect interface: %s", autoDetectInterfaceName) log.Warnln("Auto detect interface: %s", autoDetectInterfaceName)
@ -860,7 +883,7 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
if _, after, ok := strings.Cut(d, "://"); ok { if _, after, ok := strings.Cut(d, "://"); ok {
d = after d = after
} }
d = strings.Replace(d, "any", "0.0.0.0", 1)
addrPort, err := netip.ParseAddrPort(d) addrPort, err := netip.ParseAddrPort(d)
if err != nil { if err != nil {
return nil, fmt.Errorf("parse dns-hijack url error: %w", err) return nil, fmt.Errorf("parse dns-hijack url error: %w", err)
@ -870,10 +893,82 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
} }
return &Tun{ return &Tun{
Enable: rawTun.Enable, Enable: rawTun.Enable,
Device: rawTun.Device, Device: rawTun.Device,
Stack: rawTun.Stack, Stack: rawTun.Stack,
DNSHijack: dnsHijack, DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute, AutoRoute: rawTun.AutoRoute,
AutoDetectInterface: rawTun.AutoDetectInterface,
}, nil }, nil
} }
func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
sniffer := &Sniffer{
Enable: snifferRaw.Enable,
}
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)
}
}
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)
}
}
return sniffer, nil
}

View File

@ -99,6 +99,10 @@ type ProxyAdapter interface {
DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error) DialContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (Conn, error)
ListenPacketContext(ctx context.Context, metadata *Metadata, opts ...dialer.Option) (PacketConn, 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 extracts the proxy from a proxy-group. It returns nil when nothing to extract.
Unwrap(metadata *Metadata) Proxy Unwrap(metadata *Metadata) Proxy
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"net/netip"
"strconv" "strconv"
) )
@ -73,17 +74,19 @@ func (t Type) MarshalJSON() ([]byte, error) {
// Metadata is used to store connection address // Metadata is used to store connection address
type Metadata struct { type Metadata struct {
NetWork NetWork `json:"network"` NetWork NetWork `json:"network"`
Type Type `json:"type"` Type Type `json:"type"`
SrcIP net.IP `json:"sourceIP"` SrcIP netip.Addr `json:"sourceIP"`
DstIP net.IP `json:"destinationIP"` DstIP netip.Addr `json:"destinationIP"`
SrcPort string `json:"sourcePort"` SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"` DstPort string `json:"destinationPort"`
AddrType int `json:"-"` AddrType int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
DNSMode DNSMode `json:"dnsMode"` DNSMode DNSMode `json:"dnsMode"`
Process string `json:"process"` Uid *int32 `json:"uid"`
ProcessPath string `json:"processPath"` Process string `json:"process"`
ProcessPath string `json:"processPath"`
RemoteDst string `json:"remoteDestination"`
} }
func (m *Metadata) RemoteAddress() string { func (m *Metadata) RemoteAddress() string {
@ -95,45 +98,49 @@ func (m *Metadata) SourceAddress() string {
} }
func (m *Metadata) SourceDetail() 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) return fmt.Sprintf("%s(%s)", m.SourceAddress(), m.Process)
} else { } else {
if m.Type == INNER {
return fmt.Sprintf("[Clash]")
}
return fmt.Sprintf("%s", m.SourceAddress()) return fmt.Sprintf("%s", m.SourceAddress())
} }
} }
func (m *Metadata) Resolved() bool { func (m *Metadata) Resolved() bool {
return m.DstIP != nil return m.DstIP.IsValid()
} }
// Pure is used to solve unexpected behavior // Pure is used to solve unexpected behavior
// when dialing proxy connection in DNSMapping mode. // when dialing proxy connection in DNSMapping mode.
func (m *Metadata) Pure() *Metadata { func (m *Metadata) Pure() *Metadata {
if m.DNSMode == DNSMapping && m.DstIP != nil { if m.DNSMode == DNSMapping && m.DstIP.IsValid() {
copy := *m copyM := *m
copy.Host = "" copyM.Host = ""
if copy.DstIP.To4() != nil { if copyM.DstIP.Is4() {
copy.AddrType = AtypIPv4 copyM.AddrType = AtypIPv4
} else { } else {
copy.AddrType = AtypIPv6 copyM.AddrType = AtypIPv6
} }
return &copy return &copyM
} }
return m return m
} }
func (m *Metadata) UDPAddr() *net.UDPAddr { func (m *Metadata) UDPAddr() *net.UDPAddr {
if m.NetWork != UDP || m.DstIP == nil { if m.NetWork != UDP || !m.DstIP.IsValid() {
return nil return nil
} }
port, _ := strconv.ParseUint(m.DstPort, 10, 16) port, _ := strconv.ParseUint(m.DstPort, 10, 16)
return &net.UDPAddr{ return &net.UDPAddr{
IP: m.DstIP, IP: m.DstIP.AsSlice(),
Port: int(port), Port: int(port),
} }
} }
@ -141,7 +148,7 @@ func (m *Metadata) UDPAddr() *net.UDPAddr {
func (m *Metadata) String() string { func (m *Metadata) String() string {
if m.Host != "" { if m.Host != "" {
return m.Host return m.Host
} else if m.DstIP != nil { } else if m.DstIP.IsValid() {
return m.DstIP.String() return m.DstIP.String()
} else { } else {
return "<nil>" return "<nil>"
@ -149,5 +156,5 @@ func (m *Metadata) String() string {
} }
func (m *Metadata) Valid() bool { 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 { type path struct {
homeDir string homeDir string
configFile string configFile string
scriptDir string
} }
// SetHomeDir is used to set the configuration path // SetHomeDir is used to set the configuration path
@ -123,23 +122,6 @@ func (p *path) GeoSite() string {
return P.Join(p.homeDir, "GeoSite.dat") 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 { func (p *path) GetAssetLocation(file string) string {
return P.Join(p.homeDir, file) return P.Join(p.homeDir, file)
} }

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package constant package constant
import ( import (
"net" "net/netip"
"strings" "strings"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
@ -9,7 +9,7 @@ import (
type RuleExtra struct { type RuleExtra struct {
Network NetWork Network NetWork
SourceIPs []*net.IPNet SourceIPs []*netip.Prefix
ProcessNames []string ProcessNames []string
} }
@ -17,7 +17,7 @@ func (re *RuleExtra) NotMatchNetwork(network NetWork) bool {
return re.Network != ALLNet && re.Network != network 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 { if re.SourceIPs == nil {
return false return false
} }
@ -46,3 +46,11 @@ func (re *RuleExtra) NotMatchProcessName(processName string) bool {
type RuleGeoSite interface { type RuleGeoSite interface {
GetDomainMatcher() *router.DomainMatcher GetDomainMatcher() *router.DomainMatcher
} }
type RuleGeoIP interface {
GetIPMatcher() *router.GeoIPMatcher
}
type RuleGroup interface {
GetRecodeSize() int
}

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 ( import (
"net" "net"
CN "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
@ -19,7 +19,7 @@ func NewConnContext(conn net.Conn, metadata *C.Metadata) *ConnContext {
return &ConnContext{ return &ConnContext{
id: id, id: id,
metadata: metadata, metadata: metadata,
conn: conn, conn: CN.NewBufferedConn(conn),
} }
} }

View File

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

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