Compare commits

..

319 Commits

Author SHA1 Message Date
be8eb7c17c feat: add port whitelist, empty is all port 2022-04-21 06:50:20 -07:00
58cd8f9ac1 fix:force-domain invalid 2022-04-17 21:17:21 +08:00
ea0d236259 chore: change comments 2022-04-17 20:03:53 +08:00
48a01adb7a refactor: sniffer param force and reverses deprecated, will be removed when release version, replace force-domain and skip-sni,
force-domain add '+' equivalent to force is true
sniffer:
  enable: true
  force-domain:
    - "google.com"
  skip-sni:
    - www.baidu.com
  sniffing:
    - tls
2022-04-17 20:02:13 +08:00
f8d7f29856 fix: PASS policy inconsistent names 2022-04-17 14:11:58 +08:00
1cf9321aa0 fix: domain tree match failed 2022-04-16 11:55:49 +08:00
71a1f5dfbd fix: domain type fix Mapping 2022-04-16 09:51:31 +08:00
25426cba33 chore: log style 2022-04-16 09:04:43 +08:00
9d364f66e9 fix: reverse error when force is false 2022-04-16 08:53:31 +08:00
7c23fa2bd4 fix: sniffer npe 2022-04-16 08:45:18 +08:00
0658ecadd3 fix: adjust loading timing 2022-04-16 08:29:38 +08:00
efbc334b3b Merge branch 'logic-rule' into Alpha 2022-04-16 08:22:08 +08:00
80764217c2 feat: add domain list for sniffer, reverse force logic
when force is false, if domain in the list, will force replace
when force is true, if sniff domain in the list, will skip it
2022-04-16 08:21:31 +08:00
45fe6e996b fix: npe when parse rule 2022-04-16 00:21:08 +08:00
36a719e2f8 feat: support http headers 2022-04-14 13:07:39 +08:00
1b6b0052c2 chore:adjust sniffer debuglog info 2022-04-13 08:38:55 +08:00
c981ef0f28 chore: update dependencies 2022-04-13 02:32:55 +08:00
1bf291d240 chore: update dependencies 2022-04-13 02:24:21 +08:00
b179d09efb Chore: adjust ipstack 2022-04-13 02:20:53 +08:00
4be17653e0 Fix: fakeip pool cycle used 2022-04-13 02:19:42 +08:00
21446ba5d4 chore: adjust code 2022-04-12 21:39:31 +08:00
75ce6b59bf Refactor: fakeip pool use netip.Prefix, supports ipv6 range 2022-04-12 20:32:08 +08:00
ce96ac35fb chore:merge & adjust code 2022-04-12 20:20:04 +08:00
173e10abe6 Chore: fix typos 2022-04-12 19:08:13 +08:00
a6eb11ce18 Refactor: DomainTrie use generics 2022-04-12 18:45:47 +08:00
0c65f6962a Refactor: queue use generics 2022-04-12 18:44:13 +08:00
baa9e02af6 Refactor: cache use generics 2022-04-12 18:44:10 +08:00
673541e2a8 Refactor: lrucache use generics 2022-04-12 18:44:07 +08:00
14878b37f6 fix: trojan fail may panic 2022-04-12 18:43:55 +08:00
83e0abaa8c chore: adjust code 2022-04-11 13:23:59 +08:00
7166db2ac9 fix: code logic error 2022-04-10 20:01:35 +08:00
815a060309 Update metadata.go
revet commit 13012a9
2022-04-10 00:47:22 +08:00
544e0f137d feat: sniffer support
sniffer:
  enable: true
  force: false # Overwrite domain
  sniffing:
    - tls
