Compare commits

..

322 Commits

Author SHA1 Message Date
Skyxim
10d2d14938 Merge branch 'Beta' into Meta
# Conflicts:
#	rules/provider/classical_strategy.go
2022-07-02 10:41:41 +08:00
世界
8ce9737f3d Update dependencies 2022-06-28 08:15:03 +08:00
世界
6b44178108 Fix concurrency vmess udp write 2022-06-28 08:12:56 +08:00
Skyxim
6664547f43 chore: upgrade dependencies 2022-06-26 22:37:59 +08:00
Skyxim
10383e2701 Merge branch 'dev' into Alpha 2022-06-26 21:53:03 +08:00
Skyxim
f4b9f2965f fix: hysteria dial use external context 2022-06-26 21:52:22 +08:00
Skyxim
2ba933d16a chore: hysteria params verify 2022-06-25 12:43:47 +08:00
Skyxim
669961e496 fix: proxy provider force update on init 2022-06-25 12:42:52 +08:00
Skyxim
f979491013 fix: tcp concurrent force close when context done 2022-06-25 09:16:53 +08:00
Skyxim
0d55b28805 chore: dns interface name 2022-06-25 09:16:51 +08:00
Skyxim
9c70e649ca fix: disable doq skip verify cert 2022-06-25 09:16:49 +08:00
Skyxim
8c079bf5bc fix: tcp concurrent force close when context done 2022-06-25 09:16:28 +08:00
Skyxim
2cdf4a0532 chore: RESTful test group use request context 2022-06-25 08:53:11 +08:00
Skyxim
4ba34ce672 chore: healthcheck only once check at same time 2022-06-25 08:53:04 +08:00
mrFq1
637f1b5aed ClashX hack. (#102) 2022-06-24 20:08:33 +08:00
MetaCubeX
56a87125e0 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	common/convert/converter.go
2022-06-23 00:55:34 +08:00
MetaCubeX
6fedc8d942 fix: Converter for password of ss2022
fix: Converter for password of ss2022

fixup! fix: Converter for password of ss2022 and ws

fix: Converter for password of ss2022 and ws
2022-06-23 00:54:58 +08:00
MetaCubeX
dbb834d964 fix: Converter for password of ss2022 2022-06-23 00:40:08 +08:00
MetaCubeX
449946cc15 fixup! fix: Converter for password of ss2022 and ws 2022-06-23 00:18:30 +08:00
MetaCubeX
c3671a154d fix: Converter for password of ss2022 and ws 2022-06-22 22:18:13 +08:00
MetaCubeX
6874fb785b Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	common/convert/converter.go
2022-06-21 00:29:57 +08:00
MetaCubeX
5141ddc96e fix: Converter for vless/vmess/ss URI Scheme 2022-06-21 00:28:33 +08:00
MetaCubeX
6a03371731 fix: Converter for vless/vmess/ss URI Scheme 2022-06-21 00:18:34 +08:00
MetaCubeX
b658bb415b chore: remove unused 2022-06-20 22:25:59 +08:00
MetaCubeX
85405a54c7 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	go.mod
#	go.sum
2022-06-19 22:30:02 +08:00
世界
30a0834e72 chore: update shadowsocks 2022-06-19 22:26:17 +08:00
Skyxim
109a76e1fc fix: url test http response not closed 2022-06-19 17:29:46 +08:00
Skyxim
c1a99b9be4 fix: IDNA domain match 2022-06-18 18:13:54 +08:00
Skyxim
bf55428954 style: rule provider strategy 2022-06-18 17:53:40 +08:00
Skyxim
5e55d6b08f Merge branch 'Alpha' into dev 2022-06-18 17:30:49 +08:00
Skyxim
21098d2627 feat: RESTful api add interface-name field on patch config 2022-06-18 17:29:19 +08:00
wwqgtxx
8da67ba61c Add shadowsocks uot in relay 2022-06-18 16:38:44 +08:00
Skyxim
54a0947bb4 fix: force update provider happen loopback 2022-06-18 16:05:09 +08:00
世界
a562b249a2 Add shadowsocks uot and test 2022-06-18 10:50:33 +08:00
世界
5af17f70b4 Fix buffered shadowsocks aead tcp request 2022-06-18 10:50:33 +08:00
Skyxim
ca5bb91977 Merge branch 'dev' into Alpha 2022-06-17 21:44:54 +08:00
Skyxim
bbac54433e fix: resolve ipv4 of 4 in 6 2022-06-17 21:44:06 +08:00
世界
b6a5ec6490 fix: fix async conn usage 2022-06-16 10:21:20 +08:00
MetaCubeX
aaf700f0b5 chore: Allow VLESS protocol TLS to be FALSE 2022-06-16 01:20:33 +08:00
MetaCubeX
2ce89aca1e Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	go.mod
#	go.sum
2022-06-16 01:13:33 +08:00
世界
efdf69022a fix: fix async conn usage 2022-06-16 01:04:27 +08:00
MetaCubeX
d4d1d4cc2a Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-06-16 01:01:03 +08:00
世界
a8c4900891 fix: fix async conn usage maybe 2022-06-16 00:49:30 +08:00
MetaCubeX
930a7af8e7 chore: hy URI Scheme 解析 2022-06-15 23:18:06 +08:00
bash99
77acd4ba8d Update README.md
add permissions for systemctl services
clash-dashboard change to updated one
2022-06-15 19:22:18 +08:00
wwqgtxx
691cf1d8d6 Merge pull request #94 from bash99/Meta
Update README.md
2022-06-15 19:15:51 +08:00
bash99
d1decb8e58 Update README.md
add permissions for systemctl services
clash-dashboard change to updated one
2022-06-15 14:00:05 +08:00
MetaCubeX
625c4a1079 Update util.go 2022-06-15 08:44:16 +08:00
MetaCubeX
341ef19099 fix: ss/ssr URI Scheme 解析问题 2022-06-15 03:20:58 +08:00
MetaCubeX
2563b20019 fix: ss/ssr URI Scheme 解析问题 2022-06-15 03:03:26 +08:00
Skyxim
1b3b5b4dfe fix: find process error 2022-06-14 23:14:43 +08:00
Skyxim
2e6bdc5636 feat: add param general.enable-process, it will always find process or uid, default value is false 2022-06-14 23:08:07 +08:00
Skyxim
be298cfa16 refactor: finding process and uid should to find with match process or uid rule, reduce memory allocation 2022-06-14 22:52:56 +08:00
Skyxim
277e71b26a chore: hysteria test 2022-06-14 21:05:52 +08:00
Skyxim
f7c903a586 Merge branch 'dev' into Alpha 2022-06-14 20:23:51 +08:00
Skyxim
ff4a5bef9b fix: up/down of hysteria must be a valid value 2022-06-14 20:23:36 +08:00
世界
d8dc44e786 Refactor: vmess
Add support for vmess length masking/packetaddr/authenticated length

Add support for zero/aes-128-cfb protcol
2022-06-14 13:21:22 +08:00
Skyxim
c968104a19 fix: udp listen use udp4 when general.ipv6 is false; general.ipv6 default value is true 2022-06-14 12:36:05 +08:00
Skyxim
f7481ecadf chore: delete DOQ meaningless ALPN 2022-06-12 23:17:26 +08:00
Skyxim
85c37b473a fix: DOQ blocked dns return result because DOQ goroutine leak 2022-06-12 21:41:01 +08:00
Skyxim
23bc231df3 chore: doq default port change to 853, ALPN use doq 2022-06-12 17:53:11 +08:00
Skyxim
2146b605f7 refactor: deprecated params(up_mbps,down_mpbs,auth) in hysteria; up/down no use append unit equivalent up_mbps/down_mbps, default unit is Mbps; up/down become a required option. 2022-06-12 11:52:15 +08:00
Skyxim
8853e97b40 fix: sni invalid on hysteria 2022-06-12 00:00:42 +08:00
Skyxim
7d04904109 fix: leak dns when domain in hosts list 2022-06-11 18:51:26 +08:00
Skyxim
a5acd3aa97 refactor: clear linkname,reduce cycle dependencies,transport init geosite function 2022-06-11 18:51:22 +08:00
世界
099aa1e3c2 fix: disable unsafe buffer in windows by default
ref: f49cd6f979
2022-06-10 15:51:34 +08:00
Skyxim
63fdb348db fix: leak dns when domain in hosts list 2022-06-10 14:29:19 +08:00
Skyxim
81ee44f6c0 Merge branch 'rule' into A 2022-06-10 13:38:43 +08:00
Skyxim
130a3a261d refactor: clear linkname,reduce cycle dependencies,transport init geosite function 2022-06-10 13:38:19 +08:00
MetaCubeX
94368f43eb fix: Vmess URI Scheme 解析问题 2022-06-10 03:15:30 +08:00
世界
9a55213ddc chore: add more shadowsocks tests 2022-06-09 18:10:31 +08:00
世界
5055542d61 chore: update dependencies 2022-06-09 18:05:45 +08:00
Dreamacro
23063ae0b9 fix: make CodeQL happy Dreamacro 2022-06-09 17:59:17 +08:00
Dreamacro
a7f9aa909a fix: upgrade to yaml v3 2022-06-09 17:59:17 +08:00
Dreamacro
15ecc451f3 fix: benchmark read bytes 2022-06-09 17:58:15 +08:00
Dreamacro
186a4cfdf3 fix: test broken on opensource repo 2022-06-09 17:58:10 +08:00
Dreamacro
220ef9e2e2 chore: add benchmark r/w 2022-06-09 17:58:05 +08:00
Dreamacro
7079116aa8 chore: cleanup test code 2022-06-09 17:57:59 +08:00
Dreamacro
b9e6de45e6 chore: make linter happy 2022-06-09 17:57:53 +08:00
Kr328
d3503ff940 fix: fix upgrade header detect (#2134) 2022-06-09 17:57:48 +08:00
Kaming Chan
c3f4e1ba2e fix: add length check for ssr auth_aes128_sha1 (#2129) 2022-06-09 17:57:41 +08:00
世界
695fb64fa8 fix: vmess ws 2022-06-09 16:23:15 +08:00
Skyxim
eea9a12560 fix: 规则匹配默认策略组返回错误 2022-06-09 14:18:35 +08:00
adlyq
0a4570b55c fix: group filter touch provider 2022-06-09 14:18:29 +08:00
Skyxim
d32ab9ce74 fix: 规则匹配默认策略组返回错误 2022-06-09 13:52:02 +08:00
MetaCubeX
94648989b8 fix: hysteria URI Scheme 解析问题 2022-06-09 00:22:47 +08:00
MetaCubeX
07522d3c2e chore: 修改test文件 2022-06-08 02:03:34 +08:00
MetaCubeX
c14c07d2e3 feat: 代理集支持 Hysteria 分享格式订阅解析 2022-06-08 01:50:14 +08:00
MetaCubeX
9511ccfe47 chore: refine code 2022-06-08 01:47:50 +08:00
adlyq
ed17a1bf23 fix: group filter touch provider 2022-06-07 17:19:25 +08:00
世界
2a4f2f3942 fix: hysteria dialer 2022-06-07 15:49:10 +08:00
世界
3254eaf51c fix: hysteria parse auth 2022-06-07 15:24:46 +08:00
adlyq
7941bae141 fix: hysteria parse 2022-06-07 14:53:00 +08:00
世界
35a6666a84 feat: add hysteria 2022-06-07 13:46:54 +08:00
世界
73d5042774 fix: some test 2022-06-07 10:45:32 +08:00
世界
9126cbab91 fix: shadowsocks-2022 on 32-bit systems 2022-06-07 10:44:22 +08:00
MetaCubeX
f8366f6e42 fix: 代理集转换ws类型
feat: 新增grpc h2 http 等支持
2022-06-07 03:17:33 +08:00
Skyxim
100c9b94ba Merge branch 'dev' into Alpha 2022-06-06 23:15:33 +08:00
Skyxim
8343c3597e fix: doq maybe crash when use adapter 2022-06-06 21:45:08 +08:00
Skyxim
d31adafa11 Merge pull request #76 from nekohasekai/Alpha
feat: add support for shadowsocks 2022 ciphers
2022-06-06 20:21:01 +08:00
世界
1acc6759e9 feat: add support for shadowsocks 2022 ciphers 2022-06-06 19:56:36 +08:00
Skyxim
e995003c27 Merge branch 'dev' into Alpha 2022-06-05 21:23:14 +08:00
Skyxim
9edee2435c chore: amd64v3 改名为amd64,amd64v2改为amd64-compatible, darwin删除v2 2022-06-05 21:23:06 +08:00
MetaCubeX
5d337b75f9 feat: proxy provider 支持V2ray格式订阅链接 2022-06-05 17:14:24 +08:00
yaling888
2188fad9c1 Feature: add V2Ray subscription support to proxy provider 2022-06-05 15:59:47 +08:00
MetaCubeX
edc73f3f33 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	dns/client.go
#	dns/doh.go
#	dns/doq.go
2022-06-05 14:10:18 +08:00
MetaCubeX
ade424cbb4 chore: 调整dns interface与adapter部分 2022-06-05 13:37:00 +08:00
MetaCubeX
6e03773134 chore: 调整dns interface与adapter部分 2022-06-05 12:52:29 +08:00
MetaCubeX
4ad0294655 chore: adjust makefile 2022-06-04 21:21:42 +08:00
MetaCubeX
43d3a0c8ea chore: mix the proxy adapter and interface to dns client 2022-06-04 21:18:49 +08:00
MetaCubeX
c045a4f2a7 Chore: make hadowsocks2 lib embed 2022-06-04 20:29:33 +08:00
Skyxim
20611eb8dc Merge branch 'dev' into Alpha 2022-06-04 19:16:56 +08:00
Skyxim
8ff7e180a4 fix: 当初始化失败时,定时更新失效 2022-06-04 19:15:30 +08:00
Skyxim
3827e00b54 refactor: 抽离http请求方法 2022-06-04 19:14:39 +08:00
Skyxim
c52e689d0d fix: classical rule-set 更新未清理 2022-06-04 19:12:50 +08:00
MetaCubeX
cb517cb529 chore: 调整目录与包名一致 2022-06-04 03:25:33 +08:00
MetaCubeX
50bb620aa1 chore: 调整parseRule代码 2022-06-04 03:22:41 +08:00
MetaCubeX
c745ea63b2 chore: 优化GeoSite初始化代码 2022-06-04 02:58:14 +08:00
Skyxim
8e959bd245 chore: 当无tag时不输出无效日志 2022-06-03 21:00:45 +08:00
Skyxim
298ca42369 chore: 启动参数v,查看版本同时打印使用的tags 2022-06-03 20:23:53 +08:00
Skyxim
ed9b9ce3c5 refactor: 添加no_gvisor 编译tag, 剔除gvisor stack支持, 方便在arm设备上debug 2022-06-03 20:07:30 +08:00
Skyxim
1298d2f8b6 chore: 添加tag no_doq 编译不含doq版本, 仅减少1.5MB(macOS-arm64) 2022-06-03 18:12:06 +08:00
adlyq
6e84f685ce chore: 更新geox时通过内存存储 2022-06-03 16:50:49 +08:00
adlyq
1ad87cfec9 chore: 选择fallback时,当节点不可用时触发urltest 2022-06-03 13:32:11 +08:00
Skyxim
9e9f459c0e refactor: 优化proxy server nameserver, 当节点专用dns全部查询失败会回落到正常逻辑 2022-06-02 20:58:25 +08:00
adlyq
fa3e0c726e chore: 调整解析逻辑 2022-06-02 17:03:08 +08:00
adlyq
3b038310ab fix: 类型转换错误导致规则解析错误 2022-06-02 15:43:27 +08:00
adlyq
6709936a8f refactor: 归类规则解析代码 2022-06-02 13:42:18 +08:00
adlyq
04e5d02ab9 feat: IP-SUFFIX
eg. IP-SUFFIX,0.0.0.124/6,匹配ip二进制后四位(IP-CIDR的倒序),支持ipv6
2022-06-02 12:53:19 +08:00
adlyq
1af39cb228 fix: OpenClash 回环 2022-06-01 12:32:45 +08:00
Meta
c95735f083 chore: 调整内置winTun.dll部分 2022-06-01 12:01:08 +08:00
adlyq
fa2e6be05d fix: TUN file exists 2022-05-31 10:34:13 +08:00
adlyq
4092a7c84b feat: proxies group URLTest api 2022-05-30 22:07:09 +08:00
MetaCubeX
58d299c737 chore: 调整geosite初始化位置 2022-05-30 21:26:41 +08:00
Skyxim
11ddac2b5f refactor: 逻辑规则显示效果 2022-05-30 13:58:37 +08:00
Skyxim
1f95c74f1e chore: 识别线程数 2022-05-29 21:04:47 +08:00
Skyxim
a197fbd4b5 Merge branch 'Alpha' into dev 2022-05-29 20:43:39 +08:00
adlyq
708b8beadf fix: compile 2022-05-29 20:02:30 +08:00
Skyxim
0e1601e5b6 fix: 调整not规则判断子规则数量,逻辑规则返回payload采用解析后结果 2022-05-29 19:54:11 +08:00
Skyxim
c7355510a2 chore: 调整uid系统判断位置 2022-05-29 18:55:08 +08:00
Skyxim
1faa172944 chore: 调整uid系统判断位置 2022-05-29 18:12:43 +08:00
adlyq
7a8c98cd90 refactor: 使用 netlink 获取默认网卡 2022-05-29 15:35:08 +08:00
adlyq
13e907bbd0 refactor: 使用 netlink 配置 ip rule 2022-05-29 15:03:27 +08:00
adlyq
39e7832676 fix: route on android 2022-05-28 23:29:22 +08:00
adlyq
9b999e72ce fix: npe 2022-05-28 23:29:03 +08:00
adlyq
e1a61503e4 Merge branch 'netlink' into Alpha 2022-05-28 21:59:04 +08:00
adlyq
9272d02149 refactor: 合并部分android代码入linux && ip 使用netlink配置路由 2022-05-28 21:58:29 +08:00
Skyxim
067c02aba1 fix: 调整获取远程目的的位置 2022-05-28 20:01:27 +08:00
Skyxim
d0268bb9a2 chore: 降低并发查询时IPv6等待 2022-05-28 09:58:45 +08:00
Skyxim
fb4872ff7f fix: 关闭并发时双栈使用错误 2022-05-27 20:43:39 +08:00
Skyxim
2044458df9 fix: npe 2022-05-27 20:33:27 +08:00
Meta
d6df026550 chore: 更换GeoData下载地址
Signed-off-by: Meta <maze.y2b@gmail.com>
2022-05-27 18:32:23 +08:00
adlyq
7858ca6cc5 fix: geox url setting 2022-05-27 12:32:59 +08:00
Skyxim
ac36473d13 refactor: 获取远程目的从tunnel中剔除,移至tracker 2022-05-27 09:00:48 +08:00
Skyxim
72fb153fe0 refactor: 优化UDP远程目标获取 2022-05-26 23:41:09 +08:00
Skyxim
527a602eba fix: 更新错误时未停止后续流程,日志修改 2022-05-26 23:13:36 +08:00
Skyxim
a71fd3b4df fix: 启动时检测provider文件是否过期,强制更新 2022-05-26 21:05:00 +08:00
adlyq
2ebc0383b5 feat: RESTful API support set tcp-concurrent 2022-05-26 19:49:12 +08:00
adlyq
7431001ed6 feat: RESTful API support update Geo file
and can set update url by user, eg.
geox-url:
  geoip: "http://xxxx/gepip.dat"
  mmdb: "http://xxxx/country.mmdb"
  geosite: "http://xxxx/geosite.dat"
2022-05-24 15:04:13 +08:00
adlyq
149b4b5b43 feat: RESTful API support disable sniffer 2022-05-24 13:44:52 +08:00
adlyq
c0eb9aac1c feat: fallback can be select by user 2022-05-24 10:17:44 +08:00
adlyq
79469fc8d6 feat: uid rule support for logic and rule-set 2022-05-22 13:07:20 +08:00
Skyxim
948700eed6 fix: 并发dns查询,由于ipv6阻塞导致某些情况下的网络不通 2022-05-21 00:34:15 +08:00
adlyq
3ab82849d4 feat: IN-TYPE rule support
eg. IN-TYPE,SOCKS/REDIR/INNER,Proxy
support list: HTTP HTTPS SOCKS SOCKS4 SOCKS5 REDIR TPROXY TUN INNER
2022-05-20 23:17:16 +08:00
adlyq
0f43a19fdb refactor: new way to get interface change even for linux 2022-05-20 21:44:19 +08:00
adlyq
cc1c1340a3 feat: 安卓恢复进程规则,可通过enable-process开关,默认true 2022-05-19 20:44:09 +08:00
Skyxim
fe25ae83df refactor: 修改sticky-session尝试逻辑 2022-05-19 20:27:26 +08:00
adlyq
c787bbe0e5 fix: 热重载Tun配置 2022-05-19 19:19:19 +08:00
Skyxim
7aff9aac82 fix: sticky-sessions异常 2022-05-18 22:29:27 +08:00
adlyq
8b09db5f7f fix: Rule-Set中不解析DNS
feat: RULE-SET支持no-resolve
2022-05-18 18:43:44 +08:00
adlyq
b5623602f5 chore: Android auto-detect-interface plus 2022-05-18 12:00:57 +08:00
Skyxim
16b27b3a1f fix: doq过代理错误 2022-05-17 21:30:54 +08:00
Skyxim
8b00be9039 fix: 删除udp触发的错误逻辑 2022-05-17 21:23:28 +08:00
Skyxim
fa9e27c5e4 refactor: 重构失败主动健康检测 2022-05-17 21:15:14 +08:00
adlyq
f4d9384603 chore: debug log print dns result 2022-05-17 18:21:18 +08:00
adlyq
c4408612b3 chore: 暴露数据给前端 2022-05-17 16:47:21 +08:00
Skyxim
0742f7db26 refactor: 重构StickySessions 2022-05-17 13:28:54 +08:00
Skyxim
891c2fe899 fix: 当dns被禁用时,dns将根据general ipv6设置解析dns 2022-05-17 09:01:41 +08:00
adlyq
b831eb178b chore: remove noisy log 2022-05-16 18:20:13 +08:00
adlyq
962ceaa89e refactor: strategyStickySessions 2022-05-16 17:46:28 +08:00
adlyq
d52b00bd34 refactor: remove useless code 2022-05-16 17:29:08 +08:00
MetaCubeX
aa0d174ccb fix: strategyStickySessions nil pointer 2022-05-16 17:06:44 +08:00
adlyq
b8e9c3d55a fix: geoip ReverseMatch 2022-05-16 17:06:44 +08:00
adlyq
0b4c498c93 refactor: new way to get interface for android 2022-05-16 17:06:44 +08:00
adlyq
efc7c82cac feat: "!"(not) support for geosite
eg. GEOSITE,!CN,Proxy & dns.fallback-filter.geosite: ['!CN']
2022-05-15 13:16:45 +08:00
Skyxim
63917aa020 fix: uuid-map return failed error 2022-05-14 23:45:10 +08:00
Skyxim
5016f529af revert: yaml v2 2022-05-14 23:36:19 +08:00
CHIZI-0618
5bd5f1bfda chore: remove Script mode residual code. 2022-05-14 13:00:33 +08:00
Skyxim
d4dcbce9cb chore: log show all ips when all ips shake hands failed 2022-05-13 21:43:42 +08:00
adlyq
df8196a68c fix: print process path logic 2022-05-12 18:57:30 +08:00
lelemka0
c1631759fc feat: add strategy:sticky-sessions for LoadBalance
Signed-off-by: Meta <maze.y2b@gmail.com>
2022-05-09 18:56:36 +08:00
lelemka0
9e9c3c810f fixed: make log api unblocked 2022-05-09 18:54:00 +08:00
Skyxim
463101aec1 fix: limit load provider concurrent size 2022-05-08 22:52:46 +08:00
Skyxim
2072964701 revert: tls handshake timeout recovery 10s 2022-05-08 21:56:59 +08:00
Skyxim
aded1b78b5 chore: sniffer give the err to the caller 2022-05-08 09:09:39 +08:00
Skyxim
ca9c859059 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-05-08 07:59:17 +08:00
Skyxim
55811dae32 fix: Adjust the timing of loading proxy selection 2022-05-08 07:58:26 +08:00
MetaCubeX
7136d145f8 chore: update dependencies 2022-05-08 00:47:01 +08:00
Skyxim
2fbbf7519f fix: provider auto update 2022-05-08 00:04:16 +08:00
Skyxim
663bf4fbb0 fix: remove misjudgment 2022-05-07 12:53:13 +08:00
Skyxim
f0a22a4a4c chore: modify sniff error log 2022-05-07 12:44:28 +08:00
Skyxim
4ab91520bd refactor: reuse uuid namespace 2022-05-07 12:35:14 +08:00
Skyxim
980d8a2641 refactor: string map to uuidv5 2022-05-06 14:02:34 +08:00
Skyxim
a95d439852 chore: the uuid-map is transferred to the protocol 2022-05-06 13:28:09 +08:00
wwqgtxx
a08e39faec fix uuid match 2022-05-06 13:08:27 +08:00
Skyxim
b3295262c1 chore: Initialize provider ahead of time 2022-05-05 21:14:46 +08:00
Skyxim
27aa026568 fix: use actual metadata 2022-05-04 20:13:12 +08:00
Skyxim
9969e1706e fix: loadbalance group npe 2022-05-04 19:52:48 +08:00
Skyxim
fb58595d44 feat: Expose remote destination (udp proxy maybe domain of node) 2022-05-04 16:57:08 +08:00
adlyq
bdfa16ca6f fix: wrong parameters 2022-05-04 01:04:43 +08:00
MetaCubeX
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
MetaCubeX
f26941091b fix: default router with fakeIP when tun enable 2022-05-04 00:59:04 +08:00
MetaCubeX
6cd5769ed7 fix: default router with fakeIP when tun enable 2022-05-04 00:36:44 +08:00
MetaCubeX
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
MetaCubeX
3fbb7c7a2d chore: add default router when tun enable 2022-05-03 23:58:11 +08:00
MetaCubeX
0aa82c04ea chore: add default router 198.18.0.0/16 when tun enable 2022-05-03 23:28:02 +08:00
MetaCubeX
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
adlyq
eca7615f08 fix: patch update support tun 2022-05-03 19:31:00 +08:00
adlyq
52d559bb38 feat: rule-provider support NetWork rule 2022-05-03 01:36:03 +08:00
adlyq
259736390a feat: rule-provider support rules field 2022-05-03 00:53:22 +08:00
adlyq
7db07630a7 fix: DNS mapping error when sniffing result is ip, Discard sniffs that result in ip 2022-05-02 22:24:14 +08:00
adlyq
d617b0f447 style: uid log tidy 2022-05-02 19:52:34 +08:00
Skyxim
80ff5917f7 fix: The sniffer does not clean up the original address 2022-05-02 17:09:24 +08:00
Skyxim
b401da5eba refactor: provider init order 2022-05-02 16:47:48 +08:00
Skyxim
05b25c334f Merge branch 'makefile' into Alpha 2022-05-02 14:43:51 +08:00
Skyxim
2c5a47a275 fix: Failed to get version tag 2022-05-02 14:43:01 +08:00
Skyxim
b2605a9012 fix: tun dns 2022-05-02 14:21:37 +08:00
Skyxim
b929a19f48 refactor: Unified active health detection, supported by load balancing policy group 2022-05-02 13:50:10 +08:00
MetaCubeX
4b04faa88b fix: http sniffer return host that was handled correctly 2022-05-02 09:51:26 +08:00
MetaCubeX
5fee0b5bf1 chore: adjust pass to reject.go 2022-05-02 09:16:47 +08:00
MetaCubeX
27120fb0f5 Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-05-02 08:49:23 +08:00
MetaCubeX
0cf539fb82 chore: adjust sniffer constant 2022-05-02 08:49:18 +08:00
MetaCubeX
26a38bd8de chore: adjust sniffer constant 2022-05-02 08:46:24 +08:00
Skyxim
ebbce4d061 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-05-02 08:28:00 +08:00
Skyxim
5acd2f6c3a chore: workflow 2022-05-02 08:27:17 +08:00
wwqgtxx
fe2bc903b8 fix trojan and snell's normal udp 2022-05-02 06:28:27 +08:00
Meta
658f1f5cda fix code mistake 2022-05-02 05:34:20 +08:00
MetaCubeX
5ccc047fe4 chore: adjust sniffer err info 2022-05-02 05:17:13 +08:00
MetaCubeX
6d704b9cd1 feat: sniffer support http 2022-05-02 05:10:18 +08:00
MetaCubeX
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
MetaCubeX
8681728b8d chore: doq parameters 2022-05-02 05:01:07 +08:00
Skyxim
bb1eb5979b chore: Merge alpha and beta 2022-05-02 01:14:30 +08:00
Skyxim
032b6a2cc5 chore: workflow 2022-05-02 00:59:41 +08:00
Skyxim
45a02e3439 chore: workflow 2022-05-02 00:19:28 +08:00
Skyxim
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
wwqgtxx
2aad9818e8 fix trojan and snell's udp over tcp 2022-04-30 22:26:38 +08:00
adlyq
4d5c0d2bf4 fix: auto-route priority wlan0 in Android 2022-04-30 17:43:37 +08:00
wwqgtxx
861205dbbe support udp in relay if last proxy could udp-over-tcp 2022-04-30 11:36:42 +08:00
Skyxim
9dbe20f2c5 fix: npe when with resolver is nil 2022-04-29 13:03:55 +08:00
adlyq
8dea62cd63 refactor: del useless file 2022-04-28 23:49:24 +08:00
adlyq
2d99f86780 fix: dhcp ifacename type 2022-04-28 23:44:37 +08:00
adlyq
fac63625fc fix: replace with sync.map for GroupBase 2022-04-28 23:43:10 +08:00
adlyq
b9f270162a refactor: field name 2022-04-28 23:10:08 +08:00
Skyxim
bbbe371ea9 fix: dns specified interface does not change 2022-04-28 22:40:06 +08:00
Skyxim
e2409f9639 Merge branch 'doq' into Alpha 2022-04-28 22:24:02 +08:00
Skyxim
4f634edaa9 refactor: doq dialer 2022-04-28 22:21:48 +08:00
adlyq
f1dab9e9ce refactor: optimize the performance of filter in proxy-group 2022-04-28 19:01:13 +08:00
adlyq
8217db82ce Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-28 17:47:29 +08:00
Meta
f9dd0a6bfc [Skip CI] chore: add docker workflow 2022-04-28 14:24:38 +08:00
Meta
b9b25be68d Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	.github/workflows/docker.yaml
2022-04-28 14:19:25 +08:00
Meta
179e5aad58 chore: add docker workflow 2022-04-28 14:18:54 +08:00
Meta
fc1eaf9a9d chore: add docker workflow 2022-04-28 14:11:35 +08:00
Skyxim
1756730693 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-04-28 12:44:51 +08:00
Skyxim
c7bc67e44c fix: handle metadata when dst is ip:port 2022-04-28 12:44:27 +08:00
Meta Gowork
949c1551f8 fixup! chore: system err log 2022-04-28 12:37:53 +08:00
adlyq
b23a333acf Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-28 11:51:57 +08:00
adlyq
47568051bf fix: problems caused when uid is 0 2022-04-28 11:51:40 +08:00
Skyxim
22c1e05e1c fix: rule provider http api crash 2022-04-28 09:44:29 +08:00
Skyxim
c161d5e6be fix: inner request error 2022-04-28 09:24:40 +08:00
Skyxim
573be855f7 fix: file not change modify time when updated rule 2022-04-28 09:07:58 +08:00
Skyxim
a38b2bcb6d Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-04-28 08:56:00 +08:00
Skyxim
2e74986fe4 refactor: adjust provider loading order, remove meaningless pointers 2022-04-28 08:55:45 +08:00
Skyxim
30eaa8add6 fix: count error 2022-04-28 08:54:33 +08:00
MetaCubeX
b498d2dda2 chore: system err log 2022-04-27 22:54:12 +08:00
Skyxim
96a32f5038 refactor: tcp concurrent 2022-04-27 21:37:20 +08:00
Meta
e11f6f84df chore: adjust workflows 2022-04-27 18:05:03 +08:00
Meta
5a1e1050b7 chore: adjust sniffer log 2022-04-27 18:04:02 +08:00
Meta
73aa8c7be7 feat: support uuid with custom string 2022-04-27 18:02:29 +08:00
Meta
183973e823 chore: Adjust the tcp-concurrent and sniffer log 2022-04-27 15:22:42 +08:00
Meta
2e08a4b4e1 fix: undefined parameter 2022-04-27 14:42:58 +08:00
yaling888
564a6fdf35 Chore: http 2022-04-27 12:52:11 +08:00
Kr328
cca3a1a934 Fix: http proxy Upgrade behavior (#2097) 2022-04-27 12:38:31 +08:00
Meta
ffb49ba4c5 fix: gvisor panic 2022-04-25 18:39:45 +08:00
yaling888
e4fb10faf9 Chore: update dependencies 2022-04-25 13:23:56 +08:00
yaling888
16a35527b7 Chore: increase nattable capacity 2022-04-25 13:18:41 +08:00
yaling888
4fd7d0f707 Chore: use generics as possible 2022-04-25 13:18:30 +08:00
adlyq
dee1aeb6c3 fix: logic of auto-detect-interface 2022-04-23 23:42:42 +08:00
adlyq
2f95d56a12 pref: uid style in log 2022-04-23 17:37:50 +08:00
adlyq
c77993eed3 refactor: tidy auto-route code 2022-04-23 17:31:16 +08:00
adlyq
ce663a7b96 fix: ipv6 enable logic 2022-04-23 14:21:58 +08:00
adlyq
9c2ca0feb0 fix: uid match 2022-04-23 13:37:37 +08:00
adlyq
b8d5321615 feat: cache uid 2022-04-23 12:11:26 +08:00
Skyxim
eb6f7e3158 fix: relay conn error when addr is domain 2022-04-23 10:26:22 +08:00
Skyxim
0947cb4a5a fix: whitelist 2022-04-23 09:52:23 +08:00
Skyxim
0368bb4180 fix: sniffer port whitelist error 2022-04-23 09:36:11 +08:00
Skyxim
4aeac0e227 chore: Adjust the connection IP log 2022-04-23 08:53:51 +08:00
adlyq
bd3c493c9f fix: ipv6 enable logic 2022-04-23 02:03:10 +08:00
Skyxim
0a99fc4d74 fix: wrong parameter name 2022-04-23 00:45:43 +08:00
Skyxim
19fc70b2c4 fix: general ipv6 is false should be broke ipv6 conn 2022-04-23 00:30:25 +08:00
Skyxim
81b5543b0d feat: support tcp concurrent, Separate dialing and dns resolver ipv6
tcp-concurrent:true
2022-04-23 00:27:22 +08:00
adlyq
2e1d9a4f2e fix: hotspot for android 2022-04-22 22:25:45 +08:00
adlyq
de4341c8cd Revert: "fix: proxy-groups filter logic"
This reverts commit 8a85c63b08.
2022-04-22 18:56:35 +08:00
adlyq
8a85c63b08 fix: proxy-groups filter logic 2022-04-22 17:27:55 +08:00
adlyq
b0dd74e74e fix: sniffer 2022-04-22 17:00:39 +08:00
adlyq
4dd9e199b7 fix: uid rule only support linux and android 2022-04-22 16:51:01 +08:00
adlyq
3d6aea4c1e feat: support uid rule
eg. UID,1000/5000-6000,Proxy
2022-04-22 16:27:51 +08:00
adlyq
0cb5270452 Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-04-22 15:58:57 +08:00
Meta Gowork
3f6d2e5f91 feat: dnsHijack support "any"
chore: adjust process debug display logic
2022-04-22 13:30:04 +08:00
yaling888
3b16fcef92 Chore: wait for system stack to close 2022-04-22 12:44:51 +08:00
yaling888
f91d106cdf Chore: fix typos 2022-04-22 12:42:20 +08:00
Skyxim
9e6ba64940 fix: add wait timeout, and log 2022-04-21 08:08:37 -07:00
Skyxim
bee1bddceb feat: add sniffer port whitelist, when empty will add all ports 2022-04-21 07:06:08 -07:00
adlyq
e98dcc4267 [fix] logic 2022-04-21 18:56:33 +08:00
adlyq
4b79f8de93 [fix] auto-route for android 2022-04-21 17:47:04 +08:00
yaling888
f40c2eb71d Chore: update dependencies 2022-04-20 22:55:30 +08:00
yaling888
7ca1a03d73 Refactor: metadata use netip.Addr 2022-04-20 22:52:05 +08:00
yaling888
6c4791480e Chore: IpToAddr 2022-04-20 22:09:16 +08:00
MetaCubeX
42d853a7e6 chore: upgrade dependencies 2022-04-20 01:31:33 +08:00
Kr328
5d36d8b139 Improve: replace bootstrap dns (#2080) 2022-04-19 22:49:39 +08:00
Anankke
5a4441c47f Chore: add none alias to dummy on ShadowsocksR (#2056) 2022-04-19 22:49:22 +08:00
yaling888
0ca10798ea Chore: fix typo 2022-04-19 22:38:20 +08:00
yaling888
3ea3653d7a Chore: persistence fakeip pool state 2022-04-19 22:37:47 +08:00
273 changed files with 9408 additions and 4497 deletions

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

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

View File

@@ -1,8 +1,15 @@
name: Alpha 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,48 +31,40 @@ 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:
NAME: Clash.Meta NAME: Clash.Meta
BINDIR: bin BINDIR: bin
run: make -j releases run: make -j$(($(nproc) + 1)) releases
- name: Delete current release assets - name: Delete current release assets
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: alpha tag_name: Prerelease-${{ github.ref_name }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Alpha - 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: alpha tag_name: Prerelease-${{ github.ref_name }}
files: bin/* files: bin/*
prerelease: true 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$(($(nproc) + 1)) 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,20 +1,26 @@
NAME=Clash.Meta NAME=Clash.Meta
BINDIR=bin BINDIR=bin
BRANCH=$(shell git rev-parse --abbrev-ref HEAD) BRANCH=$(shell git branch --show-current)
ifeq ($(BRANCH),Alpha)
VERSION=alpha-$(shell git rev-parse --short HEAD) 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=$(shell git rev-parse --short HEAD)
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)" \
-w -s -buildid=' -w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64v1 \ darwin-amd64 \
darwin-amd64v2 \
darwin-amd64v3 \
darwin-arm64 \ darwin-arm64 \
linux-amd64v1 \ linux-amd64-compatible \
linux-amd64v2 \ linux-amd64 \
linux-amd64v3 \
linux-armv5 \ linux-armv5 \
linux-armv6 \ linux-armv6 \
linux-armv7 \ linux-armv7 \
@@ -32,9 +38,8 @@ PLATFORM_LIST = \
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64v1 \ windows-amd64-compatible \
windows-amd64v2 \ windows-amd64 \
windows-amd64v3 \
windows-arm64 \ windows-arm64 \
windows-arm32v7 windows-arm32v7
@@ -43,32 +48,26 @@ 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-amd64:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v2: darwin-amd64-compatible:
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v1:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64: darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-386: linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v3: linux-amd64:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v2: linux-amd64-compatible:
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v1:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64: linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@@ -114,15 +113,12 @@ freebsd-arm64:
windows-386: windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v3: windows-amd64:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v2: windows-amd64-compatible:
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v1:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64: windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
@@ -150,4 +146,4 @@ lint:
golangci-lint run ./... golangci-lint run ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

@@ -251,8 +251,8 @@ User=clash-meta
Group=clash-meta Group=clash-meta
LimitNPROC=500 LimitNPROC=500
LimitNOFILE=1000000 LimitNOFILE=1000000
CapabilityBoundingSet=cap_net_admin CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=cap_net_admin AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
Restart=always Restart=always
ExecStartPre=/usr/bin/sleep 1s ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
@@ -274,7 +274,7 @@ $ systemctl start Clash-Meta
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`. Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard). To display process name in GUI please use [Dashboard For Meta](https://github.com/MetaCubeX/clash-dashboard).
![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png) ![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png)

View File

@@ -4,15 +4,15 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"time"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@@ -64,9 +64,9 @@ 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) histories = append(histories, item)
} }
return histories return histories
@@ -95,7 +95,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()
@@ -129,7 +129,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 {
@@ -138,7 +140,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
@@ -149,26 +151,32 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second,
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
} }
_ = resp.Body.Close()
if unifiedDelay { if unifiedDelay {
start = time.Now() second := time.Now()
resp, err = client.Do(req) resp, err = client.Do(req)
if err != nil { if err == nil {
return _ = resp.Body.Close()
start = second
} }
} }
resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
@@ -199,7 +207,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

@@ -11,7 +11,7 @@ import (
// NewHTTPS receive CONNECT request and return ConnContext // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext { func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPCONNECT metadata.Type = C.HTTPS
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port

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"
@@ -33,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,7 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/gofrs/uuid"
"net" "net"
"strings"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@@ -17,6 +19,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 +27,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
@@ -34,11 +51,25 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support") return c, errors.New("no support")
} }
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return nil, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
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 +79,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(),
}) })
} }
@@ -101,7 +133,12 @@ func NewBase(opt BaseOption) *Base {
type conn struct { type conn struct {
net.Conn net.Conn
chain C.Chain chain C.Chain
actualRemoteDestination string
}
func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination
} }
// Chains implements C.Connection // Chains implements C.Connection
@@ -115,12 +152,17 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
} }
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}} return &conn{c, []string{a.Name()}, parseRemoteDestination(a.Addr())}
} }
type packetConn struct { type packetConn struct {
net.PacketConn net.PacketConn
chain C.Chain chain C.Chain
actualRemoteDestination string
}
func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination
} }
// Chains implements C.Connection // Chains implements C.Connection
@@ -134,5 +176,17 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
} }
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}} return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
}
func parseRemoteDestination(addr string) string {
if dst, _, err := net.SplitHostPort(addr); err == nil {
return dst
} else {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
return dst
} else {
return ""
}
}
} }

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

@@ -0,0 +1,276 @@
package outbound
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"regexp"
"strconv"
"time"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/lucas-clemente/quic-go"
"github.com/lucas-clemente/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/tobyxdd/hysteria/pkg/core"
"github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/tobyxdd/hysteria/pkg/pmtud_fix"
"github.com/tobyxdd/hysteria/pkg/transport"
)
const (
mbpsToBps = 125000
minSpeedBPS = 16384
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
DefaultMaxIncomingStreams = 1024
DefaultALPN = "hysteria"
DefaultProtocol = "udp"
)
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct {
*Base
client *core.Client
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
hdc := hyDialerWithContext{
ctx: ctx,
hyDialer: func() (net.PacketConn, error) {
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
},
}
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc)
if err != nil {
return nil, err
}
return NewConn(tcpConn, h), nil
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
hdc := hyDialerWithContext{
ctx: ctx,
hyDialer: func() (net.PacketConn, error) {
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
},
}
udpConn, err := h.client.DialUDP(&hdc)
if err != nil {
return nil, err
}
return newPacketConn(&hyPacketConn{udpConn}, h), nil
}
type HysteriaOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Protocol string `proxy:"protocol,omitempty"`
Up string `proxy:"up"`
Down string `proxy:"down"`
AuthString string `proxy:"auth_str,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ALPN string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca_str,omitempty"`
ReceiveWindowConn int `proxy:"recv_window_conn,omitempty"`
ReceiveWindow int `proxy:"recv_window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable_mtu_discovery,omitempty"`
}
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
}
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
}
return up, down, nil
}
func NewHysteria(option HysteriaOption) (*Hysteria, error) {
clientTransport := &transport.ClientTransport{
Dialer: &net.Dialer{
Timeout: 8 * time.Second,
},
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server
if option.SNI != "" {
serverName = option.SNI
}
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
if len(option.ALPN) > 0 {
tlsConfig.NextProtos = []string{option.ALPN}
} else {
tlsConfig.NextProtos = []string{DefaultALPN}
}
if len(option.CustomCA) > 0 {
bs, err := ioutil.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
}
tlsConfig.RootCAs = cp
} else if option.CustomCAString != "" {
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM([]byte(option.CustomCAString)) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
}
tlsConfig.RootCAs = cp
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
KeepAlive: true,
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
EnableDatagrams: true,
}
if option.Protocol == "" {
option.Protocol = DefaultProtocol
}
if option.ReceiveWindowConn == 0 {
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
}
if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
}
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
}
var auth = []byte(option.AuthString)
var obfuscator obfs.Obfuscator
if len(option.Obfs) > 0 {
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
}
up, down, err := option.Speed()
if err != nil {
return nil, err
}
client, err := core.NewClient(
addr, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator,
)
if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
}
return &Hysteria{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Hysteria,
udp: true,
iface: option.Interface,
rmark: option.RoutingMark,
},
client: client,
}, nil
}
func stringToBps(s string) uint64 {
if s == "" {
return 0
}
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return stringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}
type hyPacketConn struct {
core.UDPConn
}
func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
b, addrStr, err := c.UDPConn.ReadFrom()
if err != nil {
return
}
n = copy(p, b)
addr = M.ParseSocksaddr(addrStr).UDPAddr()
return
}
func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
if err != nil {
return
}
n = len(p)
return
}
type hyDialerWithContext struct {
hyDialer func() (net.PacketConn, error)
ctx context.Context
}
func (h *hyDialerWithContext) ListenPacket() (net.PacketConn, error) {
return h.hyDialer()
}
func (h *hyDialerWithContext) Context() context.Context {
return h.ctx
}

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

@@ -7,20 +7,30 @@ import (
"net" "net"
"strconv" "strconv"
"github.com/Dreamacro/clash/common/pool"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/sagernet/sing-shadowsocks"
"github.com/Dreamacro/go-shadowsocks2/core" "github.com/sagernet/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
) )
func init() {
buf.DefaultAllocator = pool.DefaultAllocator
}
type ShadowSocks struct { type ShadowSocks struct {
*Base *Base
cipher core.Cipher method shadowsocks.Method
option *ShadowSocksOption
// obfs // obfs
obfsMode string obfsMode string
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
@@ -37,6 +47,7 @@ type ShadowSocksOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@@ -69,9 +80,11 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
} }
c = ss.cipher.StreamConn(c) if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
_, err := c.Write(serializesSocksAddr(metadata)) metadata.Host = uot.UOTMagicAddress
return c, err metadata.DstPort = "443"
}
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@@ -90,6 +103,13 @@ func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, op
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
if ss.option.UDPOverTCP {
tcpConn, err := ss.DialContext(ctx, metadata, opts...)
if err != nil {
return nil, err
}
return newPacketConn(uot.NewClientConn(tcpConn), ss), nil
}
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -100,16 +120,26 @@ func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Meta
pc.Close() pc.Close()
return nil, err return nil, err
} }
pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
return newPacketConn(pc, ss), nil
}
pc = ss.cipher.PacketConn(pc) // ListenPacketOnStreamConn implements C.ProxyAdapter
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
return newPacketConn(uot.NewClientConn(c), ss), nil
}
return nil, errors.New("no support")
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
} }
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
password := option.Password
ciph, err := core.PickCipher(cipher, nil, password)
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
} }
@@ -162,8 +192,9 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
}, },
cipher: ciph, method: method,
option: &option,
obfsMode: obfsMode, obfsMode: obfsMode,
v2rayOption: v2rayOption, v2rayOption: v2rayOption,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@@ -8,12 +8,11 @@ import (
"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/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol" "github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
) )
type ShadowSocksR struct { type ShadowSocksR struct {
@@ -92,6 +91,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 +108,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

@@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/common/convert"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -18,8 +19,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 +34,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 {
@@ -71,27 +70,32 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
header.Add(key, value) wsOpts.Headers.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{ wsOpts.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
}
} else {
if host := wsOpts.Headers.Get("Host"); host == "" {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@@ -249,26 +253,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

@@ -9,14 +9,17 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess" clashVMess "github.com/Dreamacro/clash/transport/vmess"
"github.com/sagernet/sing-vmess"
"golang.org/x/net/http2" "github.com/sagernet/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
) )
type Vmess struct { type Vmess struct {
@@ -27,30 +30,28 @@ 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 {
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"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"` AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
// TODO: compatible with VMESS WS older version configurations AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@@ -80,28 +81,21 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
if v.option.WSOpts.Path == "" {
v.option.WSOpts.Path = v.option.WSPath
}
if len(v.option.WSOpts.Headers) == 0 {
v.option.WSOpts.Headers = v.option.WSHeaders
}
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{ wsOpts := &clashVMess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
header.Add(key, value) wsOpts.Headers.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS { if v.option.TLS {
@@ -116,13 +110,18 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
if host := wsOpts.Headers.Get("Host"); host == "" {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = clashVMess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{ tlsOpts := &clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
} }
@@ -131,24 +130,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(c, tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
httpOpts := &vmess.HTTPConfig{ httpOpts := &clashVMess.HTTPConfig{
Host: host, Host: host,
Method: v.option.HTTPOpts.Method, Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path, Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers, Headers: v.option.HTTPOpts.Headers,
} }
c = vmess.StreamHTTPConn(c, httpOpts) c = clashVMess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := vmess.TLSConfig{ tlsOpts := clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"}, NextProtos: []string{"h2"},
@@ -158,24 +157,24 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, &tlsOpts) c, err = clashVMess.StreamTLSConn(c, &tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
h2Opts := &vmess.H2Config{ h2Opts := &clashVMess.H2Config{
Hosts: v.option.HTTP2Opts.Host, Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = vmess.StreamH2Conn(c, h2Opts) c, err = clashVMess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{ tlsOpts := &clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
} }
@@ -184,15 +183,18 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(c, tlsOpts)
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if metadata.NetWork == C.UDP {
return v.client.StreamConn(c, parseVmessAddr(metadata)) return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} else {
return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
}
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@@ -205,7 +207,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -235,6 +237,11 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
metadata.DstIP = ip metadata.DstIP = ip
} }
if v.option.PacketAddr {
metadata.Host = packetaddr.SeqPacketMagicAddress
metadata.DstPort = "443"
}
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && len(opts) == 0 {
@@ -244,7 +251,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
defer safeConnClose(c, err) defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} else { } else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
@@ -260,19 +267,36 @@ 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)
} }
if v.option.PacketAddr {
return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindClient(c)}, v), nil
} else if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if v.option.PacketAddr {
return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindClient(c)}, v), nil
} else if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(&threadSafePacketConn{PacketConn: pc}, 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{ var options []vmess.ClientOption
UUID: option.UUID, if option.AuthenticatedLength {
AlterID: uint16(option.AlterID), options = append(options, vmess.ClientWithAuthenticatedLength())
Security: security, }
HostName: option.Server, client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
Port: strconv.Itoa(option.Port),
IsAead: option.AlterID == 0,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -331,44 +355,29 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
} }
return v, nil return v, nil
} }
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr { type threadSafePacketConn struct {
var addrType byte net.PacketConn
var addr []byte access sync.Mutex
switch metadata.AddrType { }
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return &vmess.DstAddr{ c.access.Lock()
UDP: metadata.NetWork == C.UDP, defer c.access.Unlock()
AddrType: addrType, return c.PacketConn.WriteTo(b, addr)
Addr: addr,
Port: uint(port),
}
} }
type vmessPacketConn struct { type vmessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr
access sync.Mutex
} }
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
uc.access.Lock()
defer uc.access.Unlock()
return uc.Conn.Write(b) return uc.Conn.Write(b)
} }

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,19 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log" "errors"
"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"
"time"
) )
type Fallback struct { type Fallback struct {
*outbound.Base *GroupBase
disableUDP bool disableUDP bool
filter string testUrl string
single *singledo.Single selected string
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
@@ -35,8 +29,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 +43,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 +61,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,38 +77,57 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
return proxy return proxy
} }
func (f *Fallback) proxies(touch bool) []C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) { proxies := f.GetProxies(touch)
return getProvidersProxies(f.providers, touch, f.filter), nil al := proxies[0]
}) for i := len(proxies) - 1; i > -1; i-- {
proxy := proxies[i]
return elm.([]C.Proxy) if proxy.Name() == f.selected && proxy.Alive() {
return proxy
}
if proxy.Alive() {
al = proxy
}
}
return al
} }
func (f *Fallback) findAliveProxy(touch bool) C.Proxy { func (f *Fallback) Set(name string) error {
proxies := f.proxies(touch) var p C.Proxy
for _, proxy := range proxies { for _, proxy := range f.GetProxies(false) {
if proxy.Alive() { if proxy.Name() == name {
return proxy p = proxy
break
} }
} }
return proxies[0] if p == nil {
return errors.New("proxy not exist")
}
f.selected = name
if !p.Alive() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
_, _ = p.URLTest(ctx, f.testUrl)
}
return nil
} }
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, testUrl: option.URL,
disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
} }

View File

@@ -0,0 +1,188 @@
package outboundgroup
import (
"context"
"fmt"
"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 {
pd.Touch()
}
proxies = append(proxies, pd.Proxies()...)
}
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
return proxies
}
for _, pd := range gb.providers {
if touch {
pd.Touch()
}
if pd.VehicleType() == types.Compatible {
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
)
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) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
var wg sync.WaitGroup
var lock sync.Mutex
mp := map[string]uint16{}
proxies := gb.GetProxies(false)
for _, proxy := range proxies {
proxy := proxy
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay
lock.Unlock()
}
wg.Done()
}()
}
wg.Wait()
if len(mp) == 0 {
return mp, fmt.Errorf("get delay: all proxies timeout")
} else {
return mp, nil
}
}
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.Debugln("ProxyGroup: %s first failed", gb.Name())
gb.failedTime = time.Now()
} else {
if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
return
}
log.Debugln("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,51 @@ 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) maxRetry := 5
return lb.strategyFn(proxies, metadata) lruCache := cache.NewLRUCache[uint64, int](
cache.WithAge[uint64, int](int64(ttl.Seconds())),
cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
length := len(proxies)
idx, has := lruCache.Get(key)
if !has {
idx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
}
nowIdx := idx
for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx]
if proxy.Alive() {
if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx)
}
return proxy
} else {
nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
}
}
lruCache.Delete(key)
lruCache.Set(key, 0)
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 +209,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

@@ -75,8 +75,12 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
providers = append(providers, pd) providers = append(providers, pd)
providersMap[groupName] = pd providersMap[groupName] = pd
} else { } else {
if groupOption.URL == "" || groupOption.Interval == 0 { if groupOption.URL == "" {
return nil, errMissHealthCheck groupOption.URL = "http://www.gstatic.com/generate_204"
}
if groupOption.Interval == 0 {
groupOption.Interval = 300
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)

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,11 @@ 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)
} }
} }
type SelectAble interface {
Set(string) error
}

View File

@@ -81,6 +81,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewTrojan(*trojanOption) proxy, err = outbound.NewTrojan(*trojanOption)
case "hysteria":
hyOption := &outbound.HysteriaOption{}
err = decoder.Decode(mapping, hyOption)
if err != nil {
break
}
proxy, err = outbound.NewHysteria(*hyOption)
default: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@@ -16,61 +16,82 @@ var (
dirMode os.FileMode = 0o755 dirMode os.FileMode = 0o755
) )
type parser = func([]byte) (any, error) type parser[V any] func([]byte) (V, error)
type fetcher struct { type fetcher[V any] struct {
name string name string
vehicle types.Vehicle vehicle types.Vehicle
updatedAt *time.Time updatedAt *time.Time
ticker *time.Ticker ticker *time.Ticker
done chan struct{} done chan struct{}
hash [16]byte hash [16]byte
parser parser parser parser[V]
onUpdate func(any) interval time.Duration
onUpdate func(V)
} }
func (f *fetcher) Name() string { func (f *fetcher[V]) Name() string {
return f.name return f.name
} }
func (f *fetcher) VehicleType() types.VehicleType { func (f *fetcher[V]) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *fetcher) Initial() (any, error) { func (f *fetcher[V]) Initial() (V, error) {
var ( var (
buf []byte buf []byte
err error err error
isLocal bool isLocal bool
forceUpdate bool
) )
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()
f.updatedAt = &modTime f.updatedAt = &modTime
isLocal = true isLocal = true
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
log.Infoln("[Provider] %s not updated for a long time, force refresh", f.Name())
forceUpdate = true
}
} else { } else {
buf, err = f.vehicle.Read() buf, err = f.vehicle.Read()
} }
if err != nil { if err != nil {
return nil, err return getZero[V](), err
}
var proxies V
if forceUpdate {
var forceBuf []byte
if forceBuf, err = f.vehicle.Read(); err == nil {
if proxies, err = f.parser(forceBuf); err == nil {
isLocal = false
buf = forceBuf
}
}
}
if err != nil || !forceUpdate {
proxies, err = f.parser(buf)
} }
proxies, err := f.parser(buf)
if err != nil { if err != nil {
if !isLocal { if !isLocal {
return nil, err return getZero[V](), err
} }
// parse local file error, fallback to remote // parse local file error, fallback to remote
buf, err = f.vehicle.Read() buf, err = f.vehicle.Read()
if err != nil { if err != nil {
return nil, err return getZero[V](), err
} }
proxies, err = f.parser(buf) proxies, err = f.parser(buf)
if err != nil { if err != nil {
return nil, err return getZero[V](), err
} }
isLocal = false isLocal = false
@@ -78,7 +99,7 @@ func (f *fetcher) Initial() (any, error) {
if f.vehicle.Type() != types.File && !isLocal { if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err return getZero[V](), err
} }
} }
@@ -92,10 +113,10 @@ func (f *fetcher) Initial() (any, error) {
return proxies, nil return proxies, nil
} }
func (f *fetcher) Update() (any, bool, error) { func (f *fetcher[V]) Update() (V, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return nil, false, err return getZero[V](), false, err
} }
now := time.Now() now := time.Now()
@@ -103,17 +124,17 @@ func (f *fetcher) Update() (any, bool, error) {
if bytes.Equal(f.hash[:], hash[:]) { if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now) os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil return getZero[V](), true, nil
} }
proxies, err := f.parser(buf) proxies, err := f.parser(buf)
if err != nil { if err != nil {
return nil, false, err return getZero[V](), false, err
} }
if f.vehicle.Type() != types.File { if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil { if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err return getZero[V](), false, err
} }
} }
@@ -123,14 +144,14 @@ func (f *fetcher) Update() (any, bool, error) {
return proxies, false, nil return proxies, false, nil
} }
func (f *fetcher) Destroy() error { func (f *fetcher[V]) Destroy() error {
if f.ticker != nil { if f.ticker != nil {
f.done <- struct{}{} f.done <- struct{}{}
} }
return nil return nil
} }
func (f *fetcher) pullLoop() { func (f *fetcher[V]) pullLoop() {
for { for {
select { select {
case <-f.ticker.C: case <-f.ticker.C:
@@ -168,18 +189,24 @@ func safeWrite(path string, buf []byte) error {
return os.WriteFile(path, buf, fileMode) return os.WriteFile(path, buf, fileMode)
} }
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher { func newFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser parser[V], onUpdate func(V)) *fetcher[V] {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)
} }
return &fetcher{ return &fetcher[V]{
name: name, name: name,
ticker: ticker, ticker: ticker,
vehicle: vehicle, vehicle: vehicle,
parser: parser, parser: parser,
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
onUpdate: onUpdate, onUpdate: onUpdate,
interval: interval,
} }
} }
func getZero[V any]() V {
var result V
return result
}

View File

@@ -2,6 +2,7 @@ package provider
import ( import (
"context" "context"
"github.com/Dreamacro/clash/common/singledo"
"time" "time"
"github.com/Dreamacro/clash/common/batch" "github.com/Dreamacro/clash/common/batch"
@@ -26,15 +27,14 @@ type HealthCheck struct {
lazy bool lazy bool
lastTouch *atomic.Int64 lastTouch *atomic.Int64
done chan struct{} done chan struct{}
singleDo *singledo.Single[struct{}]
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go func() { go func() {
t := time.NewTicker(30 * time.Second) time.Sleep(30 * time.Second)
<-t.C
t.Stop()
hc.check() hc.check()
}() }()
@@ -65,17 +65,21 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10)) _, _, _ = hc.singleDo.Do(func() (struct{}, error) {
for _, proxy := range hc.proxies { b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
p := proxy for _, proxy := range hc.proxies {
b.Go(p.Name(), func() (any, error) { p := proxy
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) b.Go(p.Name(), func() (bool, error) {
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
p.URLTest(ctx, hc.url) defer cancel()
return nil, nil _, _ = p.URLTest(ctx, hc.url)
}) return false, nil
} })
b.Wait() }
b.Wait()
return struct{}{}, nil
})
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {
@@ -90,5 +94,6 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
lazy: lazy, lazy: lazy,
lastTouch: atomic.NewInt64(0), lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
} }
} }

View File

@@ -4,7 +4,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/common/convert"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"math"
"runtime" "runtime"
"time" "time"
@@ -12,7 +14,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
const ( const (
@@ -23,15 +25,16 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
// for auto gc // ProxySetProvider for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
*fetcher *fetcher[[]C.Proxy]
proxies []C.Proxy proxies []C.Proxy
healthCheck *HealthCheck healthCheck *HealthCheck
version uint
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@@ -40,11 +43,14 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(), "proxies": pp.Proxies(),
//TODO maybe error because year value overflow "updatedAt": pp.updatedAt,
"updatedAt": pp.updatedAt,
}) })
} }
func (pp *proxySetProvider) Version() uint {
return pp.version
}
func (pp *proxySetProvider) Name() string { func (pp *proxySetProvider) Name() string {
return pp.name return pp.name
} }
@@ -66,12 +72,7 @@ func (pp *proxySetProvider) Initial() error {
if err != nil { if err != nil {
return err return err
} }
pp.onUpdate(elm) pp.onUpdate(elm)
if pp.healthCheck.auto() {
go pp.healthCheck.process()
}
return nil return nil
} }
@@ -83,46 +84,148 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy { func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch() pp.healthCheck.touch()
return pp.Proxies()
} }
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies pp.proxies = proxies
pp.healthCheck.setProxy(proxies) pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() { if pp.healthCheck.auto() {
go pp.healthCheck.check() defer func() { go pp.healthCheck.check() }()
} }
} }
func stopProxyProvider(pd *ProxySetProvider) { func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close() pd.healthCheck.close()
pd.fetcher.Destroy() _ = pd.fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
//filterReg, err := regexp.Compile(filter)
filterReg, err := regexp2.Compile(filter, 0) filterReg, err := regexp2.Compile(filter, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err) return nil, fmt.Errorf("invalid filter regex: %w", err)
} }
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{ pd := &proxySetProvider{
proxies: []C.Proxy{}, proxies: []C.Proxy{},
healthCheck: hc, healthCheck: hc,
} }
onUpdate := func(elm any) { fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg), proxiesOnUpdate(pd))
ret := elm.([]C.Proxy) pd.fetcher = fetcher
pd.setProxies(ret)
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
}
// CompatibleProvider for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
})
}
func (cp *compatibleProvider) Version() uint {
return cp.version
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (cp *compatibleProvider) Type() types.ProviderType {
return types.Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least")
} }
proxiesParseAndFilter := func(buf []byte) (any, error) { if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
name: name,
proxies: proxies,
healthCheck: hc,
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
}
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
if pd.version == math.MaxUint {
pd.version = 0
} else {
pd.version++
}
}
}
func proxiesParseAndFilter(filter string, filterReg *regexp2.Regexp) parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil { if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%s, %w", err.Error(), err1)
}
schema.Proxies = proxies
} }
if schema.Proxies == nil { if schema.Proxies == nil {
@@ -152,88 +255,4 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
return proxies, nil return proxies, nil
} }
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
}
// for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
})
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.auto() {
go cp.healthCheck.process()
}
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (cp *compatibleProvider) Type() types.ProviderType {
return types.Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least")
}
pd := &compatibleProvider{
name: name,
proxies: proxies,
healthCheck: hc,
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil
} }

View File

@@ -2,17 +2,12 @@ package provider
import ( import (
"context" "context"
"github.com/Dreamacro/clash/component/dialer" netHttp "github.com/Dreamacro/clash/component/http"
"github.com/Dreamacro/clash/listener/inner" types "github.com/Dreamacro/clash/constant/provider"
"io" "io"
"net"
"net/http" "net/http"
"net/url"
"os" "os"
"time" "time"
netHttp "github.com/Dreamacro/clash/common/net"
types "github.com/Dreamacro/clash/constant/provider"
) )
type FileVehicle struct { type FileVehicle struct {
@@ -51,56 +46,16 @@ func (h *HTTPVehicle) Path() string {
func (h *HTTPVehicle) Read() ([]byte, error) { func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel() defer cancel()
resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
uri, err := url.Parse(h.url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
req.Header.Set("User-Agent", netHttp.UA)
if err != nil {
return nil, err
}
if user := uri.User; user != nil {
password, _ := user.Password()
req.SetBasicAuth(user.Username(), password)
}
req = req.WithContext(ctx)
transport := &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
conn := inner.HandleTcp(address, uri.Hostname())
return conn, nil
},
}
client := http.Client{Transport: transport}
resp, err := client.Do(req)
if err != nil {
transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return dialer.DialContext(ctx, network, address)
}
resp, err = client.Do(req)
if err != nil {
return nil, err
}
}
defer resp.Body.Close() defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body) buf, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return buf, nil return buf, nil
} }

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

View File

@@ -3,19 +3,20 @@ 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[K comparable, V any] func(*LruCache[K, V]) 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[K comparable, V any](cb EvictCallback) Option[K, V] { func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
return func(l *LruCache[K, V]) { return func(l *LruCache[K, V]) {
l.onEvict = cb l.onEvict = cb
} }
@@ -57,18 +58,18 @@ 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[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{ 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 {
@@ -129,7 +130,7 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value.(*entry[K, V]) e := le.Value
e.value = value e.value = value
e.expires = expires.Unix() e.expires = expires.Unix()
} else { } else {
@@ -154,11 +155,11 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
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[K, V]) elm := e.Value
n.cache[elm.key] = n.lru.PushBack(elm) n.cache[elm.key] = n.lru.PushBack(elm)
} }
} }
@@ -172,7 +173,7 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
return nil return nil
} }
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry[K, V]).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,7 +181,7 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
} }
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
el := le.Value.(*entry[K, V]) el := le.Value
if c.maxAge > 0 && c.updateAgeOnGet { if c.maxAge > 0 && c.updateAgeOnGet {
el.expires = time.Now().Unix() + c.maxAge el.expires = time.Now().Unix() + c.maxAge
} }
@@ -201,15 +202,15 @@ func (c *LruCache[K, V]) Delete(key K) {
func (c *LruCache[K, V]) 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[K, V]).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[K, V]) 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[K, V]) 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)
@@ -219,7 +220,7 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element) {
func (c *LruCache[K, V]) 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

View File

@@ -52,18 +52,18 @@ func TestLRUMaxAge(t *testing.T) {
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
c.lru.Back().Value.(*entry[string, string]).expires = now c.lru.Back().Value.expires = now
// Reset // Reset
c.Set("foo", "bar") c.Set("foo", "bar")
e := c.lru.Back().Value.(*entry[string, string]) e := c.lru.Back().Value
assert.True(t, e.expires >= now) assert.True(t, e.expires >= now)
c.lru.Back().Value.(*entry[string, string]).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[string, string]) 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[string, string]).expires = now le.Value.expires = now
} }
} }
@@ -95,11 +95,11 @@ func TestLRUpdateOnGet(t *testing.T) {
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
c.lru.Back().Value.(*entry[string, string]).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[string, string]).expires > expires) assert.True(t, c.lru.Back().Value.expires > expires)
} }
func TestMaxSize(t *testing.T) { func TestMaxSize(t *testing.T) {
@@ -126,8 +126,8 @@ 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[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))

469
common/convert/converter.go Normal file
View File

@@ -0,0 +1,469 @@
package convert
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
)
var encRaw = base64.RawStdEncoding
var enc = base64.StdEncoding
func DecodeBase64(buf []byte) []byte {
dBuf := make([]byte, encRaw.DecodedLen(len(buf)))
n, err := encRaw.Decode(dBuf, buf)
if err != nil {
n, err = enc.Decode(dBuf, buf)
if err != nil {
return buf
}
}
return dBuf[:n]
}
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data := DecodeBase64(buf)
arr := strings.Split(string(data), "\n")
proxies := make([]map[string]any, 0, len(arr))
names := make(map[string]int, 200)
for _, line := range arr {
line = strings.TrimRight(line, " \r")
if line == "" {
continue
}
scheme, body, found := strings.Cut(line, "://")
if !found {
continue
}
scheme = strings.ToLower(scheme)
switch scheme {
case "hysteria":
urlHysteria, err := url.Parse(line)
if err != nil {
continue
}
query := urlHysteria.Query()
name := uniqueName(names, urlHysteria.Fragment)
hysteria := make(map[string]any, 20)
hysteria["name"] = name
hysteria["type"] = scheme
hysteria["server"] = urlHysteria.Hostname()
hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs")
hysteria["alpn"] = query.Get("alpn")
hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol")
up := query.Get("up")
down := query.Get("down")
if up == "" {
up = query.Get("upmbps")
}
if down == "" {
down = query.Get("downmbps")
}
hysteria["down"] = down
hysteria["up"] = up
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria)
case "trojan":
urlTrojan, err := url.Parse(line)
if err != nil {
continue
}
query := urlTrojan.Query()
name := uniqueName(names, urlTrojan.Fragment)
trojan := make(map[string]any, 20)
trojan["name"] = name
trojan["type"] = scheme
trojan["server"] = urlTrojan.Hostname()
trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username()
trojan["udp"] = true
trojan["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni
}
network := strings.ToLower(query.Get("type"))
if network != "" {
trojan["network"] = network
}
switch network {
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
trojan["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
trojan["grpc-opts"] = grpcOpts
}
proxies = append(proxies, trojan)
case "vless":
urlVless, err := url.Parse(line)
if err != nil {
continue
}
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20)
vless["name"] = name
vless["type"] = scheme
vless["server"] = urlVless.Hostname()
vless["port"] = urlVless.Port()
vless["uuid"] = urlVless.User.Username()
vless["udp"] = true
vless["skip-cert-verify"] = false
vless["tls"] = false
tls := strings.ToLower(query.Get("security"))
if strings.Contains(tls, "tls") {
vless["tls"] = true
}
sni := query.Get("sni")
if sni != "" {
vless["servername"] = sni
}
flow := strings.ToLower(query.Get("flow"))
if flow != "" {
vless["flow"] = flow
}
network := strings.ToLower(query.Get("type"))
fakeType := strings.ToLower(query.Get("headerType"))
if fakeType == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
vless["network"] = network
switch network {
case "tcp":
if fakeType != "none" {
headers := make(map[string]any)
httpOpts := make(map[string]any)
httpOpts["path"] = []string{"/"}
if query.Get("host") != "" {
headers["Host"] = []string{query.Get("host")}
}
if query.Get("method") != "" {
httpOpts["method"] = query.Get("method")
}
if query.Get("path") != "" {
httpOpts["path"] = []string{query.Get("path")}
}
httpOpts["headers"] = headers
vless["http-opts"] = httpOpts
}
case "http":
headers := make(map[string]any)
h2Opts := make(map[string]any)
h2Opts["path"] = []string{"/"}
if query.Get("path") != "" {
h2Opts["path"] = []string{query.Get("path")}
}
if query.Get("host") != "" {
h2Opts["host"] = []string{query.Get("host")}
}
h2Opts["headers"] = headers
vless["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
headers["Host"] = query.Get("host")
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
vless["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
vless["grpc-opts"] = grpcOpts
}
proxies = append(proxies, vless)
case "vmess":
dcBuf, err := encRaw.DecodeString(body)
if err != nil {
continue
}
jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
values := make(map[string]any, 20)
if jsonDc.Decode(&values) != nil {
continue
}
name := uniqueName(names, values["ps"].(string))
vmess := make(map[string]any, 20)
vmess["name"] = name
vmess["type"] = scheme
vmess["server"] = values["add"]
vmess["port"] = values["port"]
vmess["uuid"] = values["id"]
vmess["alterId"] = values["aid"]
vmess["cipher"] = "auto"
vmess["udp"] = true
vmess["tls"] = false
vmess["skip-cert-verify"] = false
if values["cipher"] != nil && values["cipher"] != "" {
vmess["cipher"] = values["cipher"]
}
sni := values["sni"]
if sni != "" {
vmess["servername"] = sni
}
network := strings.ToLower(values["net"].(string))
if values["type"] == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
vmess["network"] = network
tls := strings.ToLower(values["tls"].(string))
if strings.Contains(tls, "tls") {
vmess["tls"] = true
}
switch network {
case "http":
headers := make(map[string]any)
httpOpts := make(map[string]any)
if values["host"] != "" && values["host"] != nil {
headers["Host"] = []string{values["host"].(string)}
}
httpOpts["path"] = []string{"/"}
if values["path"] != "" && values["path"] != nil {
httpOpts["path"] = []string{values["path"].(string)}
}
httpOpts["headers"] = headers
vmess["http-opts"] = httpOpts
case "h2":
headers := make(map[string]any)
h2Opts := make(map[string]any)
if values["host"] != "" && values["host"] != nil {
headers["Host"] = []string{values["host"].(string)}
}
h2Opts["path"] = values["path"]
h2Opts["headers"] = headers
vmess["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
wsOpts["path"] = []string{"/"}
if values["host"] != "" && values["host"] != nil {
headers["Host"] = values["host"].(string)
}
if values["path"] != "" && values["path"] != nil {
wsOpts["path"] = values["path"].(string)
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = values["path"]
vmess["grpc-opts"] = grpcOpts
}
proxies = append(proxies, vmess)
case "ss":
urlSS, err := url.Parse(line)
if err != nil {
continue
}
name := uniqueName(names, urlSS.Fragment)
port := urlSS.Port()
if port == "" {
dcBuf, err := encRaw.DecodeString(urlSS.Host)
if err != nil {
continue
}
urlSS, err = url.Parse("ss://" + string(dcBuf))
if err != nil {
continue
}
}
var (
cipher = urlSS.User.Username()
password string
)
if password, found = urlSS.User.Password(); !found {
dcBuf, _ := enc.DecodeString(cipher)
if !strings.Contains(string(dcBuf), "2022-blake3") {
dcBuf, _ = encRaw.DecodeString(cipher)
}
cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found {
continue
}
}
ss := make(map[string]any, 20)
ss["name"] = name
ss["type"] = scheme
ss["server"] = urlSS.Hostname()
ss["port"] = urlSS.Port()
ss["cipher"] = cipher
ss["password"] = password
query := urlSS.Query()
ss["udp"] = true
if strings.Contains(query.Get("plugin"), "obfs") {
obfsParams := strings.Split(query.Get("plugin"), ";")
ss["plugin"] = "obfs"
ss["plugin-opts"] = map[string]any{
"host": obfsParams[2][10:],
"mode": obfsParams[1][5:],
}
}
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := encRaw.DecodeString(body)
if err != nil {
continue
}
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok {
continue
}
beforeArr := strings.Split(before, ":")
if len(beforeArr) != 6 {
continue
}
host := beforeArr[0]
port := beforeArr[1]
protocol := beforeArr[2]
method := beforeArr[3]
obfs := beforeArr[4]
password := decodeUrlSafe(urlSafe(beforeArr[5]))
query, err := url.ParseQuery(urlSafe(after))
if err != nil {
continue
}
remarks := decodeUrlSafe(query.Get("remarks"))
name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam")
ssr := make(map[string]any, 20)
ssr["name"] = name
ssr["type"] = scheme
ssr["server"] = host
ssr["port"] = port
ssr["cipher"] = method
ssr["password"] = password
ssr["obfs"] = obfs
ssr["protocol"] = protocol
ssr["udp"] = true
if obfsParam != "" {
ssr["obfs-param"] = obfsParam
}
if protocolParam != "" {
ssr["protocol-param"] = protocolParam
}
proxies = append(proxies, ssr)
}
}
if len(proxies) == 0 {
return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
}
return proxies, nil
}
func urlSafe(data string) string {
return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_")
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.RawURLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}
func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok {
index++
names[name] = index
name = fmt.Sprintf("%s-%02d", name, index)
} else {
index = 0
names[name] = index
}
return name
}

316
common/convert/util.go Normal file
View File

@@ -0,0 +1,316 @@
package convert
import (
"encoding/base64"
"math/rand"
"net/http"
"strings"
"github.com/gofrs/uuid"
)
var hostsSuffix = []string{
"-cdn.aliyuncs.com",
".alicdn.com",
".pan.baidu.com",
".tbcache.com",
".aliyuncdn.com",
".vod.miguvideo.com",
".cibntv.net",
".myqcloud.com",
".smtcdns.com",
".alikunlun.com",
".smtcdns.net",
".apcdns.net",
".cdn-go.cn",
".cdntip.com",
".cdntips.com",
".alidayu.com",
".alidns.com",
".cdngslb.com",
".mxhichina.com",
".alibabadns.com",
}
var userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto C Build/NRD90M.059) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.111 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J120M Build/LMY47X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Moto G (5) Build/NPPS25.137-93-14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L03 Build/HUAWEICAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Chrome/8.0.552.237 Safari/534.10",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36",
"Mozilla/5.0 (X11; Datanyze; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; SM-J111M Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J700M Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Slackware/Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.100 Safari/534.30",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; WAS-LX3 Build/HUAWEIWAS-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.1805 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX3 Build/HUAWEITRT-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.89 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1610 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; de-de; SAMSUNG GT-I9195 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.5 Chrome/28.0.1500.94 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX3 Build/HUAWEIANE-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (X11; U; Linux i586; en-US) AppleWebKit/533.2 (KHTML, like Gecko) Chrome/5.0.342.1 Safari/533.2",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-J500M Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.7 (KHTML, like Gecko) Chrome/7.0.517.44 Safari/534.7",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; vivo 1716 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A1601 Build/LMY47I) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; TRT-LX2 Build/HUAWEITRT-LX2; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.125 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Linux; Android 6.0; CAM-L21 Build/HUAWEICAM-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.3 Safari/534.24",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi 4X Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Linux; Android 4.4.2; SM-G7102 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; HUAWEI CUN-L22 Build/HUAWEICUN-L22; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1.1; A37fw Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J730GM Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-G610F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.2; Redmi Note 5A Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Mozilla/5.0 (Unknown; Linux) AppleWebKit/538.1 (KHTML, like Gecko) Chrome/v1.0.0 Safari/538.1",
"Mozilla/5.0 (Linux; Android 7.0; BLL-L22 Build/HUAWEIBLL-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.0; SM-J710F Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1.1; CPH1723 Build/N6F26Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Linux; Android 8.0.0; FIG-LX3 Build/HUAWEIFIG-LX3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 6.1; de-DE) AppleWebKit/534.17 (KHTML, like Gecko) Chrome/10.0.649.0 Safari/534.17",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36",
"Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/533.4 (KHTML, like Gecko) Chrome/5.0.375.99 Safari/533.4",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 MVisionPlayer/1.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Linux; Android 5.1; A37f Build/LMY47V) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.93 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532M Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36",
"Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36",
}
var (
hostsLen = len(hostsSuffix)
uaLen = len(userAgents)
)
func RandHost() string {
id, _ := uuid.NewV4()
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes()))
base = strings.ReplaceAll(base, "-", "")
base = strings.ReplaceAll(base, "_", "")
buf := []byte(base)
prefix := string(buf[:3]) + "---"
prefix += string(buf[6:8]) + "-"
prefix += string(buf[len(buf)-8:])
return prefix + hostsSuffix[rand.Intn(hostsLen)]
}
func RandUserAgent() string {
return userAgents[rand.Intn(uaLen)]
}
func SetUserAgent(header http.Header) {
if header.Get("User-Agent") != "" {
return
}
userAgent := RandUserAgent()
header.Set("User-Agent", userAgent)
}

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

View File

@@ -1,5 +0,0 @@
package net
const (
UA = "Clash"
)

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

@@ -8,7 +8,7 @@ import (
"sync" "sync"
) )
var defaultAllocator = NewAllocator() var DefaultAllocator = NewAllocator()
// Allocator for incoming frames, optimized to prevent overwriting after zeroing // Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct { type Allocator struct {
@@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//lint:ignore SA6002 ignore temporarily
//nolint //nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }

View File

@@ -13,9 +13,9 @@ const (
) )
func Get(size int) []byte { func Get(size int) []byte {
return defaultAllocator.Get(size) return DefaultAllocator.Get(size)
} }
func Put(buf []byte) error { func Put(buf []byte) error {
return defaultAllocator.Put(buf) return DefaultAllocator.Put(buf)
} }

View File

@@ -5,28 +5,27 @@ 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 func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
func (s *Single) Do(fn func() (any, error)) (v any, 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 +33,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:]

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,9 +3,19 @@ package dialer
import ( import (
"context" "context"
"errors" "errors"
"net" "fmt"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"net"
"net/netip"
"sync"
)
var (
dialMux sync.Mutex
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
tcpConcurrent = false
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) {
@@ -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")
} }
@@ -70,6 +56,10 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
o(cfg) o(cfg)
} }
if DisableIPv6 {
network = "udp4"
}
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.interfaceName != "" { if cfg.interfaceName != "" {
addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address) addr, err := bindIfaceToListenConfig(cfg.interfaceName, lc, network, address)
@@ -88,7 +78,25 @@ 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()
tcpConcurrent = concurrent
if concurrent {
actualSingleDialContext = concurrentSingleDialContext
actualDualStackDialContext = concurrentDualStackDialContext
} else {
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
}
dialMux.Unlock()
}
func GetDial() bool {
return tcpConcurrent
}
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 +107,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 +140,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)
@@ -158,27 +170,159 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
go startRacer(ctx, network+"4", host, opt.direct, false) go startRacer(ctx, network+"4", host, opt.direct, false)
go startRacer(ctx, network+"6", host, opt.direct, true) go startRacer(ctx, network+"6", host, opt.direct, true)
for res := range results { count := 2
if res.error == nil { for i := 0; i < count; i++ {
return res.Conn, nil select {
} case res := <-results:
if res.error == nil {
if !res.ipv6 { return res.Conn, nil
primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else {
return nil, primary.error
} }
if !res.ipv6 {
primary = res
} else {
fallback = res
}
if primary.done && fallback.done {
if primary.resolved {
return nil, primary.error
} else if fallback.resolved {
return nil, fallback.error
} else {
return nil, primary.error
}
}
case <-ctx.Done():
break
} }
} }
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 i := 0; i < connCount; i++ {
select {
case res := <-results:
if res.error == nil {
return res.Conn, nil
}
case <-ctx.Done():
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,20 +1,19 @@
package fakeip package fakeip
import ( import (
"encoding/binary"
"errors" "errors"
"math/bits"
"net/netip" "net/netip"
"sync" "sync"
"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"
) )
type uint128 struct { const (
hi uint64 offsetKey = "key-offset-fake-ip"
lo uint64 cycleKey = "key-cycle-fake-ip"
} )
type store interface { type store interface {
GetByHost(host string) (netip.Addr, bool) GetByHost(host string) (netip.Addr, bool)
@@ -98,22 +97,15 @@ func (p *Pool) CloneFrom(o *Pool) {
} }
func (p *Pool) get(host string) netip.Addr { func (p *Pool) get(host string) netip.Addr {
for { p.offset = p.offset.Next()
p.offset = p.offset.Next()
if !p.offset.Less(p.last) { if !p.offset.Less(p.last) {
p.cycle = true p.cycle = true
p.offset = p.first p.offset = p.first
} }
if p.cycle { if p.cycle || p.store.Exist(p.offset) {
p.store.DelByIP(p.offset) p.store.DelByIP(p.offset)
break
}
if !p.store.Exist(p.offset) {
break
}
} }
p.store.PutByIP(p.offset, host) p.store.PutByIP(p.offset, host)
@@ -121,7 +113,39 @@ func (p *Pool) get(host string) netip.Addr {
} }
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 (p *Pool) StoreState() {
if s, ok := p.store.(*cachefileStore); ok {
s.PutByHost(offsetKey, p.offset)
if p.cycle {
s.PutByHost(cycleKey, p.offset)
}
}
}
func (p *Pool) restoreState() {
if s, ok := p.store.(*cachefileStore); ok {
if _, exist := s.GetByHost(cycleKey); exist {
p.cycle = true
}
if offset, exist := s.GetByHost(offsetKey); exist {
if p.ipnet.Contains(offset) {
p.offset = offset
} else {
_ = p.FlushFakeIP()
}
} else if s.Exist(p.first) {
_ = p.FlushFakeIP()
}
}
} }
type Options struct { type Options struct {
@@ -143,10 +167,10 @@ func New(options Options) (*Pool, error) {
hostAddr = options.IPNet.Masked().Addr() hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next() gateway = hostAddr.Next()
first = gateway.Next().Next() first = gateway.Next().Next()
last = add(hostAddr, 1<<uint64(hostAddr.BitLen()-options.IPNet.Bits())-1) last = nnip.UnMasked(*options.IPNet)
) )
if !options.IPNet.IsValid() || !first.Less(last) || !options.IPNet.Contains(last) { if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {
return nil, errors.New("ipnet don't have valid ip") return nil, errors.New("ipnet don't have valid ip")
} }
@@ -167,31 +191,7 @@ func New(options Options) (*Pool, error) {
pool.store = newMemoryStore(options.Size) pool.store = newMemoryStore(options.Size)
} }
pool.restoreState()
return pool, nil return pool, nil
} }
// add returns addr + n.
func add(addr netip.Addr, n uint64) netip.Addr {
buf := addr.As16()
u := uint128{
binary.BigEndian.Uint64(buf[:8]),
binary.BigEndian.Uint64(buf[8:]),
}
lo, carry := bits.Add64(u.lo, n, 0)
u.hi = u.hi + carry
u.lo = lo
binary.BigEndian.PutUint64(buf[:8], u.hi)
binary.BigEndian.PutUint64(buf[8:], u.lo)
a := netip.AddrFrom16(buf)
if addr.Is4() {
return a.Unmap()
}
return a
}

View File

@@ -240,13 +240,15 @@ 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.True(t, foo == fox) assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz) assert.False(t, foo == baz)
assert.True(t, bar == bax) assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next) assert.False(t, bar == next)
assert.True(t, baz == nero) assert.True(t, baz == nero)
} }
@@ -267,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.True(t, foo == fox) assert.True(t, foo == fox)
assert.True(t, foo == next)
assert.False(t, foo == baz) assert.False(t, foo == baz)
assert.True(t, bar == bax) assert.True(t, bar == bax)
assert.True(t, bar == baz)
assert.False(t, bar == next) assert.False(t, bar == next)
assert.True(t, baz == nero) assert.True(t, baz == nero)
} }

View File

@@ -30,7 +30,7 @@ func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*route
return nil, fmt.Errorf("empty listname in rule: %s", siteWithAttr) return nil, fmt.Errorf("empty listname in rule: %s", siteWithAttr)
} }
domains, err := l.LoadSite(file, list) domains, err := l.LoadSiteByPath(file, list)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -59,7 +59,7 @@ func (l *loader) LoadGeoSiteWithAttr(file string, siteWithAttr string) ([]*route
} }
func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) { func (l *loader) LoadGeoIP(country string) ([]*router.CIDR, error) {
return l.LoadIP(C.GeoipName, country) return l.LoadIPByPath(C.GeoipName, country)
} }
var loaders map[string]func() LoaderImplementation var loaders map[string]func() LoaderImplementation

View File

@@ -5,8 +5,10 @@ import (
) )
type LoaderImplementation interface { type LoaderImplementation interface {
LoadSite(filename, list string) ([]*router.Domain, error) LoadSiteByPath(filename, list string) ([]*router.Domain, error)
LoadIP(filename, country string) ([]*router.CIDR, error) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error)
LoadIPByPath(filename, country string) ([]*router.CIDR, error)
LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error)
} }
type Loader interface { type Loader interface {

52
component/geodata/init.go Normal file
View File

@@ -0,0 +1,52 @@
package geodata
import (
"fmt"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"io"
"net/http"
"os"
)
var initFlag bool
func InitGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
log.Infoln("Download GeoSite.dat finish")
}
if !initFlag {
if err := Verify(C.GeositeName); err != nil {
log.Warnln("GeoSite.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
initFlag = true
}
return nil
}
func downloadGeoSite(path string) (err error) {
resp, err := http.Get(C.GeoSiteUrl)
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}

View File

@@ -1,6 +1,7 @@
package memconservative package memconservative
import ( import (
"errors"
"fmt" "fmt"
"runtime" "runtime"
@@ -13,7 +14,7 @@ type memConservativeLoader struct {
geositecache GeoSiteCache geositecache GeoSiteCache
} }
func (m *memConservativeLoader) LoadIP(filename, country string) ([]*router.CIDR, error) { func (m *memConservativeLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) {
defer runtime.GC() defer runtime.GC()
geoip, err := m.geoipcache.Unmarshal(filename, country) geoip, err := m.geoipcache.Unmarshal(filename, country)
if err != nil { if err != nil {
@@ -22,7 +23,11 @@ func (m *memConservativeLoader) LoadIP(filename, country string) ([]*router.CIDR
return geoip.Cidr, nil return geoip.Cidr, nil
} }
func (m *memConservativeLoader) LoadSite(filename, list string) ([]*router.Domain, error) { func (m *memConservativeLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) {
return nil, errors.New("memConservative do not support LoadIPByBytes")
}
func (m *memConservativeLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) {
defer runtime.GC() defer runtime.GC()
geosite, err := m.geositecache.Unmarshal(filename, list) geosite, err := m.geositecache.Unmarshal(filename, list)
if err != nil { if err != nil {
@@ -31,6 +36,10 @@ func (m *memConservativeLoader) LoadSite(filename, list string) ([]*router.Domai
return geosite.Domain, nil return geosite.Domain, nil
} }
func (m *memConservativeLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) {
return nil, errors.New("memConservative do not support LoadSiteByBytes")
}
func newMemConservativeLoader() geodata.LoaderImplementation { func newMemConservativeLoader() geodata.LoaderImplementation {
return &memConservativeLoader{make(map[string]*router.GeoIP), make(map[string]*router.GeoSite)} return &memConservativeLoader{make(map[string]*router.GeoIP), make(map[string]*router.GeoSite)}
} }

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

@@ -29,11 +29,7 @@ func ReadAsset(file string) ([]byte, error) {
return ReadFile(C.Path.GetAssetLocation(file)) return ReadFile(C.Path.GetAssetLocation(file))
} }
func loadIP(filename, country string) ([]*router.CIDR, error) { func loadIP(geoipBytes []byte, country string) ([]*router.CIDR, error) {
geoipBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
var geoipList router.GeoIPList var geoipList router.GeoIPList
if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil {
return nil, err return nil, err
@@ -45,14 +41,10 @@ func loadIP(filename, country string) ([]*router.CIDR, error) {
} }
} }
return nil, fmt.Errorf("country not found in %s%s%s", filename, ": ", country) return nil, fmt.Errorf("country %s not found", country)
} }
func loadSite(filename, list string) ([]*router.Domain, error) { func loadSite(geositeBytes []byte, list string) ([]*router.Domain, error) {
geositeBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
var geositeList router.GeoSiteList var geositeList router.GeoSiteList
if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil {
return nil, err return nil, err
@@ -64,17 +56,33 @@ func loadSite(filename, list string) ([]*router.Domain, error) {
} }
} }
return nil, fmt.Errorf("list not found in %s%s%s", filename, ": ", list) return nil, fmt.Errorf("list %s not found", list)
} }
type standardLoader struct{} type standardLoader struct{}
func (d standardLoader) LoadSite(filename, list string) ([]*router.Domain, error) { func (d standardLoader) LoadSiteByPath(filename, list string) ([]*router.Domain, error) {
return loadSite(filename, list) geositeBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
return loadSite(geositeBytes, list)
} }
func (d standardLoader) LoadIP(filename, country string) ([]*router.CIDR, error) { func (d standardLoader) LoadSiteByBytes(geositeBytes []byte, list string) ([]*router.Domain, error) {
return loadIP(filename, country) return loadSite(geositeBytes, list)
}
func (d standardLoader) LoadIPByPath(filename, country string) ([]*router.CIDR, error) {
geoipBytes, err := ReadAsset(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %s, base error: %s", filename, err.Error())
}
return loadIP(geoipBytes, country)
}
func (d standardLoader) LoadIPByBytes(geoipBytes []byte, country string) ([]*router.CIDR, error) {
return loadIP(geoipBytes, country)
} }
func init() { func init() {

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"
@@ -21,20 +21,30 @@ func SetLoader(newLoader string) {
geoLoaderName = newLoader geoLoaderName = newLoader
} }
func Verify(name string) bool { func Verify(name string) error {
switch name { switch name {
case C.GeositeName: case C.GeositeName:
_, _, err := LoadGeoSiteMatcher("CN") _, _, err := LoadGeoSiteMatcher("CN")
return err == nil return err
case C.GeoipName: case C.GeoipName:
_, _, err := LoadGeoIPMatcher("CN") _, _, err := LoadGeoIPMatcher("CN")
return err == nil return err
default: default:
return false return fmt.Errorf("not support name")
} }
} }
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
@@ -50,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
} }
@@ -59,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
} }
@@ -72,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)

64
component/http/http.go Normal file
View File

@@ -0,0 +1,64 @@
package http
import (
"context"
"github.com/Dreamacro/clash/listener/inner"
"github.com/Dreamacro/clash/log"
"io"
"net"
"net/http"
URL "net/url"
"strings"
"time"
)
const (
UA = "Clash"
)
func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) {
method = strings.ToUpper(method)
urlRes, err := URL.Parse(url)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, urlRes.String(), body)
for k, v := range header {
for _, v := range v {
req.Header.Add(k, v)
}
}
if _, ok := header["User-Agent"]; !ok {
req.Header.Set("User-Agent", UA)
}
if err != nil {
return nil, err
}
if user := urlRes.User; user != nil {
password, _ := user.Password()
req.SetBasicAuth(user.Username(), password)
}
req = req.WithContext(ctx)
transport := &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
log.Infoln(urlRes.String())
conn := inner.HandleTcp(address, urlRes.Hostname())
return conn, nil
},
}
client := http.Client{Transport: transport}
return client.Do(req)
}

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,16 +2,18 @@ package process
import ( import (
"errors" "errors"
"net" "github.com/Dreamacro/clash/common/nnip"
"runtime"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"net"
"net/netip"
) )
var ( var (
ErrInvalidNetwork = errors.New("invalid network") ErrInvalidNetwork = errors.New("invalid network")
ErrPlatformNotSupport = errors.New("not support on this platform") ErrPlatformNotSupport = errors.New("not support on this platform")
ErrNotFound = errors.New("process not found") ErrNotFound = errors.New("process not found")
enableFindProcess = true
) )
const ( const (
@@ -19,35 +21,46 @@ const (
UDP = "udp" UDP = "udp"
) )
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) { func EnableFindProcess(e bool) {
enableFindProcess = e
}
func FindProcessName(network string, srcIP netip.Addr, srcPort int) (int32, string, error) {
return findProcessName(network, srcIP, srcPort) return findProcessName(network, srcIP, srcPort)
} }
func ShouldFindProcess(metadata *C.Metadata) bool { func FindUid(network string, srcIP netip.Addr, srcPort int) (int32, error) {
if runtime.GOOS == "android" { _, uid, err := resolveSocketByNetlink(network, srcIP, srcPort)
return false if err != nil {
return -1, err
} }
if metadata.Process != "" { return uid, nil
}
func ShouldFindProcess(metadata *C.Metadata) bool {
if !enableFindProcess ||
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
} }
@@ -57,7 +70,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))
} }
} }
} }
@@ -66,7 +79,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

@@ -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) (int32, string, error) {
var spath string var spath string
switch network { switch network {
case TCP: case TCP:
@@ -23,14 +29,14 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
case UDP: case UDP:
spath = "net.inet.udp.pcblist_n" spath = "net.inet.udp.pcblist_n"
default: default:
return "", ErrInvalidNetwork return -1, "", ErrInvalidNetwork
} }
isIPv4 := ip.To4() != nil isIPv4 := ip.Is4()
value, err := syscall.Sysctl(spath) value, err := syscall.Sysctl(spath)
if err != nil { if err != nil {
return "", err return -1, "", err
} }
buf := []byte(value) buf := []byte(value)
@@ -57,28 +63,29 @@ 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
} }
// xsocket_n.so_last_pid // xsocket_n.so_last_pid
pid := readNativeUint32(buf[so+68 : so+72]) pid := readNativeUint32(buf[so+68 : so+72])
return getExecPathFromPID(pid) pp, err := getExecPathFromPID(pid)
return -1, pp, err
} }
return "", ErrNotFound return -1, "", ErrNotFound
} }
func getExecPathFromPID(pid uint32) (string, error) { func getExecPathFromPID(pid uint32) (string, error) {

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) (int32, 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())
@@ -30,7 +35,7 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
}) })
if defaultSearcher == nil { if defaultSearcher == nil {
return "", ErrPlatformNotSupport return -1, "", ErrPlatformNotSupport
} }
var spath string var spath string
@@ -41,21 +46,22 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
case UDP: case UDP:
spath = "net.inet.udp.pcblist" spath = "net.inet.udp.pcblist"
default: default:
return "", ErrInvalidNetwork return -1, "", ErrInvalidNetwork
} }
value, err := syscall.Sysctl(spath) value, err := syscall.Sysctl(spath)
if err != nil { if err != nil {
return "", err return -1, "", err
} }
buf := []byte(value) buf := []byte(value)
pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP) pid, err := defaultSearcher.Search(buf, ip, uint16(srcPort), isTCP)
if err != nil { if err != nil {
return "", err return -1, "", err
} }
return getExecPathFromPID(pid) pp, err := getExecPathFromPID(pid)
return -1, pp, err
} }
func getExecPathFromPID(pid uint32) (string, error) { func getExecPathFromPID(pid uint32) (string, error) {
@@ -102,7 +108,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 +122,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 +136,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

@@ -5,8 +5,11 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net" "net"
"net/netip"
"os" "os"
"path" "path"
"path/filepath"
"runtime"
"strings" "strings"
"syscall" "syscall"
"unicode" "unicode"
@@ -31,16 +34,16 @@ 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) (int32, 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 -1, "", err
} }
pp, err := resolveProcessNameByProcSearch(inode, uid)
return resolveProcessNameByProcSearch(inode, uid) return uid, pp, err
} }
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
@@ -53,7 +56,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
@@ -65,10 +68,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,
@@ -84,7 +89,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 {
@@ -103,7 +110,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)
} }
@@ -111,14 +118,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)
@@ -195,8 +198,19 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
continue continue
} }
if bytes.Equal(buffer[:n], socket) { if runtime.GOOS == "android" {
return os.Readlink(path.Join(processPath, "exe")) if bytes.Equal(buffer[:n], socket) {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
}
} else {
if bytes.Equal(buffer[:n], socket) {
return os.Readlink(path.Join(processPath, "exe"))
}
} }
} }
} }
@@ -204,6 +218,19 @@ func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode) 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 { func isPid(s string) bool {
return strings.IndexFunc(s, func(r rune) bool { return strings.IndexFunc(s, func(r rune) bool {
return !unicode.IsDigit(r) return !unicode.IsDigit(r)

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) (int32, string, error) {
return "", ErrPlatformNotSupport return -1, "", 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) (int32, 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
} }
@@ -81,21 +86,22 @@ func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
fn = getExUDPTable fn = getExUDPTable
class = udpTablePid class = udpTablePid
default: default:
return "", ErrInvalidNetwork return -1, "", ErrInvalidNetwork
} }
buf, err := getTransportTable(fn, family, class) buf, err := getTransportTable(fn, family, class)
if err != nil { if err != nil {
return "", err return -1, "", err
} }
s := newSearcher(family == windows.AF_INET, network == TCP) s := newSearcher(family == windows.AF_INET, network == TCP)
pid, err := s.Search(buf, ip, uint16(srcPort)) pid, err := s.Search(buf, ip, uint16(srcPort))
if err != nil { if err != nil {
return "", err return -1, "", err
} }
return getExecPathFromPID(pid) pp, err := getExecPathFromPID(pid)
return -1, pp, err
} }
type searcher struct { type searcher struct {
@@ -107,7 +113,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 +137,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 +220,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,20 +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(net.IP, string) InsertHostByIP(netip.Addr, string)
StoreFakePoolState()
} }
func FakeIPEnabled() bool { func FakeIPEnabled() bool {
@@ -33,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)
} }
@@ -41,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)
} }
@@ -49,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)
} }
@@ -57,13 +56,13 @@ func IsExistFakeIP(ip net.IP) bool {
return false return false
} }
func InsertHostByIP(ip net.IP, host string) { func InsertHostByIP(ip netip.Addr, host string) {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
mapper.InsertHostByIP(ip, host) mapper.InsertHostByIP(ip, host)
} }
} }
func FindHostByIP(ip net.IP) (string, bool) { 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)
} }
@@ -77,3 +76,9 @@ func FlushFakeIP() error {
} }
return nil return nil
} }
func StoreFakePoolState() {
if mapper := DefaultHostMapper; mapper != nil {
mapper.StoreFakePoolState()
}
}

View File

@@ -6,9 +6,9 @@ import (
"math/rand" "math/rand"
"net" "net"
"net/netip" "net/netip"
"strings"
"time" "time"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
) )
@@ -37,77 +37,115 @@ 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; ip.Is4() { return ips[rand.Intn(len(ips))], nil
return ip.AsSlice(), 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 ips, err := ResolveAllIPv6WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
}
}
// ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (netip.Addr, error) {
if ips, err := ResolveAllIPPrimaryIPv4WithResolver(host, r); err == nil {
return ips[rand.Intn(len(ips))], nil
} else {
return netip.Addr{}, err
}
}
// ResolveIP with a host, return ip
func ResolveIP(host string) (netip.Addr, error) {
return ResolveIPWithResolver(host, DefaultResolver)
}
// ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPv4ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPv4WithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIPv4(host)
} else {
return ip, nil
}
}
return ResolveIPv4(host)
}
// ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPv6WithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIPv6(host)
} else {
return ip, nil
}
}
return ResolveIPv6(host)
}
// ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (netip.Addr, error) {
if ProxyServerHostResolver != nil {
if ip, err := ResolveIPWithResolver(host, ProxyServerHostResolver); err != nil {
return ResolveIP(host)
} else {
return ip, err
}
}
return ResolveIP(host)
}
func ResolveAllIPv6WithResolver(host string, r Resolver) ([]netip.Addr, error) {
if DisableIPv6 { if DisableIPv6 {
return nil, ErrIPv6Disabled return []netip.Addr{}, ErrIPv6Disabled
} }
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data; ip.Is6() { if ip := node.Data; ip.Is6() {
return ip.AsSlice(), nil return []netip.Addr{ip}, nil
} }
} }
ip := net.ParseIP(host) ip, err := netip.ParseAddr(host)
if ip != nil { if err == nil {
if strings.Contains(host, ":") { if ip.Is6() {
return ip, nil return []netip.Addr{ip}, nil
} }
return nil, ErrIPVersion return []netip.Addr{}, ErrIPVersion
} }
if r != nil { if r != nil {
return r.ResolveIPv6(host) return r.ResolveAllIPv6(host)
} }
if DefaultResolver == nil { if DefaultResolver == nil {
@@ -115,75 +153,153 @@ func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
defer cancel() defer cancel()
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host) ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
if err != nil { if err != nil {
return nil, err return []netip.Addr{}, err
} else if len(ipAddrs) == 0 { } else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound return []netip.Addr{}, ErrIPNotFound
} }
return ipAddrs[rand.Intn(len(ipAddrs))], nil return []netip.Addr{netip.AddrFrom16(*(*[16]byte)(ipAddrs[rand.Intn(len(ipAddrs))]))}, nil
} }
return nil, ErrIPNotFound return []netip.Addr{}, ErrIPNotFound
} }
// ResolveIPWithResolver same as ResolveIP, but with a resolver func ResolveAllIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
ip := node.Data if ip := node.Data; ip.Is4() {
return ip.Unmap().AsSlice(), nil return []netip.Addr{node.Data}, nil
}
}
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() || ip.Is4In6() {
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 r != nil {
if DisableIPv6 { if DisableIPv6 {
return r.ResolveIPv4(host) return r.ResolveAllIPv4(host)
} }
return r.ResolveIP(host)
return r.ResolveAllIP(host)
} else if DisableIPv6 { } else if DisableIPv6 {
return ResolveIPv4(host) return ResolveAllIPv4(host)
} }
ip := net.ParseIP(host) ip, err := netip.ParseAddr(host)
if ip != nil { if err == nil {
return ip, nil return []netip.Addr{ip}, nil
} }
if DefaultResolver == nil { if DefaultResolver == nil {
ipAddr, err := net.ResolveIPAddr("ip", host) ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil { if err != nil {
return nil, err return []netip.Addr{}, err
} }
return ipAddr.IP, nil return []netip.Addr{nnip.IpToAddr(ipAddr.IP)}, nil
} }
return nil, ErrIPNotFound return []netip.Addr{}, ErrIPNotFound
} }
// ResolveIP with a host, return ip func ResolveAllIPPrimaryIPv4WithResolver(host string, r Resolver) ([]netip.Addr, error) {
func ResolveIP(host string) (net.IP, error) { if node := DefaultHosts.Search(host); node != nil {
return ResolveIPWithResolver(host, DefaultResolver) 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
} }
// ResolveIPv4ProxyServerHost proxies server host only func ResolveAllIP(host string) ([]netip.Addr, error) {
func ResolveIPv4ProxyServerHost(host string) (net.IP, 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 { if ProxyServerHostResolver != nil {
return ResolveIPv4WithResolver(host, ProxyServerHostResolver) return ResolveAllIPv6WithResolver(host, ProxyServerHostResolver)
} }
return ResolveIPv4(host)
return ResolveAllIPv6(host)
} }
// ResolveIPv6ProxyServerHost proxies server host only func ResolveAllIPv4ProxyServerHost(host string) ([]netip.Addr, error) {
func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
if ProxyServerHostResolver != nil { if ProxyServerHostResolver != nil {
return ResolveIPv6WithResolver(host, ProxyServerHostResolver) return ResolveAllIPv4WithResolver(host, ProxyServerHostResolver)
} }
return ResolveIPv6(host)
return ResolveAllIPv4(host)
} }
// ResolveProxyServerHost proxies server host only func ResolveAllIPProxyServerHost(host string) ([]netip.Addr, error) {
func ResolveProxyServerHost(host string) (net.IP, error) {
if ProxyServerHostResolver != nil { if ProxyServerHostResolver != nil {
return ResolveIPWithResolver(host, ProxyServerHostResolver) return ResolveAllIPWithResolver(host, ProxyServerHostResolver)
} }
return ResolveIP(host)
return ResolveAllIP(host)
} }

View File

@@ -2,8 +2,11 @@ package sniffer
import ( import (
"errors" "errors"
"github.com/Dreamacro/clash/constant/sniffer"
"net" "net"
"net/netip"
"strconv" "strconv"
"time"
"github.com/Dreamacro/clash/component/trie" "github.com/Dreamacro/clash/component/trie"
@@ -17,19 +20,22 @@ import (
var ( var (
ErrorUnsupportedSniffer = errors.New("unsupported sniffer") ErrorUnsupportedSniffer = errors.New("unsupported sniffer")
ErrorSniffFailed = errors.New("all sniffer failed") ErrorSniffFailed = errors.New("all sniffer failed")
ErrNoClue = errors.New("not enough information for making a decision")
) )
var Dispatcher SnifferDispatcher var Dispatcher SnifferDispatcher
type SnifferDispatcher struct { type (
enable bool SnifferDispatcher struct {
enable bool
sniffers []C.Sniffer sniffers []sniffer.Sniffer
foreDomain *trie.DomainTrie[bool] foreDomain *trie.DomainTrie[bool]
skipSNI *trie.DomainTrie[bool] skipSNI *trie.DomainTrie[bool]
portRanges *[]utils.Range[uint16] portRanges *[]utils.Range[uint16]
} }
)
func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) { func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) {
bufConn, ok := conn.(*CN.BufferedConn) bufConn, ok := conn.(*CN.BufferedConn)
@@ -44,12 +50,18 @@ func (sd *SnifferDispatcher) TCPSniff(conn net.Conn, metadata *C.Metadata) {
return return
} }
inWhitelist := false
for _, portRange := range *sd.portRanges { for _, portRange := range *sd.portRanges {
if !portRange.Contains(uint16(port)) { if portRange.Contains(uint16(port)) {
return inWhitelist = true
break
} }
} }
if !inWhitelist {
return
}
if host, err := sd.sniffDomain(bufConn, metadata); err != nil { 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) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%s] to [%s:%s]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return return
@@ -74,7 +86,6 @@ func (sd *SnifferDispatcher) replaceDomain(metadata *C.Metadata, host string) {
metadata.Host = host metadata.Host = host
metadata.DNSMode = C.DNSMapping metadata.DNSMode = C.DNSMapping
resolver.InsertHostByIP(metadata.DstIP, host) resolver.InsertHostByIP(metadata.DstIP, host)
metadata.DstIP = nil
} }
func (sd *SnifferDispatcher) Enable() bool { func (sd *SnifferDispatcher) Enable() bool {
@@ -84,8 +95,16 @@ func (sd *SnifferDispatcher) Enable() bool {
func (sd *SnifferDispatcher) sniffDomain(conn *CN.BufferedConn, metadata *C.Metadata) (string, error) { func (sd *SnifferDispatcher) sniffDomain(conn *CN.BufferedConn, metadata *C.Metadata) (string, error) {
for _, sniffer := range sd.sniffers { for _, sniffer := range sd.sniffers {
if sniffer.SupportNetwork() == C.TCP { if sniffer.SupportNetwork() == C.TCP {
_ = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
_, err := conn.Peek(1) _, err := conn.Peek(1)
_ = conn.SetReadDeadline(time.Time{})
if err != nil { 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 return "", err
} }
@@ -98,7 +117,13 @@ func (sd *SnifferDispatcher) sniffDomain(conn *CN.BufferedConn, metadata *C.Meta
host, err := sniffer.SniffTCP(bytes) host, err := sniffer.SniffTCP(bytes)
if err != nil { if err != nil {
log.Debugln("[Sniffer] [%s] Sniff data failed %s", sniffer.Protocol(), metadata.DstIP) //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 continue
} }
@@ -117,7 +142,7 @@ func NewCloseSnifferDispatcher() (*SnifferDispatcher, error) {
return &dispatcher, nil return &dispatcher, nil
} }
func NewSnifferDispatcher(needSniffer []C.SnifferType, forceDomain *trie.DomainTrie[bool], func NewSnifferDispatcher(needSniffer []sniffer.Type, forceDomain *trie.DomainTrie[bool],
skipSNI *trie.DomainTrie[bool], ports *[]utils.Range[uint16]) (*SnifferDispatcher, error) { skipSNI *trie.DomainTrie[bool], ports *[]utils.Range[uint16]) (*SnifferDispatcher, error) {
dispatcher := SnifferDispatcher{ dispatcher := SnifferDispatcher{
enable: true, enable: true,
@@ -139,10 +164,12 @@ func NewSnifferDispatcher(needSniffer []C.SnifferType, forceDomain *trie.DomainT
return &dispatcher, nil return &dispatcher, nil
} }
func NewSniffer(name C.SnifferType) (C.Sniffer, error) { func NewSniffer(name sniffer.Type) (sniffer.Sniffer, error) {
switch name { switch name {
case C.TLS: case sniffer.TLS:
return &TLSSniffer{}, nil return &TLSSniffer{}, nil
case sniffer.HTTP:
return &HTTPSniffer{}, nil
default: default:
return nil, ErrorUnsupportedSniffer 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

@@ -11,7 +11,6 @@ import (
var ( var (
errNotTLS = errors.New("not TLS header") errNotTLS = errors.New("not TLS header")
errNotClientHello = errors.New("not client hello") errNotClientHello = errors.New("not client hello")
ErrNoClue = errors.New("not enough information for making a decision")
) )
type TLSSniffer struct { type TLSSniffer struct {

View File

@@ -4,6 +4,8 @@ import (
"container/list" "container/list"
"errors" "errors"
"fmt" "fmt"
"github.com/Dreamacro/clash/constant/sniffer"
"github.com/Dreamacro/clash/listener/tun/ipstack/commons"
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
@@ -14,8 +16,8 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/utils" "github.com/Dreamacro/clash/common/utils"
R "github.com/Dreamacro/clash/rule" R "github.com/Dreamacro/clash/rules"
RP "github.com/Dreamacro/clash/rule/provider" RP "github.com/Dreamacro/clash/rules/provider"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
@@ -29,12 +31,12 @@ 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"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v3"
) )
// General config // General config
@@ -45,10 +47,14 @@ type General struct {
UnifiedDelay bool UnifiedDelay bool
LogLevel log.LogLevel `json:"log-level"` LogLevel log.LogLevel `json:"log-level"`
IPv6 bool `json:"ipv6"` IPv6 bool `json:"ipv6"`
Interface string `json:"-"` Interface string `json:"interface-name"`
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"`
EnableProcess bool `json:"enable-process"`
Tun Tun `json:"tun"`
Sniffing bool `json:"sniffing"`
} }
// Inbound config // Inbound config
@@ -90,17 +96,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"`
@@ -109,11 +109,13 @@ 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"`
TunAddressPrefix netip.Prefix `yaml:"-" json:"-"`
} }
// IPTables config // IPTables config
@@ -125,11 +127,10 @@ type IPTables struct {
type Sniffer struct { type Sniffer struct {
Enable bool Enable bool
Force bool Sniffers []sniffer.Type
Sniffers []C.SnifferType
Reverses *trie.DomainTrie[bool] Reverses *trie.DomainTrie[bool]
ForceDomain *trie.DomainTrie[bool] ForceDomain *trie.DomainTrie[bool]
SkipSNI *trie.DomainTrie[bool] SkipDomain *trie.DomainTrie[bool]
Ports *[]utils.Range[uint16] Ports *[]utils.Range[uint16]
} }
@@ -149,7 +150,7 @@ type Config struct {
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 Sniffer *Sniffer
} }
@@ -206,8 +207,10 @@ 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"`
EnableProcess bool `yaml:"enable-process" json:"enable-process"`
Sniffer SnifferRaw `yaml:"sniffer"` 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"`
@@ -216,18 +219,23 @@ type RawConfig struct {
IPTables IPTables `yaml:"iptables"` IPTables IPTables `yaml:"iptables"`
Experimental Experimental `yaml:"experimental"` Experimental Experimental `yaml:"experimental"`
Profile Profile `yaml:"profile"` Profile Profile `yaml:"profile"`
GeoXUrl RawGeoXUrl `yaml:"geox-url"`
Proxy []map[string]any `yaml:"proxies"` Proxy []map[string]any `yaml:"proxies"`
ProxyGroup []map[string]any `yaml:"proxy-groups"` ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"` Rule []string `yaml:"rules"`
} }
type SnifferRaw struct { type RawGeoXUrl struct {
GeoIp string `yaml:"geoip" json:"geoip"`
Mmdb string `yaml:"mmdb" json:"mmdb"`
GeoSite string `yaml:"geosite" json:"geosite"`
}
type RawSniffer struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Sniffing []string `yaml:"sniffing" json:"sniffing"` Sniffing []string `yaml:"sniffing" json:"sniffing"`
Force bool `yaml:"force" json:"force"`
Reverse []string `yaml:"reverses" json:"reverses"`
ForceDomain []string `yaml:"force-domain" json:"force-domain"` ForceDomain []string `yaml:"force-domain" json:"force-domain"`
SkipSNI []string `yaml:"skip-sni" json:"skip-sni"` SkipDomain []string `yaml:"skip-domain" json:"skip-domain"`
Ports []string `yaml:"port-whitelist" json:"port-whitelist"` Ports []string `yaml:"port-whitelist" json:"port-whitelist"`
} }
@@ -246,6 +254,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
rawCfg := &RawConfig{ rawCfg := &RawConfig{
AllowLan: false, AllowLan: false,
BindAddress: "*", BindAddress: "*",
IPv6: true,
Mode: T.Rule, Mode: T.Rule,
GeodataMode: C.GeodataMode, GeodataMode: C.GeodataMode,
GeodataLoader: "memconservative", GeodataLoader: "memconservative",
@@ -256,13 +265,15 @@ 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,
EnableProcess: false,
Tun: RawTun{ Tun: RawTun{
Enable: false, Enable: false,
Device: "", Device: "",
AutoDetectInterface: true,
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,
@@ -271,6 +282,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
}, },
DNS: RawDNS{ DNS: RawDNS{
Enable: false, Enable: false,
IPv6: false,
UseHosts: true, UseHosts: true,
EnhancedMode: C.DNSMapping, EnhancedMode: C.DNSMapping,
FakeIPRange: "198.18.0.1/16", FakeIPRange: "198.18.0.1/16",
@@ -296,18 +308,21 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"www.msftconnecttest.com", "www.msftconnecttest.com",
}, },
}, },
Sniffer: SnifferRaw{ Sniffer: RawSniffer{
Enable: false, Enable: false,
Force: false,
Sniffing: []string{}, Sniffing: []string{},
Reverse: []string{},
ForceDomain: []string{}, ForceDomain: []string{},
SkipSNI: []string{}, SkipDomain: []string{},
Ports: []string{}, Ports: []string{},
}, },
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
GeoXUrl: RawGeoXUrl{
GeoIp: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat",
Mmdb: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb",
GeoSite: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat",
},
} }
if err := yaml.Unmarshal(buf, rawCfg); err != nil { if err := yaml.Unmarshal(buf, rawCfg); err != nil {
@@ -331,12 +346,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.General = general config.General = general
tunCfg, err := parseTun(rawCfg.Tun, config.General)
if err != nil {
return nil, err
}
config.Tun = tunCfg
dialer.DefaultInterface.Store(config.General.Interface) dialer.DefaultInterface.Store(config.General.Interface)
proxies, providers, err := parseProxies(rawCfg) proxies, providers, err := parseProxies(rawCfg)
@@ -365,6 +374,12 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.DNS = dnsCfg config.DNS = dnsCfg
tunCfg, err := parseTun(rawCfg.Tun, config.General, dnsCfg)
if err != nil {
return nil, err
}
config.Tun = tunCfg
config.Users = parseAuthentication(rawCfg.Authentication) config.Users = parseAuthentication(rawCfg.Authentication)
config.Sniffer, err = parseSniffer(rawCfg.Sniffer) config.Sniffer, err = parseSniffer(rawCfg.Sniffer)
@@ -412,6 +427,8 @@ 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,
EnableProcess: cfg.EnableProcess,
}, nil }, nil
} }
@@ -423,8 +440,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())
@@ -444,7 +461,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
@@ -454,7 +471,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
@@ -509,32 +526,26 @@ 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 {
rp, err := RP.ParseRuleProvider(name, mapping) rp, err := RP.ParseRuleProvider(name, mapping, R.ParseRule)
if err != nil { if err != nil {
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 {
@@ -546,10 +557,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" {
@@ -578,13 +585,6 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
} }
params = trimArr(params) params = trimArr(params)
if ruleName == "GEOSITE" {
if err := initGeoSite(); err != nil {
return nil, nil, fmt.Errorf("can't initial GeoSite: %s", err)
}
initMode = false
}
parsed, parseErr := R.ParseRule(ruleName, payload, target, params) parsed, parseErr := R.ParseRule(ruleName, payload, target, params)
if parseErr != nil { if parseErr != nil {
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())
@@ -668,7 +668,7 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
addr = u.Host addr = u.Host
dnsNetType = "dhcp" // UDP from DHCP dnsNetType = "dhcp" // UDP from DHCP
case "quic": case "quic":
addr, err = hostWithDefaultPort(u.Host, "784") addr, err = hostWithDefaultPort(u.Host, "853")
dnsNetType = "quic" // DNS over QUIC dnsNetType = "quic" // DNS over QUIC
default: default:
return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme)
@@ -684,7 +684,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,
}, },
) )
} }
@@ -708,15 +708,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
@@ -725,7 +725,7 @@ func parseFallbackIPCIDR(ips []string) ([]*net.IPNet, error) {
func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) { func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainMatcher, error) {
var sites []*router.DomainMatcher var sites []*router.DomainMatcher
if len(countries) > 0 { if len(countries) > 0 {
if err := initGeoSite(); err != nil { if err := geodata.InitGeoSite(); err != nil {
return nil, fmt.Errorf("can't initial GeoSite: %s", err) return nil, fmt.Errorf("can't initial GeoSite: %s", err)
} }
} }
@@ -769,7 +769,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.R
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{},
}, },
} }
@@ -878,10 +878,10 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
return users return users
} }
func parseTun(rawTun RawTun, general *General) (*Tun, error) { func parseTun(rawTun RawTun, general *General, dnsCfg *DNS) (*Tun, error) {
if rawTun.Enable && rawTun.AutoDetectInterface { 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)
@@ -896,7 +896,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)
@@ -905,53 +905,62 @@ func parseTun(rawTun RawTun, general *General) (*Tun, error) {
dnsHijack = append(dnsHijack, addrPort) dnsHijack = append(dnsHijack, addrPort)
} }
var tunAddressPrefix netip.Prefix
if dnsCfg.FakeIPRange != nil {
tunAddressPrefix = *dnsCfg.FakeIPRange.IPNet()
} else {
tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16")
}
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,
TunAddressPrefix: tunAddressPrefix,
}, nil }, nil
} }
func parseSniffer(snifferRaw SnifferRaw) (*Sniffer, error) { func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
sniffer := &Sniffer{ sniffer := &Sniffer{
Enable: snifferRaw.Enable, Enable: snifferRaw.Enable,
Force: snifferRaw.Force,
} }
ports := []utils.Range[uint16]{} var ports []utils.Range[uint16]
if len(snifferRaw.Ports) == 0 { if len(snifferRaw.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](0, 65535)) ports = append(ports, *utils.NewRange[uint16](0, 65535))
} else { } else {
for _, portRange := range snifferRaw.Ports { for _, portRange := range snifferRaw.Ports {
portRaws := strings.Split(portRange, "-") 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 { if len(portRaws) > 1 {
p, err := strconv.ParseUint(portRaws[0], 10, 16) p, err = strconv.ParseUint(portRaws[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("%s format error", portRange)
}
start := uint16(p)
p, err = strconv.ParseUint(portRaws[0], 10, 16)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s format error", portRange) return nil, fmt.Errorf("%s format error", portRange)
} }
end := uint16(p) end := uint16(p)
ports = append(ports, *utils.NewRange(start, end)) ports = append(ports, *utils.NewRange(start, end))
} else {
ports = append(ports, *utils.NewRange(start, start))
} }
} }
} }
sniffer.Ports = &ports sniffer.Ports = &ports
loadSniffer := make(map[C.SnifferType]struct{}) loadSniffer := make(map[snifferTypes.Type]struct{})
for _, snifferName := range snifferRaw.Sniffing { for _, snifferName := range snifferRaw.Sniffing {
find := false find := false
for _, snifferType := range C.SnifferList { for _, snifferType := range snifferTypes.List {
if snifferType.String() == strings.ToUpper(snifferName) { if snifferType.String() == strings.ToUpper(snifferName) {
find = true find = true
loadSniffer[snifferType] = struct{}{} loadSniffer[snifferType] = struct{}{}
@@ -966,7 +975,6 @@ func parseSniffer(snifferRaw SnifferRaw) (*Sniffer, error) {
for st := range loadSniffer { for st := range loadSniffer {
sniffer.Sniffers = append(sniffer.Sniffers, st) sniffer.Sniffers = append(sniffer.Sniffers, st)
} }
sniffer.ForceDomain = trie.New[bool]() sniffer.ForceDomain = trie.New[bool]()
for _, domain := range snifferRaw.ForceDomain { for _, domain := range snifferRaw.ForceDomain {
err := sniffer.ForceDomain.Insert(domain, true) err := sniffer.ForceDomain.Insert(domain, true)
@@ -975,35 +983,13 @@ func parseSniffer(snifferRaw SnifferRaw) (*Sniffer, error) {
} }
} }
sniffer.SkipSNI = trie.New[bool]() sniffer.SkipDomain = trie.New[bool]()
for _, domain := range snifferRaw.SkipSNI { for _, domain := range snifferRaw.SkipDomain {
err := sniffer.SkipSNI.Insert(domain, true) err := sniffer.SkipDomain.Insert(domain, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err) return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
} }
} }
// Compatibility, remove it when release
if strings.Contains(C.Version, "alpha") || strings.Contains(C.Version, "develop") || strings.Contains(C.Version, "1.10.0") {
log.Warnln("Sniffer param force and reverses deprecated, will be removed in the release version, see https://github.com/MetaCubeX/Clash.Meta/commit/48a01adb7a4f38974b9d9639f931d0d245aebf28")
if snifferRaw.Force {
// match all domain
sniffer.ForceDomain.Insert("+", true)
for _, domain := range snifferRaw.Reverse {
err := sniffer.SkipSNI.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s], error:%v", domain, err)
}
}
} else {
for _, domain := range snifferRaw.Reverse {
err := sniffer.ForceDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s], error:%v", domain, err)
}
}
}
}
return sniffer, nil return sniffer, nil
} }

View File

@@ -12,10 +12,8 @@ import (
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
) )
var initMode = true
func downloadMMDB(path string) (err error) { func downloadMMDB(path string) (err error) {
resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/geoip/release/Country.mmdb") resp, err := http.Get(C.MmdbUrl)
if err != nil { if err != nil {
return return
} }
@@ -32,7 +30,7 @@ func downloadMMDB(path string) (err error) {
} }
func downloadGeoIP(path string) (err error) { func downloadGeoIP(path string) (err error) {
resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat") resp, err := http.Get(C.GeoIpUrl)
if err != nil { if err != nil {
return return
} }
@@ -48,45 +46,6 @@ func downloadGeoIP(path string) (err error) {
return err return err
} }
func downloadGeoSite(path string) (err error) {
resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat")
if err != nil {
return
}
defer resp.Body.Close()
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func initGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
log.Infoln("Download GeoSite.dat finish")
}
if initMode {
if !geodata.Verify(C.GeositeName) {
log.Warnln("GeoSite.dat invalid, remove and download")
if err := os.Remove(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error())
}
if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
}
}
}
return nil
}
func initGeoIP() error { func initGeoIP() error {
if C.GeodataMode { if C.GeodataMode {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
@@ -97,8 +56,8 @@ func initGeoIP() error {
log.Infoln("Download GeoIP.dat finish") log.Infoln("Download GeoIP.dat finish")
} }
if !geodata.Verify(C.GeoipName) { if err := geodata.Verify(C.GeoipName); err != nil {
log.Warnln("GeoIP.dat invalid, remove and download") log.Warnln("GeoIP.dat invalid, remove and download: %s", err)
if err := os.Remove(C.Path.GeoIP()); err != nil { if err := os.Remove(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error())
} }
@@ -159,6 +118,9 @@ func Init(dir string) error {
if !C.GeodataMode { if !C.GeodataMode {
C.GeodataMode = rawCfg.GeodataMode C.GeodataMode = rawCfg.GeodataMode
} }
C.GeoIpUrl = rawCfg.GeoXUrl.GeoIp
C.GeoSiteUrl = rawCfg.GeoXUrl.GeoSite
C.MmdbUrl = rawCfg.GeoXUrl.Mmdb
// initial GeoIP // initial GeoIP
if err := initGeoIP(); err != nil { if err := initGeoIP(); err != nil {
return fmt.Errorf("can't initial GeoIP: %w", err) return fmt.Errorf("can't initial GeoIP: %w", err)

80
config/updateGeo.go Normal file
View File

@@ -0,0 +1,80 @@
package config
import (
"fmt"
"github.com/Dreamacro/clash/component/geodata"
_ "github.com/Dreamacro/clash/component/geodata/standard"
C "github.com/Dreamacro/clash/constant"
"github.com/oschwald/geoip2-golang"
"io/ioutil"
"net/http"
"runtime"
)
func UpdateGeoDatabases() error {
defer runtime.GC()
geoLoader, err := geodata.GetGeoDataLoader("standard")
if err != nil {
return err
}
if C.GeodataMode {
data, err := downloadForBytes(C.GeoIpUrl)
if err != nil {
return fmt.Errorf("can't download GeoIP database file: %w", err)
}
if _, err = geoLoader.LoadIPByBytes(data, "cn"); err != nil {
return fmt.Errorf("invalid GeoIP database file: %s", err)
}
if saveFile(data, C.Path.GeoIP()) != nil {
return fmt.Errorf("can't save GeoIP database file: %w", err)
}
} else {
data, err := downloadForBytes(C.MmdbUrl)
if err != nil {
return fmt.Errorf("can't download MMDB database file: %w", err)
}
instance, err := geoip2.FromBytes(data)
if err != nil {
return fmt.Errorf("invalid MMDB database file: %s", err)
}
_ = instance.Close()
if saveFile(data, C.Path.MMDB()) != nil {
return fmt.Errorf("can't save MMDB database file: %w", err)
}
}
data, err := downloadForBytes(C.GeoSiteUrl)
if err != nil {
return fmt.Errorf("can't download GeoSite database file: %w", err)
}
if _, err = geoLoader.LoadSiteByBytes(data, "cn"); err != nil {
return fmt.Errorf("invalid GeoSite database file: %s", err)
}
if saveFile(data, C.Path.GeoSite()) != nil {
return fmt.Errorf("can't save GeoSite database file: %w", err)
}
return nil
}
func downloadForBytes(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func saveFile(bytes []byte, path string) error {
return ioutil.WriteFile(path, bytes, 0o644)
}

View File

@@ -30,6 +30,7 @@ const (
Vmess Vmess
Vless Vless
Trojan Trojan
Hysteria
) )
const ( const (
@@ -41,6 +42,7 @@ const (
type Connection interface { type Connection interface {
Chains() Chain Chains() Chain
AppendToChains(adapter ProxyAdapter) AppendToChains(adapter ProxyAdapter)
RemoteDestination() string
} }
type Chain []string type Chain []string
@@ -99,10 +101,19 @@ 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
} }
type Group interface {
URLTest(ctx context.Context, url string) (mp map[string]uint16, err error)
GetProxies(touch bool) []Proxy
}
type DelayHistory struct { type DelayHistory struct {
Time time.Time `json:"time"` Time time.Time `json:"time"`
Delay uint16 `json:"delay"` Delay uint16 `json:"delay"`
@@ -151,6 +162,8 @@ func (at AdapterType) String() string {
return "Vless" return "Vless"
case Trojan: case Trojan:
return "Trojan" return "Trojan"
case Hysteria:
return "Hysteria"
case Relay: case Relay:
return "Relay" return "Relay"

View File

@@ -0,0 +1,7 @@
//go:build no_doq
package features
func init() {
TAGS = append(TAGS, "no_doq")
}

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