2022-04-09 22:30:36 +08:00
07906c0aa5 fix: parse logic rule error 2022-04-09 22:25:39 +08:00
b2981f921c chore: reduce a little memory 2022-04-09 22:24:48 +08:00
7be3e617ab disable process name on android 2022-04-09 17:54:01 +08:00
9a3bc8ef9e fix: auto detect interface add param[auto-detect-interface], default is true, only use it when tun is enabled 2022-04-07 21:36:19 +08:00
e083d1c57d Revert "Add docker workflow"
This reverts commit ce4902e5e6.
2022-04-07 10:24:23 +08:00
ce4902e5e6 Add docker workflow 2022-04-06 08:31:34 +08:00
4b9edc3b66 revert:the name of tun device on mac 2022-04-05 23:04:59 +08:00
91e48b707b Merge remote-tracking branch 'yaling888/with-tun' into Alpha 2022-04-05 14:44:40 +08:00
7a8af90b86 feat: add SMTPS/POP3S/IMAPS port to sni detect 2022-04-05 03:26:23 +08:00
93d2cfa091 fix: when ssh connect to a ip, if this ip map to a domain in clash, change ip to host may redirect to a diffrent ip 2022-04-05 03:26:23 +08:00
5719b9d22f fix: npe panic 2022-04-04 22:28:47 +08:00
b553dd749b refactor: Some adjustments 2022-04-03 19:15:16 +08:00
9461bcd44e fix: default-nameserver allow DOT and DOH with host is ip 2022-04-03 19:14:21 +08:00
6548dc90fa Merge remote-tracking branch 'Plus/with-tun' into Alpha 2022-04-02 20:48:11 +08:00
908ca20afa fix: dns over proxy may due to cancel request, but proxy live status is fine 2022-04-02 18:24:11 +08:00
13012a9f89 fix: dns over proxy may due to cancel request, but proxy live status is fine 2022-04-02 17:32:37 +08:00
afdcb6cfc7 fix: log level ajust and lint fix 2022-03-31 21:27:25 +08:00
c495d314d4 feat: 添加tls sni 嗅探
# Conflicts:
#	tunnel/statistic/tracker.go
#	tunnel/tunnel.go
2022-03-31 21:27:25 +08:00
e877b68179 Chore: revert "Feature: add tls SNI sniffing (#68)"
This reverts commit 24ce6622a2.
2022-03-31 21:20:46 +08:00
24ce6622a2 Feature: add tls SNI sniffing (#68) 2022-03-31 19:34:40 +08:00
88e4b3575e [Chore] fallback dependency 2022-03-31 00:26:01 +08:00
559b3ff9f3 [Fix] VLESS http conn with tls false
[Chore] Upgrade Dependencies
2022-03-31 00:08:43 +08:00
127634028d Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-03-30 13:19:05 +08:00
81c5a65f23 Merge remote-tracking branch 'Pro-Plus/with-tun' into Alpha
# Conflicts:
#	README.md
#	adapter/outbound/trojan.go
#	adapter/outbound/vless.go
#	transport/trojan/trojan.go
2022-03-30 13:15:45 +08:00
591ee119c2 docs: warning 2022-03-30 13:05:46 +08:00
5b03cc56e7 Merge remote-tracking branch 'Clash-dev/dev' into Alpha 2022-03-30 12:41:16 +08:00
9ff1f5530e Feature: Trojan XTLS 2022-03-30 00:15:39 +08:00
b3ea2ff8b6 Chore: adjust VLESS 2022-03-29 23:50:41 +08:00
c4216218c8 Merge pull request #24 from MarksonHon/patch-2
Fix systemd service
2022-03-29 14:53:02 +08:00
63840b3358 Fix systemd service 2022-03-29 14:50:12 +08:00
131e9d38b6 Fix: Vless UDP 2022-03-29 07:24:11 +08:00
56e2c172e1 Chore: adjust tun_wireguard cache buffer 2022-03-29 07:24:11 +08:00
b3b7a393f8 Chore: merge branch 'ogn-dev' into with-tun 2022-03-29 07:22:52 +08:00
045dd0589b fix: classical missing count 2022-03-28 21:04:50 +08:00
705311b70e [Chore]修改workflows 2022-03-28 20:52:09 +08:00
55ce40fbd1 [Chore]升级项目依赖
[Chore]隐藏TUN模式在system堆栈启动时弹窗
2022-03-28 20:44:52 +08:00
07fda93111 [Chore]升级项目依赖
[Chore]隐藏TUN模式在system堆栈启动时弹窗
2022-03-28 19:48:32 +08:00
012e044c54 [Chore]完成调试workflows 2022-03-28 19:02:51 +08:00
b323315583 [Chore]调试workflows 2022-03-28 18:58:23 +08:00
4c10d6e212 [Chore]调试workflows 2022-03-28 18:54:00 +08:00
ece3bb360a [Chore]调试workflows 2022-03-28 18:52:19 +08:00
5a7b9bdf45 [Chore]调试workflows 2022-03-28 18:49:24 +08:00
028ecb70c5 [Chore]调整workflows流程2 2022-03-28 18:44:27 +08:00
4e0b22f42d [Chore]调整workflows流程2 2022-03-28 18:41:30 +08:00
dbd27ef910 [Chore]调整workflows流程 2022-03-28 17:07:11 +08:00
ffff1418f2 [Fixed]尝试修复PASS空指针问题
[Chore]调整workflows测试
2022-03-28 16:36:34 +08:00
dd9bdf4e2f Fix: convert size to unit32 in getoridst to solve some mips64 devices cannot get redirect origin dst (#2041)
Change-Id: I40aa73dcea692132e38db980320a8a07ed427fe6

Co-authored-by: Zhao Guowei <zhaoguowei@bytedance.com>
2022-03-28 14:48:51 +08:00
64a5fd02da Merge remote-tracking branch 'tun/with-tun' into Alpha 2022-03-28 10:51:59 +08:00
8df8f8cb08 Chore: adjust gVisor stack 2022-03-28 03:25:55 +08:00
fe76cbf31c Chore: code style 2022-03-28 03:18:51 +08:00
7e2c6e5188 Chore: adjust HealthCheck at first check 2022-03-28 00:46:44 +08:00
4502776513 Refactor: MainResolver 2022-03-28 00:44:13 +08:00
611ce5f5f1 [commit]
[Feat] add Pass type for support temporary skip rule set
2022-03-27 23:44:51 +08:00
9bab2c504e Chore: regenerate protoc file 2022-03-27 07:12:12 +08:00
94b3c7e99a Chore: merge branch 'ogn-dev' into with-tun 2022-03-27 00:27:05 +08:00
0a0b8074f4 refactor: rule-set and its provider 2022-03-26 20:27:41 +08:00
f66c3b6f86 [Bilud]
正常编译
2022-03-26 16:39:50 +08:00
a3d49d1ed4 Merge remote-tracking branch 'dev/dev' into Alpha 2022-03-26 16:27:17 +08:00
0d068e7b5f [Fixed]
弃用过期函数,修复Process Name获取问题
2022-03-26 16:17:44 +08:00
275cc7edf3 Chore: structure support weakly type from float to int (#2042) 2022-03-25 15:22:31 +08:00
24583009c4 Merge remote-tracking branch 'tun/with-tun' into Alpha 2022-03-25 14:20:05 +08:00
4a4b1bdb83 Chore: adjust tun RelayDnsPacket 2022-03-25 04:09:11 +08:00
c6efa74a6b Fix: udp 4In6 of tun system stack 2022-03-25 03:42:46 +08:00
a593d68c42 build test 2022-03-24 23:42:49 +08:00
520657e953 [Fix] use direct to update http providers when proxy 寄 2022-03-24 12:34:45 +08:00
6c64164bee [skip ci] [Fix] ban auto set iptables when tun is enabled 2022-03-23 20:37:46 +08:00
9b4ddbed2c [skip ci] [Pre] avoid npe 2022-03-23 13:48:21 +08:00
79d984ee8e [Fix] url-test npe 2022-03-23 13:29:51 +08:00
7a54d616c4 [SKIP CI]
Merge remote-tracking branch 'Pro-Plus/with-tun' into Alpha

# Conflicts:
#	README.md
#	hub/route/server.go
2022-03-23 13:23:34 +08:00
f19b67fe9d bypass support for auto-iptables 2022-03-23 11:36:13 +08:00
91e83ea955 delete useless field 2022-03-23 10:18:26 +08:00
a375b85fa0 [skip ci]
# Conflicts:
#	.github/workflows/linter.yml
#	.github/workflows/release.yml
#	config/config.go
#	go.mod
#	go.sum
#	hub/executor/executor.go
2022-03-23 01:41:42 +08:00
ef915c94dc Feature: flush fakeip pool 2022-03-23 01:05:43 +08:00
4cc661920e [Fix] redir-host use host not ip 2022-03-22 23:31:23 +08:00
f4312cfa5a Chore: adjust the signal 2022-03-22 18:40:33 +08:00
ac4cde1411 Refactor: iptables auto config, disabled by default 2022-03-22 05:38:42 +08:00
b5f6f26de4 Update version.go
[BUILD TEST]
2022-03-22 01:39:00 +08:00
e068563b58 Merge pull request #22 from Adlyq/Alpha-pr
[skip ci]
[Fix] skip when country code not found in GeoIP.dat
2022-03-22 00:33:02 +08:00
bf6839e5f3 Merge pull request #23 from Adlyq/Alpha-pr-iptabls
[skip ci] auto change interface for tproxy
2022-03-22 00:32:47 +08:00
e0040b7e5d [Fix] do not monitor when auto-iptables is false 2022-03-21 20:29:07 +08:00
3beb71b6e1 auto change interface for tproxy 2022-03-21 19:51:27 +08:00
668d29d91f init sequence adjustment 2022-03-21 19:47:21 +08:00
5386c3903d delete useless code 2022-03-21 18:09:36 +08:00
6a4d2b3368 Change type conversion method 2022-03-21 12:34:32 +08:00
d9d8507c8f [Fix] skip when country code not found in GeoIP.dat 2022-03-21 12:24:39 +08:00
2c0890854e Fix: retry create TUN on Windows 2022-03-20 21:27:33 +08:00
bac04ab54b Merge branch 'ogn-dev' into with-tun 2022-03-20 21:26:25 +08:00
8c9e0b3884 Chore: use GOAMD64 v1 on build docker image 2022-03-20 11:32:18 +08:00
fc8092f7cc Fix: wintun dns address 2022-03-20 04:19:48 +08:00
5b7f46bc97 [skip ci][内容]
1.调整部分代码
2022-03-20 02:39:48 +08:00
d1838f663e Merge remote-tracking branch 'yaling888/with-tun' into Alpha
# Conflicts:
#	listener/tun/tun_adapter.go
2022-03-19 22:37:51 +08:00
2d1c031ce0 [skip ci][内容]
1.修复部分空指针问题
2.修改go.mod
2022-03-19 22:28:28 +08:00
e67f94b87a [内容]
同步至最新v1.10.0
2022-03-19 15:01:49 +08:00
2df890c4ee Merge remote-tracking branch 'clash/dev' into Alpha
# Conflicts:
#	Makefile
2022-03-19 14:53:47 +08:00
30d4668008 Chore: fix typo (#2033) 2022-03-19 13:58:51 +08:00
02333a859a Chore: split amd64 v3 to special release 2022-03-19 13:42:06 +08:00
f9cc1cc363 Fix: routing-mark option doesn't work on proxies (#2028) 2022-03-19 13:29:30 +08:00
520256365e [内容]
1.wintun.dll 0.14.1
2022-03-19 01:54:21 +08:00
9270d3c475 [内容]
1.autoIptables 开关
2.go.mod 调整
3.processName 调整
4.makefile 调整
5.Tun模块 部分代码调整
2022-03-19 01:11:27 +08:00
c8b1050c15 Merge pull request #19 from Adlyq/Alpha-pr
[skip ci]Only prompt when interface cannot be found
2022-03-18 21:45:50 +08:00
39de5d58c8 Only prompt when interface cannot be found 2022-03-18 17:41:06 +08:00
7b7abf6973 Feature: auto detect interface if switch network 2022-03-18 17:03:50 +08:00
a38f30ec3b Merge pull request #18 from Adlyq/Alpha
[Fix] Process name display for Android
2022-03-18 13:20:35 +08:00
2ea92d70f9 Merge remote-tracking branch 'upstream/Alpha' into Alpha 2022-03-18 12:38:16 +08:00
8e5f01597e Fix: build 2022-03-18 05:21:28 +08:00
546f2fa739 Chore: make fake ip pool start with the third ip 2022-03-18 05:17:47 +08:00
ea2e715da9 Merge remote-tracking branch 'origin/Alpha' into Alpha
# Conflicts:
#	go.mod
#	go.sum
2022-03-18 02:36:09 +08:00
1350330fe0 1.fix module package
2.fix govet error
2022-03-18 02:35:15 +08:00
317797acc8 1.fix module package
2.fix govet error
2022-03-18 01:25:59 +08:00
8766764d49 fix 2022-03-18 00:40:39 +08:00
b8d48e1618 Merge remote-tracking branch 'upstream/Alpha' into Alpha 2022-03-18 00:33:27 +08:00
f972d1fa58 update 2022-03-18 00:27:48 +08:00
df78ba8fa6 update 2022-03-18 00:24:38 +08:00
0c83575302 Merge remote-tracking branch 'upstream/Alpha' into Alpha 2022-03-18 00:10:37 +08:00
e9151bc43f update 2022-03-17 23:57:58 +08:00
68345b6a19 Merge remote-tracking branch 'upstream/Alpha' into Alpha 2022-03-17 23:40:51 +08:00
435bee0ca2 update 2022-03-17 23:24:07 +08:00
92d169ca81 [Fix] Process name display for Android 2022-03-17 20:31:16 +08:00
30f1b29257 Merge remote-tracking branch 'yaling888/with-tun' into Alpha
# Conflicts:
#	.github/workflows/codeql-analysis.yml
#	.github/workflows/linter.yml
#	.github/workflows/release.yml
#	Makefile
#	README.md
#	adapter/outbound/vless.go
#	component/geodata/memconservative/cache.go
#	component/geodata/router/condition.go
#	component/geodata/router/condition_geoip.go
#	component/geodata/standard/standard.go
#	component/geodata/utils.go
#	config/config.go
#	config/initial.go
#	constant/metadata.go
#	constant/path.go
#	constant/rule.go
#	constant/rule_extra.go
#	dns/client.go
#	dns/filters.go
#	dns/resolver.go
#	go.mod
#	go.sum
#	hub/executor/executor.go
#	hub/route/configs.go
#	listener/listener.go
#	listener/tproxy/tproxy_linux_iptables.go
#	listener/tun/dev/dev.go
#	listener/tun/dev/dev_darwin.go
#	listener/tun/dev/dev_linux.go
#	listener/tun/dev/dev_windows.go
#	listener/tun/dev/wintun/config.go
#	listener/tun/dev/wintun/dll_windows.go
#	listener/tun/dev/wintun/session_windows.go
#	listener/tun/dev/wintun/wintun_windows.go
#	listener/tun/ipstack/commons/dns.go
#	listener/tun/ipstack/gvisor/tun.go
#	listener/tun/ipstack/gvisor/tundns.go
#	listener/tun/ipstack/gvisor/utils.go
#	listener/tun/ipstack/stack_adapter.go
#	listener/tun/ipstack/system/dns.go
#	listener/tun/ipstack/system/tcp.go
#	listener/tun/ipstack/system/tun.go
#	listener/tun/tun_adapter.go
#	main.go
#	rule/common/base.go
#	rule/common/domain.go
#	rule/common/domain_keyword.go
#	rule/common/domain_suffix.go
#	rule/common/final.go
#	rule/common/geoip.go
#	rule/common/geosite.go
#	rule/common/ipcidr.go
#	rule/common/port.go
#	rule/parser.go
#	rule/process.go
#	test/go.mod
#	test/go.sum
#	transport/vless/xtls.go
#	tunnel/tunnel.go
2022-03-17 17:41:02 +08:00
c503e44324 Merge pull request #17 from Adlyq/Alpha
[Fix] Parse
2022-03-17 12:28:45 +08:00
ce509295c0 [Fix] Parse 2022-03-17 12:26:43 +08:00
f671d6a1fd [Fix] Parse 2022-03-17 12:23:50 +08:00
8d0ae4284d Chore: use gateway address of fake ip pool as the TUN device address 2022-03-17 07:41:18 +08:00
e194efcecb Migration: go 1.18 2022-03-17 01:51:28 +08:00
609d69191a Merge remote-tracking branch 'clash/dev' into Alpha
# Conflicts:
#	.github/workflows/docker.yml
#	adapter/outboundgroup/fallback.go
#	adapter/outboundgroup/loadbalance.go
#	adapter/outboundgroup/relay.go
#	adapter/outboundgroup/selector.go
#	adapter/outboundgroup/urltest.go
#	config/config.go
#	go.mod
#	go.sum
#	main.go
#	test/go.mod
#	test/go.sum
2022-03-17 01:41:51 +08:00
c791044ddf Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-03-17 00:12:26 +08:00
dc2abe6eeb [Build test] 1.18
[Updata] wintun.dll
2022-03-17 00:12:11 +08:00
1071e3f4a3 [Build test] 1.18
[Updata] wintun.dll
2022-03-17 00:02:22 +08:00
acc249495d [Build test] 1.18 2022-03-16 23:30:29 +08:00
5a2cc9a36f [Fix] 优化geodata初始化逻辑 2022-03-16 23:09:05 +08:00
1cc6cfab9c [Fix] 优化geodata初始化逻辑 2022-03-16 23:02:16 +08:00
0183d752a0 [Fix] 优化geodata初始化逻辑 2022-03-16 22:55:18 +08:00
b8d635a4b3 Migration: go 1.18 2022-03-16 22:00:20 +08:00
2f24e49ff6 [build test] 1.18 2022-03-16 21:47:00 +08:00
346d817dba Chore: Merge branch 'ogn-dev' into with-tun 2022-03-16 20:16:30 +08:00
3a9bbf6c73 Fix: should keep alive in tcp relay 2022-03-16 18:17:28 +08:00
016862f7a5 [build test]1.18 2022-03-16 17:54:44 +08:00
c3df768f79 [build test] 2022-03-16 17:33:08 +08:00
0f2123179a [build test] 2022-03-16 17:29:09 +08:00
fb7d340233 Fix: docker build makefile 2022-03-16 12:13:59 +08:00
6a661bff0c Migration: go 1.18 2022-03-16 12:10:13 +08:00
1034780e8e [build test] 2022-03-16 00:43:08 +08:00
f01ac69654 Merge remote-tracking branch 'clash/dev' into Alpha
# Conflicts:
#	.github/workflows/codeql-analysis.yml
#	.github/workflows/docker.yml
#	.github/workflows/linter.yml
#	.github/workflows/stale.yml
#	Makefile
#	component/dialer/dialer.go
#	config/config.go
#	constant/metadata.go
#	constant/rule.go
#	rule/common/domain.go
#	rule/common/domain_keyword.go
#	rule/common/domain_suffix.go
#	rule/common/final.go
#	rule/common/ipcidr.go
#	rule/geoip.go
#	rule/parser.go
#	rule/port.go
#	rule/process.go
2022-03-15 23:13:41 +08:00
c85305ead8 [Skip CI] 2022-03-15 22:25:33 +08:00
3e89bee524 [Skip CI] 2022-03-15 11:47:42 +08:00
d1dd21417b Feature: add tzdata to Dockerfile (#2027)
Co-authored-by: suyaqi <suyaqi@wy.net>
2022-03-15 11:30:52 +08:00
9ff32d9e29 Chore: use slice instead of map for system stack udp receiver queue 2022-03-15 05:19:29 +08:00
d486ee467a Fix: test 2022-03-15 03:39:45 +08:00
20b66d9550 Style: code style 2022-03-15 02:55:06 +08:00
5abd03e241 Fix: exclude the broadcast address to fake ip pool 2022-03-15 02:43:40 +08:00
68fccfacc0 [Skip CI] 2022-03-15 02:20:19 +08:00
cf52fbed65 [Skip CI] 2022-03-15 02:06:57 +08:00
359f8ffca3 Fix: should use the correct gateway for TUN system stack 2022-03-13 17:48:43 +08:00
46b9a1092d Chore: embed the RuleExtra into Base 2022-03-13 01:22:05 +08:00
8fbf93ccc8 Chore: Merge branch 'ogn-dev' into with-tun 2022-03-13 01:15:35 +08:00
b866f06414 Chore: move find connection process to tunnel (#2016) 2022-03-12 19:07:53 +08:00
8b4f9a35f6 Chore: bump to go1.18rc1, use netip.Addr to replace net.IP with system TUN stack 2022-03-12 02:16:13 +08:00
9683c297a7 Chore: add more details to process resolving (#2017) 2022-03-09 13:41:50 +08:00
8333815e95 Chore: refactor TUN 2022-03-09 05:08:35 +08:00
d49871224c Fix: should only resolve local process name 2022-03-09 00:32:21 +08:00
ba7bcce895 Chore: code style 2022-03-09 00:32:21 +08:00
71e002c2ef Merge branch 'ogn-dev' into with-tun 2022-03-09 00:30:38 +08:00
f6c7281bb7 Chore: update github action workflow 2022-03-06 21:48:37 +08:00
83bfe521b1 Fix: should split linux process name with space (#2008) 2022-03-05 18:25:16 +08:00
3ab784dd80 Merge pull request #16 from Dabrit/test
[Skip CI]README Improvements
2022-03-05 09:03:25 +08:00
f1c4d85eb3 Update README.md 2022-03-05 01:44:48 +08:00
b9fc393f95 Naming edited 2022-03-05 01:33:11 +08:00
557347d366 Merge pull request #15 from Dabrit/test
Optimize reading experience of linux users
2022-03-04 23:25:02 +08:00
a7f3b85200 Edit username to adapt Linux username naming rule 2022-03-04 22:36:15 +08:00
3772ad8ddb Revise mismatching targets from README. 2022-03-04 22:22:49 +08:00
17c53b92b9 Fix: iptables routing mark init 2022-03-03 05:02:17 +08:00
0b9022b868 Chore: update dependencies 2022-03-03 04:23:03 +08:00
5e0d4930cb Merge branch 'ogn-dev' into with-tun 2022-03-03 04:17:00 +08:00
b52d0c16e9 Chore: vmess test remove all alterid 2022-02-27 18:00:04 +08:00
5ad7237fa7 Merge pull request #14 from Adlyq/Alpha
Fix the filter under proxy-group to filter other groups
2022-02-27 00:34:08 +08:00
49e25f502f Merge pull request #11 from ttyykpe/patch-1
Makefile add android-armv8
2022-02-27 00:33:58 +08:00
06942c67fd Fix the filter under proxy-group to filter other groups 2022-02-23 16:17:29 +08:00
705e5098ab Chore: use SIMD for AMD64 and ARM64 system stack checksum 2022-02-23 14:51:04 +08:00
ac5c57ecef Chore: compatible with VMESS WS older version configurations 2022-02-23 14:21:53 +08:00
cd3b139c3f Chore: use "-m mark --mark" instead of "-m owner --uid-owner" 2022-02-23 14:19:59 +08:00
592b6a785e Fix: find process name by UDP network on macOS 2022-02-23 14:04:47 +08:00
2f234cf6bc Feature: process condition for rules 2022-02-23 14:01:53 +08:00
132a6a6a2f Fix: listener tcp keepalive & reuse net.BufferedConn (#1987) 2022-02-23 11:22:46 +08:00
d876d6e74c Feature: resolve ip with a proxy adapter 2022-02-23 02:38:50 +08:00
b192238699 Merge from remote branch 2022-02-23 01:00:27 +08:00
3b2ec3d880 Chore: upgrade gvisor 2022-02-22 22:30:41 +08:00
9259c9f3ff Makefile add android-armv8 2022-02-21 18:04:38 +08:00
03e4b5d525 Chore: use golangci-lint config file 2022-02-19 00:08:51 +08:00
a0221bf897 Fix: routing-mark should effect on root 2022-02-17 14:23:47 +08:00
37cf166d14 Merge pull request #10 from Adlyq/Alpha
Full regexp support
2022-02-16 23:10:07 +08:00
27292dac0c Replace the regular implementation of the filter for proxy-providers and proxy-groups with regex2 2022-02-16 22:18:05 +08:00
847c91503b [build] 2022-02-06 05:08:11 +08:00
ca8ed0a01b [Fix]GeoSite.dat initial in logic rule 2022-02-06 04:41:34 +08:00
7465eaafa1 [Fix]GeoSite.dat initial in logic rule 2022-02-06 04:30:54 +08:00
4a446c4e31 [build] 2022-02-06 01:59:35 +08:00
28a1475f66 [FEAT] Add geodata loader mode switch 2022-02-05 02:42:49 +08:00
c28f42d823 [FEAT] Add geodata loader mode switch 2022-02-05 00:51:06 +08:00
2bf34c766e [Feat]
support trojan xtls
change geodataloader mode as memconservative
2022-02-04 23:33:36 +08:00
35b19c3d7f Merge branch 'Dev' into Feature
# Conflicts:
#	Makefile
2022-02-04 18:44:35 +08:00
3b277aa8ec [Feat]
update gvisor
Chore: use "-m mark --mark" instead of "-m owner --uid-owner"
2022-02-04 06:11:24 +08:00
176eb3926b Merge remote-tracking branch 'pro-plus/plus-pro' into Feature
# Conflicts:
#	.github/workflows/Alpha.yml
#	.github/workflows/codeql-analysis.yml
#	.github/workflows/docker.yml
#	.github/workflows/linter.yml
#	.github/workflows/stale.yml
#	Makefile
#	README.md
#	adapter/outbound/vless.go
#	component/dialer/dialer.go
#	component/geodata/geodata.go
#	component/geodata/router/condition.go
#	config/config.go
#	config/initial.go
#	constant/metadata.go
#	constant/path.go
#	constant/rule.go
#	constant/rule_extra.go
#	dns/filters.go
#	go.mod
#	go.sum
#	hub/executor/executor.go
#	hub/route/configs.go
#	listener/listener.go
#	listener/tun/dev/dev.go
#	listener/tun/dev/dev_darwin.go
#	listener/tun/dev/dev_linux.go
#	listener/tun/dev/dev_windows.go
#	listener/tun/dev/dev_windows_extra.go
#	listener/tun/dev/wintun/dll_windows.go
#	listener/tun/dev/wintun/session_windows.go
#	listener/tun/ipstack/gvisor/tun.go
#	listener/tun/ipstack/gvisor/tundns.go
#	listener/tun/ipstack/stack_adapter.go
#	listener/tun/ipstack/system/tun.go
#	listener/tun/tun_adapter.go
#	main.go
#	rule/base.go
#	rule/common/process.go
#	rule/geoip.go
#	rule/parser.go
#	rule/port.go
#	test/go.mod
#	test/go.sum
#	test/vless_test.go
#	transport/vless/xtls.go
#	tunnel/tunnel.go
2022-02-04 05:30:21 +08:00
776728fb30 [Feat]
update gvisor
Chore: use "-m mark --mark" instead of "-m owner --uid-owner"
2022-02-04 04:47:40 +08:00
1cdaf782ba Merge remote-tracking branch 'clash/dev' into Feature 2022-02-04 02:38:32 +08:00
f1157d0a09 Chore: use "-m mark --mark" instead of "-m owner --uid-owner" 2022-02-02 21:59:44 +08:00
f376409041 Chore: upgrade gvisor 2022-02-01 02:00:10 +08:00
45b3afdd33 Fix: new version golangci-lint check 2022-01-30 01:49:27 +08:00
875fdb3a5b Revert "Chore: upgrade gvisor version"
This reverts commit d633e3d96e.
2022-01-30 00:45:02 +08:00
25e115d042 Feature: process condition for rules 2022-01-28 22:52:35 +08:00
d633e3d96e Chore: upgrade gvisor version 2022-01-28 22:42:58 +08:00
6e9d837a7d Merge from remote branch 2022-01-28 19:51:40 +08:00
63b9d66365 [Feat]
1.Add DNS over QUIC support
2.Replace Country.mmdb with GeoIP.dat
3.build with Alpha tag
2022-01-27 12:45:11 +08:00
be0fadc09e [Feat]
1.Add DNS over QUIC support
2.Replace Country.mmdb with GeoIP.dat
3.build with Alpha tag
2022-01-27 12:25:53 +08:00
9e4e1482d9 [chore] Replace Country.mmdb with GeoIP.dat 2022-01-26 12:01:14 +08:00
9974fba56e Update dev.yml 2022-01-25 21:59:48 +08:00
4bd5764c4e Update Makefile 2022-01-25 21:47:11 +08:00
deeab8b45f [test] dev build 2022-01-25 21:34:06 +08:00
af30664c51 [test] dev build 2022-01-25 21:12:49 +08:00
6962f0b7e1 [update] dev build 2022-01-25 20:54:56 +08:00
6e5859d1bf [update] dev build 2022-01-25 20:53:07 +08:00
87ca93b979 Update build.yaml 2022-01-25 20:40:03 +08:00
11052d8f77 github action add build
(cherry picked from commit bdec838673767977c14191861ac1b9a8291e2ffc)
2022-01-25 20:33:30 +08:00
56c38890f9 Merge from remote branch[ssh] 2022-01-18 10:05:06 +08:00
daae846db3 Merge from remote branch 2022-01-18 09:51:20 +08:00
c14dd79e69 Merge from remote branch 2022-01-05 01:46:37 +08:00
5657aa50cf Merge from remote branch 2021-12-09 21:38:24 +08:00
8e641a4e31 Fix: should return io.EOF immediately 2021-11-20 23:01:22 +08:00
3d246d5150 Merge from remote branch 2021-11-14 20:25:22 +08:00
3686446919 Fix: resolver dial context options 2021-11-12 11:05:02 +08:00
a412745314 Merge from remote branch 2021-11-11 00:54:43 +08:00
d0c23998d2 Fix: resolver dial context udp 2021-11-11 00:53:42 +08:00
038cc1f6b5 Merge from remote branch 2021-11-09 21:12:08 +08:00
6bd186d3c0 Merge from remote branch 2021-11-09 21:11:38 +08:00
4c6bb7178b Feature: resolve ip with proxy adapter 2021-11-09 19:44:16 +08:00
53287d597b Chore: use custom buffer pool for lwIP stack 2021-11-04 18:33:11 +08:00
964bbe1957 Chore: adjust all udp alloc size
Chore: adjust all udp alloc size
2021-11-04 00:44:16 +08:00
c824ace2d7 Wintun: use new swdevice-based API for upcoming Wintun 0.14 2021-11-03 15:10:31 +08:00
ac9e5c6913 Wintun: use new swdevice-based API for upcoming Wintun 0.14 2021-11-03 15:02:40 +08:00
b515a4e270 Chore: move "geodata" to package "component" 2021-11-02 18:23:01 +08:00
78cef7df59 Chore: move "geodata" to package "component" 2021-10-29 00:52:44 +08:00
62b3ebe49f Chore: update dependencies 2021-10-28 13:35:27 +08:00
325b7f455f Chore: version fmt 2021-10-28 12:55:40 +08:00
ff420ed2ee Merge from remote branch 2021-10-28 12:30:30 +08:00
d1568325e6 Merge from remote branch 2021-10-28 12:30:02 +08:00
ddf28dfe8b Merge from remote branch 2021-10-28 11:36:41 +08:00
2680e8ffa3 Merge from remote branch 2021-10-28 11:36:11 +08:00
2953772a0e Style: format code 2021-10-28 00:06:55 +08:00
5a27df899f Chore: script built 2021-10-27 23:10:11 +08:00
ab12b440aa Merge remote branch 2021-10-21 22:40:07 +08:00
4b614090f8 Merge remote branch 2021-10-21 22:37:30 +08:00
63d07db4bf Chore: script built 2021-10-21 20:22:23 +08:00
cbea46b0c8 Merge remote branch 2021-10-15 14:14:51 +08:00
c0e9d69163 Feature: add mode script 2021-10-15 14:11:14 +08:00
d29d824da8 Improve: avoid bufconn twice (#1650) 2021-09-30 04:11:37 +08:00
862174d21b Feature: add lwIP TCP/IP stack to tun listener 2021-09-30 04:05:52 +08:00
433d35e866 Chore: format with go 1.17 2021-09-24 04:37:04 +08:00
32d8f849ee Chore: update gvisor 2021-09-23 02:42:17 +08:00
8be1d5effb Merge from remote branch 2021-09-22 22:11:51 +08:00
5f03238c8a Chore: force set latest go version to action 2021-09-18 18:22:28 +08:00
6f94d56383 Fix: gvisor ipv6 routeing in Tun 2021-09-17 16:49:53 +08:00
fbda82218e Merge from remote branch 2021-09-17 15:07:27 +08:00
85dc0b5527 Fix: potential overflow in ssr (#1600) 2021-09-09 22:07:27 +08:00
77a6a08192 Fix: VLESS test cases 2021-09-08 23:34:57 +08:00
1df5317e13 Feature: add VLESS test case [ssh] 2021-09-08 22:36:54 +08:00
ae619e4163 Fix: VLESS WSOpts 2021-09-08 21:32:08 +08:00
738bd3b0dd Fix: vmess ws headers not set properly (#1595) 2021-09-08 21:20:16 +08:00
03be2512ca Merge from remote branch 2021-09-08 04:43:53 +08:00
6ddd9e6fb8 Merge from remote branch 2021-09-08 04:42:56 +08:00
9254d2411e Fix: VLESS WSOpts Headers 2021-09-08 04:34:11 +08:00
b904ca0bcc Feature: add source ipcidr condition to rule final 2021-09-01 18:29:48 +08:00
fb836fe441 Fix: remove trim source ipcidr 2021-09-01 01:02:42 +08:00
b23bc77001 Fix: source ipcidr condition for rule IPCIDR 2021-09-01 00:53:13 +08:00
16fcee802b Merge from remote branch 2021-09-01 00:41:32 +08:00
48aef1829f Merge from remote branch 2021-09-01 00:38:43 +08:00
4cc16e0136 Feature: add source ipcidr condition for all rules 2021-08-31 21:46:04 +08:00
83c9664c17 Merge from remote branch 2021-08-05 00:49:17 +08:00
ac9e90c812 Merge from remote branch 2021-07-28 22:14:20 +08:00
ba2fd00f01 Merge from remote branch 2021-07-28 22:13:21 +08:00
09299e5e5a Fix: error var name 2021-07-27 02:38:41 +08:00
96e5a52651 Style: code style 2021-07-09 02:19:43 +08:00
5852245045 Merge from remote branch 2021-07-07 03:53:32 +08:00
b4d93c4438 Feature: add xtls support for VLESS 2021-07-06 23:55:34 +08:00
56dff65149 Feature: support multiport condition for rule SRC-PORT and DST-PORT 2021-07-06 15:07:05 +08:00
e2c7b19000 Fix: fix yaml syntax 2021-07-03 22:41:31 +08:00
8a488bab72 Merge from remote branch 2021-07-03 22:33:18 +08:00
3afe3810bf Merge from remote branch 2021-07-03 22:31:12 +08:00
d7732f6ebc Code: refresh code 2021-07-01 22:49:29 +08:00
275 changed files with 8955 additions and 5538 deletions

View File

@ -1,7 +1,7 @@
name: Dev name: Alpha
on: [push] on: [push]
jobs: jobs:
dev-build: Feature-build:
if: ${{ !contains(github.event.head_commit.message, '[Skip CI]') }} if: ${{ !contains(github.event.head_commit.message, '[Skip CI]') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -15,7 +15,7 @@ jobs:
go-version: ${{ steps.version.outputs.go_version }} go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Cache go module - name: Cache go module
uses: actions/cache@v2 uses: actions/cache@v2
@ -24,9 +24,9 @@ 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 # - name: Get dependencies, run test
# run: | # run: |
# go test ./... # go test ./...
- name: Build - name: Build
if: success() if: success()
env: env:
@ -34,11 +34,38 @@ jobs:
BINDIR: bin BINDIR: bin
run: make -j releases run: make -j releases
- name: Upload Release - name: Delete current release assets
uses: andreaswilli/delete-release-assets-action@v2.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: alpha
deleteOnlyFromDrafts: false
- name: Tag Repo
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Alpha
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: ${{ env.GIT_BRANCH != 'Meta' && success() }} if: ${{ env.GIT_BRANCH != 'Meta' && success() }}
with: with:
tag_name: Dev tag: ${{ github.ref }}
tag_name: alpha
files: bin/* files: bin/*
prerelease: true prerelease: true
- name: send telegram message on push
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TTELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
${{ github.actor }} created commit:
Commit message: ${{ github.event.commits[0].message }}
Repository: ${{ github.repository }}
See changes: https://github.com/${{ github.repository }}/commit/${{github.sha}}

20
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Build All
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.18
- name: Check out code
uses: actions/checkout@v1
- name: Build
run: make all
- name: Release
uses: softprops/action-gh-release@v1
with:
files: bin/*
draft: true

View File

@ -1,76 +0,0 @@
name: Publish Docker Image
on:
push:
branches:
- dev
tags:
- '*'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: all
- name: Set up docker buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Github Package
uses: docker/login-action@v1
with:
registry: ghcr.io
username: Dreamacro
password: ${{ secrets.PACKAGE_TOKEN }}
- name: Build dev branch and push
if: github.ref == 'refs/heads/dev'
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: 'dreamacro/clash:dev,ghcr.io/dreamacro/clash:dev'
- name: Get all docker tags
if: startsWith(github.ref, 'refs/tags/')
uses: actions/github-script@v4
id: tags
with:
script: |
const ref = `${context.payload.ref.replace(/\/?refs\/tags\//, '')}`
const tags = [
'dreamacro/clash:latest',
`dreamacro/clash:${ref}`,
'ghcr.io/dreamacro/clash:latest',
`ghcr.io/dreamacro/clash:${ref}`
]
return tags.join(',')
result-encoding: string
- name: Build release and push
if: startsWith(github.ref, 'refs/tags/')
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: ${{steps.tags.outputs.result}}

16
.golangci.yaml Normal file
View File

@ -0,0 +1,16 @@
linters:
disable-all: true
enable:
- gofumpt
- staticcheck
- govet
- gci
linters-settings:
gci:
sections:
- standard
- prefix(github.com/Dreamacro/clash)
- default
staticcheck:
go: '1.18'

View File

@ -12,7 +12,7 @@ RUN go mod download && \
FROM alpine:latest FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash" LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash"
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /Country.mmdb /root/.config/clash/ COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash / COPY --from=builder /clash /
ENTRYPOINT ["/clash"] ENTRYPOINT ["/clash"]

View File

@ -1,58 +1,58 @@
NAME=Clash.Meta NAME=Clash.Meta
BINDIR=bin BINDIR=bin
BRANCH=$(shell git rev-parse --abbrev-ref HEAD) BRANCH=$(shell git rev-parse --abbrev-ref HEAD)
VERSION=$(shell git describe --tags || echo "unknown version" ) VERSION=alpha-$(shell git rev-parse --short HEAD)
ifeq ($(BRANCH),Dev)
VERSION=develop-$(shell git rev-parse --short HEAD)
endif
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
AUTOIPTABLES=Enable
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='
GOBUILDOP=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.AutoIptables=$(AUTOIPTABLES)" \
-w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64 \ darwin-amd64v1 \
darwin-amd64v2 \
darwin-amd64v3 \
darwin-arm64 \ darwin-arm64 \
linux-amd64 \ linux-amd64v1 \
linux-amd64v2 \
linux-amd64v3 \
linux-armv5 \ linux-armv5 \
linux-armv6 \ linux-armv6 \
linux-armv7 \ linux-armv7 \
linux-armv8 \ linux-arm64 \
linux-mips64 \ linux-mips64 \
linux-mips64le \ linux-mips64le \
linux-mips-softfloat \ linux-mips-softfloat \
linux-mips-hardfloat \ linux-mips-hardfloat \
linux-mipsle-softfloat \ linux-mipsle-softfloat \
linux-mipsle-hardfloat \ linux-mipsle-hardfloat \
android-arm64 \
freebsd-386 \ freebsd-386 \
freebsd-amd64 \ freebsd-amd64 \
freebsd-arm64 freebsd-arm64
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64 \ windows-amd64v1 \
windows-amd64v2 \
windows-amd64v3 \
windows-arm64 \ windows-arm64 \
windows-arm32v7 windows-arm32v7
all:linux-amd64 linux-arm64\
all:linux-amd64-AutoIptables linux-amd64\
linux-arm64 linux-arm64-AutoIptables linux-armv7\
darwin-amd64 darwin-arm64\ darwin-amd64 darwin-arm64\
windows-amd64 windows-386 \ windows-amd64 windows-arm64\
linux-mips-hardfloat linux-mips-softfloat linux-mips64 linux-mips64le linux-mipsle-hardfloat linux-mipsle-softfloat# Most used
docker: docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64: darwin-amd64v3:
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v2:
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)-$@
@ -60,42 +60,27 @@ darwin-arm64:
linux-386: linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64: linux-amd64v3:
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-AutoIptables: linux-amd64v2:
GOARCH=amd64 GOOS=linux $(GOBUILDOP) -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)-$@
linux-arm64-AutoIptables:
GOARCH=arm64 GOOS=linux $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv5: linux-armv5:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv5-AutoIptables:
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv6: linux-armv6:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv6-AutoIptables:
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv7: linux-armv7:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv7-AutoIptables:
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-armv8:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-armv8-AutoIptables:
GOARCH=arm64 GOOS=linux $(GOBUILDOP) -o $(BINDIR)/$(NAME)-$@
linux-mips-softfloat: linux-mips-softfloat:
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -114,11 +99,14 @@ linux-mips64:
linux-mips64le: linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
android-arm64:
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-386: freebsd-386:
GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-amd64: freebsd-amd64:
GOARCH=amd64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
freebsd-arm64: freebsd-arm64:
GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@ -126,8 +114,14 @@ 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-amd64: windows-amd64v3:
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v2:
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
@ -148,5 +142,12 @@ $(zip_releases): %.zip : %
all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST) all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
releases: $(gz_releases) $(zip_releases) releases: $(gz_releases) $(zip_releases)
vet:
go test ./...
lint:
golangci-lint run ./...
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*

View File

@ -172,88 +172,102 @@ proxy-providers:
Support outbound transport protocol `VLESS`. Support outbound transport protocol `VLESS`.
The XTLS support TCP/UDP by the XRAY-CORE. The XTLS support (TCP/UDP) transport by the XRAY-CORE.
```yaml ```yaml
proxies: proxies:
# Vless + TCP Sample - name: "vless"
- name: "vless-tcp"
type: vless type: vless
server: server server: server
port: 443 port: 443
uuid: uuid uuid: uuid
network: tcp
servername: example.com # AKA SNI servername: example.com # AKA SNI
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
# skip-cert-verify: true # skip-cert-verify: true
# Vless + WS Sample
- name: "vless-ws" - name: "vless-ws"
type: vless type: vless
server: server server: server
port: 443 port: 443
uuid: uuid uuid: uuid
tls: true
udp: true udp: true
network: ws network: ws
servername: example.com # priority over wss host servername: example.com # priority over wss host
# skip-cert-verify: true # skip-cert-verify: true
ws-path: /path ws-opts:
ws-headers: path: /path
Host: example.com headers: { Host: example.com, Edge: "12a00c4.fm.huawei.com:82897" }
# Trojan + XTLS Sample - name: "vless-grpc"
- name: "proxy name" type: vless
type: trojan server: server
server: server name
port: 443 port: 443
password: password uuid: uuid
tls: true
udp: true udp: true
servername: server name # AKA SNI network: grpc
flow: xtls-rprx-direct # Enable XTLS: xtls-rprx-direct | xtls-rprx-origin servername: example.com # priority over wss host
skip-cert-verify: false # skip-cert-verify: true
grpc-opts:
grpc-service-name: grpcname
``` ```
### IPTABLES auto-configuration ### IPTABLES configuration
Only work on Linux OS who support `iptables`, Clash will auto-configuration iptables for tproxy listener when `tproxy-port` value isn't zero. Work on Linux OS who's supported `iptables`
If `TPROXY` is enabled, the `TUN` must be disabled.
```yaml ```yaml
# Enable the TPROXY listener # Enable the TPROXY listener
tproxy-port: 9898 tproxy-port: 9898
# Disable the TUN listener
tun: iptables:
enable: false enable: true # default is false
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
``` ```
Create user given name `Clash.Meta`.
Run Meta Kernel by user `Clash.Meta` as a daemon.
Create the systemd configuration file at /etc/systemd/system/clash.service: ### General installation guide for Linux
+ Create user given name `clash-meta`
+ Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
+ Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
+ Create folder `/etc/Clash-Meta/` as working directory
Run Meta Kernel by user `clash-meta` as a daemon.
Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`:
``` ```
[Unit] [Unit]
Description=Clash.Meta Daemon, Another Clash Kernel. Description=Clash-Meta Daemon, Another Clash Kernel.
After=network.target After=network.target NetworkManager.service systemd-networkd.service iwd.service
[Service] [Service]
Type=simple Type=simple
User=Clash.Meta User=clash-meta
Group=Clash.Meta Group=clash-meta
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=cap_net_admin CapabilityBoundingSet=cap_net_admin
AmbientCapabilities=cap_net_admin AmbientCapabilities=cap_net_admin
Restart=always Restart=always
ExecStart=/usr/local/bin/Clash.Meta -d /etc/Clash.Meta ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
Launch clashd on system startup with: Launch clashd on system startup with:
```shell ```shell
$ systemctl enable Clash.Meta $ systemctl enable Clash-Meta
``` ```
Launch clashd immediately with: Launch clashd immediately with:
```shell ```shell
$ systemctl start Clash.Meta $ systemctl start Clash-Meta
``` ```
### Display Process name ### Display Process name
@ -266,13 +280,6 @@ To display process name in GUI please use [Dashboard For Meta](https://github.co
## Development ## Development
Build the Clash.Meta locally
```shell
$ git clone https://github.com/MetaCubeX/Clash.Meta.git
$ cd Clash.Meta
$ make # build for all platform or 'make darwin-amd64' for specific platform, darwin-amd64
```
If you want to build an application that uses clash as a library, check out the If you want to build an application that uses clash as a library, check out the
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library) the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)

View File

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"time" "time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
@ -20,7 +21,7 @@ var UnifiedDelay = atomic.NewBool(false)
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
history *queue.Queue history *queue.Queue[C.DelayHistory]
alive *atomic.Bool alive *atomic.Bool
} }
@ -39,7 +40,11 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
p.alive.Store(err == nil) wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
return conn, err return conn, err
} }
@ -62,7 +67,7 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy() queue := p.history.Copy()
histories := []C.DelayHistory{} histories := []C.DelayHistory{}
for _, item := range queue { for _, item := range queue {
histories = append(histories, item.(C.DelayHistory)) histories = append(histories, item)
} }
return histories return histories
} }
@ -75,11 +80,7 @@ func (p *Proxy) LastDelay() (delay uint16) {
return max return max
} }
last := p.history.Last() history := p.history.Last()
if last == nil {
return max
}
history := last.(C.DelayHistory)
if history.Delay == 0 { if history.Delay == 0 {
return max return max
} }
@ -93,7 +94,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
return inner, err return inner, err
} }
mapping := map[string]interface{}{} 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()
@ -173,7 +174,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)} return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {

View File

@ -13,9 +13,13 @@ func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnCo
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { remoteAddr := conn.RemoteAddr()
metadata.SrcIP = ip // Filter when net.Addr interface is nil
metadata.SrcPort = port if remoteAddr != nil {
if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
} }
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)

View File

@ -14,6 +14,7 @@ type Direct struct {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithDirect())
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -24,6 +25,7 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -54,3 +56,13 @@ func NewCompatible() *Direct {
}, },
} }
} }
func NewPass() *Direct {
return &Direct{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
},
}
}

View File

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

View File

@ -29,14 +29,14 @@ type ShadowSocks struct {
type ShadowSocksOption struct { type ShadowSocksOption 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"`
Password string `proxy:"password"` Password string `proxy:"password"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@ -160,6 +160,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
cipher: ciph, cipher: ciph,

View File

@ -142,6 +142,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
cipher: coreCiph, cipher: coreCiph,
obfs: obfs, obfs: obfs,

View File

@ -23,13 +23,13 @@ type Snell struct {
type SnellOption struct { type SnellOption 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"`
Psk string `proxy:"psk"` Psk string `proxy:"psk"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Version int `proxy:"version,omitempty"` Version int `proxy:"version,omitempty"`
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"` ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
} }
type streamOption struct { type streamOption struct {
@ -142,6 +142,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
tp: C.Snell, tp: C.Snell,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@ -154,6 +154,7 @@ func NewSocks5(option Socks5Option) *Socks5 {
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
xtls "github.com/xtls/go"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -13,6 +12,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -34,7 +34,6 @@ type TrojanOption struct {
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Password string `proxy:"password"` Password string `proxy:"password"`
Flow string `proxy:"flow,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
@ -42,6 +41,8 @@ type TrojanOption struct {
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
} }
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
@ -84,19 +85,12 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
var tc trojan.Command c, err = t.instance.PresetXTLSConn(c)
if xtlsConn, ok := c.(*xtls.Conn); ok { if err != nil {
xtlsConn.RPRX = true return nil, err
if t.instance.GetFlow() == trojan.XRD {
xtlsConn.DirectMode = true
tc = trojan.CommandXRD
} else {
tc = trojan.CommandXRO
}
} else {
tc = trojan.CommandTCP
} }
err = t.instance.WriteHeader(c, tc, serializesSocksAddr(metadata))
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err return c, err
} }
@ -109,6 +103,12 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return nil, err return nil, err
} }
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
c.Close()
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close() c.Close()
return nil, err return nil, err
@ -170,13 +170,21 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{ tOption := &trojan.Option{
Password: option.Password, Password: option.Password,
Flow: option.Flow, ALPN: option.ALPN,
ALPN: option.ALPN, ServerName: option.Server,
ServerName: option.Server, SkipCertVerify: option.SkipCertVerify,
SkipCertVerify: option.SkipCertVerify, FlowShow: option.FlowShow,
ClientSessionCache: getClientSessionCache(), }
ClientXSessionCache: getClientXSessionCache(),
if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
tOption.Flow = option.Flow
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
} }
if option.SNI != "" { if option.SNI != "" {
@ -190,6 +198,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
instance: trojan.New(tOption), instance: trojan.New(tOption),
option: &option, option: &option,
@ -212,7 +221,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) if t.option.Flow != "" {
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else {
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName, ServiceName: option.GrpcOpts.GrpcServiceName,

View File

@ -20,6 +20,13 @@ var (
once sync.Once once sync.Once
) )
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func getClientSessionCache() tls.ClientSessionCache { func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() { once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128) globalClientSessionCache = tls.NewLRUClientSessionCache(128)
@ -34,13 +41,6 @@ func getClientXSessionCache() xtls.ClientSessionCache {
return globalClientXSessionCache return globalClientXSessionCache
} }
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
aType := uint8(metadata.AddrType) aType := uint8(metadata.AddrType)
@ -67,7 +67,7 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
return nil, err return nil, err
} }
ip, err := resolver.ResolveIP(host) ip, err := resolver.ResolveProxyServerHost(host)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@ -17,12 +18,13 @@ 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" "golang.org/x/net/http2"
) )
const ( const (
// max packet length // max packet length
maxLength = 8192 maxLength = 1024 << 3
) )
type Vless struct { type Vless struct {
@ -61,12 +63,6 @@ func (v *Vless) 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 := &vmess.WebsocketConfig{
@ -85,18 +81,17 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
wsOpts.Headers = header wsOpts.Headers = header
} }
if v.option.TLS { wsOpts.TLS = true
wsOpts.TLS = true wsOpts.TLSConfig = &tls.Config{
wsOpts.TLSConfig = &tls.Config{ 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
}
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
@ -134,6 +129,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
} }
default: default:
// default tcp network
// handle TLS And XTLS // handle TLS And XTLS
c, err = v.streamTLSOrXTLSConn(c, false) c, err = v.streamTLSOrXTLSConn(c, false)
} }
@ -219,7 +215,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr // vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
@ -275,7 +271,7 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
copy(addr[1:], []byte(metadata.Host)) copy(addr[1:], []byte(metadata.Host))
} }
port, _ := strconv.Atoi(metadata.DstPort) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{ return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
@ -286,91 +282,97 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
type vlessPacketConn struct { type vlessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr
remain int remain int
mux sync.Mutex mux sync.Mutex
cache []byte cache [2]byte
} }
func (c *vlessPacketConn) writePacket(b []byte, addr net.Addr) (int, error) { func (c *vlessPacketConn) writePacket(payload []byte) (int, error) {
length := len(b) binary.BigEndian.PutUint16(c.cache[:], uint16(len(payload)))
defer func() {
c.cache = c.cache[:0] if _, err := c.Conn.Write(c.cache[:]); err != nil {
}() return 0, err
c.cache = append(c.cache, byte(length>>8), byte(length))
c.cache = append(c.cache, b...)
n, err := c.Conn.Write(c.cache)
if n > 2 {
return n - 2, err
} }
return 0, err return c.Conn.Write(payload)
} }
func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
if len(b) <= maxLength { total := len(b)
return c.writePacket(b, addr) if total == 0 {
return 0, nil
}
if total <= maxLength {
return c.writePacket(b)
} }
offset := 0 offset := 0
total := len(b)
for offset < total { for offset < total {
cursor := offset + maxLength cursor := offset + maxLength
if cursor > total { if cursor > total {
cursor = total cursor = total
} }
n, err := c.writePacket(b[offset:cursor], addr) n, err := c.writePacket(b[offset:cursor])
if err != nil { if err != nil {
return offset + n, err return offset + n, err
} }
offset = cursor offset = cursor
if offset == total {
break
}
} }
return total, nil return total, nil
} }
func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
c.mux.Lock() c.mux.Lock()
defer c.mux.Unlock() defer c.mux.Unlock()
length := len(b)
if c.remain > 0 { if c.remain > 0 {
length := len(b)
if c.remain < length { if c.remain < length {
length = c.remain length = c.remain
} }
n, err := c.Conn.Read(b[:length]) n, err := c.Conn.Read(b[:length])
if err != nil { if err != nil {
return 0, nil, err return 0, c.rAddr, err
} }
c.remain -= n c.remain -= n
return n, c.rAddr, nil return n, c.rAddr, nil
} }
var packetLength uint16 if _, err := c.Conn.Read(b[:2]); err != nil {
if err := binary.Read(c.Conn, binary.BigEndian, &packetLength); err != nil { return 0, c.rAddr, err
return 0, nil, err
} }
remain := int(packetLength) total := int(binary.BigEndian.Uint16(b[:2]))
n, err := c.Conn.Read(b[:length]) if total == 0 {
remain -= n return 0, c.rAddr, nil
if remain > 0 {
c.remain = remain
} }
return n, c.rAddr, err
length := len(b)
if length > total {
length = total
}
if _, err := io.ReadFull(c.Conn, b[:length]); err != nil {
return 0, c.rAddr, errors.New("read packet error")
}
c.remain = total - length
return length, c.rAddr, nil
} }
func NewVless(option VlessOption) (*Vless, error) { func NewVless(option VlessOption) (*Vless, error) {
if !option.TLS && option.Network == "grpc" {
return nil, fmt.Errorf("TLS must be true with vless-grpc")
}
var addons *vless.Addons var addons *vless.Addons
if option.Network != "ws" && len(option.Flow) >= 16 { if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16] option.Flow = option.Flow[:16]
@ -380,7 +382,7 @@ func NewVless(option VlessOption) (*Vless, error) {
Flow: option.Flow, Flow: option.Flow,
} }
default: default:
return nil, fmt.Errorf("unsupported vless flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
} }

View File

@ -47,6 +47,10 @@ type VmessOption struct {
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"`
// TODO: compatible with VMESS WS older version configurations
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@ -76,6 +80,13 @@ 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 := &vmess.WebsocketConfig{
Host: host, Host: host,
@ -280,6 +291,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
}, },
client: client, client: client,
option: &option, option: &option,

View File

@ -2,7 +2,7 @@ package outboundgroup
import ( import (
"github.com/Dreamacro/clash/tunnel" "github.com/Dreamacro/clash/tunnel"
"regexp" "github.com/dlclark/regexp2"
"time" "time"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -23,12 +23,18 @@ func getProvidersProxies(providers []provider.ProxyProvider, touch bool, filter
} }
} }
var filterReg *regexp.Regexp = nil var filterReg *regexp2.Regexp = nil
matchedProxies := []C.Proxy{} var matchedProxies []C.Proxy
if len(filter) > 0 { if len(filter) > 0 {
filterReg = regexp.MustCompile(filter) //filterReg = regexp.MustCompile(filter)
filterReg = regexp2.MustCompile(filter, 0)
for _, p := range proxies { for _, p := range proxies {
if filterReg.MatchString(p.Name()) { 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) matchedProxies = append(matchedProxies, p)
} }
} }

View File

@ -97,12 +97,11 @@ 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 := make([]string, 0) all := []string{}
for _, proxy := range f.proxies(false) { for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": f.Type().String(), "type": f.Type().String(),
"now": f.Now(), "now": f.Now(),
"all": all, "all": all,
@ -116,7 +115,7 @@ func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (f *Fallback) proxies(touch bool) []C.Proxy { func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) { elm, _, _ := f.single.Do(func() (any, error) {
return getProvidersProxies(f.providers, touch, f.filter), nil return getProvidersProxies(f.providers, touch, f.filter), nil
}) })

View File

@ -30,7 +30,7 @@ type LoadBalance struct {
var errStrategy = errors.New("unsupported strategy") var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]interface{}) string { func parseStrategy(config map[string]any) string {
if elm, ok := config["strategy"]; ok { if elm, ok := config["strategy"]; ok {
if strategy, ok := elm.(string); ok { if strategy, ok := elm.(string); ok {
return strategy return strategy
@ -141,7 +141,7 @@ func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
} }
func (lb *LoadBalance) proxies(touch bool) []C.Proxy { func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) { elm, _, _ := lb.single.Do(func() (any, error) {
return getProvidersProxies(lb.providers, touch, lb.filter), nil return getProvidersProxies(lb.providers, touch, lb.filter), nil
}) })
@ -150,13 +150,11 @@ func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) { func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all := make([]string, 0) all := []string{}
for _, proxy := range lb.proxies(false) { for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": lb.Type().String(), "type": lb.Type().String(),
"all": all, "all": all,
}) })

View File

@ -32,7 +32,7 @@ type GroupCommonOption struct {
Filter string `group:"filter,omitempty"` Filter string `group:"filter,omitempty"`
} }
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{ groupOption := &GroupCommonOption{

View File

@ -69,20 +69,18 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
all := make([]string, 0) all := []string{}
for _, proxy := range r.rawProxies(false) { for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": r.Type().String(), "type": r.Type().String(),
"all": all, "all": all,
}) })
} }
func (r *Relay) rawProxies(touch bool) []C.Proxy { func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (interface{}, error) { elm, _, _ := r.single.Do(func() (any, error) {
return getProvidersProxies(r.providers, touch, r.filter), nil return getProvidersProxies(r.providers, touch, r.filter), nil
}) })

View File

@ -50,13 +50,12 @@ 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 := make([]string, 0) all := []string{}
for _, proxy := range getProvidersProxies(s.providers, false, s.filter) { for _, proxy := range getProvidersProxies(s.providers, false, s.filter) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]any{
"type": s.Type().String(), "type": s.Type().String(),
"now": s.Now(), "now": s.Now(),
"all": all, "all": all,
@ -80,12 +79,12 @@ func (s *Selector) Set(name string) error {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy { func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
return s.selectedProxy(true) return s.selectedProxy(true)
} }
func (s *Selector) selectedProxy(touch bool) C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) { elm, _, _ := s.single.Do(func() (any, error) {
proxies := getProvidersProxies(s.providers, touch, s.filter) 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 {

View File

@ -49,7 +49,6 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
} else { } else {
u.onDialFailed() u.onDialFailed()
} }
return c, err return c, err
} }
@ -63,17 +62,16 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} else { } else {
u.onDialFailed() u.onDialFailed()
} }
return pc, err return pc, err
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy { func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(true) return u.fast(true)
} }
func (u *URLTest) proxies(touch bool) []C.Proxy { func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) { elm, _, _ := u.single.Do(func() (any, error) {
return getProvidersProxies(u.providers, touch, u.filter), nil return getProvidersProxies(u.providers, touch, u.filter), nil
}) })
@ -81,7 +79,7 @@ func (u *URLTest) proxies(touch bool) []C.Proxy {
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) { elm, _, _ := u.fastSingle.Do(func() (any, error) {
proxies := u.proxies(touch) proxies := u.proxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
@ -125,13 +123,11 @@ 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 := make([]string, 0) all := []string{}
for _, proxy := range u.proxies(false) { for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"type": u.Type().String(), "type": u.Type().String(),
"now": u.Now(), "now": u.Now(),
"all": all, "all": all,
@ -164,7 +160,7 @@ func (u *URLTest) onDialFailed() {
} }
} }
func parseURLTestOption(config map[string]interface{}) []urlTestOption { func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{} opts := []urlTestOption{}
// tolerance // tolerance

View File

@ -8,7 +8,7 @@ import (
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) { func ParseProxy(mapping map[string]any) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {

View File

@ -16,7 +16,7 @@ var (
dirMode os.FileMode = 0o755 dirMode os.FileMode = 0o755
) )
type parser = func([]byte) (interface{}, error) type parser = func([]byte) (any, error)
type fetcher struct { type fetcher struct {
name string name string
@ -26,7 +26,7 @@ type fetcher struct {
done chan struct{} done chan struct{}
hash [16]byte hash [16]byte
parser parser parser parser
onUpdate func(interface{}) onUpdate func(any)
} }
func (f *fetcher) Name() string { func (f *fetcher) Name() string {
@ -37,7 +37,7 @@ func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type() return f.vehicle.Type()
} }
func (f *fetcher) Initial() (interface{}, error) { func (f *fetcher) Initial() (any, error) {
var ( var (
buf []byte buf []byte
err error err error
@ -92,7 +92,7 @@ func (f *fetcher) Initial() (interface{}, error) {
return proxies, nil return proxies, nil
} }
func (f *fetcher) Update() (interface{}, bool, error) { func (f *fetcher) Update() (any, bool, error) {
buf, err := f.vehicle.Read() buf, err := f.vehicle.Read()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@ -168,7 +168,7 @@ 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(interface{})) *fetcher { func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
var ticker *time.Ticker var ticker *time.Ticker
if interval != 0 { if interval != 0 {
ticker = time.NewTicker(interval) ticker = time.NewTicker(interval)

View File

@ -31,7 +31,13 @@ type HealthCheck 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 hc.check() go func() {
t := time.NewTicker(30 * time.Second)
<-t.C
t.Stop()
hc.check()
}()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
@ -62,7 +68,7 @@ func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10)) b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range hc.proxies { for _, proxy := range hc.proxies {
p := proxy p := proxy
b.Go(p.Name(), func() (interface{}, error) { b.Go(p.Name(), func() (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel() defer cancel()
p.URLTest(ctx, hc.url) p.URLTest(ctx, hc.url)

View File

@ -28,7 +28,7 @@ type proxyProviderSchema struct {
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
} }
func ParseProxyProvider(name string, mapping map[string]interface{}) (types.ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{ schema := &proxyProviderSchema{

View File

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp" "github.com/dlclark/regexp2"
"runtime" "runtime"
"time" "time"
@ -20,7 +20,7 @@ const (
) )
type ProxySchema struct { type ProxySchema struct {
Proxies []map[string]interface{} `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
// for auto gc // for auto gc
@ -35,12 +35,13 @@ type proxySetProvider struct {
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]any{
"name": pp.Name(), "name": pp.Name(),
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(), "proxies": pp.Proxies(),
"updatedAt": pp.updatedAt, //TODO maybe error because year value overflow
"updatedAt": pp.updatedAt,
}) })
} }
@ -101,7 +102,8 @@ func stopProxyProvider(pd *ProxySetProvider) {
} }
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 := regexp.Compile(filter)
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)
} }
@ -111,12 +113,12 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
healthCheck: hc, healthCheck: hc,
} }
onUpdate := func(elm interface{}) { onUpdate := func(elm any) {
ret := elm.([]C.Proxy) ret := elm.([]C.Proxy)
pd.setProxies(ret) pd.setProxies(ret)
} }
proxiesParseAndFilter := func(buf []byte) (interface{}, error) { proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil { if err := yaml.Unmarshal(buf, schema); err != nil {
@ -129,7 +131,9 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, veh
proxies := []C.Proxy{} proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies { for idx, mapping := range schema.Proxies {
if name, ok := mapping["name"]; ok && len(filter) > 0 && !filterReg.MatchString(name.(string)) { name, ok := mapping["name"]
mat, _ := filterReg.FindStringMatch(name.(string))
if ok && len(filter) > 0 && mat == nil {
continue continue
} }
proxy, err := adapter.ParseProxy(mapping) proxy, err := adapter.ParseProxy(mapping)
@ -169,7 +173,7 @@ type compatibleProvider struct {
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]any{
"name": cp.Name(), "name": cp.Name(),
"type": cp.Type().String(), "type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(), "vehicleType": cp.VehicleType().String(),

View File

@ -2,6 +2,7 @@ package provider
import ( import (
"context" "context"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/listener/inner" "github.com/Dreamacro/clash/listener/inner"
"io" "io"
"net" "net"
@ -57,7 +58,7 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
} }
req, err := http.NewRequest(http.MethodGet, uri.String(), nil) req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
req.Header.Set("user-agent", netHttp.UA) req.Header.Set("User-Agent", netHttp.UA)
if err != nil { if err != nil {
return nil, err return nil, err
@ -85,7 +86,13 @@ func (h *HTTPVehicle) Read() ([]byte, error) {
client := http.Client{Transport: transport} client := http.Client{Transport: transport}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err 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()

View File

@ -8,7 +8,7 @@ import (
type Option = func(b *Batch) type Option = func(b *Batch)
type Result struct { type Result struct {
Value interface{} Value any
Err error Err error
} }
@ -38,7 +38,7 @@ type Batch struct {
cancel func() cancel func()
} }
func (b *Batch) Go(key string, fn func() (interface{}, error)) { func (b *Batch) Go(key string, fn func() (any, error)) {
b.wg.Add(1) b.wg.Add(1)
go func() { go func() {
defer b.wg.Done() defer b.wg.Done()

View File

@ -14,11 +14,11 @@ func TestBatch(t *testing.T) {
b, _ := New(context.Background()) b, _ := New(context.Background())
now := time.Now() now := time.Now()
b.Go("foo", func() (interface{}, error) { b.Go("foo", func() (any, error) {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
return "foo", nil return "foo", nil
}) })
b.Go("bar", func() (interface{}, error) { b.Go("bar", func() (any, error) {
time.Sleep(time.Millisecond * 150) time.Sleep(time.Millisecond * 150)
return "bar", nil return "bar", nil
}) })
@ -45,7 +45,7 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
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() (interface{}, error) { b.Go(strconv.Itoa(idx), func() (any, error) {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil return strconv.Itoa(idx), nil
}) })
@ -64,12 +64,12 @@ func TestBatchWithConcurrencyNum(t *testing.T) {
func TestBatchContext(t *testing.T) { func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background()) b, ctx := New(context.Background())
b.Go("error", func() (interface{}, error) { b.Go("error", func() (any, error) {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error") return nil, errors.New("test error")
}) })
b.Go("ctx", func() (interface{}, error) { b.Go("ctx", func() (any, error) {
<-ctx.Done() <-ctx.Done()
return nil, ctx.Err() return nil, ctx.Err()
}) })

50
common/cache/cache.go vendored
View File

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

View File

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

View File

@ -9,43 +9,43 @@ import (
) )
// Option is part of Functional Options Pattern // Option is part of Functional Options Pattern
type Option func(*LruCache) type Option[K comparable, V any] func(*LruCache[K, V])
// EvictCallback is used to get a callback when a cache entry is evicted // EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key interface{}, value interface{}) type EvictCallback = func(key any, value any)
// WithEvict set the evict callback // WithEvict set the evict callback
func WithEvict(cb EvictCallback) Option { func WithEvict[K comparable, V any](cb EvictCallback) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.onEvict = cb l.onEvict = cb
} }
} }
// WithUpdateAgeOnGet update expires when Get element // WithUpdateAgeOnGet update expires when Get element
func WithUpdateAgeOnGet() Option { func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.updateAgeOnGet = true l.updateAgeOnGet = true
} }
} }
// WithAge defined element max age (second) // WithAge defined element max age (second)
func WithAge(maxAge int64) Option { func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.maxAge = maxAge l.maxAge = maxAge
} }
} }
// WithSize defined max length of LruCache // WithSize defined max length of LruCache
func WithSize(maxSize int) Option { func WithSize[K comparable, V any](maxSize int) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.maxSize = maxSize l.maxSize = maxSize
} }
} }
// WithStale decide whether Stale return is enabled. // WithStale decide whether Stale return is enabled.
// If this feature is enabled, element will not get Evicted according to `WithAge`. // If this feature is enabled, element will not get Evicted according to `WithAge`.
func WithStale(stale bool) Option { func WithStale[K comparable, V any](stale bool) Option[K, V] {
return func(l *LruCache) { return func(l *LruCache[K, V]) {
l.staleReturn = stale l.staleReturn = stale
} }
} }
@ -53,11 +53,11 @@ func WithStale(stale bool) Option {
// LruCache is a thread-safe, in-memory lru-cache that evicts the // LruCache is a thread-safe, in-memory lru-cache that evicts the
// least recently used entries from memory when (if set) the entries are // least recently used entries from memory when (if set) the entries are
// older than maxAge (in seconds). Use the New constructor to create one. // older than maxAge (in seconds). Use the New constructor to create one.
type LruCache struct { type LruCache[K comparable, V any] struct {
maxAge int64 maxAge int64
maxSize int maxSize int
mu sync.Mutex mu sync.Mutex
cache map[interface{}]*list.Element cache map[any]*list.Element
lru *list.List // Front is least-recent lru *list.List // Front is least-recent
updateAgeOnGet bool updateAgeOnGet bool
staleReturn bool staleReturn bool
@ -65,10 +65,10 @@ type LruCache struct {
} }
// NewLRUCache creates an LruCache // NewLRUCache creates an LruCache
func NewLRUCache(options ...Option) *LruCache { func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache{ lc := &LruCache[K, V]{
lru: list.New(), lru: list.New(),
cache: make(map[interface{}]*list.Element), cache: make(map[any]*list.Element),
} }
for _, option := range options { for _, option := range options {
@ -78,33 +78,33 @@ func NewLRUCache(options ...Option) *LruCache {
return lc return lc
} }
// Get returns the interface{} representation of a cached response and a bool // Get returns the any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache) Get(key interface{}) (interface{}, bool) { func (c *LruCache[K, V]) Get(key K) (V, bool) {
entry := c.get(key) el := c.get(key)
if entry == nil { if el == nil {
return nil, false return getZero[V](), false
} }
value := entry.value value := el.value
return value, true return value, true
} }
// GetWithExpire returns the interface{} representation of a cached response, // GetWithExpire returns the any representation of a cached response,
// a time.Time Give expected expires, // a time.Time Give expected expires,
// and a bool set to true if the key was found. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache) GetWithExpire(key interface{}) (interface{}, time.Time, bool) { func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
entry := c.get(key) el := c.get(key)
if entry == nil { if el == nil {
return nil, time.Time{}, false return getZero[V](), time.Time{}, false
} }
return entry.value, time.Unix(entry.expires, 0), true return el.value, time.Unix(el.expires, 0), true
} }
// Exist returns if key exist in cache but not put item to the head of linked list // Exist returns if key exist in cache but not put item to the head of linked list
func (c *LruCache) Exist(key interface{}) bool { func (c *LruCache[K, V]) Exist(key K) bool {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -112,8 +112,8 @@ func (c *LruCache) Exist(key interface{}) bool {
return ok return ok
} }
// Set stores the interface{} representation of a response for a given key. // Set stores the any representation of a response for a given key.
func (c *LruCache) Set(key interface{}, value interface{}) { func (c *LruCache[K, V]) Set(key K, value V) {
expires := int64(0) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge expires = time.Now().Unix() + c.maxAge
@ -121,23 +121,23 @@ func (c *LruCache) Set(key interface{}, value interface{}) {
c.SetWithExpire(key, value, time.Unix(expires, 0)) c.SetWithExpire(key, value, time.Unix(expires, 0))
} }
// SetWithExpire stores the interface{} representation of a response for a given key and given expires. // SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second. // The expires time will round to second.
func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires time.Time) { func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value.(*entry) e := le.Value.(*entry[K, V])
e.value = value e.value = value
e.expires = expires.Unix() e.expires = expires.Unix()
} else { } else {
e := &entry{key: key, value: value, expires: expires.Unix()} e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
c.cache[key] = c.lru.PushBack(e) c.cache[key] = c.lru.PushBack(e)
if c.maxSize > 0 { if c.maxSize > 0 {
if len := c.lru.Len(); len > c.maxSize { if elLen := c.lru.Len(); elLen > c.maxSize {
c.deleteElement(c.lru.Front()) c.deleteElement(c.lru.Front())
} }
} }
@ -147,7 +147,7 @@ func (c *LruCache) SetWithExpire(key interface{}, value interface{}, expires tim
} }
// CloneTo clone and overwrite elements to another LruCache // CloneTo clone and overwrite elements to another LruCache
func (c *LruCache) CloneTo(n *LruCache) { func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -155,15 +155,15 @@ func (c *LruCache) CloneTo(n *LruCache) {
defer n.mu.Unlock() defer n.mu.Unlock()
n.lru = list.New() n.lru = list.New()
n.cache = make(map[interface{}]*list.Element) n.cache = make(map[any]*list.Element)
for e := c.lru.Front(); e != nil; e = e.Next() { for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry) elm := e.Value.(*entry[K, V])
n.cache[elm.key] = n.lru.PushBack(elm) n.cache[elm.key] = n.lru.PushBack(elm)
} }
} }
func (c *LruCache) get(key interface{}) *entry { func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -172,7 +172,7 @@ func (c *LruCache) get(key interface{}) *entry {
return nil return nil
} }
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry).expires <= time.Now().Unix() { if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry[K, V]).expires <= time.Now().Unix() {
c.deleteElement(le) c.deleteElement(le)
c.maybeDeleteOldest() c.maybeDeleteOldest()
@ -180,15 +180,15 @@ func (c *LruCache) get(key interface{}) *entry {
} }
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
entry := le.Value.(*entry) el := le.Value.(*entry[K, V])
if c.maxAge > 0 && c.updateAgeOnGet { if c.maxAge > 0 && c.updateAgeOnGet {
entry.expires = time.Now().Unix() + c.maxAge el.expires = time.Now().Unix() + c.maxAge
} }
return entry return el
} }
// Delete removes the value associated with a key. // Delete removes the value associated with a key.
func (c *LruCache) Delete(key interface{}) { func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock() c.mu.Lock()
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
@ -198,26 +198,40 @@ func (c *LruCache) Delete(key interface{}) {
c.mu.Unlock() c.mu.Unlock()
} }
func (c *LruCache) maybeDeleteOldest() { func (c *LruCache[K, V]) maybeDeleteOldest() {
if !c.staleReturn && c.maxAge > 0 { if !c.staleReturn && c.maxAge > 0 {
now := time.Now().Unix() now := time.Now().Unix()
for le := c.lru.Front(); le != nil && le.Value.(*entry).expires <= now; le = c.lru.Front() { for le := c.lru.Front(); le != nil && le.Value.(*entry[K, V]).expires <= now; le = c.lru.Front() {
c.deleteElement(le) c.deleteElement(le)
} }
} }
} }
func (c *LruCache) deleteElement(le *list.Element) { func (c *LruCache[K, V]) deleteElement(le *list.Element) {
c.lru.Remove(le) c.lru.Remove(le)
e := le.Value.(*entry) e := le.Value.(*entry[K, V])
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)
} }
} }
type entry struct { func (c *LruCache[K, V]) Clear() error {
key interface{} c.mu.Lock()
value interface{}
c.cache = make(map[any]*list.Element)
c.mu.Unlock()
return nil
}
type entry[K comparable, V any] struct {
key K
value V
expires int64 expires int64
} }
func getZero[T any]() T {
var result T
return result
}

View File

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

36
common/cmd/cmd.go Normal file
View File

@ -0,0 +1,36 @@
package cmd
import (
"fmt"
"os/exec"
"strings"
)
func ExecCmd(cmdStr string) (string, error) {
args := splitArgs(cmdStr)
var cmd *exec.Cmd
if len(args) == 1 {
cmd = exec.Command(args[0])
} else {
cmd = exec.Command(args[0], args[1:]...)
}
prepareBackgroundCommand(cmd)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%v, %s", err, string(out))
}
return string(out), nil
}
func splitArgs(cmd string) []string {
args := strings.Split(cmd, " ")
// use in pipeline
if len(args) > 2 && strings.ContainsAny(cmd, "|") {
suffix := strings.Join(args[2:], " ")
args = append(args[:2], suffix)
}
return args
}

11
common/cmd/cmd_other.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !windows
package cmd
import (
"os/exec"
)
func prepareBackgroundCommand(cmd *exec.Cmd) {
}

40
common/cmd/cmd_test.go Normal file
View File

@ -0,0 +1,40 @@
package cmd
import (
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitArgs(t *testing.T) {
args := splitArgs("ls")
args1 := splitArgs("ls -la")
args2 := splitArgs("bash -c ls")
args3 := splitArgs("bash -c ls -lahF | grep 'cmd'")
assert.Equal(t, 1, len(args))
assert.Equal(t, 2, len(args1))
assert.Equal(t, 3, len(args2))
assert.Equal(t, 3, len(args3))
}
func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" {
_, err := ExecCmd("dir")
assert.Nil(t, err)
return
}
_, err := ExecCmd("ls")
_, err1 := ExecCmd("ls -la")
_, err2 := ExecCmd("bash -c ls")
_, err3 := ExecCmd("bash -c ls -la")
_, err4 := ExecCmd("bash -c ls -la | grep 'cmd'")
assert.Nil(t, err)
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.Nil(t, err3)
assert.Nil(t, err4)
}

12
common/cmd/cmd_windows.go Normal file
View File

@ -0,0 +1,12 @@
//go:build windows
package cmd
import (
"os/exec"
"syscall"
)
func prepareBackgroundCommand(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}

View File

@ -1,3 +1,3 @@
package observable package observable
type Iterable <-chan interface{} type Iterable <-chan any

View File

@ -9,8 +9,8 @@ import (
"go.uber.org/atomic" "go.uber.org/atomic"
) )
func iterator(item []interface{}) chan interface{} { func iterator(item []any) chan any {
ch := make(chan interface{}) ch := make(chan any)
go func() { go func() {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
for _, elm := range item { for _, elm := range item {
@ -22,7 +22,7 @@ func iterator(item []interface{}) chan interface{} {
} }
func TestObservable(t *testing.T) { func TestObservable(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
data, err := src.Subscribe() data, err := src.Subscribe()
assert.Nil(t, err) assert.Nil(t, err)
@ -34,7 +34,7 @@ func TestObservable(t *testing.T) {
} }
func TestObservable_MultiSubscribe(t *testing.T) { func TestObservable_MultiSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
ch1, _ := src.Subscribe() ch1, _ := src.Subscribe()
ch2, _ := src.Subscribe() ch2, _ := src.Subscribe()
@ -42,7 +42,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
waitCh := func(ch <-chan interface{}) { waitCh := func(ch <-chan any) {
for range ch { for range ch {
count.Inc() count.Inc()
} }
@ -55,7 +55,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
} }
func TestObservable_UnSubscribe(t *testing.T) { func TestObservable_UnSubscribe(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
data, err := src.Subscribe() data, err := src.Subscribe()
assert.Nil(t, err) assert.Nil(t, err)
@ -65,7 +65,7 @@ func TestObservable_UnSubscribe(t *testing.T) {
} }
func TestObservable_SubscribeClosedSource(t *testing.T) { func TestObservable_SubscribeClosedSource(t *testing.T) {
iter := iterator([]interface{}{1}) iter := iterator([]any{1})
src := NewObservable(iter) src := NewObservable(iter)
data, _ := src.Subscribe() data, _ := src.Subscribe()
<-data <-data
@ -75,14 +75,14 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
} }
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {
sub := Subscription(make(chan interface{})) sub := Subscription(make(chan any))
iter := iterator([]interface{}{1}) iter := iterator([]any{1})
src := NewObservable(iter) src := NewObservable(iter)
src.UnSubscribe(sub) src.UnSubscribe(sub)
} }
func TestObservable_SubscribeGoroutineLeak(t *testing.T) { func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
iter := iterator([]interface{}{1, 2, 3, 4, 5}) iter := iterator([]any{1, 2, 3, 4, 5})
src := NewObservable(iter) src := NewObservable(iter)
max := 100 max := 100
@ -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 interface{}) { waitCh := func(ch <-chan any) {
for range ch { for range ch {
} }
wg.Done() wg.Done()
@ -115,7 +115,7 @@ func TestObservable_SubscribeGoroutineLeak(t *testing.T) {
} }
func Benchmark_Observable_1000(b *testing.B) { func Benchmark_Observable_1000(b *testing.B) {
ch := make(chan interface{}) ch := make(chan any)
o := NewObservable(ch) o := NewObservable(ch)
num := 1000 num := 1000

View File

@ -4,14 +4,14 @@ import (
"sync" "sync"
) )
type Subscription <-chan interface{} type Subscription <-chan any
type Subscriber struct { type Subscriber struct {
buffer chan interface{} buffer chan any
once sync.Once once sync.Once
} }
func (s *Subscriber) Emit(item interface{}) { func (s *Subscriber) Emit(item any) {
s.buffer <- item s.buffer <- item
} }
@ -27,7 +27,7 @@ func (s *Subscriber) Close() {
func newSubscriber() *Subscriber { func newSubscriber() *Subscriber {
sub := &Subscriber{ sub := &Subscriber{
buffer: make(chan interface{}, 200), buffer: make(chan any, 200),
} }
return sub return sub
} }

View File

@ -17,7 +17,7 @@ type Picker struct {
once sync.Once once sync.Once
errOnce sync.Once errOnce sync.Once
result interface{} result any
err error err error
} }
@ -43,7 +43,7 @@ func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.C
// 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() interface{} { func (p *Picker) Wait() any {
p.wg.Wait() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
@ -58,7 +58,7 @@ func (p *Picker) Error() error {
// 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() (interface{}, error)) { func (p *Picker) Go(f func() (any, error)) {
p.wg.Add(1) p.wg.Add(1)
go func() { go func() {

View File

@ -8,8 +8,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func sleepAndSend(ctx context.Context, delay int, input interface{}) func() (interface{}, error) { func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) {
return func() (interface{}, error) { return func() (any, 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:

View File

@ -23,7 +23,7 @@ func NewAllocator() *Allocator {
alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K alloc.buffers = make([]sync.Pool, 17) // 1B -> 64K
for k := range alloc.buffers { for k := range alloc.buffers {
i := k i := k
alloc.buffers[k].New = func() interface{} { alloc.buffers[k].New = func() any {
return make([]byte, 1<<uint32(i)) return make([]byte, 1<<uint32(i))
} }
} }

View File

@ -5,7 +5,7 @@ import (
"sync" "sync"
) )
var bufferPool = sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} var bufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }}
func GetBuffer() *bytes.Buffer { func GetBuffer() *bytes.Buffer {
return bufferPool.Get().(*bytes.Buffer) return bufferPool.Get().(*bytes.Buffer)

View File

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

View File

@ -7,7 +7,7 @@ import (
type call struct { type call struct {
wg sync.WaitGroup wg sync.WaitGroup
val interface{} val any
err error err error
} }
@ -20,13 +20,13 @@ type Single struct {
} }
type Result struct { type Result struct {
Val interface{} Val any
Err error Err error
} }
// Do single.Do likes sync.singleFlight // Do single.Do likes sync.singleFlight
//lint:ignore ST1008 it likes sync.singleFlight //lint:ignore ST1008 it likes sync.singleFlight
func (s *Single) Do(fn func() (interface{}, error)) (v interface{}, 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)) {

View File

@ -13,7 +13,7 @@ func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
shardCount := atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
return nil, nil return nil, nil
@ -40,7 +40,7 @@ func TestBasic(t *testing.T) {
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
return nil, nil return nil, nil
} }
@ -56,7 +56,7 @@ func TestTimer(t *testing.T) {
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle(time.Millisecond * 30)
foo := 0 foo := 0
call := func() (interface{}, error) { call := func() (any, error) {
foo++ foo++
return nil, nil return nil, nil
} }

View File

@ -1,5 +1,4 @@
//go:build !linux //go:build !linux
// +build !linux
package sockopt package sockopt

View File

@ -28,8 +28,8 @@ func NewDecoder(option Option) *Decoder {
return &Decoder{option: &option} return &Decoder{option: &option}
} }
// Decode transform a map[string]interface{} to a struct // Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) 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")
} }
@ -45,12 +45,8 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
} }
tag := field.Tag.Get(d.option.TagName) tag := field.Tag.Get(d.option.TagName)
str := strings.SplitN(tag, ",", 2) key, omitKey, found := strings.Cut(tag, ",")
key := str[0] omitempty := found && omitKey == "omitempty"
omitempty := false
if len(str) > 1 {
omitempty = str[1] == "omitempty"
}
value, ok := src[key] value, ok := src[key]
if !ok || value == nil { if !ok || value == nil {
@ -68,7 +64,7 @@ func (d *Decoder) Decode(src map[string]interface{}, dst interface{}) error {
return nil return nil
} }
func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decode(name string, data any, val reflect.Value) error {
switch val.Kind() { switch val.Kind() {
case reflect.Int: case reflect.Int:
return d.decodeInt(name, data, val) return d.decodeInt(name, data, val)
@ -89,12 +85,14 @@ func (d *Decoder) decode(name string, data interface{}, val reflect.Value) error
} }
} }
func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.Int: case kind == reflect.Int:
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits()) i, err = strconv.ParseInt(dataVal.String(), 0, val.Type().Bits())
@ -112,7 +110,7 @@ func (d *Decoder) decodeInt(name string, data interface{}, val reflect.Value) (e
return err return err
} }
func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -129,7 +127,7 @@ func (d *Decoder) decodeString(name string, data interface{}, val reflect.Value)
return err return err
} }
func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
@ -146,7 +144,7 @@ func (d *Decoder) decodeBool(name string, data interface{}, val reflect.Value) (
return err return err
} }
func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
valType := val.Type() valType := val.Type()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -173,7 +171,7 @@ func (d *Decoder) decodeSlice(name string, data interface{}, val reflect.Value)
return nil return nil
} }
func (d *Decoder) decodeMap(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeMap(name string, data any, val reflect.Value) error {
valType := val.Type() valType := val.Type()
valKeyType := valType.Key() valKeyType := valType.Key()
valElemType := valType.Elem() valElemType := valType.Elem()
@ -245,7 +243,7 @@ func (d *Decoder) decodeMapFromMap(name string, dataVal reflect.Value, val refle
return nil return nil
} }
func (d *Decoder) decodeStruct(name string, data interface{}, val reflect.Value) error { func (d *Decoder) decodeStruct(name string, data any, val reflect.Value) error {
dataVal := reflect.Indirect(reflect.ValueOf(data)) dataVal := reflect.Indirect(reflect.ValueOf(data))
// If the type of the value to write to and the data match directly, // If the type of the value to write to and the data match directly,
@ -273,7 +271,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
} }
dataValKeys := make(map[reflect.Value]struct{}) dataValKeys := make(map[reflect.Value]struct{})
dataValKeysUnused := make(map[interface{}]struct{}) dataValKeysUnused := make(map[any]struct{})
for _, dataValKey := range dataVal.MapKeys() { for _, dataValKey := range dataVal.MapKeys() {
dataValKeys[dataValKey] = struct{}{} dataValKeys[dataValKey] = struct{}{}
dataValKeysUnused[dataValKey.Interface()] = struct{}{} dataValKeysUnused[dataValKey.Interface()] = struct{}{}
@ -398,7 +396,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
return nil return nil
} }
func (d *Decoder) setInterface(name string, data interface{}, val reflect.Value) (err error) { func (d *Decoder) setInterface(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
val.Set(dataVal) val.Set(dataVal)
return nil return nil

View File

@ -27,7 +27,7 @@ type BazOptional struct {
} }
func TestStructure_Basic(t *testing.T) { func TestStructure_Basic(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": "test", "bar": "test",
"extra": false, "extra": false,
@ -45,7 +45,7 @@ func TestStructure_Basic(t *testing.T) {
} }
func TestStructure_Slice(t *testing.T) { func TestStructure_Slice(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": []string{"one", "two"}, "bar": []string{"one", "two"},
} }
@ -62,7 +62,7 @@ func TestStructure_Slice(t *testing.T) {
} }
func TestStructure_Optional(t *testing.T) { func TestStructure_Optional(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
} }
@ -77,7 +77,7 @@ func TestStructure_Optional(t *testing.T) {
} }
func TestStructure_MissingKey(t *testing.T) { func TestStructure_MissingKey(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
} }
@ -87,14 +87,14 @@ func TestStructure_MissingKey(t *testing.T) {
} }
func TestStructure_ParamError(t *testing.T) { func TestStructure_ParamError(t *testing.T) {
rawMap := map[string]interface{}{} rawMap := map[string]any{}
s := Baz{} s := Baz{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
assert.NotNilf(t, err, "should throw error: %#v", s) assert.NotNilf(t, err, "should throw error: %#v", s)
} }
func TestStructure_SliceTypeError(t *testing.T) { func TestStructure_SliceTypeError(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
"bar": []int{1, 2}, "bar": []int{1, 2},
} }
@ -105,7 +105,7 @@ func TestStructure_SliceTypeError(t *testing.T) {
} }
func TestStructure_WeakType(t *testing.T) { func TestStructure_WeakType(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": "1", "foo": "1",
"bar": []int{1}, "bar": []int{1},
} }
@ -122,7 +122,7 @@ func TestStructure_WeakType(t *testing.T) {
} }
func TestStructure_Nest(t *testing.T) { func TestStructure_Nest(t *testing.T) {
rawMap := map[string]interface{}{ rawMap := map[string]any{
"foo": 1, "foo": 1,
} }

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

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

View File

@ -36,7 +36,7 @@ func NewAuthenticator(users []AuthUser) Authenticator {
au.storage.Store(user.User, user.Pass) au.storage.Store(user.User, user.Pass)
} }
usernames := make([]string, 0, len(users)) usernames := make([]string, 0, len(users))
au.storage.Range(func(key, value interface{}) bool { au.storage.Range(func(key, value any) bool {
usernames = append(usernames, key.(string)) usernames = append(usernames, key.(string))
return true return true
}) })

View File

@ -4,9 +4,9 @@ import (
"net" "net"
"syscall" "syscall"
"golang.org/x/sys/unix"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
"golang.org/x/sys/unix"
) )
type controlFn = func(network, address string, c syscall.RawConn) error type controlFn = func(network, address string, c syscall.RawConn) error

View File

@ -1,5 +1,4 @@
//go:build !linux && !darwin //go:build !linux && !darwin
// +build !linux,!darwin
package dialer package dialer

View File

@ -32,14 +32,14 @@ func DialContext(ctx context.Context, network, address string, options ...Option
var ip net.IP var ip net.IP
switch network { switch network {
case "tcp4", "udp4": case "tcp4", "udp4":
if opt.interfaceName != "" { if !opt.direct {
ip, err = resolver.ResolveIPv4WithMain(host) ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else { } else {
ip, err = resolver.ResolveIPv4(host) ip, err = resolver.ResolveIPv4(host)
} }
default: default:
if opt.interfaceName != "" { if !opt.direct {
ip, err = resolver.ResolveIPv6WithMain(host) ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else { } else {
ip, err = resolver.ResolveIPv6(host) ip, err = resolver.ResolveIPv6(host)
} }
@ -121,7 +121,7 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
results := make(chan dialResult) results := make(chan dialResult)
var primary, fallback dialResult var primary, fallback dialResult
startRacer := func(ctx context.Context, network, host string, ipv6 bool) { startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) {
result := dialResult{ipv6: ipv6, done: true} result := dialResult{ipv6: ipv6, done: true}
defer func() { defer func() {
select { select {
@ -135,14 +135,14 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
var ip net.IP var ip net.IP
if ipv6 { if ipv6 {
if opt.interfaceName != "" { if !direct {
ip, result.error = resolver.ResolveIPv6WithMain(host) ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)
} else { } else {
ip, result.error = resolver.ResolveIPv6(host) ip, result.error = resolver.ResolveIPv6(host)
} }
} else { } else {
if opt.interfaceName != "" { if !direct {
ip, result.error = resolver.ResolveIPv4WithMain(host) ip, result.error = resolver.ResolveIPv4ProxyServerHost(host)
} else { } else {
ip, result.error = resolver.ResolveIPv4(host) ip, result.error = resolver.ResolveIPv4(host)
} }
@ -155,8 +155,8 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
result.Conn, result.error = dialContext(ctx, network, ip, port, opt) result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
} }
go startRacer(ctx, network+"4", host, false) go startRacer(ctx, network+"4", host, opt.direct, false)
go startRacer(ctx, network+"6", host, true) go startRacer(ctx, network+"6", host, opt.direct, true)
for res := range results { for res := range results {
if res.error == nil { if res.error == nil {

View File

@ -1,5 +1,4 @@
//go:build linux //go:build linux
// +build linux
package dialer package dialer

View File

@ -1,5 +1,4 @@
//go:build !linux //go:build !linux
// +build !linux
package dialer package dialer

View File

@ -12,6 +12,7 @@ type option struct {
interfaceName string interfaceName string
addrReuse bool addrReuse bool
routingMark int routingMark int
direct bool
} }
type Option func(opt *option) type Option func(opt *option)
@ -33,3 +34,9 @@ func WithRoutingMark(mark int) Option {
opt.routingMark = mark opt.routingMark = mark
} }
} }
func WithDirect() Option {
return func(opt *option) {
opt.direct = true
}
}

View File

@ -0,0 +1,28 @@
package dialer
import (
"context"
"net"
)
func init() {
// We must use this DialContext to query DNS
// when using net default resolver.
net.DefaultResolver.PreferGo = true
net.DefaultResolver.Dial = resolverDialContext
}
func resolverDialContext(ctx context.Context, network, address string) (net.Conn, error) {
d := &net.Dialer{}
interfaceName := DefaultInterface.Load()
if interfaceName != "" {
dstIP := net.ParseIP(address)
if dstIP != nil {
bindIfaceToDialer(interfaceName, d, network, dstIP)
}
}
return d.DialContext(ctx, network, address)
}

View File

@ -1,5 +1,4 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows
package dialer package dialer

View File

@ -1,5 +1,4 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package dialer package dialer

View File

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

View File

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

View File

@ -1,39 +1,47 @@
package fakeip package fakeip
import ( import (
"encoding/binary"
"errors" "errors"
"net" "math/bits"
"net/netip"
"sync" "sync"
"github.com/Dreamacro/clash/common/cache"
"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 {
hi uint64
lo uint64
}
type store interface { type store interface {
GetByHost(host string) (net.IP, bool) GetByHost(host string) (netip.Addr, bool)
PutByHost(host string, ip net.IP) PutByHost(host string, ip netip.Addr)
GetByIP(ip net.IP) (string, bool) GetByIP(ip netip.Addr) (string, bool)
PutByIP(ip net.IP, host string) PutByIP(ip netip.Addr, host string)
DelByIP(ip net.IP) DelByIP(ip netip.Addr)
Exist(ip net.IP) bool Exist(ip netip.Addr) bool
CloneTo(store) CloneTo(store)
FlushFakeIP() error
} }
// Pool is a implementation about fake ip generator without storage // Pool is a implementation about fake ip generator without storage
type Pool struct { type Pool struct {
max uint32 gateway netip.Addr
min uint32 first netip.Addr
gateway uint32 last netip.Addr
offset uint32 offset netip.Addr
cycle bool
mux sync.Mutex mux sync.Mutex
host *trie.DomainTrie host *trie.DomainTrie[bool]
ipnet *net.IPNet ipnet *netip.Prefix
store store store store
} }
// Lookup return a fake ip with host // Lookup return a fake ip with host
func (p *Pool) Lookup(host string) net.IP { func (p *Pool) Lookup(host string) netip.Addr {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip, exist := p.store.GetByHost(host); exist { if ip, exist := p.store.GetByHost(host); exist {
@ -46,14 +54,10 @@ func (p *Pool) Lookup(host string) net.IP {
} }
// LookBack return host with the fake ip // LookBack return host with the fake ip
func (p *Pool) LookBack(ip net.IP) (string, bool) { func (p *Pool) LookBack(ip netip.Addr) (string, bool) {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return "", false
}
return p.store.GetByIP(ip) return p.store.GetByIP(ip)
} }
@ -66,24 +70,25 @@ func (p *Pool) ShouldSkipped(domain string) bool {
} }
// Exist returns if given ip exists in fake-ip pool // Exist returns if given ip exists in fake-ip pool
func (p *Pool) Exist(ip net.IP) bool { func (p *Pool) Exist(ip netip.Addr) bool {
p.mux.Lock() p.mux.Lock()
defer p.mux.Unlock() defer p.mux.Unlock()
if ip = ip.To4(); ip == nil {
return false
}
return p.store.Exist(ip) return p.store.Exist(ip)
} }
// Gateway return gateway ip // Gateway return gateway ip
func (p *Pool) Gateway() net.IP { func (p *Pool) Gateway() netip.Addr {
return uintToIP(p.gateway) return p.gateway
}
// Broadcast return the last ip
func (p *Pool) Broadcast() netip.Addr {
return p.last
} }
// IPNet return raw ipnet // IPNet return raw ipnet
func (p *Pool) IPNet() *net.IPNet { func (p *Pool) IPNet() *netip.Prefix {
return p.ipnet return p.ipnet
} }
@ -92,43 +97,36 @@ func (p *Pool) CloneFrom(o *Pool) {
o.store.CloneTo(p.store) o.store.CloneTo(p.store)
} }
func (p *Pool) get(host string) net.IP { func (p *Pool) get(host string) netip.Addr {
current := p.offset
for { for {
p.offset = (p.offset + 1) % (p.max - p.min) p.offset = p.offset.Next()
// Avoid infinite loops
if p.offset == current { if !p.offset.Less(p.last) {
p.offset = (p.offset + 1) % (p.max - p.min) p.cycle = true
ip := uintToIP(p.min + p.offset - 1) p.offset = p.first
p.store.DelByIP(ip) }
if p.cycle {
p.store.DelByIP(p.offset)
break break
} }
ip := uintToIP(p.min + p.offset - 1) if !p.store.Exist(p.offset) {
if !p.store.Exist(ip) {
break break
} }
} }
ip := uintToIP(p.min + p.offset - 1)
p.store.PutByIP(ip, host) p.store.PutByIP(p.offset, host)
return ip return p.offset
} }
func ipToUint(ip net.IP) uint32 { func (p *Pool) FlushFakeIP() error {
v := uint32(ip[0]) << 24 return p.store.FlushFakeIP()
v += uint32(ip[1]) << 16
v += uint32(ip[2]) << 8
v += uint32(ip[3])
return v
}
func uintToIP(v uint32) net.IP {
return net.IP{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
} }
type Options struct { type Options struct {
IPNet *net.IPNet IPNet *netip.Prefix
Host *trie.DomainTrie Host *trie.DomainTrie[bool]
// Size sets the maximum number of entries in memory // Size sets the maximum number of entries in memory
// and does not work if Persistence is true // and does not work if Persistence is true
@ -141,20 +139,23 @@ type Options struct {
// New return Pool instance // New return Pool instance
func New(options Options) (*Pool, error) { func New(options Options) (*Pool, error) {
min := ipToUint(options.IPNet.IP) + 2 var (
hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next()
first = gateway.Next().Next()
last = add(hostAddr, 1<<uint64(hostAddr.BitLen()-options.IPNet.Bits())-1)
)
ones, bits := options.IPNet.Mask.Size() if !options.IPNet.IsValid() || !first.Less(last) || !options.IPNet.Contains(last) {
total := 1<<uint(bits-ones) - 2
if total <= 0 {
return nil, errors.New("ipnet don't have valid ip") return nil, errors.New("ipnet don't have valid ip")
} }
max := min + uint32(total) - 1
pool := &Pool{ pool := &Pool{
min: min, gateway: gateway,
max: max, first: first,
gateway: min - 1, last: last,
offset: first.Prev(),
cycle: false,
host: options.Host, host: options.Host,
ipnet: options.IPNet, ipnet: options.IPNet,
} }
@ -163,10 +164,34 @@ func New(options Options) (*Pool, error) {
cache: cachefile.Cache(), cache: cachefile.Cache(),
} }
} else { } else {
pool.store = &memoryStore{ pool.store = newMemoryStore(options.Size)
cache: cache.NewLRUCache(cache.WithSize(options.Size * 2)),
}
} }
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

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

View File

@ -1,7 +1,10 @@
package router package router
import ( import (
"encoding/binary"
"fmt" "fmt"
"net"
"sort"
"strings" "strings"
"github.com/Dreamacro/clash/component/geodata/strmatcher" "github.com/Dreamacro/clash/component/geodata/strmatcher"
@ -69,3 +72,279 @@ func NewDomainMatcher(domains []*Domain) (*DomainMatcher, error) {
func (m *DomainMatcher) ApplyDomain(domain string) bool { func (m *DomainMatcher) ApplyDomain(domain string) bool {
return len(m.matchers.Match(strings.ToLower(domain))) > 0 return len(m.matchers.Match(strings.ToLower(domain))) > 0
} }
// CIDRList is an alias of []*CIDR to provide sort.Interface.
type CIDRList []*CIDR
// Len implements sort.Interface.
func (l *CIDRList) Len() int {
return len(*l)
}
// Less implements sort.Interface.
func (l *CIDRList) Less(i int, j int) bool {
ci := (*l)[i]
cj := (*l)[j]
if len(ci.Ip) < len(cj.Ip) {
return true
}
if len(ci.Ip) > len(cj.Ip) {
return false
}
for k := 0; k < len(ci.Ip); k++ {
if ci.Ip[k] < cj.Ip[k] {
return true
}
if ci.Ip[k] > cj.Ip[k] {
return false
}
}
return ci.Prefix < cj.Prefix
}
// Swap implements sort.Interface.
func (l *CIDRList) Swap(i int, j int) {
(*l)[i], (*l)[j] = (*l)[j], (*l)[i]
}
type ipv6 struct {
a uint64
b uint64
}
type GeoIPMatcher struct {
countryCode string
reverseMatch bool
ip4 []uint32
prefix4 []uint8
ip6 []ipv6
prefix6 []uint8
}
func normalize4(ip uint32, prefix uint8) uint32 {
return (ip >> (32 - prefix)) << (32 - prefix)
}
func normalize6(ip ipv6, prefix uint8) ipv6 {
if prefix <= 64 {
ip.a = (ip.a >> (64 - prefix)) << (64 - prefix)
ip.b = 0
} else {
ip.b = (ip.b >> (128 - prefix)) << (128 - prefix)
}
return ip
}
func (m *GeoIPMatcher) Init(cidrs []*CIDR) error {
ip4Count := 0
ip6Count := 0
for _, cidr := range cidrs {
ip := cidr.Ip
switch len(ip) {
case 4:
ip4Count++
case 16:
ip6Count++
default:
return fmt.Errorf("unexpect ip length: %d", len(ip))
}
}
cidrList := CIDRList(cidrs)
sort.Sort(&cidrList)
m.ip4 = make([]uint32, 0, ip4Count)
m.prefix4 = make([]uint8, 0, ip4Count)
m.ip6 = make([]ipv6, 0, ip6Count)
m.prefix6 = make([]uint8, 0, ip6Count)
for _, cidr := range cidrs {
ip := cidr.Ip
prefix := uint8(cidr.Prefix)
switch len(ip) {
case 4:
m.ip4 = append(m.ip4, normalize4(binary.BigEndian.Uint32(ip), prefix))
m.prefix4 = append(m.prefix4, prefix)
case 16:
ip6 := ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
}
ip6 = normalize6(ip6, prefix)
m.ip6 = append(m.ip6, ip6)
m.prefix6 = append(m.prefix6, prefix)
}
}
return nil
}
func (m *GeoIPMatcher) SetReverseMatch(isReverseMatch bool) {
m.reverseMatch = isReverseMatch
}
func (m *GeoIPMatcher) match4(ip uint32) bool {
if len(m.ip4) == 0 {
return false
}
if ip < m.ip4[0] {
return false
}
size := uint32(len(m.ip4))
l := uint32(0)
r := size
for l < r {
x := ((l + r) >> 1)
if ip < m.ip4[x] {
r = x
continue
}
nip := normalize4(ip, m.prefix4[x])
if nip == m.ip4[x] {
return true
}
l = x + 1
}
return l > 0 && normalize4(ip, m.prefix4[l-1]) == m.ip4[l-1]
}
func less6(a ipv6, b ipv6) bool {
return a.a < b.a || (a.a == b.a && a.b < b.b)
}
func (m *GeoIPMatcher) match6(ip ipv6) bool {
if len(m.ip6) == 0 {
return false
}
if less6(ip, m.ip6[0]) {
return false
}
size := uint32(len(m.ip6))
l := uint32(0)
r := size
for l < r {
x := (l + r) / 2
if less6(ip, m.ip6[x]) {
r = x
continue
}
if normalize6(ip, m.prefix6[x]) == m.ip6[x] {
return true
}
l = x + 1
}
return l > 0 && normalize6(ip, m.prefix6[l-1]) == m.ip6[l-1]
}
// Match returns true if the given ip is included by the GeoIP.
func (m *GeoIPMatcher) Match(ip net.IP) bool {
switch len(ip) {
case 4:
if m.reverseMatch {
return !m.match4(binary.BigEndian.Uint32(ip))
}
return m.match4(binary.BigEndian.Uint32(ip))
case 16:
if m.reverseMatch {
return !m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
}
return m.match6(ipv6{
a: binary.BigEndian.Uint64(ip[0:8]),
b: binary.BigEndian.Uint64(ip[8:16]),
})
default:
return false
}
}
// GeoIPMatcherContainer is a container for GeoIPMatchers. It keeps unique copies of GeoIPMatcher by country code.
type GeoIPMatcherContainer struct {
matchers []*GeoIPMatcher
}
// Add adds a new GeoIP set into the container.
// If the country code of GeoIP is not empty, GeoIPMatcherContainer will try to find an existing one, instead of adding a new one.
func (c *GeoIPMatcherContainer) Add(geoip *GeoIP) (*GeoIPMatcher, error) {
if len(geoip.CountryCode) > 0 {
for _, m := range c.matchers {
if m.countryCode == geoip.CountryCode && m.reverseMatch == geoip.ReverseMatch {
return m, nil
}
}
}
m := &GeoIPMatcher{
countryCode: geoip.CountryCode,
reverseMatch: geoip.ReverseMatch,
}
if err := m.Init(geoip.Cidr); err != nil {
return nil, err
}
if len(geoip.CountryCode) > 0 {
c.matchers = append(c.matchers, m)
}
return m, nil
}
var globalGeoIPContainer GeoIPMatcherContainer
type MultiGeoIPMatcher struct {
matchers []*GeoIPMatcher
}
func NewGeoIPMatcher(geoip *GeoIP) (*GeoIPMatcher, error) {
matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil {
return nil, err
}
return matcher, nil
}
func (m *MultiGeoIPMatcher) ApplyIp(ip net.IP) bool {
for _, matcher := range m.matchers {
if matcher.Match(ip) {
return true
}
}
return false
}
func NewMultiGeoIPMatcher(geoips []*GeoIP) (*MultiGeoIPMatcher, error) {
var matchers []*GeoIPMatcher
for _, geoip := range geoips {
matcher, err := globalGeoIPContainer.Add(geoip)
if err != nil {
return nil, err
}
matchers = append(matchers, matcher)
}
matcher := &MultiGeoIPMatcher{
matchers: matchers,
}
return matcher, nil
}

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.27.1 // protoc-gen-go v1.28.0
// protoc v3.17.3 // protoc v3.19.1
// source: component/geodata/router/config.proto // source: component/geodata/router/config.proto
package router package router

View File

@ -9,6 +9,7 @@ import (
"github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata"
"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"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )

View File

@ -2,6 +2,8 @@ package geodata
import ( import (
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
C "github.com/Dreamacro/clash/constant"
"strings"
) )
var geoLoaderName = "memconservative" var geoLoaderName = "memconservative"
@ -13,9 +15,25 @@ func LoaderName() string {
} }
func SetLoader(newLoader string) { func SetLoader(newLoader string) {
if newLoader == "memc" {
newLoader = "memconservative"
}
geoLoaderName = newLoader geoLoaderName = newLoader
} }
func Verify(name string) bool {
switch name {
case C.GeositeName:
_, _, err := LoadGeoSiteMatcher("CN")
return err == nil
case C.GeoipName:
_, _, err := LoadGeoIPMatcher("CN")
return err == nil
default:
return false
}
}
func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) { func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error) {
geoLoader, err := GetGeoDataLoader(geoLoaderName) geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil { if err != nil {
@ -39,3 +57,28 @@ func LoadGeoSiteMatcher(countryCode string) (*router.DomainMatcher, int, error)
return matcher, len(domains), nil return matcher, len(domains), nil
} }
func LoadGeoIPMatcher(country string) (*router.GeoIPMatcher, int, error) {
geoLoader, err := GetGeoDataLoader(geoLoaderName)
if err != nil {
return nil, 0, err
}
records, err := geoLoader.LoadGeoIP(strings.ReplaceAll(country, "!", ""))
if err != nil {
return nil, 0, err
}
geoIP := &router.GeoIP{
CountryCode: country,
Cidr: records,
ReverseMatch: strings.Contains(country, "!"),
}
matcher, err := router.NewGeoIPMatcher(geoIP)
if err != nil {
return nil, 0, err
}
return matcher, len(records), nil
}

View File

@ -23,7 +23,7 @@ var (
var interfaces = singledo.NewSingle(time.Second * 20) var interfaces = singledo.NewSingle(time.Second * 20)
func ResolveInterface(name string) (*Interface, error) { func ResolveInterface(name string) (*Interface, error) {
value, err, _ := interfaces.Do(func() (interface{}, error) { value, err, _ := interfaces.Do(func() (any, error) {
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,12 +1,11 @@
package mmdb package mmdb
import ( import (
"github.com/oschwald/geoip2-golang"
"sync" "sync"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
"github.com/oschwald/geoip2-golang"
) )
var ( var (

View File

@ -6,17 +6,17 @@ import (
"time" "time"
) )
type Factory = func(context.Context) (interface{}, error) type Factory = func(context.Context) (any, error)
type entry struct { type entry struct {
elm interface{} elm any
time time.Time time time.Time
} }
type Option func(*pool) type Option func(*pool)
// WithEvict set the evict callback // WithEvict set the evict callback
func WithEvict(cb func(interface{})) Option { func WithEvict(cb func(any)) Option {
return func(p *pool) { return func(p *pool) {
p.evict = cb p.evict = cb
} }
@ -32,7 +32,7 @@ func WithAge(maxAge int64) Option {
// WithSize defined max size of Pool // WithSize defined max size of Pool
func WithSize(maxSize int) Option { func WithSize(maxSize int) Option {
return func(p *pool) { return func(p *pool) {
p.ch = make(chan interface{}, maxSize) p.ch = make(chan any, maxSize)
} }
} }
@ -42,13 +42,13 @@ type Pool struct {
} }
type pool struct { type pool struct {
ch chan interface{} ch chan any
factory Factory factory Factory
evict func(interface{}) evict func(any)
maxAge int64 maxAge int64
} }
func (p *pool) GetContext(ctx context.Context) (interface{}, error) { func (p *pool) GetContext(ctx context.Context) (any, error) {
now := time.Now() now := time.Now()
for { for {
select { select {
@ -68,11 +68,11 @@ func (p *pool) GetContext(ctx context.Context) (interface{}, error) {
} }
} }
func (p *pool) Get() (interface{}, error) { func (p *pool) Get() (any, error) {
return p.GetContext(context.Background()) return p.GetContext(context.Background())
} }
func (p *pool) Put(item interface{}) { func (p *pool) Put(item any) {
e := &entry{ e := &entry{
elm: item, elm: item,
time: time.Now(), time: time.Now(),
@ -100,7 +100,7 @@ func recycle(p *Pool) {
func New(factory Factory, options ...Option) *Pool { func New(factory Factory, options ...Option) *Pool {
p := &pool{ p := &pool{
ch: make(chan interface{}, 10), ch: make(chan any, 10),
factory: factory, factory: factory,
} }

View File

@ -10,7 +10,7 @@ import (
func lg() Factory { func lg() Factory {
initial := -1 initial := -1
return func(context.Context) (interface{}, error) { return func(context.Context) (any, error) {
initial++ initial++
return initial, nil return initial, nil
} }
@ -34,7 +34,7 @@ func TestPool_MaxSize(t *testing.T) {
size := 5 size := 5
pool := New(g, WithSize(size)) pool := New(g, WithSize(size))
items := []interface{}{} var items []any
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
item, _ := pool.Get() item, _ := pool.Get()

View File

@ -3,6 +3,9 @@ package process
import ( import (
"errors" "errors"
"net" "net"
"runtime"
C "github.com/Dreamacro/clash/constant"
) )
var ( var (
@ -19,3 +22,52 @@ const (
func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) { func FindProcessName(network string, srcIP net.IP, srcPort int) (string, error) {
return findProcessName(network, srcIP, srcPort) return findProcessName(network, srcIP, srcPort)
} }
func ShouldFindProcess(metadata *C.Metadata) bool {
if runtime.GOOS == "android" {
return false
}
if metadata.Process != "" {
return false
}
for _, ip := range localIPs {
if ip.Equal(metadata.SrcIP) {
return true
}
}
return false
}
func AppendLocalIPs(ip ...net.IP) {
localIPs = append(ip, localIPs...)
}
func getLocalIPs() []net.IP {
ips := []net.IP{net.IPv4zero, net.IPv6zero}
netInterfaces, err := net.Interfaces()
if err != nil {
ips = append(ips, net.IPv4(127, 0, 0, 1), net.IPv6loopback)
return ips
}
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
adds, _ := netInterfaces[i].Addrs()
for _, address := range adds {
if ipNet, ok := address.(*net.IPNet); ok {
ips = append(ips, ipNet.IP)
}
}
}
}
return ips
}
var localIPs []net.IP
func init() {
localIPs = getLocalIPs()
}

View File

@ -3,7 +3,6 @@ package process
import ( import (
"encoding/binary" "encoding/binary"
"net" "net"
"path/filepath"
"syscall" "syscall"
"unsafe" "unsafe"
@ -70,7 +69,7 @@ func findProcessName(network string, ip net.IP, port int) (string, error) {
continue continue
} }
if !ip.Equal(srcIP) { if !ip.Equal(srcIP) && (network == TCP || !srcIP.IsUnspecified()) {
continue continue
} }
@ -96,7 +95,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
return "", errno return "", errno
} }
return filepath.Base(unix.ByteSliceToString(buf)), nil return unix.ByteSliceToString(buf), nil
} }
func readNativeUint32(b []byte) uint32 { func readNativeUint32(b []byte) uint32 {

View File

@ -4,7 +4,6 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"net" "net"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -77,7 +76,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
return "", errno return "", errno
} }
return filepath.Base(string(buf[:size-1])), nil return string(buf[:size-1]), nil
} }
func readNativeUint32(b []byte) uint32 { func readNativeUint32(b []byte) uint32 {

View File

@ -4,12 +4,12 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io"
"net" "net"
"os" "os"
"path" "path"
"path/filepath" "strings"
"syscall" "syscall"
"unicode"
"unsafe" "unsafe"
"github.com/Dreamacro/clash/common/pool" "github.com/Dreamacro/clash/common/pool"
@ -25,17 +25,6 @@ var nativeEndian = func() binary.ByteOrder {
return binary.LittleEndian return binary.LittleEndian
}() }()
type (
SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
ProcessNameResolver func(inode, uid int) (name string, err error)
)
// export for android
var (
DefaultSocketResolver SocketResolver = resolveSocketByNetlink
DefaultProcessNameResolver ProcessNameResolver = resolveProcessNameByProcSearch
)
const ( const (
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48 sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
socketDiagByFamily = 20 socketDiagByFamily = 20
@ -43,15 +32,15 @@ const (
) )
func findProcessName(network string, ip net.IP, srcPort int) (string, error) { func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := DefaultSocketResolver(network, ip, srcPort) inode, uid, err := resolveSocketByNetlink(network, ip, srcPort)
if err != nil { if err != nil {
return "", err return "", err
} }
return DefaultProcessNameResolver(inode, uid) return resolveProcessNameByProcSearch(inode, uid)
} }
func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, error) { func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int32, int32, error) {
var family byte var family byte
var protocol byte var protocol byte
@ -74,13 +63,12 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, e
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG) socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, fmt.Errorf("dial netlink: %w", err)
} }
defer syscall.Close(socket) defer syscall.Close(socket)
syscall.SetNonblock(socket, true) 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: 50}) 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: 50})
if err := syscall.Connect(socket, &syscall.SockaddrNetlink{ if err := syscall.Connect(socket, &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK, Family: syscall.AF_NETLINK,
@ -92,7 +80,7 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, e
} }
if _, err := syscall.Write(socket, req); err != nil { if _, err := syscall.Write(socket, req); err != nil {
return 0, 0, err return 0, 0, fmt.Errorf("write request: %w", err)
} }
rb := pool.Get(pool.RelayBufferSize) rb := pool.Get(pool.RelayBufferSize)
@ -100,24 +88,27 @@ func resolveSocketByNetlink(network string, ip net.IP, srcPort int) (int, int, e
n, err := syscall.Read(socket, rb) n, err := syscall.Read(socket, rb)
if err != nil { if err != nil {
return 0, 0, err return 0, 0, fmt.Errorf("read response: %w", err)
} }
messages, err := syscall.ParseNetlinkMessage(rb[:n]) messages, err := syscall.ParseNetlinkMessage(rb[:n])
if err != nil { if err != nil {
return 0, 0, err return 0, 0, fmt.Errorf("parse netlink message: %w", err)
} else if len(messages) == 0 { } else if len(messages) == 0 {
return 0, 0, io.ErrUnexpectedEOF return 0, 0, fmt.Errorf("unexcepted netlink response")
} }
message := messages[0] message := messages[0]
if message.Header.Type&syscall.NLMSG_ERROR != 0 { if message.Header.Type&syscall.NLMSG_ERROR != 0 {
return 0, 0, syscall.ESRCH return 0, 0, fmt.Errorf("netlink message: NLMSG_ERROR")
} }
uid, inode := unpackSocketDiagResponse(&messages[0]) inode, uid := unpackSocketDiagResponse(&messages[0])
if inode < 0 || uid < 0 {
return 0, 0, fmt.Errorf("invalid inode(%d) or uid(%d)", inode, uid)
}
return int(uid), int(inode), nil return inode, uid, nil
} }
func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte { func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint16) []byte {
@ -155,20 +146,20 @@ func packSocketDiagRequest(family, protocol byte, source net.IP, sourcePort uint
return buf return buf
} }
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) { func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid int32) {
if len(msg.Data) < 72 { if len(msg.Data) < 72 {
return 0, 0 return 0, 0
} }
data := msg.Data data := msg.Data
uid = nativeEndian.Uint32(data[64:68]) uid = int32(nativeEndian.Uint32(data[64:68]))
inode = nativeEndian.Uint32(data[68:72]) inode = int32(nativeEndian.Uint32(data[68:72]))
return return
} }
func resolveProcessNameByProcSearch(inode, uid int) (string, error) { func resolveProcessNameByProcSearch(inode, uid int32) (string, error) {
files, err := os.ReadDir(pathProc) files, err := os.ReadDir(pathProc)
if err != nil { if err != nil {
return "", err return "", err
@ -205,38 +196,16 @@ func resolveProcessNameByProcSearch(inode, uid int) (string, error) {
} }
if bytes.Equal(buffer[:n], socket) { if bytes.Equal(buffer[:n], socket) {
cmdline, err := os.ReadFile(path.Join(processPath, "cmdline")) return os.Readlink(path.Join(processPath, "exe"))
if err != nil {
return "", err
}
return splitCmdline(cmdline), nil
} }
} }
} }
return "", syscall.ESRCH return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
}
func splitCmdline(cmdline []byte) string {
indexOfEndOfString := len(cmdline)
for i, c := range cmdline {
if c == 0 {
indexOfEndOfString = i
break
}
}
return filepath.Base(string(cmdline[:indexOfEndOfString]))
} }
func isPid(s string) bool { func isPid(s string) bool {
for _, s := range s { return strings.IndexFunc(s, func(r rune) bool {
if s < '0' || s > '9' { return !unicode.IsDigit(r)
return false }) == -1
}
}
return true
} }

View File

@ -1,8 +1,4 @@
//go:build !darwin && !linux && !windows && (!freebsd || !amd64) //go:build !darwin && !linux && !windows && (!freebsd || !amd64)
// +build !darwin
// +build !linux
// +build !windows
// +build !freebsd !amd64
package process package process

View File

@ -3,7 +3,6 @@ package process
import ( import (
"fmt" "fmt"
"net" "net"
"path/filepath"
"sync" "sync"
"syscall" "syscall"
"unsafe" "unsafe"
@ -175,7 +174,7 @@ func newSearcher(isV4, isTCP bool) *searcher {
func getTransportTable(fn uintptr, family int, class int) ([]byte, error) { func getTransportTable(fn uintptr, family int, class int) ([]byte, error) {
for size, buf := uint32(8), make([]byte, 8); ; { for size, buf := uint32(8), make([]byte, 8); ; {
ptr := unsafe.Pointer(&buf[0]) ptr := unsafe.Pointer(&buf[0])
err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) err, _, _ := syscall.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err { switch err {
case 0: case 0:
@ -210,8 +209,8 @@ func getExecPathFromPID(pid uint32) (string, error) {
buf := make([]uint16, syscall.MAX_LONG_PATH) buf := make([]uint16, syscall.MAX_LONG_PATH)
size := uint32(len(buf)) size := uint32(len(buf))
r1, _, err := syscall.Syscall6( r1, _, err := syscall.SyscallN(
queryProcName, 4, queryProcName,
uintptr(h), uintptr(h),
uintptr(1), uintptr(1),
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
@ -220,5 +219,5 @@ func getExecPathFromPID(pid uint32) (string, error) {
if r1 == 0 { if r1 == 0 {
return "", err return "", err
} }
return filepath.Base(syscall.UTF16ToString(buf[:size])), nil return syscall.UTF16ToString(buf[:size]), nil
} }

View File

@ -132,6 +132,17 @@ func (c *CacheFile) GetFakeip(key []byte) []byte {
return bucket.Get(key) return bucket.Get(key)
} }
func (c *CacheFile) FlushFakeIP() error {
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket := t.Bucket(bucketFakeip)
if bucket == nil {
return nil
}
return t.DeleteBucket(bucketFakeip)
})
return err
}
func (c *CacheFile) Close() error { func (c *CacheFile) Close() error {
return c.DB.Close() return c.DB.Close()
} }

View File

@ -10,8 +10,11 @@ type Enhancer interface {
FakeIPEnabled() bool FakeIPEnabled() bool
MappingEnabled() bool MappingEnabled() bool
IsFakeIP(net.IP) bool IsFakeIP(net.IP) bool
IsFakeBroadcastIP(net.IP) bool
IsExistFakeIP(net.IP) bool IsExistFakeIP(net.IP) bool
FindHostByIP(net.IP) (string, bool) FindHostByIP(net.IP) (string, bool)
FlushFakeIP() error
InsertHostByIP(net.IP, string)
} }
func FakeIPEnabled() bool { func FakeIPEnabled() bool {
@ -38,6 +41,14 @@ func IsFakeIP(ip net.IP) bool {
return false return false
} }
func IsFakeBroadcastIP(ip net.IP) bool {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsFakeBroadcastIP(ip)
}
return false
}
func IsExistFakeIP(ip net.IP) bool { func IsExistFakeIP(ip net.IP) bool {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
return mapper.IsExistFakeIP(ip) return mapper.IsExistFakeIP(ip)
@ -46,6 +57,12 @@ func IsExistFakeIP(ip net.IP) bool {
return false return false
} }
func InsertHostByIP(ip net.IP, host string) {
if mapper := DefaultHostMapper; mapper != nil {
mapper.InsertHostByIP(ip, host)
}
}
func FindHostByIP(ip net.IP) (string, bool) { func FindHostByIP(ip net.IP) (string, bool) {
if mapper := DefaultHostMapper; mapper != nil { if mapper := DefaultHostMapper; mapper != nil {
return mapper.FindHostByIP(ip) return mapper.FindHostByIP(ip)
@ -53,3 +70,10 @@ func FindHostByIP(ip net.IP) (string, bool) {
return "", false return "", false
} }
func FlushFakeIP() error {
if mapper := DefaultHostMapper; mapper != nil {
return mapper.FlushFakeIP()
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"math/rand" "math/rand"
"net" "net"
"net/netip"
"strings" "strings"
"time" "time"
@ -15,15 +16,15 @@ var (
// DefaultResolver aim to resolve ip // DefaultResolver aim to resolve ip
DefaultResolver Resolver DefaultResolver Resolver
// MainResolver resolve ip with main domain server // ProxyServerHostResolver resolve ip to proxies server host
MainResolver Resolver ProxyServerHostResolver Resolver
// DisableIPv6 means don't resolve ipv6 host // DisableIPv6 means don't resolve ipv6 host
// default value is true // default value is true
DisableIPv6 = true DisableIPv6 = true
// DefaultHosts aim to resolve hosts // DefaultHosts aim to resolve hosts
DefaultHosts = trie.New() DefaultHosts = trie.New[netip.Addr]()
// DefaultDNSTimeout defined the default dns request timeout // DefaultDNSTimeout defined the default dns request timeout
DefaultDNSTimeout = time.Second * 5 DefaultDNSTimeout = time.Second * 5
@ -46,14 +47,10 @@ func ResolveIPv4(host string) (net.IP, error) {
return ResolveIPv4WithResolver(host, DefaultResolver) return ResolveIPv4WithResolver(host, DefaultResolver)
} }
func ResolveIPv4WithMain(host string) (net.IP, error) {
return ResolveIPv4WithResolver(host, MainResolver)
}
func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To4(); ip != nil { if ip := node.Data; ip.Is4() {
return ip, nil return ip.AsSlice(), nil
} }
} }
@ -69,16 +66,20 @@ func ResolveIPv4WithResolver(host string, r Resolver) (net.IP, error) {
return r.ResolveIPv4(host) return r.ResolveIPv4(host)
} }
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) if DefaultResolver == nil {
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host) defer cancel()
if err != nil { ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip4", host)
return nil, err if err != nil {
} else if len(ipAddrs) == 0 { return nil, err
return nil, ErrIPNotFound } else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
} }
return ipAddrs[rand.Intn(len(ipAddrs))], nil return nil, ErrIPNotFound
} }
// ResolveIPv6 with a host, return ipv6 // ResolveIPv6 with a host, return ipv6
@ -86,18 +87,14 @@ func ResolveIPv6(host string) (net.IP, error) {
return ResolveIPv6WithResolver(host, DefaultResolver) return ResolveIPv6WithResolver(host, DefaultResolver)
} }
func ResolveIPv6WithMain(host string) (net.IP, error) {
return ResolveIPv6WithResolver(host, MainResolver)
}
func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
if DisableIPv6 { if DisableIPv6 {
return nil, ErrIPv6Disabled return nil, ErrIPv6Disabled
} }
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
if ip := node.Data.(net.IP).To16(); ip != nil { if ip := node.Data; ip.Is6() {
return ip, nil return ip.AsSlice(), nil
} }
} }
@ -113,22 +110,27 @@ func ResolveIPv6WithResolver(host string, r Resolver) (net.IP, error) {
return r.ResolveIPv6(host) return r.ResolveIPv6(host)
} }
ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout) if DefaultResolver == nil {
defer cancel() ctx, cancel := context.WithTimeout(context.Background(), DefaultDNSTimeout)
ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host) defer cancel()
if err != nil { ipAddrs, err := net.DefaultResolver.LookupIP(ctx, "ip6", host)
return nil, err if err != nil {
} else if len(ipAddrs) == 0 { return nil, err
return nil, ErrIPNotFound } else if len(ipAddrs) == 0 {
return nil, ErrIPNotFound
}
return ipAddrs[rand.Intn(len(ipAddrs))], nil
} }
return ipAddrs[rand.Intn(len(ipAddrs))], nil return nil, ErrIPNotFound
} }
// ResolveIPWithResolver same as ResolveIP, but with a resolver // ResolveIPWithResolver same as ResolveIP, but with a resolver
func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) { func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
if node := DefaultHosts.Search(host); node != nil { if node := DefaultHosts.Search(host); node != nil {
return node.Data.(net.IP), nil ip := node.Data
return ip.Unmap().AsSlice(), nil
} }
if r != nil { if r != nil {
@ -145,12 +147,16 @@ func ResolveIPWithResolver(host string, r Resolver) (net.IP, error) {
return ip, nil return ip, nil
} }
ipAddr, err := net.ResolveIPAddr("ip", host) if DefaultResolver == nil {
if err != nil { ipAddr, err := net.ResolveIPAddr("ip", host)
return nil, err if err != nil {
return nil, err
}
return ipAddr.IP, nil
} }
return ipAddr.IP, nil return nil, ErrIPNotFound
} }
// ResolveIP with a host, return ip // ResolveIP with a host, return ip
@ -158,7 +164,26 @@ func ResolveIP(host string) (net.IP, error) {
return ResolveIPWithResolver(host, DefaultResolver) return ResolveIPWithResolver(host, DefaultResolver)
} }
// ResolveIPWithMainResolver with a host, use main resolver, return ip // ResolveIPv4ProxyServerHost proxies server host only
func ResolveIPWithMainResolver(host string) (net.IP, error) { func ResolveIPv4ProxyServerHost(host string) (net.IP, error) {
return ResolveIPWithResolver(host, MainResolver) if ProxyServerHostResolver != nil {
return ResolveIPv4WithResolver(host, ProxyServerHostResolver)
}
return ResolveIPv4(host)
}
// ResolveIPv6ProxyServerHost proxies server host only
func ResolveIPv6ProxyServerHost(host string) (net.IP, error) {
if ProxyServerHostResolver != nil {
return ResolveIPv6WithResolver(host, ProxyServerHostResolver)
}
return ResolveIPv6(host)
}
// ResolveProxyServerHost proxies server host only
func ResolveProxyServerHost(host string) (net.IP, error) {
if ProxyServerHostResolver != nil {
return ResolveIPWithResolver(host, ProxyServerHostResolver)
}
return ResolveIP(host)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,21 +4,25 @@ import (
"container/list" "container/list"
"errors" "errors"
"fmt" "fmt"
R "github.com/Dreamacro/clash/rule"
RP "github.com/Dreamacro/clash/rule/provider"
"net" "net"
"net/netip"
"net/url" "net/url"
"os" "os"
"regexp"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
"github.com/Dreamacro/clash/common/utils"
R "github.com/Dreamacro/clash/rule"
RP "github.com/Dreamacro/clash/rule/provider"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/outboundgroup" "github.com/Dreamacro/clash/adapter/outboundgroup"
"github.com/Dreamacro/clash/adapter/provider" "github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/component/auth" "github.com/Dreamacro/clash/component/auth"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/fakeip"
"github.com/Dreamacro/clash/component/geodata" "github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/geodata/router"
@ -26,6 +30,7 @@ import (
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"
"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"
@ -41,11 +46,12 @@ type General struct {
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:"-"`
RoutingMark int `json:"-"`
GeodataMode bool `json:"geodata-mode"`
GeodataLoader string `json:"geodata-loader"` GeodataLoader string `json:"geodata-loader"`
AutoIptables bool `json:"auto-iptables"`
} }
// Inbound // Inbound config
type Inbound struct { type Inbound struct {
Port int `json:"port"` Port int `json:"port"`
SocksPort int `json:"socks-port"` SocksPort int `json:"socks-port"`
@ -57,7 +63,7 @@ type Inbound struct {
BindAddress string `json:"bind-address"` BindAddress string `json:"bind-address"`
} }
// Controller // Controller config
type Controller struct { type Controller struct {
ExternalController string `json:"-"` ExternalController string `json:"-"`
ExternalUI string `json:"-"` ExternalUI string `json:"-"`
@ -66,17 +72,18 @@ type Controller struct {
// DNS config // DNS config
type DNS struct { type DNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"` IPv6 bool `yaml:"ipv6"`
NameServer []dns.NameServer `yaml:"nameserver"` NameServer []dns.NameServer `yaml:"nameserver"`
Fallback []dns.NameServer `yaml:"fallback"` Fallback []dns.NameServer `yaml:"fallback"`
FallbackFilter FallbackFilter `yaml:"fallback-filter"` FallbackFilter FallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
EnhancedMode C.DNSMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
DefaultNameserver []dns.NameServer `yaml:"default-nameserver"` DefaultNameserver []dns.NameServer `yaml:"default-nameserver"`
FakeIPRange *fakeip.Pool FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
NameServerPolicy map[string]dns.NameServer NameServerPolicy map[string]dns.NameServer
ProxyServerNameserver []dns.NameServer
} }
// FallbackFilter config // FallbackFilter config
@ -102,16 +109,28 @@ 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"`
Stack string `yaml:"stack" json:"stack"` Device string `yaml:"device" json:"device"`
DnsHijack []string `yaml:"dns-hijack" json:"dns-hijack"` Stack C.TUNStack `yaml:"stack" json:"stack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"` DNSHijack []netip.AddrPort `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
} }
// Script config // IPTables config
type Script struct { type IPTables struct {
MainCode string `yaml:"code" json:"code"` Enable bool `yaml:"enable" json:"enable"`
ShortcutsCode map[string]string `yaml:"shortcuts" json:"shortcuts"` InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"`
Bypass []string `yaml:"bypass" json:"bypass"`
}
type Sniffer struct {
Enable bool
Force bool
Sniffers []C.SnifferType
Reverses *trie.DomainTrie[bool]
ForceDomain *trie.DomainTrie[bool]
SkipSNI *trie.DomainTrie[bool]
Ports *[]utils.Range[uint16]
} }
// Experimental config // Experimental config
@ -121,30 +140,33 @@ type Experimental struct{}
type Config struct { type Config struct {
General *General General *General
Tun *Tun Tun *Tun
IPTables *IPTables
DNS *DNS DNS *DNS
Experimental *Experimental Experimental *Experimental
Hosts *trie.DomainTrie Hosts *trie.DomainTrie[netip.Addr]
Profile *Profile Profile *Profile
Rules []C.Rule Rules []C.Rule
Users []auth.AuthUser Users []auth.AuthUser
Proxies map[string]C.Proxy Proxies map[string]C.Proxy
Providers map[string]providerTypes.ProxyProvider Providers map[string]providerTypes.ProxyProvider
RuleProviders map[string]*providerTypes.RuleProvider RuleProviders map[string]*providerTypes.RuleProvider
Sniffer *Sniffer
} }
type RawDNS struct { type RawDNS struct {
Enable bool `yaml:"enable"` Enable bool `yaml:"enable"`
IPv6 bool `yaml:"ipv6"` IPv6 bool `yaml:"ipv6"`
UseHosts bool `yaml:"use-hosts"` UseHosts bool `yaml:"use-hosts"`
NameServer []string `yaml:"nameserver"` NameServer []string `yaml:"nameserver"`
Fallback []string `yaml:"fallback"` Fallback []string `yaml:"fallback"`
FallbackFilter RawFallbackFilter `yaml:"fallback-filter"` FallbackFilter RawFallbackFilter `yaml:"fallback-filter"`
Listen string `yaml:"listen"` Listen string `yaml:"listen"`
EnhancedMode C.DNSMode `yaml:"enhanced-mode"` EnhancedMode C.DNSMode `yaml:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range"` FakeIPRange string `yaml:"fake-ip-range"`
FakeIPFilter []string `yaml:"fake-ip-filter"` FakeIPFilter []string `yaml:"fake-ip-filter"`
DefaultNameserver []string `yaml:"default-nameserver"` DefaultNameserver []string `yaml:"default-nameserver"`
NameServerPolicy map[string]string `yaml:"nameserver-policy"` NameServerPolicy map[string]string `yaml:"nameserver-policy"`
ProxyServerNameserver []string `yaml:"proxy-server-nameserver"`
} }
type RawFallbackFilter struct { type RawFallbackFilter struct {
@ -155,6 +177,15 @@ type RawFallbackFilter struct {
GeoSite []string `yaml:"geosite"` GeoSite []string `yaml:"geosite"`
} }
type RawTun struct {
Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"`
Stack C.TUNStack `yaml:"stack" json:"stack"`
DNSHijack []string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface bool `yaml:"auto-detect-interface"`
}
type RawConfig struct { type RawConfig struct {
Port int `yaml:"port"` Port int `yaml:"port"`
SocksPort int `yaml:"socks-port"` SocksPort int `yaml:"socks-port"`
@ -172,20 +203,32 @@ type RawConfig struct {
ExternalUI string `yaml:"external-ui"` ExternalUI string `yaml:"external-ui"`
Secret string `yaml:"secret"` Secret string `yaml:"secret"`
Interface string `yaml:"interface-name"` Interface string `yaml:"interface-name"`
RoutingMark int `yaml:"routing-mark"`
GeodataMode bool `yaml:"geodata-mode"`
GeodataLoader string `yaml:"geodata-loader"` GeodataLoader string `yaml:"geodata-loader"`
AutoIptables bool `yaml:"auto-iptables"`
ProxyProvider map[string]map[string]interface{} `yaml:"proxy-providers"` Sniffer SnifferRaw `yaml:"sniffer"`
RuleProvider map[string]map[string]interface{} `yaml:"rule-providers"` ProxyProvider map[string]map[string]any `yaml:"proxy-providers"`
Hosts map[string]string `yaml:"hosts"` RuleProvider map[string]map[string]any `yaml:"rule-providers"`
DNS RawDNS `yaml:"dns"` Hosts map[string]string `yaml:"hosts"`
Tun Tun `yaml:"tun"` DNS RawDNS `yaml:"dns"`
Experimental Experimental `yaml:"experimental"` Tun RawTun `yaml:"tun"`
Profile Profile `yaml:"profile"` IPTables IPTables `yaml:"iptables"`
Proxy []map[string]interface{} `yaml:"proxies"` Experimental Experimental `yaml:"experimental"`
ProxyGroup []map[string]interface{} `yaml:"proxy-groups"` Profile Profile `yaml:"profile"`
Rule []string `yaml:"rules"` Proxy []map[string]any `yaml:"proxies"`
Script Script `yaml:"script"` ProxyGroup []map[string]any `yaml:"proxy-groups"`
Rule []string `yaml:"rules"`
}
type SnifferRaw struct {
Enable bool `yaml:"enable" json:"enable"`
Sniffing []string `yaml:"sniffing" json:"sniffing"`
Force bool `yaml:"force" json:"force"`
Reverse []string `yaml:"reverses" json:"reverses"`
ForceDomain []string `yaml:"force-domain" json:"force-domain"`
SkipSNI []string `yaml:"skip-sni" json:"skip-sni"`
Ports []string `yaml:"port-whitelist" json:"port-whitelist"`
} }
// Parse config // Parse config
@ -204,25 +247,33 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
AllowLan: false, AllowLan: false,
BindAddress: "*", BindAddress: "*",
Mode: T.Rule, Mode: T.Rule,
GeodataMode: C.GeodataMode,
GeodataLoader: "memconservative", GeodataLoader: "memconservative",
AutoIptables: false,
UnifiedDelay: false, UnifiedDelay: false,
Authentication: []string{}, Authentication: []string{},
LogLevel: log.INFO, LogLevel: log.INFO,
Hosts: map[string]string{}, Hosts: map[string]string{},
Rule: []string{}, Rule: []string{},
Proxy: []map[string]interface{}{}, Proxy: []map[string]any{},
ProxyGroup: []map[string]interface{}{}, ProxyGroup: []map[string]any{},
Tun: Tun{ Tun: RawTun{
Enable: false, Enable: false,
Stack: "gvisor", Device: "",
DnsHijack: []string{"198.18.0.2:53"}, AutoDetectInterface: true,
AutoRoute: false, Stack: C.TunGvisor,
DNSHijack: []string{"0.0.0.0:53"}, // default hijack all dns query
AutoRoute: true,
},
IPTables: IPTables{
Enable: false,
InboundInterface: "lo",
Bypass: []string{},
}, },
DNS: RawDNS{ DNS: RawDNS{
Enable: false, Enable: false,
UseHosts: true, UseHosts: true,
FakeIPRange: "198.18.0.1/16", EnhancedMode: C.DNSMapping,
FakeIPRange: "198.18.0.1/16",
FallbackFilter: RawFallbackFilter{ FallbackFilter: RawFallbackFilter{
GeoIP: true, GeoIP: true,
GeoIPCode: "CN", GeoIPCode: "CN",
@ -236,8 +287,8 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"1.0.0.1", "1.0.0.1",
}, },
NameServer: []string{ NameServer: []string{
"223.5.5.5", "https://doh.pub/dns-query",
"119.29.29", "tls://223.5.5.5:853",
}, },
FakeIPFilter: []string{ FakeIPFilter: []string{
"dns.msftnsci.com", "dns.msftnsci.com",
@ -245,13 +296,18 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
"www.msftconnecttest.com", "www.msftconnecttest.com",
}, },
}, },
Sniffer: SnifferRaw{
Enable: false,
Force: false,
Sniffing: []string{},
Reverse: []string{},
ForceDomain: []string{},
SkipSNI: []string{},
Ports: []string{},
},
Profile: Profile{ Profile: Profile{
StoreSelected: true, StoreSelected: true,
}, },
Script: Script{
MainCode: "",
ShortcutsCode: map[string]string{},
},
} }
if err := yaml.Unmarshal(buf, rawCfg); err != nil { if err := yaml.Unmarshal(buf, rawCfg); err != nil {
@ -263,9 +319,11 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) {
func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config := &Config{} config := &Config{}
log.Infoln("Start initial configuration in progress") //Segment finished in xxm
startTime := time.Now()
config.Experimental = &rawCfg.Experimental config.Experimental = &rawCfg.Experimental
config.Profile = &rawCfg.Profile config.Profile = &rawCfg.Profile
config.IPTables = &rawCfg.IPTables
general, err := parseGeneral(rawCfg) general, err := parseGeneral(rawCfg)
if err != nil { if err != nil {
@ -273,7 +331,13 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
} }
config.General = general config.General = general
config.Tun = &rawCfg.Tun tunCfg, err := parseTun(rawCfg.Tun, config.General)
if err != nil {
return nil, err
}
config.Tun = tunCfg
dialer.DefaultInterface.Store(config.General.Interface)
proxies, providers, err := parseProxies(rawCfg) proxies, providers, err := parseProxies(rawCfg)
if err != nil { if err != nil {
@ -282,11 +346,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Proxies = proxies config.Proxies = proxies
config.Providers = providers config.Providers = providers
err = parseScript(rawCfg)
if err != nil {
return nil, err
}
rules, ruleProviders, err := parseRules(rawCfg, proxies) rules, ruleProviders, err := parseRules(rawCfg, proxies)
if err != nil { if err != nil {
return nil, err return nil, err
@ -308,6 +367,13 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
config.Users = parseAuthentication(rawCfg.Authentication) config.Users = parseAuthentication(rawCfg.Authentication)
config.Sniffer, err = parseSniffer(rawCfg.Sniffer)
if err != nil {
return nil, err
}
elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm
return config, nil return config, nil
} }
@ -343,24 +409,27 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
LogLevel: cfg.LogLevel, LogLevel: cfg.LogLevel,
IPv6: cfg.IPv6, IPv6: cfg.IPv6,
Interface: cfg.Interface, Interface: cfg.Interface,
RoutingMark: cfg.RoutingMark,
GeodataMode: cfg.GeodataMode,
GeodataLoader: cfg.GeodataLoader, GeodataLoader: cfg.GeodataLoader,
AutoIptables: cfg.AutoIptables,
}, nil }, nil
} }
func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]providerTypes.ProxyProvider, err error) { func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[string]providerTypes.ProxyProvider, err error) {
proxies = make(map[string]C.Proxy) proxies = make(map[string]C.Proxy)
providersMap = make(map[string]providerTypes.ProxyProvider) providersMap = make(map[string]providerTypes.ProxyProvider)
var proxyList []string
_proxiesList := list.New()
_groupsList := list.New()
proxiesConfig := cfg.Proxy proxiesConfig := cfg.Proxy
groupsConfig := cfg.ProxyGroup groupsConfig := cfg.ProxyGroup
providersConfig := cfg.ProxyProvider providersConfig := cfg.ProxyProvider
var proxyList []string
_proxiesList := 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())
proxies["COMPATIBLE"] = adapter.NewProxy(outbound.NewCompatible()) proxies["COMPATIBLE"] = adapter.NewProxy(outbound.NewCompatible())
proxies["PASS"] = adapter.NewProxy(outbound.NewPass())
proxyList = append(proxyList, "DIRECT", "REJECT") proxyList = append(proxyList, "DIRECT", "REJECT")
// parse proxy // parse proxy
@ -424,6 +493,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
var ps []C.Proxy var ps []C.Proxy
for _, v := range proxyList { for _, v := range proxyList {
if proxies[v].Type() == C.Pass {
continue
}
ps = append(ps, proxies[v]) ps = append(ps, proxies[v])
} }
hc := provider.NewHealthCheck(ps, "", 0, true) hc := provider.NewHealthCheck(ps, "", 0, true)
@ -446,53 +518,9 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
return proxies, providersMap, nil return proxies, providersMap, nil
} }
func parseScript(cfg *RawConfig) error {
mode := cfg.Mode
script := cfg.Script
mainCode := cleanPyKeywords(script.MainCode)
shortcutsCode := script.ShortcutsCode
if mode != T.Script && len(shortcutsCode) == 0 {
return nil
} else if mode == T.Script && len(mainCode) == 0 {
return fmt.Errorf("initialized script module failure, can't find script code in the config file")
}
content :=
`# -*- coding: UTF-8 -*-
from datetime import datetime as whatever
class ClashTime:
def now(self):
return whatever.now()
def unix(self):
return int(whatever.now().timestamp())
def unix_nano(self):
return int(round(whatever.now().timestamp() * 1000))
time = ClashTime()
`
for k, v := range shortcutsCode {
v = cleanPyKeywords(v)
v = strings.TrimSpace(v)
if len(v) == 0 {
return fmt.Errorf("initialized rule SCRIPT failure, shortcut [%s] code invalid syntax", k)
}
content += "def " + strings.ToLower(k) + "(ctx, network, process_name, host, src_ip, src_port, dst_ip, dst_port):\n return " + v + "\n\n"
}
return 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())
startTime := time.Now()
// 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)
@ -522,32 +550,30 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
continue continue
} }
l := len(rule)
if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" { if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" {
payload = strings.Join(rule[1:len(rule)-1], ",") target = rule[l-1]
target = rule[len(rule)-1] payload = strings.Join(rule[1:l-1], ",")
} else { } else {
switch l := len(rule); { if l < 2 {
case l == 2:
target = rule[1]
case l == 3:
if ruleName == "MATCH" {
payload = ""
target = rule[1]
params = rule[2:]
break
}
payload = rule[1]
target = rule[2]
case l >= 4:
payload = rule[1]
target = rule[2]
params = rule[3:]
default:
return nil, nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line) return nil, nil, fmt.Errorf("rules[%d] [%s] error: format invalid", idx, line)
} }
if l < 4 {
rule = append(rule, make([]string, 4-l)...)
}
if ruleName == "MATCH" {
l = 2
}
if l >= 3 {
l = 3
payload = rule[1]
}
target = rule[l-1]
params = rule[l:]
} }
if _, ok := proxies[target]; mode != T.Script && !ok { if _, ok := proxies[target]; !ok {
return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target) return nil, nil, fmt.Errorf("rules[%d] [%s] error: proxy [%s] not found", idx, line, target)
} }
@ -555,38 +581,35 @@ func parseRules(cfg *RawConfig, proxies map[string]C.Proxy) ([]C.Rule, map[strin
if ruleName == "GEOSITE" { if ruleName == "GEOSITE" {
if err := initGeoSite(); err != nil { if err := initGeoSite(); err != nil {
return nil, nil, fmt.Errorf("can't initial GeoSite: %w", err) 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())
} }
if mode != T.Script { rules = append(rules, parsed)
rules = append(rules, parsed)
}
} }
elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms
log.Infoln("Initialization time consuming %dms", elapsedTime) //Segment finished in xxm
log.Infoln("Geodata Loader mode: %s", geodata.LoaderName())
runtime.GC() runtime.GC()
return rules, ruleProviders, nil return rules, ruleProviders, nil
} }
func parseHosts(cfg *RawConfig) (*trie.DomainTrie, error) { func parseHosts(cfg *RawConfig) (*trie.DomainTrie[netip.Addr], error) {
tree := trie.New() tree := trie.New[netip.Addr]()
// add default hosts // add default hosts
if err := tree.Insert("localhost", net.IP{127, 0, 0, 1}); err != nil { if err := tree.Insert("localhost", netip.AddrFrom4([4]byte{127, 0, 0, 1})); err != nil {
log.Errorln("insert localhost to host error: %s", err.Error()) log.Errorln("insert localhost to host error: %s", err.Error())
} }
if len(cfg.Hosts) != 0 { if len(cfg.Hosts) != 0 {
for domain, ipStr := range cfg.Hosts { for domain, ipStr := range cfg.Hosts {
ip := net.ParseIP(ipStr) ip, err := netip.ParseAddr(ipStr)
if ip == nil { if err != nil {
return nil, fmt.Errorf("%s is not a valid IP", ipStr) return nil, fmt.Errorf("%s is not a valid IP", ipStr)
} }
_ = tree.Insert(domain, ip) _ = tree.Insert(domain, ip)
@ -644,6 +667,9 @@ func parseNameServer(servers []string) ([]dns.NameServer, error) {
case "dhcp": case "dhcp":
addr = u.Host addr = u.Host
dnsNetType = "dhcp" // UDP from DHCP dnsNetType = "dhcp" // UDP from DHCP
case "quic":
addr, err = hostWithDefaultPort(u.Host, "784")
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)
} }
@ -658,6 +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(),
}, },
) )
} }
@ -699,7 +726,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
var sites []*router.DomainMatcher var sites []*router.DomainMatcher
if len(countries) > 0 { if len(countries) > 0 {
if err := initGeoSite(); err != nil { if err := initGeoSite(); err != nil {
return nil, fmt.Errorf("can't initial GeoSite: %w", err) return nil, fmt.Errorf("can't initial GeoSite: %s", err)
} }
} }
@ -730,7 +757,7 @@ func parseFallbackGeoSite(countries []string, rules []C.Rule) ([]*router.DomainM
return sites, nil return sites, nil
} }
func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS, error) { func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], rules []C.Rule) (*DNS, error) {
cfg := rawCfg.DNS cfg := rawCfg.DNS
if cfg.Enable && len(cfg.NameServer) == 0 { if cfg.Enable && len(cfg.NameServer) == 0 {
return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty")
@ -759,6 +786,10 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
return nil, err return nil, err
} }
if dnsCfg.ProxyServerNameserver, err = parseNameServer(cfg.ProxyServerNameserver); err != nil {
return nil, err
}
if len(cfg.DefaultNameserver) == 0 { if len(cfg.DefaultNameserver) == 0 {
return nil, errors.New("default nameserver should have at least one nameserver") return nil, errors.New("default nameserver should have at least one nameserver")
} }
@ -769,20 +800,23 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
for _, ns := range dnsCfg.DefaultNameserver { for _, ns := range dnsCfg.DefaultNameserver {
host, _, err := net.SplitHostPort(ns.Addr) host, _, err := net.SplitHostPort(ns.Addr)
if err != nil || net.ParseIP(host) == nil { if err != nil || net.ParseIP(host) == nil {
return nil, errors.New("default nameserver should be pure IP") u, err := url.Parse(ns.Addr)
if err != nil || net.ParseIP(u.Host) == nil {
return nil, errors.New("default nameserver should be pure IP")
}
} }
} }
if cfg.EnhancedMode == C.DNSFakeIP { if cfg.EnhancedMode == C.DNSFakeIP {
_, ipnet, err := net.ParseCIDR(cfg.FakeIPRange) ipnet, err := netip.ParsePrefix(cfg.FakeIPRange)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var host *trie.DomainTrie var host *trie.DomainTrie[bool]
// fake ip skip host filter // fake ip skip host filter
if len(cfg.FakeIPFilter) != 0 { if len(cfg.FakeIPFilter) != 0 {
host = trie.New() host = trie.New[bool]()
for _, domain := range cfg.FakeIPFilter { for _, domain := range cfg.FakeIPFilter {
_ = host.Insert(domain, true) _ = host.Insert(domain, true)
} }
@ -790,7 +824,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
if len(dnsCfg.Fallback) != 0 { if len(dnsCfg.Fallback) != 0 {
if host == nil { if host == nil {
host = trie.New() host = trie.New[bool]()
} }
for _, fb := range dnsCfg.Fallback { for _, fb := range dnsCfg.Fallback {
if net.ParseIP(fb.Addr) != nil { if net.ParseIP(fb.Addr) != nil {
@ -801,7 +835,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
} }
pool, err := fakeip.New(fakeip.Options{ pool, err := fakeip.New(fakeip.Options{
IPNet: ipnet, IPNet: &ipnet,
Size: 1000, Size: 1000,
Host: host, Host: host,
Persistence: rawCfg.Profile.StoreFakeIP, Persistence: rawCfg.Profile.StoreFakeIP,
@ -835,25 +869,141 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie, rules []C.Rule) (*DNS,
} }
func parseAuthentication(rawRecords []string) []auth.AuthUser { func parseAuthentication(rawRecords []string) []auth.AuthUser {
users := make([]auth.AuthUser, 0) var users []auth.AuthUser
for _, line := range rawRecords { for _, line := range rawRecords {
userData := strings.SplitN(line, ":", 2) if user, pass, found := strings.Cut(line, ":"); found {
if len(userData) == 2 { users = append(users, auth.AuthUser{User: user, Pass: pass})
users = append(users, auth.AuthUser{User: userData[0], Pass: userData[1]})
} }
} }
return users return users
} }
func cleanPyKeywords(code string) string { func parseTun(rawTun RawTun, general *General) (*Tun, error) {
if len(code) == 0 { if rawTun.Enable && rawTun.AutoDetectInterface {
return code autoDetectInterfaceName, err := commons.GetAutoDetectInterface()
} if err != nil || autoDetectInterfaceName == "" {
keywords := []string{"import", "print"} log.Warnln("Can not find auto detect interface.[%s]", err)
} else {
log.Warnln("Auto detect interface: %s", autoDetectInterfaceName)
}
for _, kw := range keywords { general.Interface = autoDetectInterfaceName
reg := regexp.MustCompile("(?m)[\r\n]+^.*" + kw + ".*$")
code = reg.ReplaceAllString(code, "")
} }
return code
var dnsHijack []netip.AddrPort
for _, d := range rawTun.DNSHijack {
if _, after, ok := strings.Cut(d, "://"); ok {
d = after
}
addrPort, err := netip.ParseAddrPort(d)
if err != nil {
return nil, fmt.Errorf("parse dns-hijack url error: %w", err)
}
dnsHijack = append(dnsHijack, addrPort)
}
return &Tun{
Enable: rawTun.Enable,
Device: rawTun.Device,
Stack: rawTun.Stack,
DNSHijack: dnsHijack,
AutoRoute: rawTun.AutoRoute,
}, nil
}
func parseSniffer(snifferRaw SnifferRaw) (*Sniffer, error) {
sniffer := &Sniffer{
Enable: snifferRaw.Enable,
Force: snifferRaw.Force,
}
ports := []utils.Range[uint16]{}
if len(snifferRaw.Ports) == 0 {
ports = append(ports, *utils.NewRange[uint16](0, 65535))
} else {
for _, portRange := range snifferRaw.Ports {
portRaws := strings.Split(portRange, "-")
if len(portRaws) > 1 {
p, err := strconv.ParseUint(portRaws[0], 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 {
return nil, fmt.Errorf("%s format error", portRange)
}
end := uint16(p)
ports = append(ports, *utils.NewRange(start, end))
}
}
}
sniffer.Ports = &ports
loadSniffer := make(map[C.SnifferType]struct{})
for _, snifferName := range snifferRaw.Sniffing {
find := false
for _, snifferType := range C.SnifferList {
if snifferType.String() == strings.ToUpper(snifferName) {
find = true
loadSniffer[snifferType] = struct{}{}
}
}
if !find {
return nil, fmt.Errorf("not find the sniffer[%s]", snifferName)
}
}
for st := range loadSniffer {
sniffer.Sniffers = append(sniffer.Sniffers, st)
}
sniffer.ForceDomain = trie.New[bool]()
for _, domain := range snifferRaw.ForceDomain {
err := sniffer.ForceDomain.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
}
sniffer.SkipSNI = trie.New[bool]()
for _, domain := range snifferRaw.SkipSNI {
err := sniffer.SkipSNI.Insert(domain, true)
if err != nil {
return nil, fmt.Errorf("error domian[%s] in force-domain, error:%v", domain, err)
}
}
// Compatibility, remove it when release
if strings.Contains(C.Version, "alpha") || strings.Contains(C.Version, "develop") || strings.Contains(C.Version, "1.10.0") {
log.Warnln("Sniffer param force and reverses deprecated, will be removed in the release version, see https://github.com/MetaCubeX/Clash.Meta/commit/48a01adb7a4f38974b9d9639f931d0d245aebf28")
if snifferRaw.Force {
// match all domain
sniffer.ForceDomain.Insert("+", true)
for _, domain := range snifferRaw.Reverse {
err := sniffer.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
} }

View File

@ -2,17 +2,20 @@ package config
import ( import (
"fmt" "fmt"
"github.com/Dreamacro/clash/component/geodata"
"github.com/Dreamacro/clash/component/mmdb"
"io" "io"
"net/http" "net/http"
"os" "os"
"github.com/Dreamacro/clash/component/mmdb"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"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://cdn.jsdelivr.net/gh/Loyalsoldier/geoip@release/Country.mmdb") resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/geoip/release/Country.mmdb")
if err != nil { if err != nil {
return return
} }
@ -28,30 +31,25 @@ func downloadMMDB(path string) (err error) {
return err return err
} }
func initMMDB() error { func downloadGeoIP(path string) (err error) {
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) { resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat")
log.Infoln("Can't find MMDB, start download") if err != nil {
if err := downloadMMDB(C.Path.MMDB()); err != nil { return
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
} }
defer resp.Body.Close()
if !mmdb.Verify() { f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
log.Warnln("MMDB invalid, remove and download") if err != nil {
if err := os.Remove(C.Path.MMDB()); err != nil { return err
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
}
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
} }
defer f.Close()
_, err = io.Copy(f, resp.Body)
return nil return err
} }
func downloadGeoSite(path string) (err error) { func downloadGeoSite(path string) (err error) {
resp, err := http.Get("https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat") resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat")
if err != nil { if err != nil {
return return
} }
@ -69,12 +67,65 @@ func downloadGeoSite(path string) (err error) {
func initGeoSite() error { func initGeoSite() error {
if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) {
log.Infoln("Need GeoSite but can't find GeoSite.dat, start download") log.Infoln("Can't find GeoSite.dat, start download")
if err := downloadGeoSite(C.Path.GeoSite()); err != nil { if err := downloadGeoSite(C.Path.GeoSite()); err != nil {
return fmt.Errorf("can't download GeoSite.dat: %s", err.Error()) return fmt.Errorf("can't download GeoSite.dat: %s", err.Error())
} }
log.Infoln("Download GeoSite.dat finish") 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 {
if C.GeodataMode {
if _, err := os.Stat(C.Path.GeoIP()); os.IsNotExist(err) {
log.Infoln("Can't find GeoIP.dat, start download")
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
log.Infoln("Download GeoIP.dat finish")
}
if !geodata.Verify(C.GeoipName) {
log.Warnln("GeoIP.dat invalid, remove and download")
if err := os.Remove(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error())
}
if err := downloadGeoIP(C.Path.GeoIP()); err != nil {
return fmt.Errorf("can't download GeoIP.dat: %s", err.Error())
}
}
return nil
}
if _, err := os.Stat(C.Path.MMDB()); os.IsNotExist(err) {
log.Infoln("Can't find MMDB, start download")
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
if !mmdb.Verify() {
log.Warnln("MMDB invalid, remove and download")
if err := os.Remove(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't remove invalid MMDB: %s", err.Error())
}
if err := downloadMMDB(C.Path.MMDB()); err != nil {
return fmt.Errorf("can't download MMDB: %s", err.Error())
}
}
return nil return nil
} }
@ -98,15 +149,19 @@ func Init(dir string) error {
f.Write([]byte(`mixed-port: 7890`)) f.Write([]byte(`mixed-port: 7890`))
f.Close() f.Close()
} }
buf, _ := os.ReadFile(C.Path.Config())
//// initial GeoIP rawCfg, err := UnmarshalRawConfig(buf)
//if err := initGeoIP(); err != nil { if err != nil {
// return fmt.Errorf("can't initial GeoIP: %w", err) log.Errorln(err.Error())
//} fmt.Printf("configuration file %s test failed\n", C.Path.Config())
os.Exit(1)
// initial mmdb }
if err := initMMDB(); err != nil { if !C.GeodataMode {
return fmt.Errorf("can't initial MMDB: %w", err) C.GeodataMode = rawCfg.GeodataMode
}
// initial GeoIP
if err := initGeoIP(); err != nil {
return fmt.Errorf("can't initial GeoIP: %w", err)
} }
return nil return nil

View File

@ -18,13 +18,13 @@ func trimArr(arr []string) (r []string) {
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order. // Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
// Meanwhile, record the original index in the config file. // Meanwhile, record the original index in the config file.
// If loop is detected, return an error with location of loop. // If loop is detected, return an error with location of loop.
func proxyGroupsDagSort(groupsConfig []map[string]interface{}) error { func proxyGroupsDagSort(groupsConfig []map[string]any) error {
type graphNode struct { type graphNode struct {
indegree int indegree int
// topological order // topological order
topo int topo int
// the original data in `groupsConfig` // the original data in `groupsConfig`
data map[string]interface{} data map[string]any
// `outdegree` and `from` are used in loop locating // `outdegree` and `from` are used in loop locating
outdegree int outdegree int
option *outboundgroup.GroupCommonOption option *outboundgroup.GroupCommonOption

View File

@ -14,6 +14,14 @@ const (
Direct AdapterType = iota Direct AdapterType = iota
Reject Reject
Compatible Compatible
Pass
Relay
Selector
Fallback
URLTest
LoadBalance
Shadowsocks Shadowsocks
ShadowsocksR ShadowsocksR
Snell Snell
@ -22,12 +30,6 @@ const (
Vmess Vmess
Vless Vless
Trojan Trojan
Relay
Selector
Fallback
URLTest
LoadBalance
) )
const ( const (
@ -131,6 +133,8 @@ func (at AdapterType) String() string {
return "Reject" return "Reject"
case Compatible: case Compatible:
return "Compatible" return "Compatible"
case Pass:
return "Pass"
case Shadowsocks: case Shadowsocks:
return "Shadowsocks" return "Shadowsocks"
case ShadowsocksR: case ShadowsocksR:

View File

@ -21,7 +21,7 @@ const (
type DNSMode int type DNSMode int
// UnmarshalYAML unserialize EnhancedMode with yaml // UnmarshalYAML unserialize EnhancedMode with yaml
func (e *DNSMode) UnmarshalYAML(unmarshal func(interface{}) error) error { func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error {
var tp string var tp string
if err := unmarshal(&tp); err != nil { if err := unmarshal(&tp); err != nil {
return err return err
@ -35,7 +35,7 @@ func (e *DNSMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
// MarshalYAML serialize EnhancedMode with yaml // MarshalYAML serialize EnhancedMode with yaml
func (e DNSMode) MarshalYAML() (interface{}, error) { func (e DNSMode) MarshalYAML() (any, error) {
return e.String(), nil return e.String(), nil
} }

3
constant/geodata.go Normal file
View File

@ -0,0 +1,3 @@
package constant
var GeodataMode bool

View File

@ -73,20 +73,22 @@ func (t Type) MarshalJSON() ([]byte, error) {
// Metadata is used to store connection address // Metadata is used to store connection address
type Metadata struct { type Metadata struct {
NetWork NetWork `json:"network"` NetWork NetWork `json:"network"`
Type Type `json:"type"` Type Type `json:"type"`
SrcIP net.IP `json:"sourceIP"` SrcIP net.IP `json:"sourceIP"`
DstIP net.IP `json:"destinationIP"` DstIP net.IP `json:"destinationIP"`
SrcPort string `json:"sourcePort"` SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"` DstPort string `json:"destinationPort"`
AddrType int `json:"-"` AddrType int `json:"-"`
Host string `json:"host"` Host string `json:"host"`
Process string `json:"process"` DNSMode DNSMode `json:"dnsMode"`
DNSMode DNSMode `json:"dnsMode"` Process string `json:"process"`
ProcessPath string `json:"processPath"`
} }
func (m *Metadata) RemoteAddress() string { func (m *Metadata) RemoteAddress() string {
return net.JoinHostPort(m.String(), m.DstPort) return net.JoinHostPort(m.String(), m.DstPort)
} }
func (m *Metadata) SourceAddress() string { func (m *Metadata) SourceAddress() string {

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