Compare commits

..

170 commits

Author SHA1 Message Date
wwqgtxx
d5243adf89 chore: better global-client-fingerprint handle 2025-04-19 02:04:09 +08:00
wwqgtxx
6236cb1cf0 chore: cleanup trojan code 2025-04-19 01:32:55 +08:00
wwqgtxx
619c9dc0c6 chore: apply the default interface/mark of the dialer in the final stage 2025-04-18 20:16:51 +08:00
wwqgtxx
9c5067e519 action: disable MinGW's path conversion in test 2025-04-18 19:48:22 +08:00
wwqgtxx
feee9b320c chore: remove unneeded tls timeout in anytls 2025-04-18 16:59:53 +08:00
wwqgtxx
63e66f49ca chore: cleanup trojan code 2025-04-18 16:59:28 +08:00
wwqgtxx
bad61f918f fix: avoid panic in inbound test 2025-04-18 11:40:37 +08:00
wwqgtxx
69ce4d0f8c chore: speed up inbound test 2025-04-17 23:40:46 +08:00
wwqgtxx
b59f11f7ac chore: add singMux inbound test for shadowsocks/trojan/vless/vmess 2025-04-17 21:07:35 +08:00
wwqgtxx
30d90d49f0 chore: update option checks to use IsZeroOptions 2025-04-17 21:06:55 +08:00
wwqgtxx
76052b5b26 fix: grpc in trojan not apply client-fingerprint 2025-04-17 12:54:36 +08:00
wwqgtxx
7d7f5c8980 chore: add inbound test for tuic 2025-04-17 10:02:48 +08:00
wwqgtxx
e79465d306 chore: add inbound test for hysteria2 2025-04-17 09:26:12 +08:00
wwqgtxx
345d3d7052 chore: add inbound test for anytls 2025-04-17 09:01:26 +08:00
wwqgtxx
3d806b5e4c chore: add inbound test for shadowsocks/trojan 2025-04-17 01:36:14 +08:00
wwqgtxx
b5fcd1d1d1 fix: chacha8-ietf-poly1305 not work 2025-04-17 00:11:24 +08:00
wwqgtxx
b21b8ee046 fix: panic in ssr packet 2025-04-16 22:22:56 +08:00
wwqgtxx
d0d0c392d7 chore: add inbound test for vmess/vless 2025-04-16 20:44:48 +08:00
wwqgtxx
a75e570cca fix: vision conn read short buffer error 2025-04-16 20:38:10 +08:00
wwqgtxx
9e0889c02c fix: observable test 2025-04-16 13:16:11 +08:00
wwqgtxx
55cbbf7f41 fix: singledo test 2025-04-16 13:13:01 +08:00
wwqgtxx
664b134015 fix: websocket data losing 2025-04-16 13:02:50 +08:00
wwqgtxx
ba3c44a169 chore: code cleanup 2025-04-16 09:54:02 +08:00
wwqgtxx
dcb20e2824 fix: websocket server upgrade in golang1.20 2025-04-16 08:47:44 +08:00
wwqgtxx
3d2cb992fa fix: grpc outbound not apply ca fingerprint 2025-04-16 01:00:06 +08:00
wwqgtxx
984535f006 action: run tests on more platforms 2025-04-15 22:02:40 +08:00
wwqgtxx
8fa4e8122c chore: remove internal crypto/tls fork in reality server 2025-04-13 03:03:28 +08:00
wwqgtxx
7551c8a545 chore: remove unneed code 2025-04-12 23:42:57 +08:00
wwqgtxx
237e2edea4 chore: tun will add firewall rule for Profile ALL on windows system stack 2025-04-12 22:46:26 +08:00
wwqgtxx
fe01033efe chore: quic sniffer should use the exact length of crypto stream when assembling 2025-04-12 22:27:07 +08:00
wwqgtxx
84cd0ef688 chore: remove internal crypto/tls fork in shadowtls 2025-04-12 20:28:26 +08:00
wwqgtxx
cedb36df5f chore: using SetupContextForConn to reduce the DialContext cannot be cancelled 2025-04-12 11:19:03 +08:00
HiMetre
7a260f7bcf fix: udp dial support ip4p (#1377) 2025-04-11 09:20:58 +08:00
wwqgtxx
8085c68b6d chore: decrease direct using *net.TCPConn 2025-04-11 00:33:07 +08:00
wwqgtxx
dbb5b7db1c fix: SetupContextForConn should return context error to user 2025-04-11 00:03:46 +08:00
wwqgtxx
bfd06ebad0 chore: rebuild SetupContextForConn with context.AfterFunc 2025-04-10 01:29:55 +08:00
wwqgtxx
e8af058694 fix: websocketWithEarlyDataConn can't close underlay conn when is dialing or not dialed 2025-04-10 00:13:14 +08:00
wwqgtxx
487d7fa81f fix: panic under some stupid input config 2025-04-09 18:02:13 +08:00
wwqgtxx
4b15568a29 chore: cleanup metadata code 2025-04-09 18:02:13 +08:00
wwqgtxx
cac2bf72e1 chore: cleanup netip code 2025-04-09 18:02:13 +08:00
wwqgtxx
b2d2890866 chore: cleanup resolveUDPAddr code 2025-04-09 18:02:12 +08:00
anytls
8752f80595 fix: anytls stream read error (#1970)
Co-authored-by: anytls <anytls>
2025-04-09 18:02:12 +08:00
wwqgtxx
a6c0c02e0d chore: ignore interfaces not in IfOperStatusUp when fetch system dns server on windows 2025-04-09 18:02:12 +08:00
wwqgtxx
2acb0b71ee fix: tun IncludeInterface/ExcludeInterface priority 2025-04-08 19:20:29 +08:00
wwqgtxx
2a40eba0ca feat: tun add exclude-src-port,exclude-src-port-range,exclude-dst-port and exclude-dst-port-range on linux 2025-04-08 19:07:39 +08:00
okhowang
a22efd5c91
feat: add exclude port and exclude port range options (#1951)
Fixes #1769
2025-04-08 12:10:30 +08:00
wwqgtxx
9e8f4ada47 chore: better addr parsing 2025-04-06 10:43:21 +08:00
wwqgtxx
09c7ee0d12 fix: grpc server panic 2025-04-06 10:12:57 +08:00
wwqgtxx
2a08c44f51 action: fix run build on pull_request 2025-04-05 10:48:07 +08:00
wwqgtxx
190047c8c0 fix: grpc transport not apply dial timeout 2025-04-04 21:05:54 +08:00
wwqgtxx
24a9ff6d03 fix: disallow dialFunc be called after grpc transport has be closed 2025-04-04 13:33:00 +08:00
wwqgtxx
efa224373f fix: shut it down more aggressively in grpc transport closing 2025-04-04 11:54:19 +08:00
wwqgtxx
b0bd4f4caf fix: resources not released when hysteria2 verification failed 2025-04-04 11:12:08 +08:00
wwqgtxx
eaaccffcef fix: race in Single.Do 2025-04-04 10:55:16 +08:00
wwqgtxx
e81f3a97af fix: correctly implement references to proxies 2025-04-04 09:08:52 +08:00
wwqgtxx
323973f22f fix: converter judgment conditions 2025-04-04 00:22:52 +08:00
5aaee9
ed7533ca1a
fix: tproxy high cpu usage (#1957) 2025-04-03 23:52:19 +08:00
wwqgtxx
7de24e26b4 fix: StreamGunWithConn not synchronously close the incoming net.Conn 2025-04-03 23:41:24 +08:00
wwqgtxx
622d99d000 chore: rebuild outdated proxy auto close mechanism 2025-04-03 22:42:32 +08:00
wwqgtxx
7f1225b0c4 fix: grpc transport can't be closed 2025-04-03 22:41:05 +08:00
wwqgtxx
23ffe451f4 chore: using http/httptrace to get local/remoteAddr for grpc client 2025-04-03 19:47:49 +08:00
wwqgtxx
7b37fcfc8d fix: auto_redirect should only hijack DNS requests from local addresses 2025-04-02 23:47:34 +08:00
wwqgtxx
daa592c7f3 fix: converter panic 2025-04-02 21:13:46 +08:00
wwqgtxx
577f64a601 fix: X25519MLKEM768 does not work properly with reality 2025-04-02 14:39:07 +08:00
wwqgtxx
025ff19fab fix: wrong conditional judgment in removeExtraHTTPHostPort
https://github.com/MetaCubeX/mihomo/issues/1939
2025-03-28 11:08:01 +08:00
anytls
f61534602b
chore: anytls protocol version 2 (#1936) 2025-03-27 20:25:31 +08:00
wwqgtxx
7b382611bb chore: update gvisor 2025-03-25 01:19:39 +08:00
enfein
0f32c054f4
feat: support UDP over TCP in mieru (#1926) 2025-03-20 13:58:04 +08:00
5aaee9
4f8b70c8c6
fix: buffer in tproxy not recycle (#1923) 2025-03-19 12:20:48 +08:00
wwqgtxx
dcef78782b chore: update utls 2025-03-18 10:06:53 +08:00
wwqgtxx
7c444a91d3 fix: correctly handle ipv6 zone 2025-03-17 23:51:21 +08:00
wwqgtxx
e3d4ec2476 fix: race at interfaceName setting 2025-03-17 14:00:51 +08:00
xishang0128
14217e7847 chore: update service capabilities to include CAP_SYS_TIME and CAP_DAC_OVERRIDE 2025-03-17 13:21:23 +08:00
wwqgtxx
68abb1348a chore: support longest-prefix matches in local interface finding 2025-03-17 11:10:27 +08:00
Cesaryuan
dee5898e36
fix: memory leak due to unclosed session (#1908) 2025-03-15 13:27:29 +08:00
wwqgtxx
1e22f4daa9 chore: reduce data copying in quic sniffer and better handle data fragmentation and overlap 2025-03-14 13:14:42 +08:00
wwqgtxx
a7a796bb30 chore: cleanup quic sniff's code 2025-03-13 16:29:07 +08:00
Cesaryuan
ff89bf0ea0
feat: add gost-plugin in which only ws and mws are currently supported. (#1896) 2025-03-13 13:28:40 +08:00
5aaee9
801f3c35ce
feat: support sniff quic fragment data (#1899) 2025-03-13 13:19:36 +08:00
wwqgtxx
7ff046a455 chore: modify UDPSniff's function signature to prepare for its ability to handle multiple packets. 2025-03-13 08:52:27 +08:00
wwqgtxx
0ed159e41d chore: code cleanup 2025-03-12 13:33:52 +08:00
wwqgtxx
070eb3142b chore: speedup system stack in tun 2025-03-12 12:27:41 +08:00
wwqgtxx
f318b80557 chore: better cache implement for group's getProxies 2025-03-11 23:27:18 +08:00
wwqgtxx
c0de3c0e42 fix: some default value in dialer not restore in tun when config reload 2025-03-10 11:10:39 +08:00
wwqgtxx
4bd3ae52bd chore: dialer will consider the routing of the local interface when auto-detect-interface in tun is enabled
for #1881 #1819
2025-03-10 10:45:31 +08:00
Skyxim
00e6466153 chore: update checksum generation step 2025-03-10 09:13:38 +08:00
Skyxim
c94b4421e5 chore: add checksum generation for production artifacts 2025-03-10 09:02:08 +08:00
ForestL
8bc6f77e36
fix DEB packaging (#1868) 2025-03-03 11:37:36 +08:00
anytls
a7e56f1c43
fix: anytls client close (#1871)
Co-authored-by: anytls <anytls>
2025-03-02 10:47:10 +08:00
wwqgtxx
05e8f13a8d fix: integer overflow in ports iteration 2025-02-28 15:48:25 +08:00
wwqgtxx
136d114196 feat: socks5/http/mixed inbound support setting tls in listeners 2025-02-28 13:13:53 +08:00
wwqgtxx
938ab7f44d fix: syscall packet read waiter for windows 2025-02-28 12:18:59 +08:00
wwqgtxx
a00f4f1108 fix: vless inbound allow not use flow when request send empty flow 2025-02-28 08:30:36 +08:00
wwqgtxx
1213023f11 fix: reality not work with vmess+grpc outbound 2025-02-28 08:24:22 +08:00
wwqgtxx
3b40bf76b7 fix: grpc server's ALPN order 2025-02-27 22:12:49 +08:00
wwqgtxx
1dc4155195 feat: inbound's port can use ports format 2025-02-27 09:59:09 +08:00
wwqgtxx
d81c19a7c8 fix: grpc server panic 2025-02-26 13:17:26 +08:00
anytls
e2140e62ca
chore: update anytls (#1863)
Co-authored-by: anytls <anytls>
2025-02-26 11:17:12 +08:00
wwqgtxx
8d783c65c1 feat: inbound support grpc(lite) 2025-02-26 11:00:11 +08:00
wwqgtxx
91324b76d2 feat: inbound support trojan 2025-02-25 10:30:27 +08:00
wwqgtxx
e23f40a56b chore: tradition shadowsocks server could handle smux 2025-02-24 16:27:20 +08:00
Larvan2
5830afcbde chore: add MinIdleSession option to AnyTLS configuration 2025-02-21 13:30:24 +08:00
anytls
e2b75b35bb
chore: update anytls (#1851)
* Implement deadline for `Stream`

* chore: code cleanup

* fix: buffer release

* fix: do not use buffer for `cmdUpdatePaddingScheme`

---------

Co-authored-by: anytls <anytls>
2025-02-19 15:54:56 +08:00
wwqgtxx
06b9e6c367 chore: update dependencies 2025-02-19 08:55:51 +08:00
anytls
dc1145a484
fix: anytls padding send (#1848)
Co-authored-by: anytls <anytls>
2025-02-17 21:18:40 +08:00
wwqgtxx
b151e7d69c chore: support fingerprint for anytls 2025-02-17 20:14:54 +08:00
wwqgtxx
808fdcf624 chore: code cleanup 2025-02-17 19:43:58 +08:00
anytls
9962a0d091
feat: implement anytls client and server (#1844) 2025-02-17 18:51:11 +08:00
ForestL
ef29e4501e
chore: complete classical rule parse error log (#1839) 2025-02-13 17:25:45 +08:00
wwqgtxx
d1d846f1ab fix: s390x golang1.24 build 2025-02-13 08:50:53 +08:00
wwqgtxx
eaaccbc6dd chore: update dependencies 2025-02-13 00:34:37 +08:00
wwqgtxx
447c416391 chore: update quic-go to 0.49.0 2025-02-13 00:22:56 +08:00
wwqgtxx
6d24ca9ae6 action: remove 32-bit windows/arm build
windows/arm32 has been broken since Go 1.17
2025-02-12 23:23:05 +08:00
wwqgtxx
b52b7537fc action: force s390x build using golang1.23 2025-02-12 23:22:16 +08:00
wwqgtxx
3cc67fd759 chore: update golang to 1.24 2025-02-12 23:07:05 +08:00
5aaee9
9074b78e36
chore(dns): increase MaxConnsPerHost (#1834) 2025-02-10 15:21:18 +08:00
clash-meta-maintainer[bot]
ccc3f84da2 license: any downstream projects not affiliated with MetaCubeX shall not contain the word mihomo in their names 2025-02-08 23:37:04 +08:00
ForestL
9bfb10d7ae
chore: extracting compressed files to correct location (#1823) 2025-02-05 10:10:58 +08:00
wwqgtxx
0a5ea37c07 chore: update dependencies 2025-02-04 15:28:24 +08:00
wwqgtxx
a440f64080 chore: alignment capability for vmess inbound 2025-02-04 15:28:24 +08:00
wwqgtxx
0ac6c3b185 feat: inbound support vless 2025-02-04 00:44:18 +08:00
wwqgtxx
b69e52d4d7 chore: deprecated routing-mark and interface-name of the group, please set it directly on the proxy instead 2025-01-21 00:45:49 +08:00
wwqgtxx
9c73b5b750 fix: the trustcerts not add to globalCerts after ca.ResetCertificate (#1801)
support PEM format for custom-certificates too
2025-01-20 23:01:26 +08:00
wwqgtxx
fc233184fd feat: add receive window config for hy2
https://github.com/MetaCubeX/mihomo/issues/1796
2025-01-19 09:56:16 +08:00
tnextday
192d769f75
chore: ensure forced domains are always sniffed (#1793)
When a domain matches forceDomain:
- SkipList is not checked
- Failed attempts are not cached
- Sniffing is attempted every time

This ensures forced domains are always sniffed regardless of previous failures.
2025-01-16 10:17:32 +08:00
wwqgtxx
c99c71a969 chore: listening tcp together for dns server (#1792) 2025-01-16 10:16:37 +08:00
lucidhz
c7661d7765 fix: initialize error message with cipher (#1760) 2025-01-07 14:28:56 +08:00
Mossia
56c128880c
fix: empty proxy provider subscription info not omitted (#1759) 2025-01-07 13:26:56 +08:00
enfein
f4806b49b4
chore: update mieru version (#1762) 2025-01-07 13:25:32 +08:00
J.K.SAGE
49d54cc293
fix: remote conn statistic error (#1776)
TCP handshake traffic should be counted as upload traffic for the remote connection
2025-01-07 13:23:05 +08:00
wwqgtxx
1c5f4a3ab1 chore: update dependencies 2024-12-31 16:42:33 +08:00
wwqgtxx
368b1e1296 chore: rollback tfo-go version 2024-12-30 22:33:13 +08:00
wwqgtxx
a9ce5da09d fix: key missing for tun inbound
https://github.com/MetaCubeX/mihomo/issues/1672
2024-12-28 11:39:45 +08:00
wwqgtxx
301c78ff9a chore: update sing-tun to v0.4.5 2024-12-26 10:50:08 +08:00
wwqgtxx
72a126e580 feat: support inline proxy provider 2024-12-25 10:34:16 +08:00
wwqgtxx
20739f5db7 chore: code cleanup 2024-12-25 10:34:16 +08:00
valord577
89dfabe9b3
chore: align time fields in logs (#1704)
ref: A comma or decimal point followed by one or more zeros represents a fractional second, printed to the given number of decimal places. A comma or decimal point followed by one or more nines represents a fractional second, printed to the given number of decimal places, with trailing zeros removed. For example "15:04:05,000" or "15:04:05.000" formats or parses with millisecond precision.
2024-12-19 15:55:47 +08:00
wwqgtxx
5a9ad0ed3c chore: code cleanup 2024-12-19 09:29:17 +08:00
qianlongzt
bb803249fa
feat: support inline rule provider (#1731) 2024-12-19 09:16:45 +08:00
wwqgtxx
3f6823ba49 fix: handle invalid values in Decoder's decode method 2024-12-16 09:26:11 +08:00
wwqgtxx
c786b72030 chore: update dependencies 2024-12-14 13:27:28 +08:00
wwqgtxx
269c52575c chore: update gopsutil to v4 2024-12-14 11:09:51 +08:00
laburaps
c7fc93df37
fix: the TLS Sniffer fails when the length of the ClientHello packet exceeds the TCP MSS (#1711)
* chore: add uniformly formatted debug info to sniffDomain

* fix: when data is not enough, attempt to peek more data and retry

* chore: reduce debug info of sniffDomain
2024-12-12 19:02:34 +08:00
laburaps
5d9d8f4d3b
fix: check whether the dst port is within the specified range (#1706) 2024-12-10 16:15:08 +08:00
wwqgtxx
f3a43fe3a6 feat: support read config file from stdin
via `-f -`
2024-12-10 09:57:20 +08:00
wwqgtxx
9a959202ed chore: support config multiplexing of mieru 2024-12-10 09:19:59 +08:00
enfein
cd23112dc5
chore: remove gRPC dependency from mieru (#1705) 2024-12-10 08:03:17 +08:00
enfein
613becd8ea
feat: support mieru protocol (#1702) 2024-12-09 12:05:11 +08:00
hingbong
d6b496d3c0
chore: allow upgrade ui in embed mode (#1692) 2024-12-04 08:54:01 +08:00
ForestL
5a24efdabf
fix: DisableKeepAlive default value of android (#1690) 2024-12-02 22:49:16 +08:00
wwqgtxx
9de9f1ef51 fix: don't panic when listen on localhost
https://github.com/MetaCubeX/mihomo/issues/1655
2024-11-27 11:03:38 +08:00
wwqgtxx
fbead56ec9 feat: add size-limit for provider
https://github.com/MetaCubeX/mihomo/issues/1645
2024-11-27 09:28:38 +08:00
wwqgtxx
1fff34d30e chore: update quic-go to 0.48.2 2024-11-26 13:39:54 +08:00
wwqgtxx
a35f712478 chore: update gvisor 2024-11-26 10:28:07 +08:00
wwqgtxx
f805a9f4c6 chore: cleaned up some weird code 2024-11-26 10:04:41 +08:00
xishang0128
eb985b002e chore: restful api displays more information 2024-11-21 22:50:54 +08:00
wwqgtxx
462343531e chore: update sing-tun to v0.4.1 2024-11-21 11:06:25 +08:00
wwqgtxx
671d901ee2 ci: align loongarch golang version when it is not abi1 2024-11-18 10:41:15 +08:00
wwqgtxx
80e4eaad14 fix: process IPv6 Link-Local address (#1657) 2024-11-18 10:34:43 +08:00
xishang0128
25b3c86d31 ci: update loongarch golang and android ndk 2024-11-17 23:31:46 +08:00
Chenx Dust
de19f927e8 chore: restful api display smux and mptcp 2024-11-14 10:08:02 +08:00
Larvan2
792f16265e fix: find process panic 2024-11-08 16:29:32 +08:00
wwqgtxx
215bf0995f chore: switch syscall.SyscallN back to syscall.Syscall6
Until the current version, SyscallN always escapes the variadic argument
2024-11-08 09:40:38 +08:00
wwqgtxx
91d54bdac1 fix: android tun start error 2024-11-06 20:04:14 +08:00
wwqgtxx
ce52c3438b chore: cleaned up some confusing code 2024-11-05 10:03:21 +08:00
wwqgtxx
d4478dbfa2 chore: reduce the performance overhead of not enabling LoopBackDetector 2024-11-05 09:29:56 +08:00
wwqgtxx
69454b030e chore: allow disabled overrideAndroidVPN by environment variable DISABLE_OVERRIDE_ANDROID_VPN 2024-11-05 09:15:30 +08:00
wwqgtxx
e6d1c8cedf chore: update sing-tun to v0.4.0-rc.5 2024-11-05 09:12:20 +08:00
wwqgtxx
fabd216c34 chore: update quic-go to 0.48.1 2024-11-05 08:58:41 +08:00
xishang0128
a86c562852 chore: Increase support for other format of ASN 2024-11-04 19:31:43 +08:00
218 changed files with 10432 additions and 2362 deletions

View file

@ -1,17 +1,17 @@
[Unit] [Unit]
Description=mihomo Daemon, Another Clash Kernel. Description=mihomo Daemon, Another Clash Kernel.
After=network.target NetworkManager.service systemd-networkd.service iwd.service Documentation=https://wiki.metacubex.one
After=network.target nss-lookup.target network-online.target
[Service] [Service]
Type=simple Type=simple
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
Restart=always
ExecStartPre=/usr/bin/sleep 2s
ExecStart=/usr/bin/mihomo -d /etc/mihomo ExecStart=/usr/bin/mihomo -d /etc/mihomo
ExecReload=/bin/kill -HUP $MAINPID ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
LimitNOFILE=infinity
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

17
.github/mihomo@.service vendored Normal file
View file

@ -0,0 +1,17 @@
[Unit]
Description=mihomo Daemon, Another Clash Kernel.
Documentation=https://wiki.metacubex.one
After=network.target nss-lookup.target network-online.target
[Service]
Type=simple
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
ExecStart=/usr/bin/mihomo -d /etc/mihomo
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
LimitNOFILE=infinity
[Install]
WantedBy=multi-user.target

View file

@ -14,7 +14,7 @@ on:
- Alpha - Alpha
tags: tags:
- "v*" - "v*"
pull_request_target: pull_request:
branches: branches:
- Alpha - Alpha
concurrency: concurrency:
@ -54,7 +54,6 @@ jobs:
- { goos: windows, goarch: '386', output: '386' } - { goos: windows, goarch: '386', output: '386' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 } - { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: windows, goarch: arm, goarm: '7', output: armv7 }
- { goos: windows, goarch: arm64, output: arm64 } - { goos: windows, goarch: arm64, output: arm64 }
- { goos: freebsd, goarch: '386', output: '386' } - { goos: freebsd, goarch: '386', output: '386' }
@ -67,6 +66,12 @@ jobs:
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
# Go 1.23 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
- { goos: windows, goarch: '386', output: '386-go123', goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' }
# Go 1.22 with special patch can work on Windows 7 # Go 1.22 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.22/ # https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
- { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' } - { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' }
@ -95,6 +100,11 @@ jobs:
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
# Go 1.23 is the last release that requires Linux kernel version 2.6.32 or later. Go 1.24 will require Linux kernel version 3.2 or later.
- { goos: linux, goarch: '386', output: '386-go123', goversion: '1.23' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23', test: test }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' }
# only for test # only for test
- { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' } - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test } - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test }
@ -104,30 +114,41 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }} if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Set up Go - name: Set up Go
if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }} if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.jobs.goversion }} go-version: ${{ matrix.jobs.goversion }}
- name: Set up Go1.22 loongarch abi1 - name: Set up Go1.23 loongarch abi1
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
run: | run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.22.4/go1.22.4.linux-amd64-abi1.tar.gz wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.23.0/go1.23.0.linux-amd64-abi1.tar.gz
sudo tar zxf go1.22.4.linux-amd64-abi1.tar.gz -C /usr/local sudo tar zxf go1.23.0.linux-amd64-abi1.tar.gz -C /usr/local
echo "/usr/local/go/bin" >> $GITHUB_PATH echo "/usr/local/go/bin" >> $GITHUB_PATH
- name: Set up Go1.22 loongarch abi2 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }} # this patch file only works on golang1.24.x
# that means after golang1.25 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.24 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
run: | run: |
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.22.4/go1.22.4.linux-amd64-abi2.tar.gz cd $(go env GOROOT)
sudo tar zxf go1.22.4.linux-amd64-abi2.tar.gz -C /usr/local curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1
echo "/usr/local/go/bin" >> $GITHUB_PATH curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x # this patch file only works on golang1.23.x
@ -139,7 +160,7 @@ jobs:
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.23 commit for Windows7/8 - name: Revert Golang1.23 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }} if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.23' }}
run: | run: |
cd $(go env GOROOT) cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
@ -194,7 +215,7 @@ jobs:
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
id: setup-ndk id: setup-ndk
with: with:
ndk-version: r27 ndk-version: r28-beta1
- name: Set NDK path - name: Set NDK path
if: ${{ matrix.jobs.goos == 'android' }} if: ${{ matrix.jobs.goos == 'android' }}
@ -255,17 +276,20 @@ jobs:
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo
mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/lib/systemd/system
cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin/mihomo cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin/
cp LICENSE mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo/ cp LICENSE mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo/
cp .github/mihomo.service mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/ cp .github/{mihomo.service,mihomo@.service} mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/lib/systemd/system/
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo/config.yaml <<EOF cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo/config.yaml <<EOF
mixed-port: 7890 mixed-port: 7890
external-controller: 127.0.0.1:9090 EOF
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/conffiles <<EOF
/etc/mihomo/config.yaml
EOF EOF
cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control <<EOF cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control <<EOF
@ -305,7 +329,6 @@ jobs:
run: | run: |
echo ${VERSION} > version.txt echo ${VERSION} > version.txt
shell: bash shell: bash
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
@ -316,6 +339,7 @@ jobs:
mihomo*.rpm mihomo*.rpm
mihomo*.zip mihomo*.zip
version.txt version.txt
checksums.txt
Upload-Prerelease: Upload-Prerelease:
permissions: write-all permissions: write-all
@ -329,6 +353,13 @@ jobs:
path: bin/ path: bin/
merge-multiple: true merge-multiple: true
- name: Calculate checksums
run: |
cd bin/
find . -type f -not -name "checksums.*" -not -name "version.txt" | sort | xargs sha256sum > checksums.txt
cat checksums.txt
shell: bash
- name: Delete current release assets - name: Delete current release assets
uses: 8Mi-Tech/delete-release-assets-action@main uses: 8Mi-Tech/delete-release-assets-action@main
with: with:

115
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,115 @@
name: Test
on:
push:
paths-ignore:
- "docs/**"
- "README.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- Alpha
tags:
- "v*"
pull_request:
branches:
- Alpha
jobs:
test:
strategy:
matrix:
os:
- 'ubuntu-latest' # amd64 linux
- 'windows-latest' # amd64 windows
- 'macos-latest' # arm64 macos
- 'ubuntu-24.04-arm' # arm64 linux
- 'macos-13' # amd64 macos
go-version:
- '1.24'
- '1.23'
- '1.22'
- '1.21'
- '1.20'
fail-fast: false
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
env:
CGO_ENABLED: 0
GOTOOLCHAIN: local
# Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919
MSYS_NO_PATHCONV: true
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.24.x
# that means after golang1.25 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.24 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.24' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.23 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.23' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.22.x
# that means after golang1.23 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.22 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.22' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
- name: Revert Golang1.21 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.21' }}
run: |
cd $(go env GOROOT)
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
- name: Test
run: go test ./... -v -count=1
- name: Test with tag with_gvisor
run: go test ./... -v -count=1 -tags "with_gvisor"

View file

@ -98,3 +98,4 @@ API.
This software is released under the GPL-3.0 license. This software is released under the GPL-3.0 license.
**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.**

View file

@ -163,8 +163,17 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping["alive"] = p.alive.Load() mapping["alive"] = p.alive.Load()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP() mapping["uot"] = p.SupportUOT()
mapping["tfo"] = p.SupportTFO()
proxyInfo := p.ProxyInfo()
mapping["xudp"] = proxyInfo.XUDP
mapping["tfo"] = proxyInfo.TFO
mapping["mptcp"] = proxyInfo.MPTCP
mapping["smux"] = proxyInfo.SMUX
mapping["interface"] = proxyInfo.Interface
mapping["dialer-proxy"] = proxyInfo.DialerProxy
mapping["routing-mark"] = proxyInfo.RoutingMark
return json.Marshal(mapping) return json.Marshal(mapping)
} }
@ -281,6 +290,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{ return &Proxy{
ProxyAdapter: adapter, ProxyAdapter: adapter,

View file

@ -34,12 +34,5 @@ func SkipAuthRemoteAddress(addr string) bool {
} }
func skipAuth(addr netip.Addr) bool { func skipAuth(addr netip.Addr) bool {
if addr.IsValid() { return prefixesContains(skipAuthPrefixes, addr)
for _, prefix := range skipAuthPrefixes {
if prefix.Contains(addr.Unmap()) {
return true
}
}
}
return false
} }

View file

@ -31,27 +31,17 @@ func IsRemoteAddrDisAllowed(addr net.Addr) bool {
if err := m.SetRemoteAddr(addr); err != nil { if err := m.SetRemoteAddr(addr); err != nil {
return false return false
} }
return isAllowed(m.AddrPort().Addr().Unmap()) && !isDisAllowed(m.AddrPort().Addr().Unmap()) ipAddr := m.AddrPort().Addr()
if ipAddr.IsValid() {
return isAllowed(ipAddr) && !isDisAllowed(ipAddr)
}
return false
} }
func isAllowed(addr netip.Addr) bool { func isAllowed(addr netip.Addr) bool {
if addr.IsValid() { return prefixesContains(lanAllowedIPs, addr)
for _, prefix := range lanAllowedIPs {
if prefix.Contains(addr) {
return true
}
}
}
return false
} }
func isDisAllowed(addr netip.Addr) bool { func isDisAllowed(addr netip.Addr) bool {
if addr.IsValid() { return prefixesContains(lanDisAllowedIPs, addr)
for _, prefix := range lanDisAllowedIPs {
if prefix.Contains(addr) {
return true
}
}
}
return false
} }

View file

@ -2,7 +2,9 @@ package inbound
import ( import (
"context" "context"
"fmt"
"net" "net"
"net/netip"
"sync" "sync"
"github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/keepalive"
@ -41,7 +43,36 @@ func MPTCP() bool {
return getMultiPathTCP(&lc.ListenConfig) return getMultiPathTCP(&lc.ListenConfig)
} }
func preResolve(network, address string) (string, error) {
switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6":
if host, port, err := net.SplitHostPort(address); err == nil {
switch host {
case "localhost":
switch network {
case "tcp6", "udp6", "ip6":
address = net.JoinHostPort("::1", port)
default:
address = net.JoinHostPort("127.0.0.1", port)
}
case "": // internetAddrList can handle this special case
break
default:
if _, err := netip.ParseAddr(host); err != nil { // not ip
return "", fmt.Errorf("invalid network address: %s", address)
}
}
}
}
return address, nil
}
func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { func ListenContext(ctx context.Context, network, address string) (net.Listener, error) {
address, err := preResolve(network, address)
if err != nil {
return nil, err
}
mutex.RLock() mutex.RLock()
defer mutex.RUnlock() defer mutex.RUnlock()
return lc.Listen(ctx, network, address) return lc.Listen(ctx, network, address)
@ -51,6 +82,21 @@ func Listen(network, address string) (net.Listener, error) {
return ListenContext(context.Background(), network, address) return ListenContext(context.Background(), network, address)
} }
func ListenPacketContext(ctx context.Context, network, address string) (net.PacketConn, error) {
address, err := preResolve(network, address)
if err != nil {
return nil, err
}
mutex.RLock()
defer mutex.RUnlock()
return lc.ListenPacket(ctx, network, address)
}
func ListenPacket(network, address string) (net.PacketConn, error) {
return ListenPacketContext(context.Background(), network, address)
}
func init() { func init() {
keepalive.SetDisableKeepAliveCallback.Register(func(b bool) { keepalive.SetDisableKeepAliveCallback.Register(func(b bool) {
mutex.Lock() mutex.Lock()

View file

@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/metacubex/mihomo/common/nnip"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/socks5"
) )
@ -21,13 +20,13 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4: case socks5.AtypIPv4:
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len])) metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len])
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip6.Unmap()
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
metadata.DstIP = metadata.DstIP.Unmap()
return metadata return metadata
} }
@ -61,3 +60,19 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
return metadata return metadata
} }
func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool {
if len(prefixes) == 0 {
return false
}
if !addr.IsValid() {
return false
}
addr = addr.Unmap().WithZone("") // netip.Prefix.Contains returns false if ip has an IPv6 zone
for _, prefix := range prefixes {
if prefix.Contains(addr) {
return true
}
}
return false
}

137
adapter/outbound/anytls.go Normal file
View file

@ -0,0 +1,137 @@
package outbound
import (
"context"
"errors"
"net"
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls"
"github.com/metacubex/mihomo/transport/vmess"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
)
type AnyTLS struct {
*Base
client *anytls.Client
dialer proxydialer.SingDialer
option *AnyTLSOption
}
type AnyTLSOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"`
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
MinIdleSession int `proxy:"min-idle-session,omitempty"`
}
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := t.Base.DialOptions(opts...)
t.dialer.SetDialer(dialer.NewDialer(options...))
c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil {
return nil, err
}
return NewConn(c, t), nil
}
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// create tcp
options := t.Base.DialOptions(opts...)
t.dialer.SetDialer(dialer.NewDialer(options...))
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
if err != nil {
return nil, err
}
// create uot on tcp
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
}
// SupportUOT implements C.ProxyAdapter
func (t *AnyTLS) SupportUOT() bool {
return true
}
// ProxyInfo implements C.ProxyAdapter
func (t *AnyTLS) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (t *AnyTLS) Close() error {
return t.client.Close()
}
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
tOption := anytls.ClientConfig{
Password: option.Password,
Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
Dialer: singDialer,
IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second,
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
MinIdleSession: option.MinIdleSession,
}
tlsConfig := &vmess.TLSConfig{
Host: option.SNI,
SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN,
FingerPrint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
}
if tlsConfig.Host == "" {
tlsConfig.Host = option.Server
}
tOption.TLSConfig = tlsConfig
outbound := &AnyTLS{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.AnyTLS,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
client: anytls.NewClient(context.TODO(), tOption),
option: &option,
dialer: singDialer,
}
return outbound, nil
}

View file

@ -4,15 +4,23 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net" "net"
"runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
) )
type ProxyAdapter interface {
C.ProxyAdapter
DialOptions(opts ...dialer.Option) []dialer.Option
}
type Base struct { type Base struct {
name string name string
addr string addr string
@ -85,14 +93,15 @@ func (b *Base) SupportUDP() bool {
return b.udp return b.udp
} }
// SupportXUDP implements C.ProxyAdapter // ProxyInfo implements C.ProxyAdapter
func (b *Base) SupportXUDP() bool { func (b *Base) ProxyInfo() (info C.ProxyInfo) {
return b.xudp info.XUDP = b.xudp
} info.TFO = b.tfo
info.MPTCP = b.mpTcp
// SupportTFO implements C.ProxyAdapter info.SMUX = false
func (b *Base) SupportTFO() bool { info.Interface = b.iface
return b.tfo info.RoutingMark = b.rmark
return
} }
// IsL3Protocol implements C.ProxyAdapter // IsL3Protocol implements C.ProxyAdapter
@ -151,12 +160,16 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
return opts return opts
} }
func (b *Base) Close() error {
return nil
}
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` TFO bool `proxy:"tfo,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"` MPTCP bool `proxy:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` IPVersion string `proxy:"ip-version,omitempty"`
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
} }
@ -220,6 +233,10 @@ func (c *conn) ReaderReplaceable() bool {
return true return true
} }
func (c *conn) AddRef(ref any) {
c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
@ -266,6 +283,10 @@ func (c *packetConn) ReaderReplaceable() bool {
return true return true
} }
func (c *packetConn) AddRef(ref any) {
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc) epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
@ -285,3 +306,75 @@ func parseRemoteDestination(addr string) string {
} }
} }
} }
type AddRef interface {
AddRef(ref any)
}
type autoCloseProxyAdapter struct {
ProxyAdapter
closeOnce sync.Once
closeErr error
}
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
if err != nil {
return nil, err
}
if c, ok := c.(AddRef); ok {
c.AddRef(p)
}
return c, nil
}
func (p *autoCloseProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := p.ProxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
if c, ok := c.(AddRef); ok {
c.AddRef(p)
}
return c, nil
}
func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
if err != nil {
return nil, err
}
if pc, ok := pc.(AddRef); ok {
pc.AddRef(p)
}
return pc, nil
}
func (p *autoCloseProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc, err := p.ProxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
if pc, ok := pc.(AddRef); ok {
pc.AddRef(p)
}
return pc, nil
}
func (p *autoCloseProxyAdapter) Close() error {
p.closeOnce.Do(func() {
log.Debugln("Closing outdated proxy [%s]", p.Name())
runtime.SetFinalizer(p, nil)
p.closeErr = p.ProxyAdapter.Close()
})
return p.closeErr
}
func NewAutoCloseProxyAdapter(adapter ProxyAdapter) ProxyAdapter {
proxy := &autoCloseProxyAdapter{
ProxyAdapter: adapter,
}
// auto close ProxyAdapter
runtime.SetFinalizer(proxy, (*autoCloseProxyAdapter).Close)
return proxy
}

View file

@ -3,18 +3,12 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"os"
"strconv"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/loopback"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
) )
var DisableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR"))
type Direct struct { type Direct struct {
*Base *Base
loopBack *loopback.Detector loopBack *loopback.Detector
@ -27,10 +21,8 @@ type DirectOption 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) {
if !features.CMFA && !DisableLoopBackDetector { if err := d.loopBack.CheckConn(metadata); err != nil {
if err := d.loopBack.CheckConn(metadata); err != nil { return nil, err
return nil, err
}
} }
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver)) opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
@ -42,10 +34,8 @@ 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) {
if !features.CMFA && !DisableLoopBackDetector { if err := d.loopBack.CheckPacketConn(metadata); err != nil {
if err := d.loopBack.CheckPacketConn(metadata); err != nil { return nil, err
return nil, err
}
} }
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr // net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {

View file

@ -7,11 +7,11 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -51,7 +51,7 @@ func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Me
} }
} }
if err := h.shakeHand(metadata, c); err != nil { if err := h.shakeHandContext(ctx, c, metadata); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
@ -92,7 +92,19 @@ func (h *Http) SupportWithDialer() C.NetWork {
return C.TCP return C.TCP
} }
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { // ProxyInfo implements C.ProxyAdapter
func (h *Http) ProxyInfo() C.ProxyInfo {
info := h.Base.ProxyInfo()
info.DialerProxy = h.option.DialerProxy
return info
}
func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
addr := metadata.RemoteAddress() addr := metadata.RemoteAddress()
HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n"
tempHeaders := map[string]string{ tempHeaders := map[string]string{
@ -116,13 +128,13 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
HeaderString += "\r\n" HeaderString += "\r\n"
_, err := rw.Write([]byte(HeaderString)) _, err = c.Write([]byte(HeaderString))
if err != nil { if err != nil {
return err return err
} }
resp, err := http.ReadResponse(bufio.NewReader(rw), nil) resp, err := http.ReadResponse(bufio.NewReader(c), nil)
if err != nil { if err != nil {
return err return err

View file

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strconv" "strconv"
"time" "time"
@ -15,7 +14,6 @@ import (
"github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -45,8 +43,6 @@ type Hysteria struct {
option *HysteriaOption option *HysteriaOption
client *core.Client client *core.Client
closeCh chan struct{} // for test
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
@ -55,7 +51,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(tcpConn, h), h), nil return NewConn(tcpConn, h), nil
} }
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
@ -63,7 +59,7 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), h), nil return newPacketConn(&hyPacketConn{udpConn}, h), nil
} }
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer { func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
@ -82,11 +78,18 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack
return cDialer.ListenPacket(ctx, network, "", rAddrPort) return cDialer.ListenPacket(ctx, network, "", rAddrPort)
}, },
remoteAddr: func(addr string) (net.Addr, error) { remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) return resolveUDPAddr(ctx, "udp", addr, h.prefer)
}, },
} }
} }
// ProxyInfo implements C.ProxyAdapter
func (h *Hysteria) ProxyInfo() C.ProxyInfo {
info := h.Base.ProxyInfo()
info.DialerProxy = h.option.DialerProxy
return info
}
type HysteriaOption struct { type HysteriaOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
@ -232,18 +235,16 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
option: &option, option: &option,
client: client, client: client,
} }
runtime.SetFinalizer(outbound, closeHysteria)
return outbound, nil return outbound, nil
} }
func closeHysteria(h *Hysteria) { // Close implements C.ProxyAdapter
func (h *Hysteria) Close() error {
if h.client != nil { if h.client != nil {
_ = h.client.Close() return h.client.Close()
}
if h.closeCh != nil {
close(h.closeCh)
} }
return nil
} }
type hyPacketConn struct { type hyPacketConn struct {

View file

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"runtime"
"strconv" "strconv"
"time" "time"
@ -21,6 +20,7 @@ import (
"github.com/metacubex/sing-quic/hysteria2" "github.com/metacubex/sing-quic/hysteria2"
"github.com/metacubex/quic-go"
"github.com/metacubex/randv2" "github.com/metacubex/randv2"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
) )
@ -38,8 +38,6 @@ type Hysteria2 struct {
option *Hysteria2Option option *Hysteria2Option
client *hysteria2.Client client *hysteria2.Client
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
closeCh chan struct{} // for test
} }
type Hysteria2Option struct { type Hysteria2Option struct {
@ -62,6 +60,12 @@ type Hysteria2Option struct {
CustomCAString string `proxy:"ca-str,omitempty"` CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"` CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"` UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"`
InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"`
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
} }
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
@ -71,7 +75,7 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(c, h), h), nil return NewConn(c, h), nil
} }
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
@ -84,16 +88,22 @@ func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil { if pc == nil {
return nil, errors.New("packetConn is nil") return nil, errors.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil
} }
func closeHysteria2(h *Hysteria2) { // Close implements C.ProxyAdapter
func (h *Hysteria2) Close() error {
if h.client != nil { if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed")) return h.client.CloseWithError(errors.New("proxy removed"))
}
if h.closeCh != nil {
close(h.closeCh)
} }
return nil
}
// ProxyInfo implements C.ProxyAdapter
func (h *Hysteria2) ProxyInfo() C.ProxyInfo {
info := h.Base.ProxyInfo()
info.DialerProxy = h.option.DialerProxy
return info
} }
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
@ -138,6 +148,13 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
option.UdpMTU = 1200 - 3 option.UdpMTU = 1200 - 3
} }
quicConfig := &quic.Config{
InitialStreamReceiveWindow: option.InitialStreamReceiveWindow,
MaxStreamReceiveWindow: option.MaxStreamReceiveWindow,
InitialConnectionReceiveWindow: option.InitialConnectionReceiveWindow,
MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow,
}
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()) singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
clientOptions := hysteria2.ClientOptions{ clientOptions := hysteria2.ClientOptions{
@ -149,11 +166,12 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
SalamanderPassword: salamanderPassword, SalamanderPassword: salamanderPassword,
Password: option.Password, Password: option.Password,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
QUICConfig: quicConfig,
UDPDisabled: false, UDPDisabled: false,
CWND: option.CWND, CWND: option.CWND,
UdpMTU: option.UdpMTU, UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
}, },
} }
@ -170,7 +188,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
}) })
if len(serverAddress) > 0 { if len(serverAddress) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
} }
if option.HopInterval == 0 { if option.HopInterval == 0 {
@ -204,7 +222,6 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
client: client, client: client,
dialer: singDialer, dialer: singDialer,
} }
runtime.SetFinalizer(outbound, closeHysteria2)
return outbound, nil return outbound, nil
} }

View file

@ -1,38 +0,0 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteria2GC(t *testing.T) {
option := Hysteria2Option{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.HopInterval = 30
option.Password = "password"
option.Obfs = "salamander"
option.ObfsPassword = "password"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria2(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

View file

@ -1,39 +0,0 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteriaGC(t *testing.T) {
option := HysteriaOption{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.Protocol = "udp"
option.Up = "1Mbps"
option.Down = "1Mbps"
option.HopInterval = 30
option.Obfs = "salamander"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

301
adapter/outbound/mieru.go Normal file
View file

@ -0,0 +1,301 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"sync"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
mieruclient "github.com/enfein/mieru/v3/apis/client"
mierucommon "github.com/enfein/mieru/v3/apis/common"
mierumodel "github.com/enfein/mieru/v3/apis/model"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
"google.golang.org/protobuf/proto"
)
type Mieru struct {
*Base
option *MieruOption
client mieruclient.Client
mu sync.Mutex
}
type MieruOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
PortRange string `proxy:"port-range,omitempty"`
Transport string `proxy:"transport"`
UDP bool `proxy:"udp,omitempty"`
UserName string `proxy:"username"`
Password string `proxy:"password"`
Multiplexing string `proxy:"multiplexing,omitempty"`
}
// DialContext implements C.ProxyAdapter
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if err := m.ensureClientIsRunning(opts...); err != nil {
return nil, err
}
addr := metadataToMieruNetAddrSpec(metadata)
c, err := m.client.DialContext(ctx, addr)
if err != nil {
return nil, fmt.Errorf("dial to %s failed: %w", addr, err)
}
return NewConn(c, m), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
if err := m.ensureClientIsRunning(opts...); err != nil {
return nil, err
}
c, err := m.client.DialContext(ctx, metadata.UDPAddr())
if err != nil {
return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
}
return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
}
// SupportUOT implements C.ProxyAdapter
func (m *Mieru) SupportUOT() bool {
return true
}
// ProxyInfo implements C.ProxyAdapter
func (m *Mieru) ProxyInfo() C.ProxyInfo {
info := m.Base.ProxyInfo()
info.DialerProxy = m.option.DialerProxy
return info
}
func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.client.IsRunning() {
return nil
}
// Create a dialer and add it to the client config, before starting the client.
var dialer C.Dialer = dialer.NewDialer(m.Base.DialOptions(opts...)...)
var err error
if len(m.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
if err != nil {
return err
}
}
config, err := m.client.Load()
if err != nil {
return err
}
config.Dialer = dialer
if err := m.client.Store(config); err != nil {
return err
}
if err := m.client.Start(); err != nil {
return fmt.Errorf("failed to start mieru client: %w", err)
}
return nil
}
func NewMieru(option MieruOption) (*Mieru, error) {
config, err := buildMieruClientConfig(option)
if err != nil {
return nil, fmt.Errorf("failed to build mieru client config: %w", err)
}
c := mieruclient.NewClient()
if err := c.Store(config); err != nil {
return nil, fmt.Errorf("failed to store mieru client config: %w", err)
}
// Client is started lazily on the first use.
var addr string
if option.Port != 0 {
addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
} else {
beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange)
addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort))
}
outbound := &Mieru{
Base: &Base{
name: option.Name,
addr: addr,
iface: option.Interface,
tp: C.Mieru,
udp: option.UDP,
xudp: false,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
client: c,
}
return outbound, nil
}
// Close implements C.ProxyAdapter
func (m *Mieru) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
if m.client != nil && m.client.IsRunning() {
return m.client.Stop()
}
return nil
}
func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {
if metadata.Host != "" {
return mierumodel.NetAddrSpec{
AddrSpec: mierumodel.AddrSpec{
FQDN: metadata.Host,
Port: int(metadata.DstPort),
},
Net: "tcp",
}
} else {
return mierumodel.NetAddrSpec{
AddrSpec: mierumodel.AddrSpec{
IP: metadata.DstIP.AsSlice(),
Port: int(metadata.DstPort),
},
Net: "tcp",
}
}
}
func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) {
if err := validateMieruOption(option); err != nil {
return nil, fmt.Errorf("failed to validate mieru option: %w", err)
}
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
var server *mierupb.ServerEndpoint
if net.ParseIP(option.Server) != nil {
// server is an IP address
if option.PortRange != "" {
server = &mierupb.ServerEndpoint{
IpAddress: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
PortRange: proto.String(option.PortRange),
Protocol: transportProtocol,
},
},
}
} else {
server = &mierupb.ServerEndpoint{
IpAddress: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(int32(option.Port)),
Protocol: transportProtocol,
},
},
}
}
} else {
// server is a domain name
if option.PortRange != "" {
server = &mierupb.ServerEndpoint{
DomainName: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
PortRange: proto.String(option.PortRange),
Protocol: transportProtocol,
},
},
}
} else {
server = &mierupb.ServerEndpoint{
DomainName: proto.String(option.Server),
PortBindings: []*mierupb.PortBinding{
{
Port: proto.Int32(int32(option.Port)),
Protocol: transportProtocol,
},
},
}
}
}
config := &mieruclient.ClientConfig{
Profile: &mierupb.ClientProfile{
ProfileName: proto.String(option.Name),
User: &mierupb.User{
Name: proto.String(option.UserName),
Password: proto.String(option.Password),
},
Servers: []*mierupb.ServerEndpoint{server},
},
}
if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok {
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
}
}
return config, nil
}
func validateMieruOption(option MieruOption) error {
if option.Name == "" {
return fmt.Errorf("name is empty")
}
if option.Server == "" {
return fmt.Errorf("server is empty")
}
if option.Port == 0 && option.PortRange == "" {
return fmt.Errorf("either port or port-range must be set")
}
if option.Port != 0 && option.PortRange != "" {
return fmt.Errorf("port and port-range cannot be set at the same time")
}
if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
return fmt.Errorf("port must be between 1 and 65535")
}
if option.PortRange != "" {
begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
if err != nil {
return fmt.Errorf("invalid port-range format")
}
if begin < 1 || begin > 65535 {
return fmt.Errorf("begin port must be between 1 and 65535")
}
if end < 1 || end > 65535 {
return fmt.Errorf("end port must be between 1 and 65535")
}
if begin > end {
return fmt.Errorf("begin port must be less than or equal to end port")
}
}
if option.Transport != "TCP" {
return fmt.Errorf("transport must be TCP")
}
if option.UserName == "" {
return fmt.Errorf("username is empty")
}
if option.Password == "" {
return fmt.Errorf("password is empty")
}
if option.Multiplexing != "" {
if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok {
return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing)
}
}
return nil
}
func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
var begin, end int
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
return begin, end, err
}

View file

@ -0,0 +1,92 @@
package outbound
import "testing"
func TestNewMieru(t *testing.T) {
testCases := []struct {
option MieruOption
wantBaseAddr string
}{
{
option: MieruOption{
Name: "test",
Server: "1.2.3.4",
Port: 10000,
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "1.2.3.4:10000",
},
{
option: MieruOption{
Name: "test",
Server: "2001:db8::1",
PortRange: "10001-10002",
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "[2001:db8::1]:10001",
},
{
option: MieruOption{
Name: "test",
Server: "example.com",
Port: 10003,
Transport: "TCP",
UserName: "test",
Password: "test",
},
wantBaseAddr: "example.com:10003",
},
}
for _, testCase := range testCases {
mieru, err := NewMieru(testCase.option)
if err != nil {
t.Error(err)
}
if mieru.addr != testCase.wantBaseAddr {
t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr)
}
}
}
func TestBeginAndEndPortFromPortRange(t *testing.T) {
testCases := []struct {
input string
begin int
end int
hasErr bool
}{
{"1-10", 1, 10, false},
{"1000-2000", 1000, 2000, false},
{"65535-65535", 65535, 65535, false},
{"1", 0, 0, true},
{"1-", 0, 0, true},
{"-10", 0, 0, true},
{"a-b", 0, 0, true},
{"1-b", 0, 0, true},
{"a-10", 0, 0, true},
}
for _, testCase := range testCases {
begin, end, err := beginAndEndPortFromPortRange(testCase.input)
if testCase.hasErr {
if err == nil {
t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input)
}
} else {
if err != nil {
t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err)
}
if begin != testCase.begin {
t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin)
}
if end != testCase.end {
t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end)
}
}
}
}

View file

@ -20,16 +20,19 @@ func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
config := new(tlsC.RealityConfig) config := new(tlsC.RealityConfig)
const x25519ScalarSize = 32 const x25519ScalarSize = 32
var publicKey [x25519ScalarSize]byte publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)
n, err := base64.RawURLEncoding.Decode(publicKey[:], []byte(o.PublicKey)) if err != nil || len(publicKey) != x25519ScalarSize {
if err != nil || n != x25519ScalarSize {
return nil, errors.New("invalid REALITY public key") return nil, errors.New("invalid REALITY public key")
} }
config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey[:]) config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("fail to create REALITY public key: %w", err) return nil, fmt.Errorf("fail to create REALITY public key: %w", err)
} }
n := hex.DecodedLen(len(o.ShortID))
if n > tlsC.RealityMaxShortIDLen {
return nil, errors.New("invalid REALITY short id")
}
n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID)) n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID))
if err != nil || n > tlsC.RealityMaxShortIDLen { if err != nil || n > tlsC.RealityMaxShortIDLen {
return nil, errors.New("invalid REALITY short ID") return nil, errors.New("invalid REALITY short ID")

View file

@ -13,6 +13,7 @@ import (
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs" obfs "github.com/metacubex/mihomo/transport/simple-obfs"
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
@ -34,6 +35,7 @@ type ShadowSocks struct {
obfsMode string obfsMode string
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option v2rayOption *v2rayObfs.Option
gostOption *gost.Option
shadowTLSOption *shadowtls.ShadowTLSOption shadowTLSOption *shadowtls.ShadowTLSOption
restlsConfig *restlsC.Config restlsConfig *restlsC.Config
} }
@ -71,6 +73,17 @@ type v2rayObfsOption struct {
V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"` V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"`
} }
type gostObfsOption struct {
Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"`
}
type shadowTLSOption struct { type shadowTLSOption struct {
Password string `obfs:"password"` Password string `obfs:"password"`
Host string `obfs:"host"` Host string `obfs:"host"`
@ -87,7 +100,7 @@ type restlsOption struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
useEarly := false useEarly := false
switch ss.obfsMode { switch ss.obfsMode {
case "tls": case "tls":
@ -96,20 +109,23 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
_, port, _ := net.SplitHostPort(ss.addr) _, port, _ := net.SplitHostPort(ss.addr)
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket": case "websocket":
var err error if ss.v2rayOption != nil {
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
} else if ss.gostOption != nil {
c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption)
} else {
return nil, fmt.Errorf("plugin options is required")
}
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
case shadowtls.Mode: case shadowtls.Mode:
var err error
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
if err != nil { if err != nil {
return nil, err return nil, err
} }
useEarly = true useEarly = true
case restls.Mode: case restls.Mode:
var err error
c, err = restls.NewRestls(ctx, c, ss.restlsConfig) c, err = restls.NewRestls(ctx, c, ss.restlsConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err)
@ -117,6 +133,12 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
useEarly = true useEarly = true
} }
useEarly = useEarly || N.NeedHandshake(c) useEarly = useEarly || N.NeedHandshake(c)
if !useEarly {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
}
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion)) uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion))
if useEarly { if useEarly {
@ -178,7 +200,7 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
return nil, err return nil, err
} }
} }
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -196,6 +218,13 @@ func (ss *ShadowSocks) SupportWithDialer() C.NetWork {
return C.ALLNet return C.ALLNet
} }
// ProxyInfo implements C.ProxyAdapter
func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
info := ss.Base.ProxyInfo()
info.DialerProxy = ss.option.DialerProxy
return info
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP { if ss.option.UDPOverTCP {
@ -229,10 +258,11 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
Password: option.Password, Password: option.Password,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err)
} }
var v2rayOption *v2rayObfs.Option var v2rayOption *v2rayObfs.Option
var gostOption *gost.Option
var obfsOption *simpleObfsOption var obfsOption *simpleObfsOption
var shadowTLSOpt *shadowtls.ShadowTLSOption var shadowTLSOpt *shadowtls.ShadowTLSOption
var restlsConfig *restlsC.Config var restlsConfig *restlsC.Config
@ -274,6 +304,28 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint v2rayOption.Fingerprint = opts.Fingerprint
} }
} else if option.Plugin == "gost-plugin" {
opts := gostObfsOption{Host: "bing.com", Mux: true}
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
}
if opts.Mode != "websocket" {
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
}
obfsMode = opts.Mode
gostOption = &gost.Option{
Host: opts.Host,
Path: opts.Path,
Headers: opts.Headers,
Mux: opts.Mux,
}
if opts.TLS {
gostOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint
}
} else if option.Plugin == shadowtls.Mode { } else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode obfsMode = shadowtls.Mode
opt := &shadowTLSOption{ opt := &shadowTLSOption{
@ -329,6 +381,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
option: &option, option: &option,
obfsMode: obfsMode, obfsMode: obfsMode,
v2rayOption: v2rayOption, v2rayOption: v2rayOption,
gostOption: gostOption,
obfsOption: obfsOption, obfsOption: obfsOption,
shadowTLSOption: shadowTLSOpt, shadowTLSOption: shadowTLSOpt,
restlsConfig: restlsConfig, restlsConfig: restlsConfig,

View file

@ -42,12 +42,15 @@ type ShadowSocksROption struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
c = ssr.obfs.StreamConn(c) c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c) c = ssr.cipher.StreamConn(c)
var ( var (
iv []byte iv []byte
err error
) )
switch conn := c.(type) { switch conn := c.(type) {
case *shadowstream.Conn: case *shadowstream.Conn:
@ -102,7 +105,7 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err return nil, err
} }
} }
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer) addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -122,6 +125,13 @@ func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork {
return C.ALLNet return C.ALLNet
} }
// ProxyInfo implements C.ProxyAdapter
func (ssr *ShadowSocksR) ProxyInfo() C.ProxyInfo {
info := ssr.Base.ProxyInfo()
info.DialerProxy = ssr.option.DialerProxy
return info
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility // SSR protocol compatibility
// https://github.com/metacubex/mihomo/pull/2056 // https://github.com/metacubex/mihomo/pull/2056
@ -134,7 +144,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
password := option.Password password := option.Password
coreCiph, err := core.PickCipher(cipher, nil, password) coreCiph, err := core.PickCipher(cipher, nil, password)
if err != nil { if err != nil {
return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err) return nil, fmt.Errorf("ssr %s cipher: %s initialize error: %w", addr, cipher, err)
} }
var ( var (
ivSize int ivSize int

View file

@ -3,7 +3,6 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"runtime"
CN "github.com/metacubex/mihomo/common/net" CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
@ -18,8 +17,7 @@ import (
) )
type SingMux struct { type SingMux struct {
C.ProxyAdapter ProxyAdapter
base ProxyBase
client *mux.Client client *mux.Client
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
onlyTcp bool onlyTcp bool
@ -43,25 +41,21 @@ type BrutalOption struct {
Down string `proxy:"down,omitempty"` Down string `proxy:"down,omitempty"`
} }
type ProxyBase interface {
DialOptions(opts ...dialer.Option) []dialer.Option
}
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := s.base.DialOptions(opts...) options := s.ProxyAdapter.DialOptions(opts...)
s.dialer.SetDialer(dialer.NewDialer(options...)) s.dialer.SetDialer(dialer.NewDialer(options...))
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(c, s), s.ProxyAdapter), err return NewConn(c, s), err
} }
func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
if s.onlyTcp { if s.onlyTcp {
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
} }
options := s.base.DialOptions(opts...) options := s.ProxyAdapter.DialOptions(opts...)
s.dialer.SetDialer(dialer.NewDialer(options...)) s.dialer.SetDialer(dialer.NewDialer(options...))
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
@ -80,7 +74,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if pc == nil { if pc == nil {
return nil, E.New("packetConn is nil") return nil, E.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), s), s.ProxyAdapter), nil return newPacketConn(CN.NewThreadSafePacketConn(pc), s), nil
} }
func (s *SingMux) SupportUDP() bool { func (s *SingMux) SupportUDP() bool {
@ -97,11 +91,21 @@ func (s *SingMux) SupportUOT() bool {
return true return true
} }
func closeSingMux(s *SingMux) { func (s *SingMux) ProxyInfo() C.ProxyInfo {
_ = s.client.Close() info := s.ProxyAdapter.ProxyInfo()
info.SMUX = true
return info
} }
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) { // Close implements C.ProxyAdapter
func (s *SingMux) Close() error {
if s.client != nil {
_ = s.client.Close()
}
return s.ProxyAdapter.Close()
}
func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error) {
// TODO // TODO
// "TCP Brutal is only supported on Linux-based systems" // "TCP Brutal is only supported on Linux-based systems"
@ -125,11 +129,9 @@ func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.P
} }
outbound := &SingMux{ outbound := &SingMux{
ProxyAdapter: proxy, ProxyAdapter: proxy,
base: base,
client: client, client: client,
dialer: singDialer, dialer: singDialer,
onlyTcp: option.OnlyTcp, onlyTcp: option.OnlyTcp,
} }
runtime.SetFinalizer(outbound, closeSingMux)
return outbound, nil return outbound, nil
} }

View file

@ -6,6 +6,7 @@ import (
"net" "net"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -41,7 +42,7 @@ type streamOption struct {
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
} }
func streamConn(c net.Conn, option streamOption) *snell.Snell { func snellStreamConn(c net.Conn, option streamOption) *snell.Snell {
switch option.obfsOption.Mode { switch option.obfsOption.Mode {
case "tls": case "tls":
c = obfs.NewTLSObfs(c, option.obfsOption.Host) c = obfs.NewTLSObfs(c, option.obfsOption.Host)
@ -54,25 +55,35 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP { err := s.writeHeaderContext(ctx, c, metadata)
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return c, err return c, err
} }
func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP {
err = snell.WriteUDPHeader(c, s.version)
return
}
err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 { if s.version == snell.Version2 && dialer.IsZeroOptions(opts) {
c, err := s.pool.Get() c, err := s.pool.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil { if err = s.writeHeaderContext(ctx, c, metadata); err != nil {
c.Close() _ = c.Close()
return nil, err return nil, err
} }
return NewConn(c, s), err return NewConn(c, s), err
@ -120,12 +131,8 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, err return nil, err
} }
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version) c, err = s.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c) pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil return newPacketConn(pc, s), nil
@ -141,6 +148,13 @@ func (s *Snell) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter
func (s *Snell) ProxyInfo() C.ProxyInfo {
info := s.Base.ProxyInfo()
info.DialerProxy = s.option.DialerProxy
return info
}
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk) psk := []byte(option.Psk)
@ -204,8 +218,8 @@ func NewSnell(option SnellOption) (*Snell, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
}) })
} }
return s, nil return s, nil

View file

@ -10,6 +10,7 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -58,7 +59,7 @@ func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C
Password: ss.pass, Password: ss.pass,
} }
} }
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { if _, err := ss.clientHandshakeContext(ctx, c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
@ -135,7 +136,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user) bindAddr, err := ss.clientHandshakeContext(ctx, c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
if err != nil { if err != nil {
err = fmt.Errorf("client hanshake error: %w", err) err = fmt.Errorf("client hanshake error: %w", err)
return return
@ -147,7 +148,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
err = errors.New("invalid UDP bind address") err = errors.New("invalid UDP bind address")
return return
} else if bindUDPAddr.IP.IsUnspecified() { } else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr()) serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr(), C.IPv4Prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -171,6 +172,21 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
} }
// ProxyInfo implements C.ProxyAdapter
func (ss *Socks5) ProxyInfo() C.ProxyInfo {
info := ss.Base.ProxyInfo()
info.DialerProxy = ss.option.DialerProxy
return info
}
func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr socks5.Addr, command socks5.Command, user *socks5.User) (_ socks5.Addr, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
return socks5.ClientHandshake(c, addr, command, user)
}
func NewSocks5(option Socks5Option) (*Socks5, error) { func NewSocks5(option Socks5Option) (*Socks5, error) {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {

View file

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -25,7 +24,10 @@ type Ssh struct {
*Base *Base
option *SshOption option *SshOption
client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer
config *ssh.ClientConfig
client *ssh.Client
cMutex sync.Mutex
} }
type SshOption struct { type SshOption struct {
@ -49,7 +51,7 @@ func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dia
return nil, err return nil, err
} }
} }
client, err := s.client.connect(ctx, cDialer, s.addr) client, err := s.connect(ctx, cDialer, s.addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,16 +60,10 @@ func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dia
return nil, err return nil, err
} }
return NewConn(N.NewRefConn(c, s), s), nil return NewConn(c, s), nil
} }
type sshClient struct { func (s *Ssh) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) {
config *ssh.ClientConfig
client *ssh.Client
cMutex sync.Mutex
}
func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) {
s.cMutex.Lock() s.cMutex.Lock()
defer s.cMutex.Unlock() defer s.cMutex.Unlock()
if s.client != nil { if s.client != nil {
@ -108,7 +104,15 @@ func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string)
return client, nil return client, nil
} }
func (s *sshClient) Close() error { // ProxyInfo implements C.ProxyAdapter
func (s *Ssh) ProxyInfo() C.ProxyInfo {
info := s.Base.ProxyInfo()
info.DialerProxy = s.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (s *Ssh) Close() error {
s.cMutex.Lock() s.cMutex.Lock()
defer s.cMutex.Unlock() defer s.cMutex.Unlock()
if s.client != nil { if s.client != nil {
@ -117,10 +121,6 @@ func (s *sshClient) Close() error {
return nil return nil
} }
func closeSsh(s *Ssh) {
_ = s.client.Close()
}
func NewSsh(option SshOption) (*Ssh, error) { func NewSsh(option SshOption) (*Ssh, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
@ -197,11 +197,8 @@ func NewSsh(option SshOption) (*Ssh, error) {
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, option: &option,
client: &sshClient{ config: &config,
config: &config,
},
} }
runtime.SetFinalizer(outbound, closeSsh)
return outbound, nil return outbound, nil
} }

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -17,12 +18,13 @@ import (
"github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/shadowsocks/core"
"github.com/metacubex/mihomo/transport/trojan" "github.com/metacubex/mihomo/transport/trojan"
"github.com/metacubex/mihomo/transport/vmess"
) )
type Trojan struct { type Trojan struct {
*Base *Base
instance *trojan.Trojan option *TrojanOption
option *TrojanOption hexPassword [trojan.KeyLength]byte
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
@ -60,15 +62,21 @@ type TrojanSSOption struct {
Password string `proxy:"password,omitempty"` Password string `proxy:"password,omitempty"`
} }
func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) { // StreamConnContext implements C.ProxyAdapter
if t.option.Network == "ws" { func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
switch t.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(t.addr) host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: t.option.WSOpts.Path, Path: t.option.WSOpts.Path,
MaxEarlyData: t.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -82,57 +90,95 @@ func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error)
} }
} }
return t.instance.StreamWebsocketConn(ctx, c, wsOpts) alpn := trojan.DefaultWebsocketALPN
} if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
}
return t.instance.StreamConn(ctx, c) wsOpts.TLS = true
} tlsConfig := &tls.Config{
NextProtos: alpn,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: t.option.SkipCertVerify,
ServerName: t.option.SNI,
}
// StreamConnContext implements C.ProxyAdapter wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint)
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
t.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
} else {
c, err = t.plainStream(ctx, c)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c)
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if t.ssCipher != nil { c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
c = t.ssCipher.StreamConn(c) case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
default:
// default tcp network
// handle TLS
alpn := trojan.DefaultALPN
if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
} }
c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{
Host: t.option.SNI,
SkipCertVerify: t.option.SkipCertVerify,
FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn,
Reality: t.realityConfig,
})
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { return t.streamConnContext(ctx, c, metadata)
c.Close() }
func (t *Trojan) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c)
}
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
command := trojan.CommandTCP
if metadata.NetWork == C.UDP {
command = trojan.CommandUDP
}
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
return c, err
}
func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
command := trojan.CommandTCP
if metadata.NetWork == C.UDP {
command = trojan.CommandUDP
}
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
return err
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if t.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err return nil, err
} }
@ -171,7 +217,7 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
var c net.Conn var c net.Conn
// grpc transport // grpc transport
if t.transport != nil && len(opts) == 0 { if t.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@ -180,16 +226,12 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
if t.ssCipher != nil { c, err = t.streamConnContext(ctx, c, metadata)
c = t.ssCipher.StreamConn(c)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
@ -210,21 +252,12 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = t.plainStream(ctx, c) c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
@ -235,7 +268,7 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
@ -244,22 +277,24 @@ func (t *Trojan) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter
func (t *Trojan) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (t *Trojan) Close() error {
if t.transport != nil {
return t.transport.Close()
}
return nil
}
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
}
if option.SNI != "" {
tOption.ServerName = option.SNI
}
t := &Trojan{ t := &Trojan{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
@ -272,8 +307,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
instance: trojan.New(tOption), option: &option,
option: &option, hexPassword: trojan.Key(option.Password),
} }
var err error var err error
@ -281,7 +316,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
tOption.Reality = t.realityConfig
if option.SSOpts.Enabled { if option.SSOpts.Enabled {
if option.SSOpts.Password == "" { if option.SSOpts.Password == "" {
@ -298,7 +332,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
} }
if option.Network == "grpc" { if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...)
if len(t.option.DialerProxy) > 0 { if len(t.option.DialerProxy) > 0 {
@ -307,7 +341,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", t.addr) c, err := cDialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
@ -317,8 +351,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
NextProtos: option.ALPN, NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tOption.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: tOption.ServerName, ServerName: option.SNI,
} }
var err error var err error
@ -327,12 +361,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName, ServiceName: option.GrpcOpts.GrpcServiceName,
Host: tOption.ServerName, Host: option.SNI,
ClientFingerprint: option.ClientFingerprint,
} }
} }

View file

@ -130,7 +130,7 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
return nil, nil, err return nil, nil, err
} }
} }
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer) udpAddr, err := resolveUDPAddr(ctx, "udp", t.addr, t.prefer)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -146,6 +146,13 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
return return
} }
// ProxyInfo implements C.ProxyAdapter
func (t *Tuic) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
func NewTuic(option TuicOption) (*Tuic, error) { func NewTuic(option TuicOption) (*Tuic, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server serverName := option.Server

View file

@ -3,118 +3,59 @@ package outbound
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"regexp" "regexp"
"strconv" "strconv"
"sync"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/socks5"
) )
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() addrType := metadata.AddrType()
aType := uint8(addrType)
p := uint(metadata.DstPort) p := uint(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch addrType {
case socks5.AtypDomainName: case C.AtypDomainName:
lenM := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
buf = [][]byte{{aType, lenM}, host, port} buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port}
case socks5.AtypIPv4: case C.AtypIPv4:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{socks5.AtypIPv4}, host, port}
case socks5.AtypIPv6: case C.AtypIPv6:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{socks5.AtypIPv6}, host, port}
} }
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
} }
func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) { func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var ip netip.Addr var ip netip.Addr
var fallback netip.Addr
switch prefer { switch prefer {
case C.IPv4Only: case C.IPv4Only:
ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
case C.IPv6Only: case C.IPv6Only:
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
case C.IPv6Prefer: case C.IPv6Prefer:
var ips []netip.Addr ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err == nil {
for _, addr := range ips {
if addr.Is6() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
default: default:
// C.IPv4Prefer, C.DualStack and other ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
var ips []netip.Addr
ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err == nil {
for _, addr := range ips {
if addr.Is4() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
}
if !ip.IsValid() && fallback.IsValid() {
ip = fallback
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
ip, port = resolver.LookupIP4P(ip, port)
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
} }

View file

@ -21,9 +21,7 @@ import (
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/transport/vless" "github.com/metacubex/mihomo/transport/vless"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
@ -77,13 +75,7 @@ type VlessOption struct {
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
@ -157,7 +149,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = vmess.StreamH2Conn(c, h2Opts) c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
@ -170,10 +162,14 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return nil, err return nil, err
} }
return v.streamConn(c, metadata) return v.streamConnContext(ctx, c, metadata)
} }
func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
if v.option.PacketAddr { if v.option.PacketAddr {
metadata = &C.Metadata{ metadata = &C.Metadata{
@ -230,9 +226,10 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -240,7 +237,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -285,7 +282,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -294,7 +291,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.streamConn(c, metadata) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
@ -379,19 +376,34 @@ func (v *Vless) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter
func (v *Vless) ProxyInfo() C.ProxyInfo {
info := v.Base.ProxyInfo()
info.DialerProxy = v.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
if v.transport != nil {
return v.transport.Close()
}
return nil
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
var addrType byte var addrType byte
var addr []byte var addr []byte
switch metadata.AddrType() { switch metadata.AddrType() {
case socks5.AtypIPv4: case C.AtypIPv4:
addrType = vless.AtypIPv4 addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypIPv6: case C.AtypIPv6:
addrType = vless.AtypIPv6 addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypDomainName: case C.AtypDomainName:
addrType = vless.AtypDomainName addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host)) addr[0] = byte(len(metadata.Host))
@ -506,8 +518,6 @@ func NewVless(option VlessOption) (*Vless, error) {
if option.Flow != vless.XRV { if option.Flow != vless.XRV {
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
addons = &vless.Addons{ addons = &vless.Addons{
Flow: option.Flow, Flow: option.Flow,
} }
@ -559,7 +569,7 @@ func NewVless(option VlessOption) (*Vless, error) {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 { if len(v.option.DialerProxy) > 0 {
@ -568,7 +578,7 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr) c, err := cDialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -585,10 +595,13 @@ func NewVless(option VlessOption) (*Vless, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) }, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" { if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host

View file

@ -96,13 +96,7 @@ type WSOptions struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
var err error
if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
@ -199,7 +193,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = mihomoVMess.StreamH2Conn(c, h2Opts) c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
@ -226,17 +220,24 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
if err != nil { if err != nil {
return nil, err return nil, err
} }
return v.streamConn(c, metadata) return v.streamConnContext(ctx, c, metadata)
} }
func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
useEarly := N.NeedHandshake(c)
if !useEarly {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
if v.option.XUDP { if v.option.XUDP {
var globalID [8]byte var globalID [8]byte
if metadata.SourceValid() { if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress()) globalID = utils.GlobalID(metadata.SourceAddress())
} }
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyXUDPPacketConn(c, conn = v.client.DialEarlyXUDPPacketConn(c,
globalID, globalID,
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
@ -246,7 +247,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
} }
} else if v.option.PacketAddr { } else if v.option.PacketAddr {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyPacketConn(c, conn = v.client.DialEarlyPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
} else { } else {
@ -255,7 +256,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
} }
conn = packetaddr.NewBindConn(conn) conn = packetaddr.NewBindConn(conn)
} else { } else {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyPacketConn(c, conn = v.client.DialEarlyPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
} else { } else {
@ -264,7 +265,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
} }
} }
} else { } else {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyConn(c, conn = v.client.DialEarlyConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
} else { } else {
@ -280,9 +281,10 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -290,7 +292,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -332,7 +334,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -341,7 +343,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.streamConn(c, metadata) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
@ -388,6 +390,21 @@ func (v *Vmess) SupportWithDialer() C.NetWork {
return C.ALLNet return C.ALLNet
} }
// ProxyInfo implements C.ProxyAdapter
func (v *Vmess) ProxyInfo() C.ProxyInfo {
info := v.Base.ProxyInfo()
info.DialerProxy = v.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (v *Vmess) Close() error {
if v.transport != nil {
return v.transport.Close()
}
return nil
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
@ -452,13 +469,18 @@ func NewVmess(option VmessOption) (*Vmess, error) {
option: &option, option: &option,
} }
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 { if len(v.option.DialerProxy) > 0 {
@ -467,7 +489,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr) c, err := cDialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
@ -484,10 +506,13 @@ func NewVmess(option VmessOption) (*Vmess, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) }, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" { if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host
@ -500,11 +525,6 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
} }
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
return v, nil return v, nil
} }

View file

@ -8,14 +8,12 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
@ -45,7 +43,6 @@ type WireGuard struct {
tunDevice wireguard.Device tunDevice wireguard.Device
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
resolver resolver.Resolver resolver resolver.Resolver
refP *refProxyAdapter
initOk atomic.Bool initOk atomic.Bool
initMutex sync.Mutex initMutex sync.Mutex
@ -57,8 +54,6 @@ type WireGuard struct {
serverAddrMap map[M.Socksaddr]netip.AddrPort serverAddrMap map[M.Socksaddr]netip.AddrPort
serverAddrTime atomic.TypedValue[time.Time] serverAddrTime atomic.TypedValue[time.Time]
serverAddrMutex sync.Mutex serverAddrMutex sync.Mutex
closeCh chan struct{} // for test
} }
type WireGuardOption struct { type WireGuardOption struct {
@ -173,7 +168,6 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
}, },
dialer: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()), dialer: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()),
} }
runtime.SetFinalizer(outbound, closeWireGuard)
var reserved [3]uint8 var reserved [3]uint8
if len(option.Reserved) > 0 { if len(option.Reserved) > 0 {
@ -286,15 +280,13 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
} }
} }
refP := &refProxyAdapter{}
outbound.refP = refP
if option.RemoteDnsResolve && len(option.Dns) > 0 { if option.RemoteDnsResolve && len(option.Dns) > 0 {
nss, err := dns.ParseNameServer(option.Dns) nss, err := dns.ParseNameServer(option.Dns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for i := range nss { for i := range nss {
nss[i].ProxyAdapter = refP nss[i].ProxyAdapter = outbound
} }
outbound.resolver = dns.NewResolver(dns.Config{ outbound.resolver = dns.NewResolver(dns.Config{
Main: nss, Main: nss,
@ -309,7 +301,7 @@ func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.Add
if address.Addr.IsValid() { if address.Addr.IsValid() {
return address.AddrPort(), nil return address.AddrPort(), nil
} }
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", address.String(), w.prefer) udpAddr, err := resolveUDPAddr(ctx, "udp", address.String(), w.prefer)
if err != nil { if err != nil {
return netip.AddrPort{}, err return netip.AddrPort{}, err
} }
@ -488,13 +480,12 @@ func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, er
return ipcConf, nil return ipcConf, nil
} }
func closeWireGuard(w *WireGuard) { // Close implements C.ProxyAdapter
func (w *WireGuard) Close() error {
if w.device != nil { if w.device != nil {
w.device.Close() w.device.Close()
} }
if w.closeCh != nil { return nil
close(w.closeCh)
}
} }
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
@ -507,8 +498,6 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
if !metadata.Resolved() || w.resolver != nil { if !metadata.Resolved() || w.resolver != nil {
r := resolver.DefaultResolver r := resolver.DefaultResolver
if w.resolver != nil { if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver r = w.resolver
} }
options = append(options, dialer.WithResolver(r)) options = append(options, dialer.WithResolver(r))
@ -523,7 +512,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
if conn == nil { if conn == nil {
return nil, E.New("conn is nil") return nil, E.New("conn is nil")
} }
return NewConn(CN.NewRefConn(conn, w), w), nil return NewConn(conn, w), nil
} }
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
@ -536,8 +525,6 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver r := resolver.DefaultResolver
if w.resolver != nil { if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver r = w.resolver
} }
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
@ -553,146 +540,10 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil { if pc == nil {
return nil, E.New("packetConn is nil") return nil, E.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil return newPacketConn(pc, w), nil
} }
// IsL3Protocol implements C.ProxyAdapter // IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool { func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true return true
} }
type refProxyAdapter struct {
proxyAdapter C.ProxyAdapter
count int
mutex sync.Mutex
}
func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.proxyAdapter = proxyAdapter
r.count++
}
func (r *refProxyAdapter) ClearProxyAdapter() {
r.mutex.Lock()
defer r.mutex.Unlock()
r.count--
if r.count == 0 {
r.proxyAdapter = nil
}
}
func (r *refProxyAdapter) Name() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Name()
}
return ""
}
func (r *refProxyAdapter) Type() C.AdapterType {
if r.proxyAdapter != nil {
return r.proxyAdapter.Type()
}
return C.AdapterType(0)
}
func (r *refProxyAdapter) Addr() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Addr()
}
return ""
}
func (r *refProxyAdapter) SupportUDP() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUDP()
}
return false
}
func (r *refProxyAdapter) SupportXUDP() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportXUDP()
}
return false
}
func (r *refProxyAdapter) SupportTFO() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportTFO()
}
return false
}
func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.MarshalJSON()
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) SupportUOT() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUOT()
}
return false
}
func (r *refProxyAdapter) SupportWithDialer() C.NetWork {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportWithDialer()
}
return C.InvalidNet
}
func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.IsL3Protocol(metadata)
}
return false
}
func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
if r.proxyAdapter != nil {
return r.proxyAdapter.Unwrap(metadata, touch)
}
return nil
}
var _ C.ProxyAdapter = (*refProxyAdapter)(nil)

View file

@ -1,45 +0,0 @@
//go:build with_gvisor
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestWireGuardGC(t *testing.T) {
option := WireGuardOption{}
option.Server = "162.159.192.1"
option.Port = 2408
option.PrivateKey = "iOx7749AdqH3IqluG7+0YbGKd0m1mcEXAfGRzpy9rG8="
option.PublicKey = "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo="
option.Ip = "172.16.0.2"
option.Ipv6 = "2606:4700:110:8d29:be92:3a6a:f4:c437"
option.Reserved = []uint8{51, 69, 125}
wg, err := NewWireGuard(option)
if err != nil {
t.Error(err)
}
closeCh := make(chan struct{})
wg.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
err = wg.init(ctx)
if err != nil {
t.Error(err)
return
}
// must do a small sleep before test GC
// because it maybe deadlocks if w.device.Close call too fast after w.device.Start
time.Sleep(10 * time.Millisecond)
wg = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

View file

@ -37,7 +37,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
} else { } else {
f.onDialFailed(proxy.Type(), err) f.onDialFailed(proxy.Type(), err, f.healthCheck)
} }
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
@ -45,7 +45,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
if err == nil { if err == nil {
f.onDialSuccess() f.onDialSuccess()
} else { } else {
f.onDialFailed(proxy.Type(), err) f.onDialFailed(proxy.Type(), err, f.healthCheck)
} }
}) })
} }

View file

@ -2,6 +2,7 @@ package outboundgroup
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
@ -17,22 +18,26 @@ import (
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"golang.org/x/exp/slices"
) )
type GroupBase struct { type GroupBase struct {
*outbound.Base *outbound.Base
filterRegs []*regexp2.Regexp filterRegs []*regexp2.Regexp
excludeFilterReg *regexp2.Regexp excludeFilterRegs []*regexp2.Regexp
excludeTypeArray []string excludeTypeArray []string
providers []provider.ProxyProvider providers []provider.ProxyProvider
failedTestMux sync.Mutex failedTestMux sync.Mutex
failedTimes int failedTimes int
failedTime time.Time failedTime time.Time
failedTesting atomic.Bool failedTesting atomic.Bool
proxies [][]C.Proxy TestTimeout int
versions []atomic.Uint32 maxFailedTimes int
TestTimeout int
maxFailedTimes int // for GetProxies
getProxiesMutex sync.Mutex
providerVersions []uint32
providerProxies []C.Proxy
} }
type GroupBaseOption struct { type GroupBaseOption struct {
@ -46,15 +51,26 @@ type GroupBaseOption struct {
} }
func NewGroupBase(opt GroupBaseOption) *GroupBase { func NewGroupBase(opt GroupBaseOption) *GroupBase {
var excludeFilterReg *regexp2.Regexp if opt.RoutingMark != 0 {
if opt.excludeFilter != "" { log.Warnln("The group [%s] with routing-mark configuration is deprecated, please set it directly on the proxy instead", opt.Name)
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, regexp2.None)
} }
if opt.Interface != "" {
log.Warnln("The group [%s] with interface-name configuration is deprecated, please set it directly on the proxy instead", opt.Name)
}
var excludeTypeArray []string var excludeTypeArray []string
if opt.excludeType != "" { if opt.excludeType != "" {
excludeTypeArray = strings.Split(opt.excludeType, "|") excludeTypeArray = strings.Split(opt.excludeType, "|")
} }
var excludeFilterRegs []*regexp2.Regexp
if opt.excludeFilter != "" {
for _, excludeFilter := range strings.Split(opt.excludeFilter, "`") {
excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None)
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
}
}
var filterRegs []*regexp2.Regexp var filterRegs []*regexp2.Regexp
if opt.filter != "" { if opt.filter != "" {
for _, filter := range strings.Split(opt.filter, "`") { for _, filter := range strings.Split(opt.filter, "`") {
@ -64,14 +80,14 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase {
} }
gb := &GroupBase{ gb := &GroupBase{
Base: outbound.NewBase(opt.BaseOption), Base: outbound.NewBase(opt.BaseOption),
filterRegs: filterRegs, filterRegs: filterRegs,
excludeFilterReg: excludeFilterReg, excludeFilterRegs: excludeFilterRegs,
excludeTypeArray: excludeTypeArray, excludeTypeArray: excludeTypeArray,
providers: opt.providers, providers: opt.providers,
failedTesting: atomic.NewBool(false), failedTesting: atomic.NewBool(false),
TestTimeout: opt.TestTimeout, TestTimeout: opt.TestTimeout,
maxFailedTimes: opt.maxFailedTimes, maxFailedTimes: opt.maxFailedTimes,
} }
if gb.TestTimeout == 0 { if gb.TestTimeout == 0 {
@ -81,9 +97,6 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase {
gb.maxFailedTimes = 5 gb.maxFailedTimes = 5
} }
gb.proxies = make([][]C.Proxy, len(opt.providers))
gb.versions = make([]atomic.Uint32, len(opt.providers))
return gb return gb
} }
@ -94,56 +107,55 @@ func (gb *GroupBase) Touch() {
} }
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy { func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
providerVersions := make([]uint32, len(gb.providers))
for i, pd := range gb.providers {
if touch { // touch first
pd.Touch()
}
providerVersions[i] = pd.Version()
}
// thread safe
gb.getProxiesMutex.Lock()
defer gb.getProxiesMutex.Unlock()
// return the cached proxies if version not changed
if slices.Equal(providerVersions, gb.providerVersions) {
return gb.providerProxies
}
var proxies []C.Proxy var proxies []C.Proxy
if len(gb.filterRegs) == 0 { if len(gb.filterRegs) == 0 {
for _, pd := range gb.providers { for _, pd := range gb.providers {
if touch {
pd.Touch()
}
proxies = append(proxies, pd.Proxies()...) proxies = append(proxies, pd.Proxies()...)
} }
} else { } else {
for i, pd := range gb.providers { for _, pd := range gb.providers {
if touch { if pd.VehicleType() == types.Compatible { // compatible provider unneeded filter
pd.Touch() proxies = append(proxies, pd.Proxies()...)
}
if pd.VehicleType() == types.Compatible {
gb.versions[i].Store(pd.Version())
gb.proxies[i] = pd.Proxies()
continue continue
} }
version := gb.versions[i].Load() var newProxies []C.Proxy
if version != pd.Version() && gb.versions[i].CompareAndSwap(version, pd.Version()) { proxiesSet := map[string]struct{}{}
var ( for _, filterReg := range gb.filterRegs {
proxies []C.Proxy for _, p := range pd.Proxies() {
newProxies []C.Proxy name := p.Name()
) if mat, _ := filterReg.MatchString(name); mat {
if _, ok := proxiesSet[name]; !ok {
proxies = pd.Proxies() proxiesSet[name] = struct{}{}
proxiesSet := map[string]struct{}{} newProxies = append(newProxies, p)
for _, filterReg := range gb.filterRegs {
for _, p := range proxies {
name := p.Name()
if mat, _ := filterReg.MatchString(name); mat {
if _, ok := proxiesSet[name]; !ok {
proxiesSet[name] = struct{}{}
newProxies = append(newProxies, p)
}
} }
} }
} }
gb.proxies[i] = newProxies
} }
} proxies = append(proxies, newProxies...)
for _, p := range gb.proxies {
proxies = append(proxies, p...)
} }
} }
// Multiple filers means that proxies are sorted in the order in which the filers appear.
// Although the filter has been performed once in the previous process,
// when there are multiple providers, the array needs to be reordered as a whole.
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
var newProxies []C.Proxy var newProxies []C.Proxy
proxiesSet := map[string]struct{}{} proxiesSet := map[string]struct{}{}
@ -167,32 +179,31 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
} }
proxies = newProxies proxies = newProxies
} }
if gb.excludeTypeArray != nil {
var newProxies []C.Proxy
for _, p := range proxies {
mType := p.Type().String()
flag := false
for i := range gb.excludeTypeArray {
if strings.EqualFold(mType, gb.excludeTypeArray[i]) {
flag = true
break
}
} if len(gb.excludeFilterRegs) > 0 {
if flag { var newProxies []C.Proxy
continue LOOP1:
for _, p := range proxies {
name := p.Name()
for _, excludeFilterReg := range gb.excludeFilterRegs {
if mat, _ := excludeFilterReg.MatchString(name); mat {
continue LOOP1
}
} }
newProxies = append(newProxies, p) newProxies = append(newProxies, p)
} }
proxies = newProxies proxies = newProxies
} }
if gb.excludeFilterReg != nil { if gb.excludeTypeArray != nil {
var newProxies []C.Proxy var newProxies []C.Proxy
LOOP2:
for _, p := range proxies { for _, p := range proxies {
name := p.Name() mType := p.Type().String()
if mat, _ := gb.excludeFilterReg.MatchString(name); mat { for _, excludeType := range gb.excludeTypeArray {
continue if strings.EqualFold(mType, excludeType) {
continue LOOP2
}
} }
newProxies = append(newProxies, p) newProxies = append(newProxies, p)
} }
@ -200,9 +211,13 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
} }
if len(proxies) == 0 { if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"]) return []C.Proxy{tunnel.Proxies()["COMPATIBLE"]}
} }
// only cache when proxies not empty
gb.providerVersions = providerVersions
gb.providerProxies = proxies
return proxies return proxies
} }
@ -234,17 +249,21 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus uti
} }
} }
func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) { func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func()) {
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop { if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop {
return return
} }
if strings.Contains(err.Error(), "connection refused") { if errors.Is(err, C.ErrNotSupport) {
go gb.healthCheck()
return return
} }
go func() { go func() {
if strings.Contains(err.Error(), "connection refused") {
fn()
return
}
gb.failedTestMux.Lock() gb.failedTestMux.Lock()
defer gb.failedTestMux.Unlock() defer gb.failedTestMux.Unlock()
@ -261,7 +280,7 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) {
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes { if gb.failedTimes >= gb.maxFailedTimes {
log.Warnln("because %s failed multiple times, active health check", gb.Name()) log.Warnln("because %s failed multiple times, active health check", gb.Name())
gb.healthCheck() fn()
} }
} }
}() }()

View file

@ -95,7 +95,7 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
if err == nil { if err == nil {
c.AppendToChains(lb) c.AppendToChains(lb)
} else { } else {
lb.onDialFailed(proxy.Type(), err) lb.onDialFailed(proxy.Type(), err, lb.healthCheck)
} }
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
@ -103,7 +103,7 @@ func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, op
if err == nil { if err == nil {
lb.onDialSuccess() lb.onDialSuccess()
} else { } else {
lb.onDialFailed(proxy.Type(), err) lb.onDialFailed(proxy.Type(), err, lb.healthCheck)
} }
}) })
} }

View file

@ -4,8 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"sync"
"time" "time"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outbound"
@ -54,13 +52,13 @@ func (u *URLTest) Set(name string) error {
if p == nil { if p == nil {
return errors.New("proxy not exist") return errors.New("proxy not exist")
} }
u.selected = name u.ForceSet(name)
u.fast(false)
return nil return nil
} }
func (u *URLTest) ForceSet(name string) { func (u *URLTest) ForceSet(name string) {
u.selected = name u.selected = name
u.fastSingle.Reset()
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -70,7 +68,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
} else { } else {
u.onDialFailed(proxy.Type(), err) u.onDialFailed(proxy.Type(), err, u.healthCheck)
} }
if N.NeedHandshake(c) { if N.NeedHandshake(c) {
@ -78,7 +76,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
if err == nil { if err == nil {
u.onDialSuccess() u.onDialSuccess()
} else { } else {
u.onDialFailed(proxy.Type(), err) u.onDialFailed(proxy.Type(), err, u.healthCheck)
} }
}) })
} }
@ -88,9 +86,12 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...) proxy := u.fast(true)
pc, err := proxy.ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
} else {
u.onDialFailed(proxy.Type(), err, u.healthCheck)
} }
return pc, err return pc, err
@ -101,22 +102,27 @@ func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return u.fast(touch) return u.fast(touch)
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) healthCheck() {
u.fastSingle.Reset()
u.GroupBase.healthCheck()
u.fastSingle.Reset()
}
proxies := u.GetProxies(touch) func (u *URLTest) fast(touch bool) C.Proxy {
if u.selected != "" { elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
for _, proxy := range proxies { proxies := u.GetProxies(touch)
if !proxy.AliveForTestUrl(u.testUrl) { if u.selected != "" {
continue for _, proxy := range proxies {
} if !proxy.AliveForTestUrl(u.testUrl) {
if proxy.Name() == u.selected { continue
u.fastNode = proxy }
return proxy if proxy.Name() == u.selected {
u.fastNode = proxy
return proxy, nil
}
} }
} }
}
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
fast := proxies[0] fast := proxies[0]
minDelay := fast.LastDelayForTestUrl(u.testUrl) minDelay := fast.LastDelayForTestUrl(u.testUrl)
fastNotExist := true fastNotExist := true
@ -182,31 +188,7 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
} }
func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
var wg sync.WaitGroup return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
var lock sync.Mutex
mp := map[string]uint16{}
proxies := u.GetProxies(false)
for _, proxy := range proxies {
proxy := proxy
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, u.testUrl, expectedStatus)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay
lock.Unlock()
}
wg.Done()
}()
}
wg.Wait()
if len(mp) == 0 {
return mp, fmt.Errorf("get delay: all proxies timeout")
} else {
return mp, nil
}
} }
func parseURLTestOption(config map[string]any) []urlTestOption { func parseURLTestOption(config map[string]any) []urlTestOption {

View file

@ -3,8 +3,6 @@ package adapter
import ( import (
"fmt" "fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/structure"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@ -18,12 +16,12 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
var ( var (
proxy C.ProxyAdapter proxy outbound.ProxyAdapter
err error err error
) )
switch proxyType { switch proxyType {
case "ss": case "ss":
ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption) err = decoder.Decode(mapping, ssOption)
if err != nil { if err != nil {
break break
@ -56,7 +54,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
Method: "GET", Method: "GET",
Path: []string{"/"}, Path: []string{"/"},
}, },
ClientFingerprint: tlsC.GetGlobalFingerprint(),
} }
err = decoder.Decode(mapping, vmessOption) err = decoder.Decode(mapping, vmessOption)
@ -65,7 +62,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
proxy, err = outbound.NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "vless": case "vless":
vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} vlessOption := &outbound.VlessOption{}
err = decoder.Decode(mapping, vlessOption) err = decoder.Decode(mapping, vlessOption)
if err != nil { if err != nil {
break break
@ -79,7 +76,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
proxy, err = outbound.NewSnell(*snellOption) proxy, err = outbound.NewSnell(*snellOption)
case "trojan": case "trojan":
trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption) err = decoder.Decode(mapping, trojanOption)
if err != nil { if err != nil {
break break
@ -141,6 +138,20 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewSsh(*sshOption) proxy, err = outbound.NewSsh(*sshOption)
case "mieru":
mieruOption := &outbound.MieruOption{}
err = decoder.Decode(mapping, mieruOption)
if err != nil {
break
}
proxy, err = outbound.NewMieru(*mieruOption)
case "anytls":
anytlsOption := &outbound.AnyTLSOption{}
err = decoder.Decode(mapping, anytlsOption)
if err != nil {
break
}
proxy, err = outbound.NewAnyTLS(*anytlsOption)
default: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }
@ -156,12 +167,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
return nil, err return nil, err
} }
if muxOption.Enabled { if muxOption.Enabled {
proxy, err = outbound.NewSingMux(*muxOption, proxy, proxy.(outbound.ProxyBase)) proxy, err = outbound.NewSingMux(*muxOption, proxy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
proxy = outbound.NewAutoCloseProxyAdapter(proxy)
return NewProxy(proxy), nil return NewProxy(proxy), nil
} }

View file

@ -57,15 +57,17 @@ type OverrideSchema struct {
} }
type proxyProviderSchema struct { type proxyProviderSchema struct {
Type string `provider:"type"` Type string `provider:"type"`
Path string `provider:"path,omitempty"` Path string `provider:"path,omitempty"`
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Proxy string `provider:"proxy,omitempty"` Proxy string `provider:"proxy,omitempty"`
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"` Filter string `provider:"filter,omitempty"`
ExcludeFilter string `provider:"exclude-filter,omitempty"` ExcludeFilter string `provider:"exclude-filter,omitempty"`
ExcludeType string `provider:"exclude-type,omitempty"` ExcludeType string `provider:"exclude-type,omitempty"`
DialerProxy string `provider:"dialer-proxy,omitempty"` DialerProxy string `provider:"dialer-proxy,omitempty"`
SizeLimit int64 `provider:"size-limit,omitempty"`
Payload []map[string]any `provider:"payload,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
Override OverrideSchema `provider:"override,omitempty"` Override OverrideSchema `provider:"override,omitempty"`
@ -98,6 +100,11 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
} }
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus) hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus)
parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
if err != nil {
return nil, err
}
var vehicle types.Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
@ -111,17 +118,14 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
return nil, fmt.Errorf("%w: %s", errSubPath, path) return nil, fmt.Errorf("%w: %s", errSubPath, path)
} }
} }
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout) vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
case "inline":
return NewInlineProvider(name, schema.Payload, parser, hc)
default: default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter
excludeFilter := schema.ExcludeFilter
excludeType := schema.ExcludeType
dialerProxy := schema.DialerProxy
override := schema.Override
return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, dialerProxy, override, vehicle, hc) return NewProxySetProvider(name, interval, parser, vehicle, hc)
} }

View file

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
@ -30,44 +31,102 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
type providerForApi struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicleType"`
Proxies []C.Proxy `json:"proxies"`
TestUrl string `json:"testUrl"`
ExpectedStatus string `json:"expectedStatus"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"`
}
type baseProvider struct {
name string
proxies []C.Proxy
healthCheck *HealthCheck
version uint32
}
func (bp *baseProvider) Name() string {
return bp.name
}
func (bp *baseProvider) Version() uint32 {
return bp.version
}
func (bp *baseProvider) HealthCheck() {
bp.healthCheck.check()
}
func (bp *baseProvider) Type() types.ProviderType {
return types.Proxy
}
func (bp *baseProvider) Proxies() []C.Proxy {
return bp.proxies
}
func (bp *baseProvider) Count() int {
return len(bp.proxies)
}
func (bp *baseProvider) Touch() {
bp.healthCheck.touch()
}
func (bp *baseProvider) HealthCheckURL() string {
return bp.healthCheck.url
}
func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
bp.proxies = proxies
bp.version += 1
bp.healthCheck.setProxy(proxies)
if bp.healthCheck.auto() {
go bp.healthCheck.check()
}
}
func (bp *baseProvider) Close() error {
bp.healthCheck.close()
return nil
}
// ProxySetProvider for auto gc // ProxySetProvider for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
baseProvider
*resource.Fetcher[[]C.Proxy] *resource.Fetcher[[]C.Proxy]
proxies []C.Proxy
healthCheck *HealthCheck
version uint32
subscriptionInfo *SubscriptionInfo subscriptionInfo *SubscriptionInfo
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{ return json.Marshal(providerForApi{
"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(),
"testUrl": pp.healthCheck.url, TestUrl: pp.healthCheck.url,
"expectedStatus": pp.healthCheck.expectedStatus.String(), ExpectedStatus: pp.healthCheck.expectedStatus.String(),
"updatedAt": pp.UpdatedAt(), UpdatedAt: pp.UpdatedAt(),
"subscriptionInfo": pp.subscriptionInfo, SubscriptionInfo: pp.subscriptionInfo,
}) })
} }
func (pp *proxySetProvider) Version() uint32 {
return pp.version
}
func (pp *proxySetProvider) Name() string { func (pp *proxySetProvider) Name() string {
return pp.Fetcher.Name() return pp.Fetcher.Name()
} }
func (pp *proxySetProvider) HealthCheck() {
pp.healthCheck.check()
}
func (pp *proxySetProvider) Update() error { func (pp *proxySetProvider) Update() error {
_, _, err := pp.Fetcher.Update() _, _, err := pp.Fetcher.Update()
return err return err
@ -79,54 +138,12 @@ func (pp *proxySetProvider) Initial() error {
return err return err
} }
if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" {
pp.SetSubscriptionInfo(subscriptionInfo) pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo)
} }
pp.closeAllConnections() pp.closeAllConnections()
return nil return nil
} }
func (pp *proxySetProvider) Type() types.ProviderType {
return types.Proxy
}
func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) Count() int {
return len(pp.proxies)
}
func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
func (pp *proxySetProvider) HealthCheckURL() string {
return pp.healthCheck.url
}
func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.check()
}
}
func (pp *proxySetProvider) SetSubscriptionInfo(userInfo string) {
pp.subscriptionInfo = NewSubscriptionInfo(userInfo)
}
func (pp *proxySetProvider) SetProvider(provider types.ProxyProvider) {
if httpVehicle, ok := pp.Vehicle().(*resource.HTTPVehicle); ok {
httpVehicle.SetProvider(provider)
}
}
func (pp *proxySetProvider) closeAllConnections() { func (pp *proxySetProvider) closeAllConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool { statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
for _, chain := range c.Chains() { for _, chain := range c.Chains() {
@ -140,11 +157,176 @@ func (pp *proxySetProvider) closeAllConnections() {
} }
func (pp *proxySetProvider) Close() error { func (pp *proxySetProvider) Close() error {
pp.healthCheck.close() _ = pp.baseProvider.Close()
return pp.Fetcher.Close() return pp.Fetcher.Close()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
baseProvider: baseProvider{
name: name,
proxies: []C.Proxy{},
healthCheck: hc,
},
}
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
pd.Fetcher = fetcher
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
httpVehicle.SetInRead(func(resp *http.Response) {
if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" {
cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo)
pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo)
}
})
}
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close)
return wrapper, nil
}
func (pp *ProxySetProvider) Close() error {
runtime.SetFinalizer(pp, nil)
return pp.proxySetProvider.Close()
}
// InlineProvider for auto gc
type InlineProvider struct {
*inlineProvider
}
type inlineProvider struct {
baseProvider
updateAt time.Time
}
func (ip *inlineProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(providerForApi{
Name: ip.Name(),
Type: ip.Type().String(),
VehicleType: ip.VehicleType().String(),
Proxies: ip.Proxies(),
TestUrl: ip.healthCheck.url,
ExpectedStatus: ip.healthCheck.expectedStatus.String(),
UpdatedAt: ip.updateAt,
})
}
func (ip *inlineProvider) VehicleType() types.VehicleType {
return types.Inline
}
func (ip *inlineProvider) Initial() error {
return nil
}
func (ip *inlineProvider) Update() error {
// make api update happy
ip.updateAt = time.Now()
return nil
}
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
if hc.auto() {
go hc.process()
}
ps := ProxySchema{Proxies: payload}
buf, err := yaml.Marshal(ps)
if err != nil {
return nil, err
}
proxies, err := parser(buf)
if err != nil {
return nil, err
}
ip := &inlineProvider{
baseProvider: baseProvider{
name: name,
proxies: proxies,
healthCheck: hc,
},
updateAt: time.Now(),
}
wrapper := &InlineProvider{ip}
runtime.SetFinalizer(wrapper, (*InlineProvider).Close)
return wrapper, nil
}
func (ip *InlineProvider) Close() error {
runtime.SetFinalizer(ip, nil)
return ip.baseProvider.Close()
}
// CompatibleProvider for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
baseProvider
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(providerForApi{
Name: cp.Name(),
Type: cp.Type().String(),
VehicleType: cp.VehicleType().String(),
Proxies: cp.Proxies(),
TestUrl: cp.healthCheck.url,
ExpectedStatus: cp.healthCheck.expectedStatus.String(),
})
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
cp.HealthCheck()
}
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
baseProvider: baseProvider{
name: name,
proxies: proxies,
healthCheck: hc,
},
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close)
return wrapper, nil
}
func (cp *CompatibleProvider) Close() error {
runtime.SetFinalizer(cp, nil)
return cp.compatibleProvider.Close()
}
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
@ -163,151 +345,6 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
filterRegs = append(filterRegs, filterReg) filterRegs = append(filterRegs, filterReg)
} }
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
proxies: []C.Proxy{},
healthCheck: hc,
}
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy, override), proxiesOnUpdate(pd))
pd.Fetcher = fetcher
wrapper := &ProxySetProvider{pd}
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
httpVehicle.SetProvider(wrapper)
}
runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close)
return wrapper, nil
}
func (pp *ProxySetProvider) Close() error {
runtime.SetFinalizer(pp, nil)
return pp.proxySetProvider.Close()
}
func (pp *ProxySetProvider) SetProvider(provider types.ProxyProvider) {
pp.proxySetProvider.SetProvider(provider)
}
// CompatibleProvider for auto gc
type CompatibleProvider struct {
*compatibleProvider
}
type compatibleProvider struct {
name string
healthCheck *HealthCheck
subscriptionInfo *SubscriptionInfo
proxies []C.Proxy
version uint32
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": cp.Name(),
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
"testUrl": cp.healthCheck.url,
"expectedStatus": cp.healthCheck.expectedStatus.String(),
})
}
func (cp *compatibleProvider) Version() uint32 {
return cp.version
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error {
return nil
}
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
cp.HealthCheck()
}
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible
}
func (cp *compatibleProvider) Type() types.ProviderType {
return types.Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Count() int {
return len(cp.proxies)
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func (cp *compatibleProvider) HealthCheckURL() string {
return cp.healthCheck.url
}
func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (cp *compatibleProvider) Close() error {
cp.healthCheck.close()
return nil
}
func (cp *compatibleProvider) SetSubscriptionInfo(userInfo string) {
cp.subscriptionInfo = NewSubscriptionInfo(userInfo)
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
name: name,
proxies: proxies,
healthCheck: hc,
}
wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close)
return wrapper, nil
}
func (cp *CompatibleProvider) Close() error {
runtime.SetFinalizer(cp, nil)
return cp.compatibleProvider.Close()
}
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
pd.version += 1
}
}
func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp, dialerProxy string, override OverrideSchema) resource.Parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) { return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
@ -422,5 +459,5 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
} }
return proxies, nil return proxies, nil
} }, nil
} }

View file

@ -42,7 +42,6 @@ func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) {
si.Expire = intValue si.Expire = intValue
} }
} }
return si return si
} }

View file

@ -0,0 +1,31 @@
package contextutils
import (
"context"
"sync"
)
func afterFunc(ctx context.Context, f func()) (stop func() bool) {
stopc := make(chan struct{})
once := sync.Once{} // either starts running f or stops f from running
if ctx.Done() != nil {
go func() {
select {
case <-ctx.Done():
once.Do(func() {
go f()
})
case <-stopc:
}
}()
}
return func() bool {
stopped := false
once.Do(func() {
stopped = true
close(stopc)
})
return stopped
}
}

View file

@ -0,0 +1,11 @@
//go:build !go1.21
package contextutils
import (
"context"
)
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
return afterFunc(ctx, f)
}

View file

@ -0,0 +1,9 @@
//go:build go1.21
package contextutils
import "context"
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
return context.AfterFunc(ctx, f)
}

View file

@ -0,0 +1,100 @@
package contextutils
import (
"context"
"testing"
"time"
)
const (
shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test
veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time
)
func TestAfterFuncCalledAfterCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
t.Fatalf("AfterFunc called before context is done")
case <-time.After(shortDuration):
}
cancel()
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
if stop() {
t.Fatalf("stop() = true, want false")
}
}
func TestAfterFuncCalledAfterTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
donec := make(chan struct{})
afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
}
func TestAfterFuncCalledImmediately(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
donec := make(chan struct{})
afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called for already-canceled context")
}
}
func TestAfterFuncNotCalledAfterStop(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
close(donec)
})
if !stop() {
t.Fatalf("stop() = false, want true")
}
cancel()
select {
case <-donec:
t.Fatalf("AfterFunc called for already-canceled context")
case <-time.After(shortDuration):
}
if stop() {
t.Fatalf("stop() = true, want false")
}
}
// This test verifies that canceling a context does not block waiting for AfterFuncs to finish.
func TestAfterFuncCalledAsynchronously(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
// The channel send blocks until donec is read from.
donec <- struct{}{}
})
defer stop()
cancel()
// After cancel returns, read from donec and unblock the AfterFunc.
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
}

View file

@ -275,22 +275,24 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
vmess["skip-cert-verify"] = false vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto" vmess["cipher"] = "auto"
if cipher, ok := values["scy"]; ok && cipher != "" { if cipher, ok := values["scy"].(string); ok && cipher != "" {
vmess["cipher"] = cipher vmess["cipher"] = cipher
} }
if sni, ok := values["sni"]; ok && sni != "" { if sni, ok := values["sni"].(string); ok && sni != "" {
vmess["servername"] = sni vmess["servername"] = sni
} }
network, _ := values["net"].(string) network, ok := values["net"].(string)
network = strings.ToLower(network) if ok {
if values["type"] == "http" { network = strings.ToLower(network)
network = "http" if values["type"] == "http" {
} else if network == "http" { network = "http"
network = "h2" } else if network == "http" {
network = "h2"
}
vmess["network"] = network
} }
vmess["network"] = network
tls, ok := values["tls"].(string) tls, ok := values["tls"].(string)
if ok { if ok {
@ -307,12 +309,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "http": case "http":
headers := make(map[string]any) headers := make(map[string]any)
httpOpts := make(map[string]any) httpOpts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host.(string)} headers["Host"] = []string{host}
} }
httpOpts["path"] = []string{"/"} httpOpts["path"] = []string{"/"}
if path, ok := values["path"]; ok && path != "" { if path, ok := values["path"].(string); ok && path != "" {
httpOpts["path"] = []string{path.(string)} httpOpts["path"] = []string{path}
} }
httpOpts["headers"] = headers httpOpts["headers"] = headers
@ -321,8 +323,8 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "h2": case "h2":
headers := make(map[string]any) headers := make(map[string]any)
h2Opts := make(map[string]any) h2Opts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host.(string)} headers["Host"] = []string{host}
} }
h2Opts["path"] = values["path"] h2Opts["path"] = values["path"]
@ -334,11 +336,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
wsOpts["path"] = "/" wsOpts["path"] = "/"
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = host.(string) headers["Host"] = host
} }
if path, ok := values["path"]; ok && path != "" { if path, ok := values["path"].(string); ok && path != "" {
path := path.(string) path := path
pathURL, err := url.Parse(path) pathURL, err := url.Parse(path)
if err == nil { if err == nil {
query := pathURL.Query() query := pathURL.Query()

View file

@ -3,29 +3,37 @@ package net
import ( import (
"context" "context"
"net" "net"
"github.com/metacubex/mihomo/common/contextutils"
) )
// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine. // SetupContextForConn is a helper function that starts connection I/O interrupter.
// if ctx be canceled before done called, it will close the connection.
// should use like this:
//
// func streamConn(ctx context.Context, conn net.Conn) (_ net.Conn, err error) {
// if ctx.Done() != nil {
// done := N.SetupContextForConn(ctx, conn)
// defer done(&err)
// }
// conn, err := xxx
// return conn, err
// }
func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) { func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) {
var ( stopc := make(chan struct{})
quit = make(chan struct{}) stop := contextutils.AfterFunc(ctx, func() {
interrupt = make(chan error, 1) // Close the connection, discarding the error
) _ = conn.Close()
go func() { close(stopc)
select { })
case <-quit:
interrupt <- nil
case <-ctx.Done():
// Close the connection, discarding the error
_ = conn.Close()
interrupt <- ctx.Err()
}
}()
return func(inputErr *error) { return func(inputErr *error) {
close(quit) if !stop() {
if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil { // The AfterFunc was started, wait for it to complete.
// Return context error to user. <-stopc
inputErr = &ctxErr if ctxErr := ctx.Err(); ctxErr != nil && inputErr != nil {
// Return context error to user.
*inputErr = ctxErr
}
} }
} }
} }

103
common/net/context_test.go Normal file
View file

@ -0,0 +1,103 @@
package net_test
import (
"context"
"errors"
"net"
"testing"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/stretchr/testify/assert"
)
func testRead(ctx context.Context, conn net.Conn) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, conn)
defer done(&err)
}
_, err = conn.Read(make([]byte, 1))
return err
}
func TestSetupContextForConnWithCancel(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case <-errc:
t.Fatal("conn closed before cancel")
case <-time.After(100 * time.Millisecond):
cancel()
}
select {
case err := <-errc:
assert.ErrorIs(t, err, context.Canceled)
case <-time.After(100 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}
func TestSetupContextForConnWithTimeout1(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case err := <-errc:
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatal("conn closed before timeout")
}
assert.ErrorIs(t, err, context.DeadlineExceeded)
case <-time.After(200 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}
func TestSetupContextForConnWithTimeout2(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case <-errc:
t.Fatal("conn closed before cancel")
case <-time.After(100 * time.Millisecond):
c2.Write(make([]byte, 1))
}
select {
case err := <-errc:
assert.Nil(t, ctx.Err())
assert.Nil(t, err)
case <-time.After(200 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}

View file

@ -45,7 +45,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
addr = &net.UDPAddr{IP: ip[:], Port: from.Port} addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
case *syscall.SockaddrInet6: case *syscall.SockaddrInet6:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)} zone := ""
if from.ZoneId != 0 {
zone = strconv.FormatInt(int64(from.ZoneId), 10)
}
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
} }
} }
// udp should not convert readN == 0 to io.EOF // udp should not convert readN == 0 to io.EOF

View file

@ -54,7 +54,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
addr = &net.UDPAddr{IP: ip[:], Port: from.Port} addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
case *windows.SockaddrInet6: case *windows.SockaddrInet6:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)} zone := ""
if from.ZoneId != 0 {
zone = strconv.FormatInt(int64(from.ZoneId), 10)
}
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
} }
} }
// udp should not convert readN == 0 to io.EOF // udp should not convert readN == 0 to io.EOF

View file

@ -77,6 +77,6 @@ func (c *refConn) WriterReplaceable() bool { // Relay() will handle reference
var _ ExtendedConn = (*refConn)(nil) var _ ExtendedConn = (*refConn)(nil)
func NewRefConn(conn net.Conn, ref any) net.Conn { func NewRefConn(conn net.Conn, ref any) ExtendedConn {
return &refConn{conn: NewExtendedConn(conn), ref: ref} return &refConn{conn: NewExtendedConn(conn), ref: ref}
} }

View file

@ -3,8 +3,10 @@ package net
import ( import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/hex"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"math/big" "math/big"
@ -16,7 +18,11 @@ type Path interface {
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) { func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
if certificate == "" && privateKey == "" { if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair() var err error
certificate, privateKey, _, err = NewRandomTLSKeyPair()
if err != nil {
return tls.Certificate{}, err
}
} }
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
if painTextErr == nil { if painTextErr == nil {
@ -32,10 +38,10 @@ func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, erro
return cert, nil return cert, nil
} }
func newRandomTLSKeyPair() (tls.Certificate, error) { func NewRandomTLSKeyPair() (certificate string, privateKey string, fingerprint string, err error) {
key, err := rsa.GenerateKey(rand.Reader, 2048) key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
template := x509.Certificate{SerialNumber: big.NewInt(1)} template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate( certDER, err := x509.CreateCertificate(
@ -45,14 +51,15 @@ func newRandomTLSKeyPair() (tls.Certificate, error) {
&key.PublicKey, &key.PublicKey,
key) key)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) cert, err := x509.ParseCertificate(certDER)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
return tlsCert, nil hash := sha256.Sum256(cert.Raw)
fingerprint = hex.EncodeToString(hash[:])
privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
return
} }

View file

@ -1,73 +0,0 @@
package nnip
import (
"encoding/binary"
"net"
"net/netip"
)
// IpToAddr converts the net.IP to netip.Addr.
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
func IpToAddr(slice net.IP) netip.Addr {
ip := slice
if len(ip) != 4 {
if ip = slice.To4(); ip == nil {
ip = slice
}
}
if addr, ok := netip.AddrFromSlice(ip); ok {
return addr
}
return netip.Addr{}
}
// UnMasked returns p's last IP address.
// If p is invalid, UnMasked returns netip.Addr{}
func UnMasked(p netip.Prefix) netip.Addr {
if !p.IsValid() {
return netip.Addr{}
}
buf := p.Addr().As16()
hi := binary.BigEndian.Uint64(buf[:8])
lo := binary.BigEndian.Uint64(buf[8:])
bits := p.Bits()
if bits <= 32 {
bits += 96
}
hi = hi | ^uint64(0)>>bits
lo = lo | ^(^uint64(0) << (128 - bits))
binary.BigEndian.PutUint64(buf[:8], hi)
binary.BigEndian.PutUint64(buf[8:], lo)
addr := netip.AddrFrom16(buf)
if p.Addr().Is4() {
return addr.Unmap()
}
return addr
}
// PrefixCompare returns an integer comparing two prefixes.
// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2.
// modify from https://github.com/golang/go/issues/61642#issuecomment-1848587909
func PrefixCompare(p, p2 netip.Prefix) int {
// compare by validity, address family and prefix base address
if c := p.Masked().Addr().Compare(p2.Masked().Addr()); c != 0 {
return c
}
// compare by prefix length
f1, f2 := p.Bits(), p2.Bits()
if f1 < f2 {
return -1
}
if f1 > f2 {
return 1
}
// compare by prefix address
return p.Addr().Compare(p2.Addr())
}

View file

@ -10,6 +10,7 @@ type Observable[T any] struct {
listener map[Subscription[T]]*Subscriber[T] listener map[Subscription[T]]*Subscriber[T]
mux sync.Mutex mux sync.Mutex
done bool done bool
stopCh chan struct{}
} }
func (o *Observable[T]) process() { func (o *Observable[T]) process() {
@ -31,6 +32,7 @@ func (o *Observable[T]) close() {
for _, sub := range o.listener { for _, sub := range o.listener {
sub.Close() sub.Close()
} }
close(o.stopCh)
} }
func (o *Observable[T]) Subscribe() (Subscription[T], error) { func (o *Observable[T]) Subscribe() (Subscription[T], error) {
@ -59,6 +61,7 @@ func NewObservable[T any](iter Iterable[T]) *Observable[T] {
observable := &Observable[T]{ observable := &Observable[T]{
iterable: iter, iterable: iter,
listener: map[Subscription[T]]*Subscriber[T]{}, listener: map[Subscription[T]]*Subscriber[T]{},
stopCh: make(chan struct{}),
} }
go observable.process() go observable.process()
return observable return observable

View file

@ -70,9 +70,11 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
src := NewObservable[int](iter) src := NewObservable[int](iter)
data, _ := src.Subscribe() data, _ := src.Subscribe()
<-data <-data
select {
_, closed := src.Subscribe() case <-src.stopCh:
assert.NotNil(t, closed) case <-time.After(time.Second):
assert.Fail(t, "timeout not stop")
}
} }
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {

View file

@ -22,9 +22,10 @@ func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, err
} }
func TestPicker_Basic(t *testing.T) { func TestPicker_Basic(t *testing.T) {
t.Parallel()
picker, ctx := WithContext[int](context.Background()) picker, ctx := WithContext[int](context.Background())
picker.Go(sleepAndSend(ctx, 30, 2)) picker.Go(sleepAndSend(ctx, 200, 2))
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 100, 1))
number := picker.Wait() number := picker.Wait()
assert.NotNil(t, number) assert.NotNil(t, number)
@ -32,8 +33,9 @@ func TestPicker_Basic(t *testing.T) {
} }
func TestPicker_Timeout(t *testing.T) { func TestPicker_Timeout(t *testing.T) {
t.Parallel()
picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5) picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 100, 1))
number := picker.Wait() number := picker.Wait()
assert.Equal(t, number, lo.Empty[int]()) assert.Equal(t, number, lo.Empty[int]())

View file

@ -13,25 +13,26 @@ type call[T any] struct {
type Single[T any] struct { type Single[T any] struct {
mux sync.Mutex mux sync.Mutex
last time.Time
wait time.Duration wait time.Duration
call *call[T] call *call[T]
result *Result[T] result *Result[T]
} }
type Result[T any] struct { type Result[T any] struct {
Val T Val T
Err error Err error
Time time.Time
} }
// Do single.Do likes sync.singleFlight // Do single.Do likes sync.singleFlight
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) { func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
s.mux.Lock() s.mux.Lock()
now := time.Now() result := s.result
if now.Before(s.last.Add(s.wait)) { if result != nil && time.Since(result.Time) < s.wait {
s.mux.Unlock() s.mux.Unlock()
return s.result.Val, s.result.Err, true return result.Val, result.Err, true
} }
s.result = nil // The result has expired, clear it
if callM := s.call; callM != nil { if callM := s.call; callM != nil {
s.mux.Unlock() s.mux.Unlock()
@ -47,15 +48,19 @@ func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
callM.wg.Done() callM.wg.Done()
s.mux.Lock() s.mux.Lock()
s.call = nil if s.call == callM { // maybe reset when fn is running
s.result = &Result[T]{callM.val, callM.err} s.call = nil
s.last = now s.result = &Result[T]{callM.val, callM.err, time.Now()}
}
s.mux.Unlock() s.mux.Unlock()
return callM.val, callM.err, false return callM.val, callM.err, false
} }
func (s *Single[T]) Reset() { func (s *Single[T]) Reset() {
s.last = time.Time{} s.mux.Lock()
s.call = nil
s.result = nil
s.mux.Unlock()
} }
func NewSingle[T any](wait time.Duration) *Single[T] { func NewSingle[T any](wait time.Duration) *Single[T] {

View file

@ -11,12 +11,13 @@ import (
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
shardCount := atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (int, error) { call := func() (int, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 20)
return 0, nil return 0, nil
} }
@ -39,7 +40,8 @@ func TestBasic(t *testing.T) {
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
callM := func() (int, error) { callM := func() (int, error) {
foo++ foo++
@ -47,7 +49,7 @@ func TestTimer(t *testing.T) {
} }
_, _, _ = single.Do(callM) _, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond) time.Sleep(100 * time.Millisecond)
_, _, shard := single.Do(callM) _, _, shard := single.Do(callM)
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
@ -55,7 +57,8 @@ func TestTimer(t *testing.T) {
} }
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
callM := func() (int, error) { callM := func() (int, error) {
foo++ foo++

View file

@ -0,0 +1,30 @@
package sockopt
import (
"net"
"syscall"
)
func RawConnReuseaddr(rc syscall.RawConn) (err error) {
var innerErr error
err = rc.Control(func(fd uintptr) {
innerErr = reuseControl(fd)
})
if innerErr != nil {
err = innerErr
}
return
}
func UDPReuseaddr(c net.PacketConn) error {
if c, ok := c.(syscall.Conn); ok {
rc, err := c.SyscallConn()
if err != nil {
return err
}
return RawConnReuseaddr(rc)
}
return nil
}

View file

@ -1,9 +1,5 @@
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
package dialer package sockopt
import ( func reuseControl(fd uintptr) error { return nil }
"net"
)
func addrReuseToListenConfig(*net.ListenConfig) {}

View file

@ -0,0 +1,22 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package sockopt
import (
"golang.org/x/sys/unix"
)
func reuseControl(fd uintptr) error {
e1 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
e2 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
if e1 != nil {
return e1
}
if e2 != nil {
return e2
}
return nil
}

View file

@ -0,0 +1,9 @@
package sockopt
import (
"golang.org/x/sys/windows"
)
func reuseControl(fd uintptr) error {
return windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
}

View file

@ -1,19 +0,0 @@
package sockopt
import (
"net"
"syscall"
)
func UDPReuseaddr(c *net.UDPConn) (err error) {
rc, err := c.SyscallConn()
if err != nil {
return
}
rc.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
})
return
}

View file

@ -1,11 +0,0 @@
//go:build !linux
package sockopt
import (
"net"
)
func UDPReuseaddr(c *net.UDPConn) (err error) {
return
}

View file

@ -86,7 +86,27 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
return nil return nil
} }
// isNil returns true if the input is nil or a typed nil pointer.
func isNil(input any) bool {
if input == nil {
return true
}
val := reflect.ValueOf(input)
return val.Kind() == reflect.Pointer && val.IsNil()
}
func (d *Decoder) decode(name string, data any, val reflect.Value) error { func (d *Decoder) decode(name string, data any, val reflect.Value) error {
if isNil(data) {
// If the data is nil, then we don't set anything
// Maybe we should set to zero value?
return nil
}
if !reflect.ValueOf(data).IsValid() {
// If the input value is invalid, then we just set the value
// to be the zero value.
val.Set(reflect.Zero(val.Type()))
return nil
}
for { for {
kind := val.Kind() kind := val.Kind()
if kind == reflect.Pointer && val.IsNil() { if kind == reflect.Pointer && val.IsNil() {

View file

@ -267,3 +267,21 @@ func TestStructure_TextUnmarshaller(t *testing.T) {
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_Null(t *testing.T) {
rawMap := map[string]any{
"opt": map[string]any{
"bar": nil,
},
}
s := struct {
Opt struct {
Bar string `test:"bar,optional"`
} `test:"opt,optional"`
}{}
err := decoder.Decode(rawMap, &s)
assert.Nil(t, err)
assert.Equal(t, s.Opt.Bar, "")
}

View file

@ -3,6 +3,7 @@ package utils
import ( import (
"errors" "errors"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -139,10 +140,34 @@ func (ranges IntRanges[T]) Range(f func(t T) bool) {
} }
for _, r := range ranges { for _, r := range ranges {
for i := r.Start(); i <= r.End(); i++ { for i := r.Start(); i <= r.End() && i >= r.Start(); i++ {
if !f(i) { if !f(i) {
return return
} }
if i+1 < i { // integer overflow
break
}
} }
} }
} }
func (ranges IntRanges[T]) Merge() (mergedRanges IntRanges[T]) {
if len(ranges) == 0 {
return
}
sort.Slice(ranges, func(i, j int) bool {
return ranges[i].Start() < ranges[j].Start()
})
mergedRanges = ranges[:1]
var rangeIndex int
for _, r := range ranges[1:] {
if mergedRanges[rangeIndex].End()+1 > mergedRanges[rangeIndex].End() && // integer overflow
r.Start() > mergedRanges[rangeIndex].End()+1 {
mergedRanges = append(mergedRanges, r)
rangeIndex++
} else if r.End() > mergedRanges[rangeIndex].End() {
mergedRanges[rangeIndex].end = r.End()
}
}
return
}

View file

@ -0,0 +1,82 @@
package utils
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestMergeRanges(t *testing.T) {
t.Parallel()
for _, testRange := range []struct {
ranges IntRanges[uint16]
expected IntRanges[uint16]
}{
{
ranges: IntRanges[uint16]{
NewRange[uint16](0, 1),
NewRange[uint16](1, 2),
},
expected: IntRanges[uint16]{
NewRange[uint16](0, 2),
},
},
{
ranges: IntRanges[uint16]{
NewRange[uint16](0, 3),
NewRange[uint16](5, 7),
NewRange[uint16](8, 9),
NewRange[uint16](10, 10),
},
expected: IntRanges[uint16]{
NewRange[uint16](0, 3),
NewRange[uint16](5, 10),
},
},
{
ranges: IntRanges[uint16]{
NewRange[uint16](1, 3),
NewRange[uint16](2, 6),
NewRange[uint16](8, 10),
NewRange[uint16](15, 18),
},
expected: IntRanges[uint16]{
NewRange[uint16](1, 6),
NewRange[uint16](8, 10),
NewRange[uint16](15, 18),
},
},
{
ranges: IntRanges[uint16]{
NewRange[uint16](1, 3),
NewRange[uint16](2, 7),
NewRange[uint16](2, 6),
},
expected: IntRanges[uint16]{
NewRange[uint16](1, 7),
},
},
{
ranges: IntRanges[uint16]{
NewRange[uint16](1, 3),
NewRange[uint16](2, 6),
NewRange[uint16](2, 7),
},
expected: IntRanges[uint16]{
NewRange[uint16](1, 7),
},
},
{
ranges: IntRanges[uint16]{
NewRange[uint16](1, 3),
NewRange[uint16](2, 65535),
NewRange[uint16](2, 7),
NewRange[uint16](3, 16),
},
expected: IntRanges[uint16]{
NewRange[uint16](1, 65535),
},
},
} {
assert.Equal(t, testRange.expected, testRange.ranges.Merge())
}
}

View file

@ -17,7 +17,6 @@ import (
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
) )
var trustCerts []*x509.Certificate
var globalCertPool *x509.CertPool var globalCertPool *x509.CertPool
var mutex sync.RWMutex var mutex sync.RWMutex
var errNotMatch = errors.New("certificate fingerprints do not match") var errNotMatch = errors.New("certificate fingerprints do not match")
@ -30,11 +29,19 @@ var DisableSystemCa, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_CA"))
func AddCertificate(certificate string) error { func AddCertificate(certificate string) error {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
if certificate == "" { if certificate == "" {
return fmt.Errorf("certificate is empty") return fmt.Errorf("certificate is empty")
} }
if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil {
trustCerts = append(trustCerts, cert) if globalCertPool == nil {
initializeCertPool()
}
if globalCertPool.AppendCertsFromPEM([]byte(certificate)) {
return nil
} else if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil {
globalCertPool.AddCert(cert)
return nil return nil
} else { } else {
return fmt.Errorf("add certificate failed") return fmt.Errorf("add certificate failed")
@ -51,9 +58,6 @@ func initializeCertPool() {
globalCertPool = x509.NewCertPool() globalCertPool = x509.NewCertPool()
} }
} }
for _, cert := range trustCerts {
globalCertPool.AddCert(cert)
}
if !DisableEmbedCa { if !DisableEmbedCa {
globalCertPool.AppendCertsFromPEM(_CaCertificates) globalCertPool.AppendCertsFromPEM(_CaCertificates)
} }
@ -62,7 +66,6 @@ func initializeCertPool() {
func ResetCertificate() { func ResetCertificate() {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
trustCerts = nil
initializeCertPool() initializeCertPool()
} }
@ -138,7 +141,6 @@ func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, cu
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig = GetGlobalTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes) tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
} }

View file

@ -6,7 +6,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -86,12 +85,14 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
return return
} }
dnsAddr := make([]netip.Addr, l) results := make([]netip.Addr, 0, len(dns))
for i := 0; i < l; i++ { for _, ip := range dns {
dnsAddr[i] = nnip.IpToAddr(dns[i]) if addr, ok := netip.AddrFromSlice(ip); ok {
results = append(results, addr.Unmap())
}
} }
result <- dnsAddr result <- results
return return
} }

View file

@ -7,14 +7,12 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/keepalive"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/log"
) )
const ( const (
@ -22,34 +20,16 @@ const (
DefaultUDPTimeout = DefaultTCPTimeout DefaultUDPTimeout = DefaultTCPTimeout
) )
type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error)
var ( var (
dialMux sync.Mutex dialMux sync.Mutex
IP4PEnable bool
actualSingleStackDialContext = serialSingleStackDialContext actualSingleStackDialContext = serialSingleStackDialContext
actualDualStackDialContext = serialDualStackDialContext actualDualStackDialContext = serialDualStackDialContext
tcpConcurrent = false tcpConcurrent = false
fallbackTimeout = 300 * time.Millisecond fallbackTimeout = 300 * time.Millisecond
) )
func applyOptions(options ...Option) *option {
opt := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(opt)
}
for _, o := range options {
o(opt)
}
return opt
}
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := applyOptions(options...) opt := applyOptions(options...)
@ -79,28 +59,43 @@ func DialContext(ctx context.Context, network, address string, options ...Option
} }
func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) {
cfg := applyOptions(options...) opt := applyOptions(options...)
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.addrReuse { if opt.addrReuse {
addrReuseToListenConfig(lc) addrReuseToListenConfig(lc)
} }
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
socketHookToListenConfig(lc) socketHookToListenConfig(lc)
} else { } else {
if cfg.interfaceName != "" { if opt.interfaceName == "" {
opt.interfaceName = DefaultInterface.Load()
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil {
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
}
}
if rAddrPort.Addr().Unmap().IsLoopback() {
// avoid "The requested address is not valid in its context."
opt.interfaceName = ""
}
if opt.interfaceName != "" {
bind := bindIfaceToListenConfig bind := bindIfaceToListenConfig
if cfg.fallbackBind { if opt.fallbackBind {
bind = fallbackBindIfaceToListenConfig bind = fallbackBindIfaceToListenConfig
} }
addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort) addr, err := bind(opt.interfaceName, lc, network, address, rAddrPort)
if err != nil { if err != nil {
return nil, err return nil, err
} }
address = addr address = addr
} }
if cfg.routingMark != 0 { if opt.routingMark == 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address) opt.routingMark = int(DefaultRoutingMark.Load())
}
if opt.routingMark != 0 {
bindMarkToListenConfig(opt.routingMark, lc, network, address)
} }
} }
@ -126,11 +121,9 @@ func GetTcpConcurrent() bool {
return tcpConcurrent return tcpConcurrent
} }
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) { func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt option) (net.Conn, error) {
var address string var address string
if IP4PEnable { destination, port = resolver.LookupIP4P(destination, port)
destination, port = lookupIP4P(destination, port)
}
address = net.JoinHostPort(destination.String(), port) address = net.JoinHostPort(destination.String(), port)
netDialer := opt.netDialer netDialer := opt.netDialer
@ -153,6 +146,14 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
socketHookToToDialer(dialer) socketHookToToDialer(dialer)
} else { } else {
if opt.interfaceName == "" {
opt.interfaceName = DefaultInterface.Load()
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil {
opt.interfaceName = finder.FindInterfaceName(destination)
}
}
if opt.interfaceName != "" { if opt.interfaceName != "" {
bind := bindIfaceToDialer bind := bindIfaceToDialer
if opt.fallbackBind { if opt.fallbackBind {
@ -162,6 +163,9 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
return nil, err return nil, err
} }
} }
if opt.routingMark == 0 {
opt.routingMark = int(DefaultRoutingMark.Load())
}
if opt.routingMark != 0 { if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination) bindMarkToDialer(opt.routingMark, dialer, network, destination)
} }
@ -173,26 +177,26 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)
} }
func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return serialDialContext(ctx, network, ips, port, opt) return serialDialContext(ctx, network, ips, port, opt)
} }
func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt) return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt)
} }
func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return parallelDialContext(ctx, network, ips, port, opt) return parallelDialContext(ctx, network, ips, port, opt)
} }
func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if opt.prefer != 4 && opt.prefer != 6 { if opt.prefer != 4 && opt.prefer != 6 {
return parallelDialContext(ctx, network, ips, port, opt) return parallelDialContext(ctx, network, ips, port, opt)
} }
return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt) return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt)
} }
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
ipv4s, ipv6s := resolver.SortationAddr(ips) ipv4s, ipv6s := resolver.SortationAddr(ips)
if len(ipv4s) == 0 && len(ipv6s) == 0 { if len(ipv4s) == 0 && len(ipv6s) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
@ -273,7 +277,7 @@ loop:
return nil, errors.Join(errs...) return nil, errors.Join(errs...)
} }
func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if len(ips) == 0 { if len(ips) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
} }
@ -312,7 +316,7 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr,
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if len(ips) == 0 { if len(ips) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
} }
@ -373,33 +377,10 @@ func (d Dialer) DialContext(ctx context.Context, network, address string) (net.C
} }
func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) { func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
opt := d.Opt // make a copy return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(d.Opt))
if rAddrPort.Addr().Unmap().IsLoopback() {
// avoid "The requested address is not valid in its context."
WithInterface("")(&opt)
}
return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, rAddrPort, WithOption(opt))
} }
func NewDialer(options ...Option) Dialer { func NewDialer(options ...Option) Dialer {
opt := applyOptions(options...) opt := applyOptions(options...)
return Dialer{Opt: *opt} return Dialer{Opt: opt}
}
func GetIP4PEnable(enableIP4PConvert bool) {
IP4PEnable = enableIP4PConvert
}
// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go
func lookupIP4P(addr netip.Addr, port string) (netip.Addr, string) {
ip := addr.AsSlice()
if ip[0] == 0x20 && ip[1] == 0x01 &&
ip[2] == 0x00 && ip[3] == 0x00 {
addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]})
port = strconv.Itoa(int(ip[10])<<8 + int(ip[11]))
log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port))
return addr, port
}
return addr, port
} }

View file

@ -3,17 +3,23 @@ package dialer
import ( import (
"context" "context"
"net" "net"
"net/netip"
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
) )
var ( var (
DefaultOptions []Option
DefaultInterface = atomic.NewTypedValue[string]("") DefaultInterface = atomic.NewTypedValue[string]("")
DefaultRoutingMark = atomic.NewInt32(0) DefaultRoutingMark = atomic.NewInt32(0)
DefaultInterfaceFinder = atomic.NewTypedValue[InterfaceFinder](nil)
) )
type InterfaceFinder interface {
FindInterfaceName(destination netip.Addr) string
}
type NetDialer interface { type NetDialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error) DialContext(ctx context.Context, network, address string) (net.Conn, error)
} }
@ -108,3 +114,15 @@ func WithOption(o option) Option {
*opt = o *opt = o
} }
} }
func IsZeroOptions(opts []Option) bool {
return applyOptions(opts...) == option{}
}
func applyOptions(options ...Option) option {
opt := option{}
for _, o := range options {
o(&opt)
}
return opt
}

View file

@ -5,13 +5,11 @@ import (
"net" "net"
"syscall" "syscall"
"golang.org/x/sys/windows" "github.com/metacubex/mihomo/common/sockopt"
) )
func addrReuseToListenConfig(lc *net.ListenConfig) { func addrReuseToListenConfig(lc *net.ListenConfig) {
addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) { return sockopt.RawConnReuseaddr(c)
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
}) })
} }

View file

@ -1,20 +0,0 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package dialer
import (
"context"
"net"
"syscall"
"golang.org/x/sys/unix"
)
func addrReuseToListenConfig(lc *net.ListenConfig) {
addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
})
}

View file

@ -6,9 +6,10 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/profile/cachefile"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"go4.org/netipx"
) )
const ( const (
@ -183,7 +184,7 @@ func New(options Options) (*Pool, error) {
hostAddr = options.IPNet.Masked().Addr() hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next() gateway = hostAddr.Next()
first = gateway.Next().Next().Next() // default start with 198.18.0.4 first = gateway.Next().Next().Next() // default start with 198.18.0.4
last = nnip.UnMasked(options.IPNet) last = netipx.PrefixLastIP(options.IPNet)
) )
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {

View file

@ -0,0 +1,37 @@
package generater
import (
"encoding/base64"
"fmt"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair")
}
switch args[0] {
case "uuid":
newUUID, err := uuid.NewV4()
if err != nil {
panic(err)
}
fmt.Println(newUUID.String())
case "reality-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
publicKey := privateKey.PublicKey()
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]))
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:]))
case "wg-keypair":
privateKey, err := GeneratePrivateKey()
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + privateKey.String())
fmt.Println("PublicKey: " + privateKey.PublicKey().String())
}
}

View file

@ -0,0 +1,97 @@
// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155
package generater
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/curve25519"
)
// KeyLen is the expected key length for a WireGuard key.
const KeyLen = 32 // wgh.KeyLen
// A Key is a public, private, or pre-shared secret key. The Key constructor
// functions in this package can be used to create Keys suitable for each of
// these applications.
type Key [KeyLen]byte
// GenerateKey generates a Key suitable for use as a pre-shared secret key from
// a cryptographically safe source.
//
// The output Key should not be used as a private key; use GeneratePrivateKey
// instead.
func GenerateKey() (Key, error) {
b := make([]byte, KeyLen)
if _, err := rand.Read(b); err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err)
}
return NewKey(b)
}
// GeneratePrivateKey generates a Key suitable for use as a private key from a
// cryptographically safe source.
func GeneratePrivateKey() (Key, error) {
key, err := GenerateKey()
if err != nil {
return Key{}, err
}
// Modify random bytes using algorithm described at:
// https://cr.yp.to/ecdh.html.
key[0] &= 248
key[31] &= 127
key[31] |= 64
return key, nil
}
// NewKey creates a Key from an existing byte slice. The byte slice must be
// exactly 32 bytes in length.
func NewKey(b []byte) (Key, error) {
if len(b) != KeyLen {
return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b))
}
var k Key
copy(k[:], b)
return k, nil
}
// ParseKey parses a Key from a base64-encoded string, as produced by the
// Key.String method.
func ParseKey(s string) (Key, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err)
}
return NewKey(b)
}
// PublicKey computes a public key from the private key k.
//
// PublicKey should only be called when k is a private key.
func (k Key) PublicKey() Key {
var (
pub [KeyLen]byte
priv = [KeyLen]byte(k)
)
// ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html,
// so no need to specify it.
curve25519.ScalarBaseMult(&pub, &priv)
return Key(pub)
}
// String returns the base64-encoded string representation of a Key.
//
// ParseKey can be used to produce a new Key from this string.
func (k Key) String() string {
return base64.StdEncoding.EncodeToString(k[:])
}

View file

@ -69,7 +69,7 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
if conn, err := inner.HandleTcp(address, specialProxy); err == nil { if conn, err := inner.HandleTcp(inner.GetTunnel(), address, specialProxy); err == nil {
return conn, nil return conn, nil
} else { } else {
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)

View file

@ -7,14 +7,17 @@ import (
"time" "time"
"github.com/metacubex/mihomo/common/singledo" "github.com/metacubex/mihomo/common/singledo"
"github.com/metacubex/bart"
) )
type Interface struct { type Interface struct {
Index int Index int
MTU int MTU int
Name string Name string
Addresses []netip.Prefix
HardwareAddr net.HardwareAddr HardwareAddr net.HardwareAddr
Flags net.Flags
Addresses []netip.Prefix
} }
var ( var (
@ -22,16 +25,23 @@ var (
ErrAddrNotFound = errors.New("addr not found") ErrAddrNotFound = errors.New("addr not found")
) )
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20) type ifaceCache struct {
ifMap map[string]*Interface
ifTable bart.Table[*Interface]
}
func Interfaces() (map[string]*Interface, error) { var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
func getCache() (*ifaceCache, error) {
value, err, _ := caches.Do(func() (*ifaceCache, error) {
ifaces, err := net.Interfaces() ifaces, err := net.Interfaces()
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := map[string]*Interface{} cache := &ifaceCache{
ifMap: make(map[string]*Interface),
}
for _, iface := range ifaces { for _, iface := range ifaces {
addrs, err := iface.Addrs() addrs, err := iface.Addrs()
@ -60,20 +70,34 @@ func Interfaces() (map[string]*Interface, error) {
} }
} }
r[iface.Name] = &Interface{ ifaceObj := &Interface{
Index: iface.Index, Index: iface.Index,
MTU: iface.MTU, MTU: iface.MTU,
Name: iface.Name, Name: iface.Name,
Addresses: ipNets,
HardwareAddr: iface.HardwareAddr, HardwareAddr: iface.HardwareAddr,
Flags: iface.Flags,
Addresses: ipNets,
}
cache.ifMap[iface.Name] = ifaceObj
for _, prefix := range ipNets {
cache.ifTable.Insert(prefix, ifaceObj)
} }
} }
return r, nil return cache, nil
}) })
return value, err return value, err
} }
func Interfaces() (map[string]*Interface, error) {
cache, err := getCache()
if err != nil {
return nil, err
}
return cache.ifMap, nil
}
func ResolveInterface(name string) (*Interface, error) { func ResolveInterface(name string) (*Interface, error) {
ifaces, err := Interfaces() ifaces, err := Interfaces()
if err != nil { if err != nil {
@ -88,23 +112,29 @@ func ResolveInterface(name string) (*Interface, error) {
return iface, nil return iface, nil
} }
func IsLocalIp(ip netip.Addr) (bool, error) { func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
ifaces, err := Interfaces() cache, err := getCache()
if err != nil {
return nil, err
}
iface, ok := cache.ifTable.Lookup(addr)
if !ok {
return nil, ErrIfaceNotFound
}
return iface, nil
}
func IsLocalIp(addr netip.Addr) (bool, error) {
cache, err := getCache()
if err != nil { if err != nil {
return false, err return false, err
} }
for _, iface := range ifaces { return cache.ifTable.Contains(addr), nil
for _, addr := range iface.Addresses {
if addr.Contains(ip) {
return true, nil
}
}
}
return false, nil
} }
func FlushCache() { func FlushCache() {
interfaces.Reset() caches.Reset()
} }
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) { func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) {

View file

@ -35,7 +35,7 @@ func KeepAliveInterval() time.Duration {
func SetDisableKeepAlive(disable bool) { func SetDisableKeepAlive(disable bool) {
if runtime.GOOS == "android" { if runtime.GOOS == "android" {
setDisableKeepAlive(false) setDisableKeepAlive(true)
} else { } else {
setDisableKeepAlive(disable) setDisableKeepAlive(disable)
} }
@ -59,7 +59,7 @@ func SetNetListenConfig(lc *net.ListenConfig) {
} }
func TCPKeepAlive(c net.Conn) { func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok && tcp != nil { if tcp, ok := c.(TCPConn); ok && tcp != nil {
tcpKeepAlive(tcp) tcpKeepAlive(tcp)
} }
} }

View file

@ -2,9 +2,18 @@
package keepalive package keepalive
import "net" import (
"net"
"time"
)
func tcpKeepAlive(tcp *net.TCPConn) { type TCPConn interface {
net.Conn
SetKeepAlive(keepalive bool) error
SetKeepAlivePeriod(d time.Duration) error
}
func tcpKeepAlive(tcp TCPConn) {
if DisableKeepAlive() { if DisableKeepAlive() {
_ = tcp.SetKeepAlive(false) _ = tcp.SetKeepAlive(false)
} else { } else {

View file

@ -4,6 +4,12 @@ package keepalive
import "net" import "net"
type TCPConn interface {
net.Conn
SetKeepAlive(keepalive bool) error
SetKeepAliveConfig(config net.KeepAliveConfig) error
}
func keepAliveConfig() net.KeepAliveConfig { func keepAliveConfig() net.KeepAliveConfig {
config := net.KeepAliveConfig{ config := net.KeepAliveConfig{
Enable: true, Enable: true,
@ -18,7 +24,7 @@ func keepAliveConfig() net.KeepAliveConfig {
return config return config
} }
func tcpKeepAlive(tcp *net.TCPConn) { func tcpKeepAlive(tcp TCPConn) {
if DisableKeepAlive() { if DisableKeepAlive() {
_ = tcp.SetKeepAlive(false) _ = tcp.SetKeepAlive(false)
} else { } else {

View file

@ -4,14 +4,25 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"os"
"strconv"
"github.com/metacubex/mihomo/common/callback" "github.com/metacubex/mihomo/common/callback"
"github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/iface"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v3"
) )
var disableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR"))
func init() {
if features.CMFA {
disableLoopBackDetector = true
}
}
var ErrReject = errors.New("reject loopback connection") var ErrReject = errors.New("reject loopback connection")
type Detector struct { type Detector struct {
@ -20,6 +31,9 @@ type Detector struct {
} }
func NewDetector() *Detector { func NewDetector() *Detector {
if disableLoopBackDetector {
return nil
}
return &Detector{ return &Detector{
connMap: xsync.NewMapOf[netip.AddrPort, struct{}](), connMap: xsync.NewMapOf[netip.AddrPort, struct{}](),
packetConnMap: xsync.NewMapOf[uint16, struct{}](), packetConnMap: xsync.NewMapOf[uint16, struct{}](),
@ -27,6 +41,9 @@ func NewDetector() *Detector {
} }
func (l *Detector) NewConn(conn C.Conn) C.Conn { func (l *Detector) NewConn(conn C.Conn) C.Conn {
if l == nil || l.connMap == nil {
return conn
}
metadata := C.Metadata{} metadata := C.Metadata{}
if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { if metadata.SetRemoteAddr(conn.LocalAddr()) != nil {
return conn return conn
@ -42,6 +59,9 @@ func (l *Detector) NewConn(conn C.Conn) C.Conn {
} }
func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn { func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn {
if l == nil || l.packetConnMap == nil {
return conn
}
metadata := C.Metadata{} metadata := C.Metadata{}
if metadata.SetRemoteAddr(conn.LocalAddr()) != nil { if metadata.SetRemoteAddr(conn.LocalAddr()) != nil {
return conn return conn
@ -58,6 +78,9 @@ func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn {
} }
func (l *Detector) CheckConn(metadata *C.Metadata) error { func (l *Detector) CheckConn(metadata *C.Metadata) error {
if l == nil || l.connMap == nil {
return nil
}
connAddr := metadata.SourceAddrPort() connAddr := metadata.SourceAddrPort()
if !connAddr.IsValid() { if !connAddr.IsValid() {
return nil return nil
@ -69,6 +92,9 @@ func (l *Detector) CheckConn(metadata *C.Metadata) error {
} }
func (l *Detector) CheckPacketConn(metadata *C.Metadata) error { func (l *Detector) CheckPacketConn(metadata *C.Metadata) error {
if l == nil || l.packetConnMap == nil {
return nil
}
connAddr := metadata.SourceAddrPort() connAddr := metadata.SourceAddrPort()
if !connAddr.IsValid() { if !connAddr.IsValid() {
return nil return nil

View file

@ -5,6 +5,7 @@ import (
"net" "net"
"strings" "strings"
"github.com/metacubex/mihomo/log"
"github.com/oschwald/maxminddb-golang" "github.com/oschwald/maxminddb-golang"
) )
@ -23,11 +24,16 @@ type ASNReader struct {
*maxminddb.Reader *maxminddb.Reader
} }
type ASNResult struct { type GeoLite2 struct {
AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
} }
type IPInfo struct {
ASN string `maxminddb:"asn"`
Name string `maxminddb:"name"`
}
func (r IPReader) LookupCode(ipAddress net.IP) []string { func (r IPReader) LookupCode(ipAddress net.IP) []string {
switch r.databaseType { switch r.databaseType {
case typeMaxmind: case typeMaxmind:
@ -66,8 +72,18 @@ func (r IPReader) LookupCode(ipAddress net.IP) []string {
} }
} }
func (r ASNReader) LookupASN(ip net.IP) ASNResult { func (r ASNReader) LookupASN(ip net.IP) (string, string) {
var result ASNResult switch r.Metadata.DatabaseType {
r.Lookup(ip, &result) case "GeoLite2-ASN", "DBIP-ASN-Lite (compat=GeoLite2-ASN)":
return result var result GeoLite2
_ = r.Lookup(ip, &result)
return fmt.Sprint(result.AutonomousSystemNumber), result.AutonomousSystemOrganization
case "ipinfo generic_asn_free.mmdb":
var result IPInfo
_ = r.Lookup(ip, &result)
return result.ASN[2:], result.Name
default:
log.Warnln("Unsupported ASN type: %s", r.Metadata.DatabaseType)
}
return "", ""
} }

View file

@ -87,6 +87,7 @@ func findProcessName(network string, ip netip.Addr, port int) (uint32, string, e
default: default:
continue continue
} }
srcIP = srcIP.Unmap()
if ip == srcIP { if ip == srcIP {
// xsocket_n.so_last_pid // xsocket_n.so_last_pid

View file

@ -10,7 +10,6 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
@ -136,13 +135,14 @@ func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (u
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = nnip.IpToAddr(buf[inp+s.ip : inp+s.ip+4]) srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = nnip.IpToAddr(buf[inp+s.ip-12 : inp+s.ip+4]) srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip-12 : inp+s.ip+4])
default: default:
continue continue
} }
srcIP = srcIP.Unmap()
if ip != srcIP { if ip != srcIP {
continue continue

View file

@ -7,7 +7,6 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@ -137,7 +136,8 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error)
continue continue
} }
srcIP := nnip.IpToAddr(row[s.ip : s.ip+s.ipSize]) srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
srcIP = srcIP.Unmap()
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue continue
@ -180,7 +180,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.SyscallN(fn, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0) err, _, _ := syscall.Syscall6(fn, 6, uintptr(ptr), uintptr(unsafe.Pointer(&size)), 0, uintptr(family), uintptr(class), 0)
switch err { switch err {
case 0: case 0:
@ -215,13 +215,13 @@ 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.SyscallN( r1, _, err := syscall.Syscall6(
queryProcName, queryProcName, 4,
uintptr(h), uintptr(h),
uintptr(0), uintptr(0),
uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&size)),
) 0, 0)
if r1 == 0 { if r1 == 0 {
return "", err return "", err
} }

View file

@ -0,0 +1,37 @@
package resolver
import (
"net"
"net/netip"
"strconv"
"github.com/metacubex/mihomo/log"
)
var (
ip4PEnable bool
)
func GetIP4PEnable() bool {
return ip4PEnable
}
func SetIP4PEnable(enableIP4PConvert bool) {
ip4PEnable = enableIP4PConvert
}
// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go
func LookupIP4P(addr netip.Addr, port string) (netip.Addr, string) {
if ip4PEnable {
ip := addr.AsSlice()
if ip[0] == 0x20 && ip[1] == 0x01 &&
ip[2] == 0x00 && ip[3] == 0x00 {
addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]})
port = strconv.Itoa(int(ip[10])<<8 + int(ip[11]))
log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port))
return addr, port
}
}
return addr, port
}

View file

@ -196,6 +196,26 @@ func ResolveIP(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPWithResolver(ctx, host, DefaultResolver) return ResolveIPWithResolver(ctx, host, DefaultResolver)
} }
// ResolveIPPrefer6WithResolver same as ResolveIP, but with a resolver
func ResolveIPPrefer6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) {
ips, err := LookupIPWithResolver(ctx, host, r)
if err != nil {
return netip.Addr{}, err
} else if len(ips) == 0 {
return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host)
}
ipv4s, ipv6s := SortationAddr(ips)
if len(ipv6s) > 0 {
return ipv6s[randv2.IntN(len(ipv6s))], nil
}
return ipv4s[randv2.IntN(len(ipv4s))], nil
}
// ResolveIPPrefer6 with a host, return ip and priority return TypeAAAA
func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
}
func ResetConnection() { func ResetConnection() {
if DefaultResolver != nil { if DefaultResolver != nil {
go DefaultResolver.ResetConnection() go DefaultResolver.ResetConnection()

View file

@ -84,12 +84,14 @@ func NewFileVehicle(path string) *FileVehicle {
} }
type HTTPVehicle struct { type HTTPVehicle struct {
url string url string
path string path string
proxy string proxy string
header http.Header header http.Header
timeout time.Duration timeout time.Duration
provider types.ProxyProvider sizeLimit int64
inRead func(response *http.Response)
provider types.ProxyProvider
} }
func (h *HTTPVehicle) Url() string { func (h *HTTPVehicle) Url() string {
@ -112,8 +114,8 @@ func (h *HTTPVehicle) Write(buf []byte) error {
return safeWrite(h.path, buf) return safeWrite(h.path, buf)
} }
func (h *HTTPVehicle) SetProvider(provider types.ProxyProvider) { func (h *HTTPVehicle) SetInRead(fn func(response *http.Response)) {
h.provider = provider h.inRead = fn
} }
func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) { func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []byte, hash utils.HashType, err error) {
@ -139,9 +141,8 @@ func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []b
} }
defer resp.Body.Close() defer resp.Body.Close()
if subscriptionInfo := resp.Header.Get("subscription-userinfo"); h.provider != nil && subscriptionInfo != "" { if h.inRead != nil {
cachefile.Cache().SetSubscriptionInfo(h.provider.Name(), subscriptionInfo) h.inRead(resp)
h.provider.SetSubscriptionInfo(subscriptionInfo)
} }
if resp.StatusCode < 200 || resp.StatusCode > 299 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
@ -151,7 +152,11 @@ func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []b
err = errors.New(resp.Status) err = errors.New(resp.Status)
return return
} }
buf, err = io.ReadAll(resp.Body) var reader io.Reader = resp.Body
if h.sizeLimit > 0 {
reader = io.LimitReader(reader, h.sizeLimit)
}
buf, err = io.ReadAll(reader)
if err != nil { if err != nil {
return return
} }
@ -166,12 +171,13 @@ func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []b
return return
} }
func NewHTTPVehicle(url string, path string, proxy string, header http.Header, timeout time.Duration) *HTTPVehicle { func NewHTTPVehicle(url string, path string, proxy string, header http.Header, timeout time.Duration, sizeLimit int64) *HTTPVehicle {
return &HTTPVehicle{ return &HTTPVehicle{
url: url, url: url,
path: path, path: path,
proxy: proxy, proxy: proxy,
header: header, header: header,
timeout: timeout, timeout: timeout,
sizeLimit: sizeLimit,
} }
} }

View file

@ -48,6 +48,10 @@ func (sd *Dispatcher) shouldOverride(metadata *C.Metadata) bool {
if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping { if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping {
return true return true
} }
return sd.forceSniff(metadata)
}
func (sd *Dispatcher) forceSniff(metadata *C.Metadata) bool {
for _, matcher := range sd.forceDomain { for _, matcher := range sd.forceDomain {
if matcher.MatchDomain(metadata.Host) { if matcher.MatchDomain(metadata.Host) {
return true return true
@ -56,28 +60,35 @@ func (sd *Dispatcher) shouldOverride(metadata *C.Metadata) bool {
return false return false
} }
func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter) bool { // UDPSniff is called when a UDP NAT is created and passed the first initialization packet.
// It may return a wrapped packetSender if the sniffer process needs to wait for multiple packets.
// This function must be non-blocking, and any blocking operations should be done in the wrapped packetSender.
func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSender) C.PacketSender {
metadata := packet.Metadata() metadata := packet.Metadata()
if sd.shouldOverride(metadata) { if sd.shouldOverride(metadata) {
for sniffer, config := range sd.sniffers { for current, config := range sd.sniffers {
if sniffer.SupportNetwork() == C.UDP || sniffer.SupportNetwork() == C.ALLNet { if current.SupportNetwork() == C.UDP || current.SupportNetwork() == C.ALLNet {
inWhitelist := sniffer.SupportPort(metadata.DstPort) inWhitelist := current.SupportPort(metadata.DstPort)
overrideDest := config.OverrideDest overrideDest := config.OverrideDest
if inWhitelist { if inWhitelist {
host, err := sniffer.SniffData(packet.Data()) if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
return wrapable.WrapperSender(packetSender, overrideDest)
}
host, err := current.SniffData(packet.Data())
if err != nil { if err != nil {
continue continue
} }
sd.replaceDomain(metadata, host, overrideDest) replaceDomain(metadata, host, overrideDest)
return true return packetSender
} }
} }
} }
} }
return false return packetSender
} }
// TCPSniff returns true if the connection is sniffed to have a domain // TCPSniff returns true if the connection is sniffed to have a domain
@ -98,16 +109,21 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
if !inWhitelist { if !inWhitelist {
return false return false
} }
forceSniffer := sd.forceSniff(metadata)
dst := metadata.AddrPort() dst := metadata.AddrPort()
if count, ok := sd.skipList.Get(dst); ok && count > 5 { if !forceSniffer {
log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) if count, ok := sd.skipList.Get(dst); ok && count > 5 {
return false log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst)
return false
}
} }
host, err := sd.sniffDomain(conn, metadata) host, err := sd.sniffDomain(conn, metadata)
if err != nil { if err != nil {
sd.cacheSniffFailed(metadata) if !forceSniffer {
sd.cacheSniffFailed(metadata)
}
log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort)
return false return false
} }
@ -121,13 +137,13 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
sd.skipList.Delete(dst) sd.skipList.Delete(dst)
sd.replaceDomain(metadata, host, overrideDest) replaceDomain(metadata, host, overrideDest)
return true return true
} }
return false return false
} }
func (sd *Dispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) { func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
metadata.SniffHost = host metadata.SniffHost = host
if overrideDest { if overrideDest {
log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]", log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]",
@ -145,8 +161,12 @@ func (sd *Dispatcher) Enable() bool {
} }
func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) { func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (string, error) {
//defer func(start time.Time) {
// log.Debugln("[Sniffer] [%s] Sniffing took %s", metadata.DstIP, time.Since(start))
//}(time.Now())
for s := range sd.sniffers { for s := range sd.sniffers {
if s.SupportNetwork() == C.TCP { if s.SupportNetwork() == C.TCP && s.SupportPort(metadata.DstPort) {
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) _ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
_, err := conn.Peek(1) _, err := conn.Peek(1)
_ = conn.SetReadDeadline(time.Time{}) _ = conn.SetReadDeadline(time.Time{})
@ -154,7 +174,7 @@ func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (s
_, ok := err.(*net.OpError) _, ok := err.(*net.OpError)
if ok { if ok {
sd.cacheSniffFailed(metadata) sd.cacheSniffFailed(metadata)
log.Errorln("[Sniffer] [%s] may not have any sent data, Consider adding skip", metadata.DstIP.String()) log.Errorln("[Sniffer] [%s] [%s] may not have any sent data, Consider adding skip", metadata.DstIP, s.Protocol())
_ = conn.Close() _ = conn.Close()
} }
@ -164,22 +184,36 @@ func (sd *Dispatcher) sniffDomain(conn *N.BufferedConn, metadata *C.Metadata) (s
bufferedLen := conn.Buffered() bufferedLen := conn.Buffered()
bytes, err := conn.Peek(bufferedLen) bytes, err := conn.Peek(bufferedLen)
if err != nil { if err != nil {
log.Debugln("[Sniffer] the data length not enough") log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err)
continue continue
} }
host, err := s.SniffData(bytes) host, err := s.SniffData(bytes)
var e *errNeedAtLeastData
if errors.As(err, &e) {
//log.Debugln("[Sniffer] [%s] [%s] %v, got length: %d", metadata.DstIP, s.Protocol(), e, len(bytes))
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
bytes, err = conn.Peek(e.length)
_ = conn.SetReadDeadline(time.Time{})
//log.Debugln("[Sniffer] [%s] [%s] try again, got length: %d", metadata.DstIP, s.Protocol(), len(bytes))
if err != nil {
log.Debugln("[Sniffer] [%s] [%s] the data length not enough, error: %v", metadata.DstIP, s.Protocol(), err)
continue
}
host, err = s.SniffData(bytes)
}
if err != nil { if err != nil {
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, error: %v", metadata.DstIP, s.Protocol(), err)
continue continue
} }
_, err = netip.ParseAddr(host) _, err = netip.ParseAddr(host)
if err == nil { if err == nil {
//log.Debugln("[Sniffer] [%s] Sniff data failed %s", s.Protocol(), metadata.DstIP) //log.Debugln("[Sniffer] [%s] [%s] Sniff data failed, got host [%s]", metadata.DstIP, s.Protocol(), host)
continue continue
} }
//log.Debugln("[Sniffer] [%s] [%s] Sniffed [%s]", metadata.DstIP, s.Protocol(), host)
return host, nil return host, nil
} }
} }

View file

@ -7,10 +7,15 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"sync"
"time"
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
"github.com/metacubex/mihomo/common/pool"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/constant"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/sniffer"
"github.com/metacubex/quic-go/quicvarint" "github.com/metacubex/quic-go/quicvarint"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
@ -21,6 +26,16 @@ import (
const ( const (
versionDraft29 uint32 = 0xff00001d versionDraft29 uint32 = 0xff00001d
version1 uint32 = 0x1 version1 uint32 = 0x1
quicPacketTypeInitial = 0x00
quicPacketType0RTT = 0x01
// Timeout before quic sniffer all packets
quicWaitConn = time.Second * 3
// maxCryptoStreamOffset is the maximum offset allowed on any of the crypto streams.
// This limits the size of the ClientHello and Certificates that can be received.
maxCryptoStreamOffset = 16 * (1 << 10)
) )
var ( var (
@ -30,6 +45,9 @@ var (
errNotQuicInitial = errors.New("not QUIC initial packet") errNotQuicInitial = errors.New("not QUIC initial packet")
) )
var _ sniffer.Sniffer = (*QuicSniffer)(nil)
var _ sniffer.MultiPacketSniffer = (*QuicSniffer)(nil)
type QuicSniffer struct { type QuicSniffer struct {
*BaseSniffer *BaseSniffer
} }
@ -44,67 +62,160 @@ func NewQuicSniffer(snifferConfig SnifferConfig) (*QuicSniffer, error) {
}, nil }, nil
} }
func (quic QuicSniffer) Protocol() string { func (sniffer *QuicSniffer) Protocol() string {
return "quic" return "quic"
} }
func (quic QuicSniffer) SupportNetwork() C.NetWork { func (sniffer *QuicSniffer) SupportNetwork() C.NetWork {
return C.UDP return C.UDP
} }
func (quic QuicSniffer) SniffData(b []byte) (string, error) { func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
return "", ErrorUnsupportedSniffer
}
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender {
return &quicPacketSender{
sender: packetSender,
chClose: make(chan struct{}),
override: override,
}
}
var _ constant.PacketSender = (*quicPacketSender)(nil)
type quicPacketSender struct {
lock sync.RWMutex
ranges utils.IntRanges[uint64]
buffer []byte
result string
override bool
sender constant.PacketSender
chClose chan struct{}
closed bool
}
// Send will send PacketAdapter nonblocking
// the implement must call UDPPacket.Drop() inside Send
func (q *quicPacketSender) Send(current constant.PacketAdapter) {
defer q.sender.Send(current)
q.lock.RLock()
if q.closed {
q.lock.RUnlock()
return
}
q.lock.RUnlock()
err := q.readQuicData(current.Data())
if err != nil {
q.close()
return
}
}
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) {
q.sender.Process(conn, proxy)
}
// ResolveUDP wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error {
select {
case <-q.chClose:
q.lock.RLock()
replaceDomain(data, q.result, q.override)
q.lock.RUnlock()
break
case <-time.After(quicWaitConn):
q.close()
}
return q.sender.ResolveUDP(data)
}
// Close stop the Process loop
func (q *quicPacketSender) Close() {
q.sender.Close()
q.close()
}
func (q *quicPacketSender) close() {
q.lock.Lock()
q.closeLocked()
q.lock.Unlock()
}
func (q *quicPacketSender) closeLocked() {
if !q.closed {
close(q.chClose)
q.closed = true
if q.buffer != nil {
_ = pool.Put(q.buffer)
q.buffer = nil
}
q.ranges = nil
}
}
func (q *quicPacketSender) readQuicData(b []byte) error {
buffer := buf.As(b) buffer := buf.As(b)
typeByte, err := buffer.ReadByte() typeByte, err := buffer.ReadByte()
if err != nil { if err != nil {
return "", errNotQuic return errNotQuic
} }
isLongHeader := typeByte&0x80 > 0 isLongHeader := typeByte&0x80 > 0
if !isLongHeader || typeByte&0x40 == 0 { if !isLongHeader || typeByte&0x40 == 0 {
return "", errNotQuicInitial return errNotQuicInitial
} }
vb, err := buffer.ReadBytes(4) vb, err := buffer.ReadBytes(4)
if err != nil { if err != nil {
return "", errNotQuic return errNotQuic
} }
versionNumber := binary.BigEndian.Uint32(vb) versionNumber := binary.BigEndian.Uint32(vb)
if versionNumber != 0 && typeByte&0x40 == 0 { if versionNumber != 0 && typeByte&0x40 == 0 {
return "", errNotQuic return errNotQuic
} else if versionNumber != versionDraft29 && versionNumber != version1 { } else if versionNumber != versionDraft29 && versionNumber != version1 {
return "", errNotQuic return errNotQuic
} }
if (typeByte&0x30)>>4 != 0x0 { connIdLen, err := buffer.ReadByte()
return "", errNotQuicInitial if err != nil || connIdLen == 0 {
return errNotQuic
}
destConnID := make([]byte, int(connIdLen))
if _, err := io.ReadFull(buffer, destConnID); err != nil {
return errNotQuic
} }
var destConnID []byte packetType := (typeByte & 0x30) >> 4
if l, err := buffer.ReadByte(); err != nil { if packetType != quicPacketTypeInitial {
return "", errNotQuic return nil
} else if destConnID, err = buffer.ReadBytes(int(l)); err != nil {
return "", errNotQuic
} }
if l, err := buffer.ReadByte(); err != nil { if l, err := buffer.ReadByte(); err != nil {
return "", errNotQuic return errNotQuic
} else if _, err := buffer.ReadBytes(int(l)); err != nil { } else if _, err := buffer.ReadBytes(int(l)); err != nil {
return "", errNotQuic return errNotQuic
} }
tokenLen, err := quicvarint.Read(buffer) tokenLen, err := quicvarint.Read(buffer)
if err != nil || tokenLen > uint64(len(b)) { if err != nil || tokenLen > uint64(len(b)) {
return "", errNotQuic return errNotQuic
} }
if _, err = buffer.ReadBytes(int(tokenLen)); err != nil { if _, err = buffer.ReadBytes(int(tokenLen)); err != nil {
return "", errNotQuic return errNotQuic
} }
packetLen, err := quicvarint.Read(buffer) packetLen, err := quicvarint.Read(buffer)
if err != nil { if err != nil {
return "", errNotQuic return errNotQuic
} }
hdrLen := len(b) - buffer.Len() hdrLen := len(b) - buffer.Len()
@ -120,7 +231,7 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16) hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16)
block, err := aes.NewCipher(hpKey) block, err := aes.NewCipher(hpKey)
if err != nil { if err != nil {
return "", err return err
} }
cache := buf.NewPacket() cache := buf.NewPacket()
@ -130,6 +241,7 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16]) block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16])
firstByte := b[0] firstByte := b[0]
// Encrypt/decrypt first byte. // Encrypt/decrypt first byte.
if isLongHeader { if isLongHeader {
// Long header: 4 bits masked // Long header: 4 bits masked
// High 4 bits are not protected. // High 4 bits are not protected.
@ -153,8 +265,8 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
packetNumber[i] ^= mask[1+i] packetNumber[i] ^= mask[1+i]
} }
if packetNumber[0] != 0 && packetNumber[0] != 1 { if int(packetLen)+hdrLen > len(b) || extHdrLen > len(b) {
return "", errNotQuicInitial return errNotQuic
} }
data := b[extHdrLen : int(packetLen)+hdrLen] data := b[extHdrLen : int(packetLen)+hdrLen]
@ -163,12 +275,13 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12) iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
aesCipher, err := aes.NewCipher(key) aesCipher, err := aes.NewCipher(key)
if err != nil { if err != nil {
return "", err return err
} }
aead, err := cipher.NewGCM(aesCipher) aead, err := cipher.NewGCM(aesCipher)
if err != nil { if err != nil {
return "", err return err
} }
// We only decrypt once, so we do not need to XOR it back. // We only decrypt once, so we do not need to XOR it back.
// https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496 // https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496
for i, b := range packetNumber { for i, b := range packetNumber {
@ -177,13 +290,20 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
dst := cache.Extend(len(data)) dst := cache.Extend(len(data))
decrypted, err := aead.Open(dst[:0], iv, data, extHdr) decrypted, err := aead.Open(dst[:0], iv, data, extHdr)
if err != nil { if err != nil {
return "", err return err
} }
buffer = buf.As(decrypted) buffer = buf.As(decrypted)
cryptoLen := uint(0)
cryptoData := cache.Extend(buffer.Len())
for i := 0; !buffer.IsEmpty(); i++ { for i := 0; !buffer.IsEmpty(); i++ {
q.lock.RLock()
if q.closed {
q.lock.RUnlock()
// close() was called, just return
return nil
}
q.lock.RUnlock()
frameType := byte(0x0) // Default to PADDING frame frameType := byte(0x0) // Default to PADDING frame
for frameType == 0x0 && !buffer.IsEmpty() { for frameType == 0x0 && !buffer.IsEmpty() {
frameType, _ = buffer.ReadByte() frameType, _ = buffer.ReadByte()
@ -193,79 +313,131 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
case 0x01: // PING frame case 0x01: // PING frame
case 0x02, 0x03: // ACK frame case 0x02, 0x03: // ACK frame
if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count
if err != nil { if err != nil {
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
} }
if frameType == 0x03 { if frameType == 0x03 {
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
} }
case 0x06: // CRYPTO frame, we will use this frame case 0x06: // CRYPTO frame, we will use this frame
offset, err := quicvarint.Read(buffer) // Field: Offset offset, err := quicvarint.Read(buffer) // Field: Offset
if err != nil { if err != nil {
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
length, err := quicvarint.Read(buffer) // Field: Length length, err := quicvarint.Read(buffer) // Field: Length
if err != nil || length > uint64(buffer.Len()) { if err != nil || length > uint64(buffer.Len()) {
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if cryptoLen < uint(offset+length) {
cryptoLen = uint(offset + length) end := offset + length
if end > maxCryptoStreamOffset {
return io.ErrShortBuffer
} }
if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data
return "", io.ErrUnexpectedEOF q.lock.Lock()
if q.closed {
q.lock.Unlock()
// close() was called, just return
return nil
} }
if q.buffer == nil {
q.buffer = pool.Get(maxCryptoStreamOffset)[:end]
} else if end > uint64(len(q.buffer)) {
q.buffer = q.buffer[:end]
}
target := q.buffer[offset:end]
if _, err := buffer.Read(target); err != nil { // Field: Crypto Data
q.lock.Unlock()
return io.ErrUnexpectedEOF
}
q.ranges = append(q.ranges, utils.NewRange(offset, end))
q.ranges = q.ranges.Merge()
q.lock.Unlock()
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length
if err != nil { if err != nil {
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase
return "", io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
default: default:
// Only above frame types are permitted in initial packet. // Only above frame types are permitted in initial packet.
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8 // See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
return "", errNotQuicInitial return errNotQuicInitial
} }
} }
domain, err := ReadClientHello(cryptoData[:cryptoLen]) return q.tryAssemble()
if err != nil { }
return "", err
func (q *quicPacketSender) tryAssemble() error {
q.lock.RLock()
if q.closed {
q.lock.RUnlock()
// close() was called, just return
return nil
} }
return *domain, nil if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) {
q.lock.RUnlock()
// incomplete fragment, just return
return nil
}
if len(q.buffer) <= 4 ||
// Handshake Type (1) + uint24 Length (3) + ClientHello body
// maxCryptoStreamOffset is in the valid range of uint16 so just ignore the q.buffer[1]
int(binary.BigEndian.Uint16([]byte{q.buffer[2], q.buffer[3]})+4) != len(q.buffer) {
q.lock.RUnlock()
// end of segment not reached, just return
return nil
}
domain, err := ReadClientHello(q.buffer)
q.lock.RUnlock()
if err != nil {
return err
}
q.lock.Lock()
q.result = *domain
q.closeLocked()
q.lock.Unlock()
return nil
} }
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte { func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {

View file

@ -3,35 +3,184 @@ package sniffer
import ( import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"github.com/stretchr/testify/assert" "net"
"net/netip"
"testing" "testing"
"github.com/metacubex/mihomo/constant"
"github.com/stretchr/testify/assert"
) )
type fakeSender struct {
resultCh chan *constant.Metadata
}
var _ constant.PacketSender = (*fakeSender)(nil)
func (e *fakeSender) Send(packet constant.PacketAdapter) {
// Ensure that the wrapper's Send can correctly handle the situation where the packet is directly discarded.
packet.Drop()
}
func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) {
panic("not implemented")
}
func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error {
e.resultCh <- metadata
return nil
}
func (e *fakeSender) Close() {
panic("not implemented")
}
type fakeUDPPacket struct {
data []byte
data2 []byte // backup
}
func (s *fakeUDPPacket) InAddr() net.Addr {
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
}
func (s *fakeUDPPacket) LocalAddr() net.Addr {
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
}
func (s *fakeUDPPacket) Data() []byte {
return s.data
}
func (s *fakeUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
return 0, net.ErrClosed
}
func (s *fakeUDPPacket) Drop() {
for i := range s.data {
if s.data[i] != s.data2[i] { // ensure input data not changed
panic("data has been changed!")
}
s.data[i] = 0 // forcing data to become illegal
}
s.data = nil
}
var _ constant.UDPPacket = (*fakeUDPPacket)(nil)
func asPacket(data string) constant.PacketAdapter {
pktData, _ := hex.DecodeString(data)
meta := &constant.Metadata{}
pkt := &fakeUDPPacket{data: pktData, data2: bytes.Clone(pktData)}
pktAdp := constant.NewPacketAdapter(pkt, meta)
return pktAdp
}
func testQuicSniffer(data []string, async bool) (string, error) {
q, err := NewQuicSniffer(SnifferConfig{})
if err != nil {
return "", err
}
resultCh := make(chan *constant.Metadata, 1)
emptySender := &fakeSender{resultCh: resultCh}
sender := q.WrapperSender(emptySender, true)
go func() {
meta := constant.Metadata{}
err = sender.ResolveUDP(&meta)
if err != nil {
panic(err)
}
}()
for _, d := range data {
if async {
go sender.Send(asPacket(d))
} else {
sender.Send(asPacket(d))
}
}
meta := <-resultCh
return meta.SniffHost, nil
}
func TestQuicHeaders(t *testing.T) { func TestQuicHeaders(t *testing.T) {
cases := []struct { cases := []struct {
input string input []string
domain string domain string
}{ }{
//Normal domain quic sniff
{ {
input: "cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b", input: []string{"cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b"},
domain: "www.google.com", domain: "www.google.com",
}, },
{ {
input: "c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738", input: []string{"c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738"},
domain: "cloudflare-dns.com", domain: "cloudflare-dns.com",
}, },
// Fragmented quic sniff
{
input: []string{
"c70000000108afb466a232f7f9f2000044d0168a15a021477ecb9731ed77784d42301462e2d59b0395adc1fa6b569d428583f100860d6b6ae29b6c1b8c0f9c0d9081475ff801f34a9e0677adf685f02b1169fe86c683fb51934915ff43921a73b98fb0b734406f8dd90ce6060d75e923b0d3c738291b421bf16de27ed4785d727ce589f5d0957c413c81d6ee75052e3ab50fe53f1abbb24a138a52e1412683992ad769e65ed301a736914843543e2a3e11eb395726d4fcc9283f8607b38685069f63d05ab8bf38aa24d4073a1e68fa1b6087cec44d7fa628342e9d88a0d20b381014cdd1a07b9d913a3bbcad0cfbddd0560617cf26054138075eb86e06db1e68781541587302e6dda86cae779f9848fcefcc33626f8953bfe4dc293d23e74c87020e79e9ffd58ee345382bd4d1d6e5a3389b0a977124708d05e3c305545857041734dc7092901ab54604b3750b3139dd3b8f2bd94cda89d85be3756fda6f0cfb6f66af3d2e36a7808ff7bce271a0272f8dbc88193ede31613433985cd35c7bd9b627d434e7b2e94b38402b8f1b5619a903572dcf4c2b864c6ee66657c9ec81e03fbe765037f83b2229171888ba08651fc78a1b50c7cc52f6dfe8273723e08932b1a16a6b717a80b5520cf3f40e46f9d9c350eaa914bf99dd4ab700cfdae21437daf695916d4f3121235e4913e0657d8cdcf4afd8f2c7ef977a2dfe49f46fef46c8fa6932e745311d4a6eb3124d5e0a204b9e3227e86a55e662f7002d4f4a72cba8c77c3adc3eff076dfb9195cf68455cecbbfc9b5444d9c4a4775bba68d57ff52edac6ce6ff4efbf6466579bf68308f2ba9a59b2c09506064091a86af621e9dae52366a90599db0d64a23944bc48966b6d3ab8e20f4afb5b0e94370d26a89a9c4207b454554e58ac74f62ffb3eb2686eaa596b9610322a5ce8eeb42f2ead1c71b11b51bc4f1800eb549a2bb529ca4a0d165ae461e45b556b2365e9459d531489d59d0dfa544a76c5c00b0a01270741d4061a331c32fd6cd0e68bbce49137b852e215c9db52f3e430416d8979520e5270be324f3d93132358c0eac35a4618ea7aad997dbbd8e99d4ea577271b935e3fe928f90abd94593806d272a565a414686b8e56c28e34b77671de6a696b09414380bc658c69a309d3225ba8493e9076dac776c845ce11a7ccd6cae58fba5434014250f3e211058b2efe3424b991d679a02ba949b086ba12144c7df3e049b5d026f386e4ae712c9b0b4b02730dd6862ed4e72730224cb6ec9101c5cbb7ee4fc30d497bb1dbf74ffdd49d8cae6c7c9a364ede453d9ae25edf27a2153ab285f3e3be66b2968d67a56480f1f74c4fe61dc69db3451f5b113d7ca02e5afa8627f579c07a9b1814853fb8fdaf0c0f220f89725c757f5617ba4e43cb4f3ad9ce18f48f23d10f9e8950b0fd737070655730532896d93df8768860ebf941365d0634db399feab1f8a88bad28d25e689c5a57321debb8d1435130e90a699e17fa5255f2063f09659a432e9ab5f89eeabe12756bfc5e02fcae2b78a9d0f570934b8d4af8f4afbd57549176f465a0cea485dd89c95a8ae915b4b99548a4c939710c16908f968368baf5f547cfee07f3cbb6142041d6e6084aac253a0d3aeac628cfe76f87b94c3806cb14a912ce8e4981e316511d5ede36f526805d6c3fab5b72d9d91f4eacd26e28cb181ec66611818f5c206ddd52488707a940dc12144ae825d25929bc32b718f46e471fdb30762d299b45c84f6310a72b60",
"c20000000108afb466a232f7f9f2000044d00582e8683e329a63e5bf4dc93e93e325ff661e74b9cabefdfbf6065c7ab203c8a629534e87e5f2d4c0f463352904642358b8f137e99802c3a26cf22235782a777769ecd134c6b4d0dce6aa10b485c45ccdcf6deb805342e99ef97e2777aee0b2a44073843fccc2f8eb837031f76a8e968cb01c13c1268af095f54f860958e4062a84e2527bcc9b25a7791650a844de1b0c4b2476282a0e00c9de9d39a41914d1e797a88a8997b96b25a4c194762912b2ddee0e01a365f1afa1e82ea266c14ae94e47c90b5679e2cd00e63ee5a834505ca33463751bac22f3b87afc80099335dc7bfd12b7df224a23ced3d2e25b58a04c4b5cb089ca187abc54d782973c7bb157cc515c7508431ff5bdc227871da58b9ca8a9a576960f38edb384112b08e4c70672a6f23d17d9d901342e56c12370deaaafcb22810eb352f1a6d9377e96bdc1ad4dd397dbc6a227b70f204c1a4e9a4db2705763b82ec4df1fab11420aae547155c6b49abceeed997ff01b7d24e369c65f7edf18665d067c7d2bda5ec8623281fce8c77d893cb8a42053756713e910894a58ef5bf3d9f3a41071026660dd7cd05e1640767ec68f78e22c1716700ca9c0f076f90a65cffc394c10a32071c6532d07b59414181070d08c9c84e3d13842718d51bf90dd36ab1b3f708df7eeb3939dc8553787308983c3e9ba971e7d447788477a7140196c2f717b9ba4f5da92d73316dd11c1d1830b4200f26f733a6c65ec1cc21549b485e3a43dc7a2b68e95466a53544082a20d9a43387a7ccbfd353f7e590b7047f13bfc0d91923c2d75dad4f8091ea96502f98e83e5c30e52e4cd5c670f6c2248ce37cd6ee8b3970531fbf0c53c5fa9a0d73200442b755c91fa4f70524ffe8a36063b6709d3aa9f6b53eb0aaecc57a8c8c9a7ac5e57e03e9cfb290b67dd8222a245ff5439914147e2799fd1cd2ca2cb22fda299443b81e8024adc59d098058432fa4bde376b8e59075f6b86427b4ef6cd7c83b5c08add0c3d3543aee8d672c41cb287c1f0a17f1bc30f62a57490afb2d9f401bf302fd473ddbaf63f6883221579743d6aa1f386b8b2f5db06d7d6c36be81f29fafd14b82e863d744f116ce2be4921631f1fb2797289fffa9ee16a3e537ddfa52350546bc544459c0c9d66fdcbd41612cfc0e2744f50927983a3224291c1ae51608fbc00f40c60ec72573a7e128c3415b0d9a7db52de8ff763dd66e2eeb03ef2e67838c9e68cfddae4b86a3f34a69e0a473b5a73ab627282648df7912c11a4bf033ade185a8f438036b99b960aa6213c800abbbd751248a7ae600357ab888433125d49c5643705ecb8c86f2980050edd7e3c579ad6fcae9bbe2c8d8b38004426f35eadb543a3bef42355acb1b94c21d7eae7b6ed422ca0d58fa03b227b035628871465ed6509254c8a3bf43dfadbb247ecbc52d80d65e9c03c4bc7bc35a829502bde3868af9c33737cd88d70f7427790313eed4ed1938955c5dd360212ef700f274efcc8c26ea94c4e2e0937d475c5c4909edfb66714d15d12e153e5586725ce0c47e8a1506bb197366754ca8960508f22fe7b83a5eaa40f05f3cb87464dc6b848080c0e0cecf2dae82bfa42cc6f52694478dc3d00ab0e1ed696b98e26c7fd34d2efd969f83e284c28ce3f27b178f4691c772011f61722266153142dd0d526393e6c6848d201115b256e65f12b911a983bc2f96a5b4b99f63f0b58485a521553a3e1d4498ac5d4ee70c3f9",
},
domain: "quic.nginx.org",
},
{
input: []string{
"c00000000108e63b9140d034563d000044d066e1913892ec1d84c179dfa9596e0ce930171a134a09446a888d9e579a6f7bd77df6deda715b028d64f7866603c6deb468d60ecc6488b5e5ee2e2daa1840b76ead998023593c9ebc4178ec89cb198d3c79a867e27177a74ee5f3db74ea194e36e328047ffc3890192665a6feba09ba1e224967fa9575dc7b094e1c29c7f3be9961ba62e3e063f674a09786b7611138e1edaee32cd1d47839e840a74f25ed786463fc48bf3d38a4c793178ab7cbf5a3eb974415b9f9ef7861dfc73460594332f5545c7b7037043afdfc1aa62ac3dfb76ec2c6ae8ebd351f7483992c762d6483b3e2c1454c8ed939ce43f858ccca22d9149cc9da16af86a010be7f3248cf19fa442e94d625ec7f7144b01ac9afb8fb8c595d4cd12fcd2b2d9986371ae65f6f216bed152b79d2782d60f1f01e06b359f88900c4bb3f987f3ce336854a5beaaa616813af4e5f9bd82dca0af6886b544fff0261807bbd8cf90213299f5802b98edc27a6606be8e2bbc18fa7519eac260dcda139f164796a082908459c31aa964a5d3f6fed8944ad61bda126991468f3b7627f2470179619864f234a395ea3bd4f7ba4c0cdf9f5f0dd95d7d59476f2d2a36521c13886265a2fbbd4345e8d1d1e7b5d01a58fb11de23730b087e2b702200155a1ebd50db5751d279438822ac158173533140998a3056893bf470ac84720cb37a4a3205fa88267abc56520bcddacee06011d929c3a114314822d8ccf7cfef89f2fcf0a4fef800afbfca4a62ee848f22066f68c7d3c5c9a24402d422fc2fd5da6d3b470b0ea253f12a883705f7f78bd67006ade4f1c8a3e8fa052656b5b40dacd8062228871cc3bfb1a9c38472b0a720c3c750430edbcbdcecd46b144dfcaa009fee06770238d0270e80671e8ee5f5df18b86dfe8df2f121245c0710ccaefecbeda0ba3db945c768624dc38f21a4ac53741f4e58a5052f3d667fc466b69905f05d0843cfcb830163fae18dd1eb0ce62a59420db9c44958a0eca9ba4258c8060a9956343f155da6c55b2060427d07d9e311729d2971439c7541ae2babfce25a3f5f361fde86c39ef6c04e4e3cf7dd70c9cc0758ec5db3f0cb368e2447080af51c8a5fa6b84ec3175d2d3e6d877b6953e433b4e94b52e1a5f2a1ca37124c27e47f9de5d4c74644181cd37f3f3863ca529c0847bba91c246dadba94b4566b08eaa06a0db4d58b8cb0c8d3070533306a3089891b24a7c4e11b3aa50d5628fc1d136388e8bfbc420a6f12701333ccdc95dec25d09ce25fa4b654260965b91f05b1542c2ee02008d01de4419f14d6749c4bcfcc45a332ba0772def720ea3c8d207802418137b733e779eb406dace0b4b5f5e5e14c787f3e044e6d8160f90fc3c65bcc7f3449205b63294fbc11e9bb92c007d1cb59183eafbf76be9680224cb442806500d71870777d087bf864890848f4a79424c02304f2a6ee2b07f9257f4a2f185ee21239625e246cf680e74b85d292cca44261c6cee6da39bfac3882d28fe547a500f79519ffcd3f54ff5a905c99f22a5e8142c903c41adbe1eb9770b6cf554688529091b126ed2168a23bb191c2b89728e31773623bc58bcb9baebc2c664c79d6ffee7e4404e039723eb05e7f7835c87212431a0131603fcc3fe090cc2fda8239b8f42188b35f98d7fff949b3044544b3bb962ae236a664d76d0c751d9c9ed1271715d240f111febdf7045502f2afd7de8aaaac650511e7bc7716a5b6622ae925abb7",
"c90000000108e63b9140d034563d000044d00b2498988864d8b7f59a00d26165f5ae638fc9b1c12d546ffd86212ccd85f654259cb8b8c9d753c696ddad7ee4847bf3b3c10063606cf3972f75e17ae23e73b6a3029f23541f674256d19677665cdd0b8ac15c3f60984bc14ff5dc7a9ae37395516204f2020965713fccaf35cb0a5823085cd6211d681dc6b39be9db46cbfef154a2b9049ed202e9088961b0b710e94bd73259b0967e4d6b8cdfd5b72774fee2f2ceb16bcafa010f247c43b0a9ca25578e7d45bfda7edb82e91f8e1c0a2cfa990223bf97ece42862d3f329521fe2d12493b717f174f966d173102e5cca10943d5b612101d65d0dd48b44416f9ac1eac4575558ecaaa39c47ade2dee6e25fd219d799b499143b47a5bf449701b939c1dde111349cd0d63efd2ff74fbd3573ed40abfdb2310e2740da40fc50c7a137a3f32c3a26b3d407f80e669fe7f9a3542fdd412a9cb53f845d9c1af0814377bf92e30f05ee387fb8675807a6de083c85d3d7860601c8170923c53e5773ee388b68e510a28cd7009c485bd4cb861eddfdd265de042e5a018d20cb810614e2bb17b0f52d6bf620a6f173e0b41951e1b83ffb29e3b3b3c5d9fff13acd3b409021195201d003e281d8cda7b0f02c273e17b1f9b9e8cec4296d65a1c4923b78a2e4273cb42e4e159980472e440078e542eeddcc5a9bfefa5a72871fbcd9ebb74fef20a50215bf75cfd8572d5ab9ac5945e8d6ca35884caf0af0446ee9aab0a1cc3a452ec79c9de786119e63bb3a75fce0ae29c15a0c320fff87e87cc23a05e75b4f4b30b75c6aa036c4b6657f8200ea014185b31ee7fcd00d1eaf40973f347fae227f89d41794fa57ac1ed1efda3ba840ef27852cf33a9dc9e2d77b56af9ced9e75707837aa8c5395cdc15134ba132de87152ce53d506c53284dab912bbc276542504cc94afaca71a5173ff13ea6cb45b47dde9965428ba5d8eb968cc2a5729c2f9b8f1c1de208943a2cd565196e040dcc415d769ceb6300c7909d7e32bbbe83c4cbf4d49f6e34fe56b651838628f3a0001e99f39cafe45c98e455aff8d98f89942a862f7505b9f7fe3f64dacf8c574affacf91c2c05f094127acaa5187f9dfa188f67db421243a02e583942138c2edf45fec4c6b6a8a791da9055be247e9b252e9f7c1330e76f9cb3aa5feebb21f871315b5fb90a1df0b8056513b74daeb6ac995f85c64150ad115a14830d145e5f4e6638c26987b676a1dd19a9775df29ab442ce6143b0fbf8f8d4618084896e34812ed59d63041e2b4ccf6c959a6c849813dd926082bb7b1adedf69246547f335552bcdbae7e466ac31e07e442530ad114abebc6f58015b786e7f35644307fa7ad3d9248c56c8ff472735c6911da1843fe53821b8f5180f8844db4a9f7a826a919fd93c4db4d25861054929260dcdc46d085827c46d60f1097424a6ef250f5aaf3235c80230eda4eb580ce93e1ac8aac422a7aa1241562af601981b84b74949f1c476705c8030eb5d447b2414f9716ff3fd606cd750030b94345c016078bdcb97b7ebc24f661fbd08802f32df18d6a2aa85bfe2e9b8dc76b121c44ae9f29e4413051b527e99fde29720724337476c0eff325cb6220a290a9eb852151c84836729d6a223032e2c638857d9e7f469b84d7d650c45e56e763aee73f902e82b055425c4568725e2d4efd7fde8b02906bda48af86bf47ea27ff00f4528494b74be9bbff001cc841449a184a4e00d64e51a72660a2c21f704f",
},
domain: "chat.openai.com",
},
// Fragmented quic and 0-rtt packet sniff
{
input: []string{
"de0000000108c2751a596bd51c6e004041948ab7d9d493e9e1e9902a7734534fb9eaddc70ca7f821d1b58a406b23ba9db1d03266ae74765b03fac21c284fd50cb0a3d1ca71d8c3cabef5553dd1cb748ac662",
"c50000000108c2751a596bd51c6e000044d0538af4ba75e226a6fc7f43e7f1f59610973b8a6670bb8338ca7ef7d90f81aa59f179dae5f8f6dbd24ec6fe576b28f6ce6cd46f26de143b8c99cdadaecf2041948a61bd5a8591486e10022fd100aa20e6423b4f4ca5773edb1aba79b73d6150ee185e66da60e658b2a698098462122b6b80c7fbc5542b0b8e9532898c1f31aa2ef55cbdf036d74c3069abbb261660f048d950b00b7db279ec2bc39912102679ddbffb53f1b1921f137fce43e164af86c72908532f4cdc48eb462a9d9e9cdd6d3c3faaf8aa8aea312dcac5d6aa75b1ade4af6901576649da7e3efd4199b92107d7acee8bbf06734b2484957c3d8cbb1f3fc0ccd56c55223628ed8ea514ffd101bac370c97b28c7da81175ab0508c0002d458cf41f7159dfce22b447c1ec502c186b782c1854718b7fc0fc39e5c09aee31113fc4c5003803fc27ca48850c08a54dbbfdef6ea9a6a138cac0ecd045cfd5607cb6c99c39c0cb21778857f97416b78fa7c6ac8ae3fa2ef2adb3b85fe3fdba70ef9265bb3d54e56ec68b8887d54d02d4a571a6b793ae4df8ff171c881a554b5c5a7848351d446ab94c90ee9c600f03b785fee6300450a4ffc2a55d417952e15449a491296d463ac6942bce4ca93c99440396bc8984073ec028b11ad412e97e26f9248031dc4b1a6ae385803bb578fa1a3b3a58a8ef19c6c511f17b28a275e8c40e51fca8f410a4a1879b5d8749a44a6a9f97c0c9df25318cc28fd0cc61eea78ddc603a17e74eb542c8c08cdbaafa3b44566db4d67e8d1429332375cd30cdaead9594c46d8ce91bce9813c3ca23f55ec2f4dd3ff141471bc3df590367bc65e4830018ff7d845ec4987d11e471d114c48acd1ae9b7670341a34077ae59ea6c3bfc4675cf419d37db48a98a5573b69867039731f537098b46415a193f50b2c85bf9e5da45d6757c5c366e21f04ea62d64b81c28be5148d89e53535414067cf609e59686b7fd135f5cb473e57f6c82dbb291308a1065e0f755935d77517adecee55e72cf37ecaab1b5c0c6e0c7463a014e7e439757913f6e43abb6af775d21ab6e43cbdbcd1935a000cf8025ebc11378d86d6f72d51bf2dfe4be1db5d3b0fcacd13e1b9fbaac6e9153c3d1f4e876f2fa9c3cfc84fd0910b778105b66be70827b1830b7b3c9633af5d83ad527efd81498cbbdd112873cc5ced573e6579acfe817b62280c2122b582b591d52b96cac047bff91192a5cfc001d15c811e055dcb1c9710dc892258ed1ab5152af2cfc57a0b93205dd41fd82b86090b4281b1493a8828ebc96bbd603b888cbca4a15799a5f3eaef93655d5609948080ca57c696d0ffc9a07665bdb063b547bb5a862c3b058c9efb2e7b79cf405fd83efacaa4b8e3a1fd126270587119756562c03d69a9cb67550369030a0204e531cb8df91ab2dfa2e4106c590c59b1b13c447843937929a574d3ea1785db0d52b4b2eeefd1a07c69729bec7c2813c9eb1249f706b3cc14a3d489d6b42a641dfd9e91aa70c7d3222e154af2d7fc1a8f48e5ba11739ae128d1f32ff929aaf4b249df5ea23f7847301e36ffda02342cdf1bd9dfd1979cbd8de32eb8b1eb8c415ddd267efe53f54678d9fc32435b34b00ee2256d8b6190e30a280df5bc48cf9fd669a52469954deccb0f1da37371d513ea57f31ead22a34f9379c7931fd18286d9fde6ecfaac8ca2a9be79d688c5401c65407543c066532f6621f256551c4a98a86b543c576ed0f3254daa4915",
"cc0000000108c2751a596bd51c6e000044d07b624bf3d95fb3b7299b67dd836fbbbeb05a51650f9b2da3b2695070a0d19ab0d5334cc04de7ea7494fbe6c438f4e84fa56a3f246132468b5b4f1ba0fcc0251cf278338e15fdd715d5bfed18c1f98ca3cd3cc7b6f904aeeea2914a8b998dd3ae7df694c49c1742dccbf4c3472ddfe2e447959655459c11f18bddd9481eb597b887fb3f90a7d0f05224a144f87a5fdad502ea1e46c1c9f4b4154bafa4542c026296040228703bcd020202acceb772b596bf788341cceca864c8907037c39739e511b04e8ba956efa0fb5cb151ac90eb5817444f6488d593325ad4466058ba45214b965c5738f33d5591624584559ba18e89913b868619d498072e3aa1f333f5d6e3d1db88b28adf7d9350c3c383c1eda894f36bf1bb2a58c7a5e5c8b20597b71a099e46bbd3d8894877e43b0183919185b4e9f059472203979d3334c535fc4eaedebebc79bd1e423184765047a50e6dcc76ba2b23ad23511cae2edd2ad8e7f7f302226dbf6c0e4dbc8c08cda26340b9abfef1ef3333cd511295f14c87197d7890576b4076dd9686047854e67733599d96a99194aecf7b927cae2e5fa4568afc71e748dabd3bd71e6c3984f45b06a068a7c9c3a1ca7b5c245a9bb2cc7e2726e833e283430a25b6ccad55bc5b7644b44f99fedeff3c3bbf995a0387cf1e45a5684e5d1c01350d0cd2d615ffb6d1011d80ad16b75925efcbee483e4e2c0e2386e9e1b35b5a107ed97058adb60e323342989559856faeaafe5149bdcd60c113230f9923b2f654c95f986944a014198686f9c2275053c05080e3bf9fab7d46302948b152e2f2fb1ecbe71b412016b3f25ae512ad45cd096d5f284a0c2808b5eab03b4b9b2dff4d81bf234e75e30d480f39a5f9737563e31a19b14d1038296915af33e0ac0dc18e9c871e539e8772d525e5fa19afc582b1c00ae573af39fe293e16d182bbe57af5bee1c0939862ffb62e3d52a60aeb71e4db2a4a1708e75afa5f37d72cd6c0e036abcb4eb8db6515fbbdf98be95d0a6d261a9445797a8f38c3579a2f04c9f5b74dfd1ffaba2c6aa05959704b9b8cb0db30bcc360711c5afef0d1e7c2b076466dcaab104c70f3cc0cda33d7a47462c3fa3d7e34a99b2d8ff3fe5cafd27ede28b9e09b547cf955b97b0d0d4ec126957601c6982d176252be422df3366118895ca25fe27a96c9c234d484fe98634fb9e970e0d2b096f2ced5d56603505990a65363726c828aed2df0f112e0c44f058424ff5c25ae60aa2cb5fdfa289e8ebb63908365aa4e4609eae87e567f1e86d92c43992e6d505f55226fe3533f9fc9c9facff9dae02a3e3c97ca54191bebab93881c0e89b9de5bb4acd5c6fed5b1e7978803f693bfdbc125b4d08fd34fdc6aaf02444c4b06010b0eb2f15d86850a7aa5af05af438f6b7345fad4315f631bc5b017c7482e7af725a09844472f48e4de79b15284932a7e99a46ae72b187ee3faaa0f31a36726056e86eb706bc8eac04b68a3302307a157c91639f30bafc2d180670625673310a9a45a171063011e59c57c8eb67353a8ea344a87853e7b600c2b49a7a1b60a2904c0ea55951af6430667ecdfa6e90a8d2d0ed9857ba5b876cf78af190d5013d16208d2b30d02cf2c23e6ad1466f76c30d11034d5d2eca113e2764b2fb6298fc4940c16d971e28e3e6e5d0e8eea1ccb9b4b89741ed675861fc3680457ee08547f4efcf68bb6247313f8218ae3ec372e51ba8786ecae115dcff241e0",
},
domain: "fonts.gstatic.com",
},
// Test sniffer packet out of order
{
input: []string{
"c50000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a448875483ec1f071622121a49c456dc3ce16bae5f61f84ceaef9e8b71db56479845b764507dd9416e8c44b8c93406a230945eb8e484471c1b6207c9afd944fa0fee555a5c966f27ccffb4bfed37fe3936f2c84e9852c0d46c7e2e94b897fcef18c4b0b83d966aef75c0af4240325a24668bc017e0d3f69680ea5b2f59bd0b964062bc40190be86aef3ed0716a18a67057f309faaf3a040222812142a399deb72ebb330d03d59961e2ca10cf78d40886dd094368a881db261068920968f6adf7a7b1266faf8842e71840a29859e877c66e3ebc47d7fe3ee586b6512d9b0e1bea82b302647706473e68dc8209f4e9ca19f1dd25fe386e62c21d9c741e75cb8b11606739ba3de6d6325ee3a9cd1bb2b9613746140ccdaaf936eefdaa1ca7ad73d684e5d82b1ba1dd3356ca0c881f6eee72c02c8b78d02a8217a8fd972e463c77374d0fcbb761459e3ab0bb5492e516d7d4304c19c16a4bed11ea7f4e75616a26a7c81b04ffa580cce04d59825b8ed929578f9219e64bdbc6352ae6e4150a993fc3cc27ce4d66c62893866b9053bb737ac40364094b53d91e8b325b9dab5f537af04f10bf8db644897b0b03b42b1bd6c3aedfe018a6e4f6533183649f4ef6a6300383430f86e802fb4e51976d056a3c40c3b53c847b8308cfbe54dc2d20b8cdc870c73f5fc22c376c35d9a85348ca6a2288ae03dda6b97f0f502f35219e19cff3a810143289cb1f0715f8785028f887bf02c656c9cc372bdc419290f05957ad3dee82b56db352db65aca58e6fa0bd2f753160dd9e7214968c0496be1ab49f978a9252e49266939fedf542760abd653dd38b1659bcf452c753cb89e8235bcf732afcff8f524331be9b6f4a5081c81255e68c358b3444fb1d57bf5659d86b6674544fe2826ca81ee52f93a17b3291826678e488c3074c259223845e4083a413af7fc93d9992823620a8d29d321438a760293e36c4232216207060dd3ee5c4036250ede71ca9cbe335a1e068eb3ff6c10a7f1c8204750d6d0f3145014949a7b4e88a723566ee5446f960a95d9f81cc45155443da561d85a3a311df8172a1c4eb118bf27ec4b3cc4573b1ab421d96d41cc1e5557797ca68f701fe75c474527144d30b9bb00a117637f88896b0b2dcb9bb29ba144ec384b5a085e82e7387e0560a4621423c306b041ad42e84928ce23bc2a7f995ef5c21616de43be8a1657847489b32c8e364846389e7c8cae99530c499f3662a2ae7090e54958ba940b5d3eaf1333ebcecc7f06f29f68ffd97defe65017519c29d355ecb0a4b47ab08dbad8cb0cc5c86de65dfa703110c60a0c55281925018fb4ef49fe5d0132dcc86602c2ab9921a8f3451480d3e931f01c2f9a81873435bb83860128aa78dcc950fb13e416d90ea969aa92763f9caefa0fe3ef4ea82e3af4a3e717fabcc589fe8cb9bfba6810ddf7def8c1445fc0048fb07be043a628e9c920bb72c04d3b9472caafc6c14bffb854a1ba2170dda919322a6d79eab92e3a88888a224093946b87840033fe41941f780f569eaf1fdac55e36b74514d72d09823d71f48f5d5f0ceb7b6d69c5da0e0408c1b13c265d4775db6a0f952ae72bd5c277b22c4be2f2728451ce31e921c856000d20da0489103bad6a6ab4",
"c60000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a44884e9e716461869ca408431e1ba92740c598aa74e9cd45706f28942f9cc64dbfd7c292cd33e82b50ae0e2e08dc478c19886718cde33e56c38517f8834d64904bf4fb1d30650caedecb9567ea8ef50157c287a2741e98a00f8e7e19e76bbb0143ac7862a49393f17ec66aa0e2c02123ffb5abcc96ccf92cd542c8f571bd7a4382ff81432d11f83796959696c38f2029db6c6a536a9ea24b74c848b95882562d74739ac95f5a069d48e8756d1a9750c7ebc23d4ee22d617b29b415b7458b3bb8106c22de3a9ace9ec689e6e00471aa33e570f7481d15911d7cf46a429cee1a416558c5e78360795d905ff1e0c81d18fcf4954131fa5b9289ed2291e122cdffd666c66209aa2cab01730739249ce293b3ba3abb31683c108bdfd51f54593f47411077e948f01105bd9bfff1578d235674e96a8b9cfdde119edaa960b84e70fd681312514151de1d5939c79abdfe4953e22be5ad3e6e242d0ce9b3f2e589ce3c768f610d4d3a32e33225d8a5ce2ad74a9b40859cdd9ea99f14fa2a7018e4b6aa6e46a0d73d46d161ec5d3b30bd55078e23987865551a605a33472931428ce222040d20c07d1ebe970e576d9d54ae688a3fe9388adda3da4d011a7cbb604f1f19d2ef1be7ef4713bfa84d4d69ffa606a08b61a1ebb99aacc4e19d0c5034642da1ce2d7d5abecc8adbbc6d7f72ff2da4ce5228ff8626509b38e17b31717c0b7821558b021ba81502d54da7e778d4526367109333383e7c67d5d5bde86bd4001fa13a703ff9259e1c2268ca8f4ed2e6c022a7466e2178bc725f59792803ba28c629e3df7696c416dc294b510920077b2d2b258fdc3506c36c42d37796c8fdb20ba797ee68fdc410325a355f6c1189aa9fc9ee220d42186677e3955cd3c844ce505cd601f04201cc390e923db2ea6407fa2fb4ca7f3f82d0a82d52697ff5ba5d4633bb0d655d7ee3348b89c9cb42870cdfe7c0c162babab4208a9a54700c5785d4134e9e33361480e3512ac8b556e11775536e90ee1270a4cb4d6bf2faa72d7e1f23ceb4fc3aded0e423b6be6a55bc25e5a99163b4f5f72ec4a24fe96f68c739d1848c92c4236a5a637d19871456b8dae671ea6ae5c16ed4fc257612a0821e6dc1cbe2ef4963a1436925dcc4e6ce528fa75e41f7721b379fae8ca09e6fb51d0c3e3ae6c19b98860ab9f74013146c6d375656dd1f530abfa64670a510390e9a54bb9a4ad19977491377c8cd743597bc156ee3f58cfcafa5a547b20852749e66fa8838c100ebde039ea25c8ec32b0c6325b793797546a095e79b9388d8e67dc6b4b3892f93ecd13e64ba4b2ad26fc810fedc374b831921531344c581927da9ba822bd625584d98c7582759ae40f01e14277a0a13d30c2c12536df698330d8aa6a3613a42c493c42692b468b4a2cc6bb6dd45684ee6115848110bf517074efd93bf212c071013f4359f140cfed17bbe10328f2026cb8ada16427122d3fc8a933119a1e3e4cfe2b95cbc73af5044cb099cf34247228972495488ebaa4696280d17665c421be5f1727c5d5b013d8aac0e9943bbbb7fbc2162a4000a306dffe3bc4425cf272f1ebb63c8e4998f867fa6b05d71a8642e29392244d4e2e2351bc149d665efe1b9519cb1b15005393f938d",
},
domain: "ogads-pa.clients6.google.com",
},
{
input: []string{
"cf0000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f9944884c88b10f198e849dddbc1e9f9bac61f98f67f27d5452da6e2bda1f5210a145b1f1416ad2fc15e60aa00444362630650bcd0ee47999b689a40100dcacf40a4c3d74fa6293d4a5cb0487d8c76787c04dd2b47ec7718df5a2dc6942069062617b3d40a95360802957419433436c9065bda5a6156291d909a079b6d3819941368d7e17a2e97e36be829bb421b44545af47e37d7815ee1f200ca28ffd361d955ebe0484fb234a7e8a7c68ad824fd14d517fa7b35f878beebaf3dd22bd9f7a39cb7e0fd8369cdd28c05a06323be7af0b2d69ed2a2f4ea9f25d000de71bf5bd6765a20ddf81d976cff2321f1a4584ad6c4b7e9a42a6d4aa3a02b59f7d994a8e4a3070a4646e51fdf354448420ebfd0aa9118d010d019cc168f2fe5a9ff0c42e6091676be11f28a372ea97d008a1a02efd58149106cfdec7ef86f5416c4b1a408d8efba6c8d4742d781374ff0a1a8ac183bffa1345dc8e3a7cce04f66cc865f434decb912dc9e8e811eb59b80d3e39d5788639ae7c5ede73a935edb47d907725656be0522195bd2c099b0241f36664fad1543e4ae43862252662707fb424a8f5f9486b8e3779ac24bac457671ad664475d1fc9eb1de3c46f624b559742b3477953552e44f20cc1725a11ab915423fdce7cfbc8dafebc0c43d1ac3d3373ca2f0210924433c46e5fcface47a65579efaa1999d52b2632f69c33c3c63537c01be68fb679f9229f8f68c5caaa23dc4c61d3c45dee90affed984dbbfb06b2659447400b4dcbf6e574719e8d49fe0dacea9509182a42f6463138d8693a3b8d797d3bb6b0b02648829d666341373939ac41a57e90fdc2469623b6e2d772199d7c806d5998f439603c0de8413f9d29f79323ec5410b409ab8c95547ab50bb921fe0c407b7aaaf663389bdea5ba56c023dc4622d6dd9cacd8f318a6a0297d041cc6ed455d906be50dc85a25ecb32f4a565432fec9f359833be1c6a6b7b4bd119d3c4b29932eeec8d140dd467ab4d969bd23e9d2a95b92835587f32428f957b6785b8206a4834e00a3013e0b6a5855f16207268bbdf311572c54d2e6ff9c659cd02c258f494c3b168ea170c69138b63e0dde487b72576e87657befa44548b0b4e1e5a837dbbe66a559cd1df8f2151ba513930243fd2b7705bd29b183dff966224d87ffabb74017d634ab2e4b368052504a7f6bc1c62d39a29dc2dcfba683bee2039e376ff391abbd13a0b89512fd8f6a4e66051dfa04e0e1a3cb4bd56a9b17e27651873bf2ed50f65cf1cc608afaf06fe7e6238347adb66f01d1f0b9b51f0078615553cb8ff8d6786b87e19dbc44000025693c4b34cfd695601a680efdc1e7465a981b0f028cbd3dbb938789f240e39223290e34ec303ff5c78a4a637ac04dad60d744f82e96c3c9e8ed6cb0248ac73b5b3a92007edfc1277c3cc6fa1d0045c1c371820f06bedaf046dd999665cc4745ddf8934084ae02e9238acae6dea330b5798e046138f5b15011875eae72d6eb6689e56e0ac5c5d9e25dc4fc1874cf37265e68ce5b8630b84ad8dab7704474f0bfd08ac295b3a508284fb6ff201f0aee6388d0e1d5cdaaf4c20429874792109f5b8e2f3eae6c397e46a510ed829a6746e523481465f64be4e145c83d6fa6951229d3",
"c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f994488828e6082edd53e228164d8862067483762ea9523c90d565b9e4b185b7805eaf8220664264e82a95164ab6cab4fb3f5e795e246e7205aca236b3c94dde0ff4fa66ce0924d654829d59c3eb690470b20c5011c739102257e9c2247dee67c0b98190d0015154d31041aadb026b8d3a828c861a15dccdab0cec8cc99b8d6c2acddbb93ab66253e87ac39016507dba42e8fa9f5d22c7f27645a02361842a59ebb2eafefd0f3b92bd9692a96b93875defcfe2796243be8861c59ce5ab03f1d65d308ae456cb9656da1f01026ef0807cc9930021b29d69b36881c3e7d70fc68799ca81922008db93c9ca4a365ee191e214d9829481fd430194ad4583a0ab2e920c25244d7d64662872b3b69ab413ccf0dfb6bf2ea9a9b93e04ed19f8a0e146613ca9d511179f80aeab40d573590d38a7c10840e3f8b9ac1bd23b0826aecabf6d1cdb2aef02deb982c2029dd6d8bc21da6c262c8116b7b383ce8c9eec69da3e16c044dd96ae08a98595d128e89dd55e6dc8eb08b8d51327278027137f60a0e1b42878f98ca898587474f6d509c3a58ff4dd7f8b10905c200cf3170bdec725ee14a1ac8ebd1022509d3e499f5e72168eac43264d7246daa0bfc81a216ca97730b7e043cad8d8a9af5c443a5d15e9a88d82b6750c740eecfd63561712a185c69532b1a18b23513d7cf871f14d164ec544f22b6a8cc77d6fae5fc6e47eb64f08617098d229da78a378d6a0864684a7978f650c7922c907f97b0ebf2be29cc834ffe995c9636b310f4a8c2c5623c3b7b533518193d226923f111da1a0e8055b9053ad7f7504d194fbc3ae2b41cef30aa099624d5e229ffb56d5883a5a09163d22455cac52e37ee0ed5367b7c3bbcd4818a46b9b363b592c53c780eeae2c8b80a1d60d296614c998a9774f76453a58bc55d1c26bb10dc321c159858d7ba2f7855ba01aadf3585632c097e5471591dcc24d87e9b76509c10e2710310e4869de710ce0f484d326be751f8e9f765a685312423f1801aefb28dfe0c8f286432356d06857101a67a432497c5849111db2792fa0ee4ffff49a9124c152bcff82da1951258f989681e4f1338357f2c9f82333f6051b188f640bf200a0a75be1d35d2301e8d3813f7ba1926a28a0df05c21413cc0c4090c1e4ba4877dca8e129876c72ab3a801b4093320f5f685120680541d97889eea5dfcaf07a7ecb00c0ba0ae193969a4cddcbd753609a5304ea88783358ab0ae005c6af27bb58b2c4282186461ea50540845e2e2a2f4efced88c8ab9cd9fb4a226a265714c77ce7b79d1a40bd00b24cbba498dafde6bbad91686cf2e13e75669234bc342218887ba910ac81680122ddd36466e7e8a983d5a0fc18a6e9a386762c32132be08abe5554e334ec7d88734cfad9a378553b71222c55f6aa114392e015dfa2bb6cb4ad241c6bca82fdd0a00eb8d6b4afac61268130dea2807a97e4c0adc0e2be39abccbe64dca5c480e09c4bebb8b598e4f60afb0e92dce710859013b1ffa9c78fbf380160f31b1e72340dea86d353ff0e95884b72e2c2c10f6eff5f36c588ee845b7bc97c3b6bec4aa879dd0eb56b838b7bc2ec6e66a5b5517908197a67566dce7df421a8daedc98848c70d1d2c39b2f3538e6f17800bee3d3",
"df0000000108277148f2b916666000403a52317841946860def0d7829c06fec03ffe8b97f84e10116fafd93d1d2d39bbfda0d148778c21bb1e1667eb789b1ff70c2e3d557ed9c31570d20d",
"d00000000108277148f2b91666600044cd5e860e3fa7ac4769ec75d9b7d20f19e69265939a42afd3c4248a7f5210358a044f42869567e72a05642e96ddffa67bf24ec2d966d860c6accdee01d6917c8c43d4d089d8bb63ff848b617c13fbeefafcbb049ab0822a9ca7716c95af84d019b755b145dfe43c218555d1a7e047deda7d8db352a386b2b6d03f2e7f4510f47ff4ab199348dfa81c86bea5d09d7c7af4ef3f04e99fe4e6c21d53c4335407e27913129152033f17580f97d0345c8487a7ad329dc5c97b298ec7b80fee7813f1d6f94945a44ff662a69453c2dc7ac5e8a1cb90400e63818632d7f9654f140a61280df183b3d9f9b824e53d82f2c14ea3de89befdc79b84a4a3eb659a41db25622add94f2ad4b0d5977f1091aae0a4b83c7b41bca61c6c8d807ef02a8ce6240b76d442559a8b338b39418d27e99aff38840fc79a20995af65b3bbe1e3177074079a47578c51655a4016363364fd2c108d384e602deebd022da3c814549cd57d73c5bfc20e279045e2ad436fbd7e7c9e1985f0ec2f422e310e7aa8cfc48e637f9ac61d06d6482cb40b4376ff3c7abff3c3c26634689ae16d704bab1343d6413fc7b6c076eb0454eda2e0d1e077db40c922ebba6b0b1fa814e3ba76d8d6c4289abbd655f0cf5968eb2aba7131680b44da8910056a76647a6dfea95f27364a7ce694b8fbe19ebcb2a47e7350d33a36f7f5ca67af5e934f449125f4aae870a5b23b4370680ee02b194784d5d188ecdf58ae5454221406bde0ddd3e50d3363a564d6ca9fe0fb57d4df8716cb430cf553be573aa690e5645075ec74edd38cf23215bd50bcda0639dfbbe08dd6c476249e35da819ea6ccef808911b0eef6efaa4947244472795bc071d7154ed87e4a43575b3d61a551fcfccfb7ca3edaee9324f33f54dc9809747e59e24e79f256e8e72f01b8647f71c4b9dc260715fd9d83d3dbd9e124c432c04b3398e74efa3869fe129e368c15b6ca234a243fcac675adcc1db247e3f8485ac4a78f4a1ce2db3b437a1960b02f0c227901d165dcc05abdb3929a80dff2eaf72816185d4af4e28eea05430b736ddd2962e03ec64fa48649dd610e0e221c48f781b45cd9963c176126110e662369874e6a55f28039a23484c5c53714fc2d2030b48f1c895102ca9ad8acae1ec4eb0ae8d8bde31cd74fc515930078d22ad07dc3c7221ddbc4027c746207fa038b31080714091459c9a66ba4f5912d8d3905d3a9a47e4d8829a8110c96c0c9c81291c7985073808814109364df15b04520dc07e8d67cafcda71f0ca59423df5fadae92417a8661b3cdbbf6b1059780fb8b43eb4dcdacf731bb8db26294f978f6be7506b87d17a95367cdb83000565a4986e66dd60d0851f9b593d68790f8097434f62ea7a7396017c3c84754845d3a97f028cf8697d929a2826451653ccf84aba4d2f40fa530b258c13f08c6523c3c02d9669fd46b6a51f20ad323857d767150e3530a66bf88976dbadf99aeea549254c07e11e14085979b60f3b7e1728a4a2d7a35b0377c6501ae7d1d4bba338fb51a17ca8f7e698bd70cd01e8f30edf3e83591a2eb0038811e347bfcfab159b0d1ff6153e0f9ee4c129cfb7687e30b82eed74130c466eee06506dde50805b58c2acccd4cf4b2cc86c52fa2af602a8a7064eb9d90e1c568373b19e43ef4e7c1e4e1c9a58ccedf80a02a46ed64e68e72d4e75c7436e2bc0ba59f95a00456e5680af9e6cf4bf3a6d302ddaf8847cfd5ea606797",
"d30000000108277148f2b91666600043d24b66b2531ed9f9c13b07b2654186b0410a608592fdf728479734933197ec06a1cde860f36b3170fb2a9c85c62a7867ba6520dcb2d0ab2f6a484d9ebf8237d7a6f3c1fb16c1e0458ccf20e6d1b298a7530cea42636166027d92812915e76fbcc436a5e414147672dd7b0d19ff24513800e63cd86984f1c93ef1430bb848d37830eed61675d7c9999b92c6e5796d384554c74dd5a163de341ab309d6b0cb028aa08e56d79c60980d4a49a1c095456ca119fc3f04e496c93a084d017f60c6e031d6e9ad2e4fa699bb4b0c92fdcb44131129db0d30ce9efb740d3db0339127d9bdd1d4f677b1cb532a33647851ba9bb20bd8d6aa593271a85c3a9dc9835065663e61faa8dc6af209a0caf183d0fda3d4839d40edd5659dd053778642db8fba21f1f793e45c5c517e68bbef8543e3a727743c7bf87d047d441d13226b9021fac56904872774cf6768dc91db8ea489a244500e9e527acdc0088437357acf9397b014e66fef2db1248f9c6a578af07d7a02b1356fee02e27b8207e57633fa7bfd87ccd382e368c14b946aea780fcbe696d6e4fa3aa589184e104177db2fc3d91d4af120d9da3bdad021d003796b8261b590d8113f995dc1db4fac1c62cb68370d41cc87c982815017ae2143d5a469b742d019e5556d813877fec9d021cb37f80e5987d9f743c2b39093a34f6654164a8185a5caefbbef8ea17f62f6801a3fd89fae333c878cec9b25d10dfee2abca65d7c909ad2e4f11736d13b1642df4c5a0761f8f29f35f37def9ed327f4a9d8e53269fa6c7cedd0f4fd67d6cde81934e291d9fca695cc9745890cb54503e29e09f4a30f80e2f574bbbeedb7d20481c583d8362d22b2dbec09494095a043cdae283e86f905d8807f7b7c0f06ce968487bbca1e20b87245b68f24537a7c7e768c838f1bf26650afdabec2c0bb9736b345473f279c9b73ecf0d2c4aea49330ecfef0949ef7cb81861b05950ec0772db856365b136ba75d5509c01d7a970c84ebc77d8d5c3ceae1ef5f3079afc7d78965ffa3bc4c64ef1b4718ffb488a571528c83b615c43022616bb4c494c838b556df5ede711a688b0315c1ce6e2892247df582b7c3f2b06cac0bd8d670e2b581f074750596ba162189060b8af3dfc650ba3b45932edc4f94f08741d3072bfd1ef8159b27a7f3673a4fc504304c12116e3c2d7636c663c9fa1b2f5571be88769f33ccb94a09abd9c5a7dc8a8c2031bb2bc256b84aeb68a9abf7673151cec41b48bdd74f395a46acf30dae43e060e596bb2e739274210701ee9bb6cc3ff81ace751e375a01f17b3c5cc5f1234c488d69611bb27f6e3ee17e3c3843ebe4a280d6aa8ef017058a872810a437f85331adb3cb8d382650897b1b1589ee6",
"dd0000000108277148f2b9166660004053972df1beb451f73eb070e33ed63f681eb9b7e1e03f20baff3f54157598c7dd90a0de49850a3ccd6eb1b1cfc9dc6d3ba9ed1c0a19c69bf433da300d3cecc4ef151c44a721d680e3e3aaaf3eefec23091c5fde22",
"c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f99448825ee873013b006b2a6c87c7581c7117bfeb4ec3d68405a68d9488f6d58474dd16539677e869812ead055e70d655a660062e17083995c0dbecd565c79800b11c8ca0c351ecffa61e707d62d443b3810bc60d4ef87aa99b979ff55ee1ea46b65436c15534e5315113138aed6daa9f04d3050d77a7e379c83b948d3797177c1793e59b2555423bd52595d93e293ea8ffa3c428c6dbba4e202d76933caf6a5609b0a4aa6cf4fd2aadb6505382381ef2d5b33efc43eba24c84b7805baf2ddab44a50180e5e6f2a31f9ea8089aef562d3b578a799d61befec99c016fadec3363f68a1be4ca1e13e8bdd2809a1dacc41134663e22f21978167c5ee8ef49652ae152fc6c1bcf52109cd3076cdd599cb43261941de7aed148d7d3e956cd615549a9647496f43f998daad4c841cc40ce1501fbfc152b957c94be558f6743061e312d746137db2ae6a44e181587dbf6b0d9508cef4aefd99ea5d3369898bd4c3df5e95ac89eaaba54019ffe0402b8f567c91b9371e80c621c67d3c831331acc063892bbd8a81cfc0498e78474b11e8c05dd8f540c449505342ae95f6281940aae973db35b8e31ff801f6bc8975f592538881ae9cc4cedcbdb39a784a9fe962a1f12be51c11b91d4dedf649bb5672dec8e03db97b0d69fce36edfbecb6836644bad1ab8e6d4e13644d9c3476db0e8a8eb4b5a5c32f7a5604c8e19700c53602839478531579cb4c4bb5cc969cf482f325dd837629318baf128920d9978e23296d7016e6c05c954f95881b4f9f7e43bcea393951e91af0e4a671400dc435bd2a1616c60618df2476d0ece060dbbda11e751e256956a0dbcd7e4a8d6d85a3319f22a2c5f26dad50e82f70f3dd91feff19c775aa60499a3b7daa57e344c07c3787e99d53303488801d2b17cdbfdee61ea3fc473f6c146f06eb60d70594a59e0ed79cee6ca4a5f78b037637ddab69fb8522c0f7bf37aa7f59cc7fa659e759db69966455944975cd22a1a1355f35a589a4978c8f3272e1c4f6793288a00ab879299aa6ad02d966e3dc67cee0c808b1a046458cff9bdac25a4071eb10038a6389a0ef7233003641bd4ee1efad0e9b2f693396a89ca0db3c05b6abfed3b246eb1b23a6b77e8b486f26d9c3dde9dd6f3637a8115940ed2ca762ca6320609f61c37ffc9c3f2f7a0f27edc9891c2eeac49ba258a0d09c35c4fe1dc52d4d9319aa9b1a271a5d8d2d3a75fef4d59fb04679ba526aecbd19d73f72fee537630444326e2543ce564c669bf378499738385dda9ac63521a1b91f580d0737a7326009f0ff0dcb05aa8b86222c934d9ddb4628e30b6e12ae370154ab39c605431b4c40683592afcfd6fccf35df9fe5850442595d24be3d9f4298bf3d541f09e7e71f552c88eed9642df46953622d5aea05b5060325304ec81c0447ac95b90f9da4359e3286938f06aea3d45030cb836be15b1c65e3edf44cbcfe2f01ef8d7209c69d7c81334c866ebee50e418a28336cea1982069b4df090eab81303761d1af337e083f1e0ad1440a02ef1eefb03506c39d2377807e335ee64bdb76527f786223cee5233299eda9fcb1d38f19c34480f790a328b0735f80908e3aa70086df828d56b6c79516f71a24c9d94f60335f86e9d29c0c5d3872b",
"dd0000000108277148f2b916666000406672db10ab41db38c01f7021709bac4d1659d872623eb5852b12b494535d13779a88d37e9685da572f6b2de35793a519a457493456ac4ee242933cf92d783f783656899c31832274bf1c26d24720d9d8ecfec598e19c58a478d2991dfc1cda3000f7bd7bd17e80",
"d60000000108277148f2b916666000404ed98b1b4ac35c0c0ef18c88adf08a6701ccb0876ea75aac8c128349936fa3cb6728e4e58de8673dd7dc8457b092957f26bc8194233bb81c7e78127844f9b833f196dc46c5cb4064c773f3c6e0bc73",
},
domain: "www.google.com",
},
} }
q, err := NewQuicSniffer(SnifferConfig{})
assert.NoError(t, err)
for _, test := range cases { for _, test := range cases {
pkt, err := hex.DecodeString(test.input) data, err := testQuicSniffer(test.input, true)
assert.NoError(t, err) assert.NoError(t, err)
oriPkt := bytes.Clone(pkt) assert.Equal(t, test.domain, data)
domain, err := q.SniffData(pkt)
data, err = testQuicSniffer(test.input, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.domain, domain) assert.Equal(t, test.domain, data)
assert.Equal(t, oriPkt, pkt) // ensure input data not changed
} }
} }

View file

@ -3,6 +3,7 @@ package sniffer
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt"
"strings" "strings"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
@ -15,6 +16,19 @@ var (
errNotClientHello = errors.New("not client hello") errNotClientHello = errors.New("not client hello")
) )
type errNeedAtLeastData struct {
length int
err error
}
func (e *errNeedAtLeastData) Error() string {
return fmt.Sprintf("%v, need at least length: %d", e.err, e.length)
}
func (e *errNeedAtLeastData) Unwrap() error {
return e.err
}
var _ sniffer.Sniffer = (*TLSSniffer)(nil) var _ sniffer.Sniffer = (*TLSSniffer)(nil)
type TLSSniffer struct { type TLSSniffer struct {
@ -160,7 +174,10 @@ func SniffTLS(b []byte) (*string, error) {
} }
headerLen := int(binary.BigEndian.Uint16(b[3:5])) headerLen := int(binary.BigEndian.Uint16(b[3:5]))
if 5+headerLen > len(b) { if 5+headerLen > len(b) {
return nil, ErrNoClue return nil, &errNeedAtLeastData{
length: 5 + headerLen,
err: ErrNoClue,
}
} }
domain, err := ReadClientHello(b[5 : 5+headerLen]) domain, err := ReadClientHello(b[5 : 5+headerLen])

View file

@ -26,6 +26,7 @@ import (
utls "github.com/metacubex/utls" utls "github.com/metacubex/utls"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"golang.org/x/exp/slices"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -36,9 +37,9 @@ type RealityConfig struct {
ShortID [RealityMaxShortIDLen]byte ShortID [RealityMaxShortIDLen]byte
} }
func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { func GetRealityConn(ctx context.Context, conn net.Conn, clientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
retry := 0 retry := 0
for fingerprint, exists := GetFingerprint(ClientFingerprint); exists; retry++ { for fingerprint, exists := GetFingerprint(clientFingerprint); exists; retry++ {
verifier := &realityVerifier{ verifier := &realityVerifier{
serverName: tlsConfig.ServerName, serverName: tlsConfig.ServerName,
} }
@ -60,6 +61,27 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
return nil, err return nil, err
} }
// ------for X25519MLKEM768 does not work properly with reality-------
// Iterate over extensions and check
for _, extension := range uConn.Extensions {
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool {
return curveID == utls.X25519MLKEM768
})
}
if ks, ok := extension.(*utls.KeyShareExtension); ok {
ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool {
return share.Group == utls.X25519MLKEM768
})
}
}
// Rebuild the client hello
err = uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
// --------------------------------------------------------------------
hello := uConn.HandshakeState.Hello hello := uConn.HandshakeState.Hello
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
for i := range rawSessionID { // https://github.com/golang/go/issues/5373 for i := range rawSessionID { // https://github.com/golang/go/issues/5373
@ -75,7 +97,15 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
//log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16]) //log.Debugln("REALITY hello.sessionId[:16]: %v", hello.SessionId[:16])
ecdheKey := uConn.HandshakeState.State13.EcdheKey keyShareKeys := uConn.HandshakeState.State13.KeyShareKeys
if keyShareKeys == nil {
// WTF???
if retry > 2 {
return nil, errors.New("nil keyShareKeys")
}
continue // retry
}
ecdheKey := keyShareKeys.Ecdhe
if ecdheKey == nil { if ecdheKey == nil {
// WTF??? // WTF???
if retry > 2 { if retry > 2 {

View file

@ -45,7 +45,7 @@ func SetGeoUpdateInterval(newGeoUpdateInterval int) {
} }
func UpdateMMDB() (err error) { func UpdateMMDB() (err error) {
vehicle := resource.NewHTTPVehicle(geodata.MmdbUrl(), C.Path.MMDB(), "", nil, defaultHttpTimeout) vehicle := resource.NewHTTPVehicle(geodata.MmdbUrl(), C.Path.MMDB(), "", nil, defaultHttpTimeout, 0)
var oldHash utils.HashType var oldHash utils.HashType
if buf, err := os.ReadFile(vehicle.Path()); err == nil { if buf, err := os.ReadFile(vehicle.Path()); err == nil {
oldHash = utils.MakeHash(buf) oldHash = utils.MakeHash(buf)
@ -76,7 +76,7 @@ func UpdateMMDB() (err error) {
} }
func UpdateASN() (err error) { func UpdateASN() (err error) {
vehicle := resource.NewHTTPVehicle(geodata.ASNUrl(), C.Path.ASN(), "", nil, defaultHttpTimeout) vehicle := resource.NewHTTPVehicle(geodata.ASNUrl(), C.Path.ASN(), "", nil, defaultHttpTimeout, 0)
var oldHash utils.HashType var oldHash utils.HashType
if buf, err := os.ReadFile(vehicle.Path()); err == nil { if buf, err := os.ReadFile(vehicle.Path()); err == nil {
oldHash = utils.MakeHash(buf) oldHash = utils.MakeHash(buf)
@ -109,7 +109,7 @@ func UpdateASN() (err error) {
func UpdateGeoIp() (err error) { func UpdateGeoIp() (err error) {
geoLoader, err := geodata.GetGeoDataLoader("standard") geoLoader, err := geodata.GetGeoDataLoader("standard")
vehicle := resource.NewHTTPVehicle(geodata.GeoIpUrl(), C.Path.GeoIP(), "", nil, defaultHttpTimeout) vehicle := resource.NewHTTPVehicle(geodata.GeoIpUrl(), C.Path.GeoIP(), "", nil, defaultHttpTimeout, 0)
var oldHash utils.HashType var oldHash utils.HashType
if buf, err := os.ReadFile(vehicle.Path()); err == nil { if buf, err := os.ReadFile(vehicle.Path()); err == nil {
oldHash = utils.MakeHash(buf) oldHash = utils.MakeHash(buf)
@ -139,7 +139,7 @@ func UpdateGeoIp() (err error) {
func UpdateGeoSite() (err error) { func UpdateGeoSite() (err error) {
geoLoader, err := geodata.GetGeoDataLoader("standard") geoLoader, err := geodata.GetGeoDataLoader("standard")
vehicle := resource.NewHTTPVehicle(geodata.GeoSiteUrl(), C.Path.GeoSite(), "", nil, defaultHttpTimeout) vehicle := resource.NewHTTPVehicle(geodata.GeoSiteUrl(), C.Path.GeoSite(), "", nil, defaultHttpTimeout, 0)
var oldHash utils.HashType var oldHash utils.HashType
if buf, err := os.ReadFile(vehicle.Path()); err == nil { if buf, err := os.ReadFile(vehicle.Path()); err == nil {
oldHash = utils.MakeHash(buf) oldHash = utils.MakeHash(buf)

View file

@ -1,7 +1,9 @@
package updater package updater
import ( import (
"archive/tar"
"archive/zip" "archive/zip"
"compress/gzip"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -22,6 +24,14 @@ type UIUpdater struct {
mutex sync.Mutex mutex sync.Mutex
} }
type compressionType int
const (
typeUnknown compressionType = iota
typeZip
typeTarGzip
)
var DefaultUiUpdater = &UIUpdater{} var DefaultUiUpdater = &UIUpdater{}
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
@ -70,6 +80,24 @@ func (u *UIUpdater) DownloadUI() error {
return u.downloadUI() return u.downloadUI()
} }
func detectFileType(data []byte) compressionType {
if len(data) < 4 {
return typeUnknown
}
// Zip: 0x50 0x4B 0x03 0x04
if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 {
return typeZip
}
// GZip: 0x1F 0x8B
if data[0] == 0x1F && data[1] == 0x8B {
return typeTarGzip
}
return typeUnknown
}
func (u *UIUpdater) downloadUI() error { func (u *UIUpdater) downloadUI() error {
err := u.prepareUIPath() err := u.prepareUIPath()
if err != nil { if err != nil {
@ -78,12 +106,23 @@ func (u *UIUpdater) downloadUI() error {
data, err := downloadForBytes(u.externalUIURL) data, err := downloadForBytes(u.externalUIURL)
if err != nil { if err != nil {
return fmt.Errorf("can't download file: %w", err) return fmt.Errorf("can't download file: %w", err)
} }
saved := path.Join(C.Path.HomeDir(), "download.zip") fileType := detectFileType(data)
if fileType == typeUnknown {
return fmt.Errorf("unknown or unsupported file type")
}
ext := ".zip"
if fileType == typeTarGzip {
ext = ".tgz"
}
saved := path.Join(C.Path.HomeDir(), "download"+ext)
log.Debugln("compression Type: %s", ext)
if err = saveFile(data, saved); err != nil { if err = saveFile(data, saved); err != nil {
return fmt.Errorf("can't save zip file: %w", err) return fmt.Errorf("can't save compressed file: %w", err)
} }
defer os.Remove(saved) defer os.Remove(saved)
@ -94,12 +133,12 @@ func (u *UIUpdater) downloadUI() error {
} }
} }
unzipFolder, err := unzip(saved, C.Path.HomeDir()) extractedFolder, err := extract(saved, C.Path.HomeDir())
if err != nil { if err != nil {
return fmt.Errorf("can't extract zip file: %w", err) return fmt.Errorf("can't extract compressed file: %w", err)
} }
err = os.Rename(unzipFolder, u.externalUIPath) err = os.Rename(extractedFolder, u.externalUIPath)
if err != nil { if err != nil {
return fmt.Errorf("rename UI folder failed: %w", err) return fmt.Errorf("rename UI folder failed: %w", err)
} }
@ -122,9 +161,66 @@ func unzip(src, dest string) (string, error) {
return "", err return "", err
} }
defer r.Close() defer r.Close()
var extractedFolder string
// check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File { for _, f := range r.File {
fpath := filepath.Join(dest, f.Name) parts := strings.Split(strings.Trim(f.Name, "/"), "/")
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := strings.HasSuffix(f.Name, "/")
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
// build the dir of extraction
var extractedFolder string
if isSingleRoot && rootDir != "" {
// if the singleRoot, use it directly
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
// or put the files/dirs into new dir
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for _, f := range r.File {
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, f.Name)
} else {
fpath = filepath.Join(extractedFolder, f.Name)
}
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath) return "", fmt.Errorf("invalid file path: %s", fpath)
} }
@ -149,13 +245,158 @@ func unzip(src, dest string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
if extractedFolder == "" { }
extractedFolder = filepath.Dir(fpath) return extractedFolder, nil
}
func untgz(src, dest string) (string, error) {
file, err := os.Open(src)
if err != nil {
return "", err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := header.Typeflag == tar.TypeDir
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
file.Seek(0, 0)
gzr, _ = gzip.NewReader(file)
tr = tar.NewReader(gzr)
var extractedFolder string
if isSingleRoot && rootDir != "" {
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
baseName = strings.TrimSuffix(baseName, ".tar")
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
var fpath string
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, cleanTarPath(header.Name))
} else {
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
}
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath)
}
switch header.Typeflag {
case tar.TypeDir:
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return "", err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return "", err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return "", err
}
outFile.Close()
} }
} }
return extractedFolder, nil return extractedFolder, nil
} }
func extract(src, dest string) (string, error) {
srcLower := strings.ToLower(src)
switch {
case strings.HasSuffix(srcLower, ".tar.gz") ||
strings.HasSuffix(srcLower, ".tgz"):
return untgz(src, dest)
case strings.HasSuffix(srcLower, ".zip"):
return unzip(src, dest)
default:
return "", fmt.Errorf("unsupported file format: %s", src)
}
}
func cleanTarPath(path string) string {
// remove prefix ./ or ../
path = strings.TrimPrefix(path, "./")
path = strings.TrimPrefix(path, "../")
// normalize path
path = filepath.Clean(path)
// transfer delimiters to system std
path = filepath.FromSlash(path)
// remove prefix path delimiters
path = strings.TrimPrefix(path, string(os.PathSeparator))
return path
}
func cleanup(root string) error { func cleanup(root string) error {
if _, err := os.Stat(root); os.IsNotExist(err) { if _, err := os.Stat(root); os.IsNotExist(err) {
return nil return nil

View file

@ -279,6 +279,10 @@ type RawTun struct {
IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"`
ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"`
ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"`
ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"`
ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"`
@ -428,12 +432,6 @@ type RawConfig struct {
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"` ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"`
} }
var (
GroupsList = list.New()
ProxiesList = list.New()
ParsingProxiesCallback func(groupsList *list.List, proxiesList *list.List)
)
// Parse config // Parse config
func Parse(buf []byte) (*Config, error) { func Parse(buf []byte) (*Config, error) {
rawCfg, err := UnmarshalRawConfig(buf) rawCfg, err := UnmarshalRawConfig(buf)
@ -927,12 +925,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
) )
proxies["GLOBAL"] = adapter.NewProxy(global) proxies["GLOBAL"] = adapter.NewProxy(global)
} }
ProxiesList = proxiesList
GroupsList = groupsList
if ParsingProxiesCallback != nil {
// refresh tray menu
go ParsingProxiesCallback(GroupsList, ProxiesList)
}
return proxies, providersMap, nil return proxies, providersMap, nil
} }
@ -1572,6 +1564,10 @@ func parseTun(rawTun RawTun, general *General) error {
IncludeUIDRange: rawTun.IncludeUIDRange, IncludeUIDRange: rawTun.IncludeUIDRange,
ExcludeUID: rawTun.ExcludeUID, ExcludeUID: rawTun.ExcludeUID,
ExcludeUIDRange: rawTun.ExcludeUIDRange, ExcludeUIDRange: rawTun.ExcludeUIDRange,
ExcludeSrcPort: rawTun.ExcludeSrcPort,
ExcludeSrcPortRange: rawTun.ExcludeSrcPortRange,
ExcludeDstPort: rawTun.ExcludeDstPort,
ExcludeDstPortRange: rawTun.ExcludeDstPortRange,
IncludeAndroidUser: rawTun.IncludeAndroidUser, IncludeAndroidUser: rawTun.IncludeAndroidUser,
IncludePackage: rawTun.IncludePackage, IncludePackage: rawTun.IncludePackage,
ExcludePackage: rawTun.ExcludePackage, ExcludePackage: rawTun.ExcludePackage,

View file

@ -42,6 +42,8 @@ const (
WireGuard WireGuard
Tuic Tuic
Ssh Ssh
Mieru
AnyTLS
) )
const ( const (
@ -99,13 +101,24 @@ type Dialer interface {
ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error)
} }
type ProxyInfo struct {
XUDP bool
TFO bool
MPTCP bool
SMUX bool
Interface string
RoutingMark int
DialerProxy string
}
type ProxyAdapter interface { type ProxyAdapter interface {
Name() string Name() string
Type() AdapterType Type() AdapterType
Addr() string Addr() string
SupportUDP() bool SupportUDP() bool
SupportXUDP() bool
SupportTFO() bool // ProxyInfo contains some extra information maybe useful for MarshalJSON
ProxyInfo() ProxyInfo
MarshalJSON() ([]byte, error) MarshalJSON() ([]byte, error)
// Deprecated: use DialContextWithDialer and ListenPacketWithDialer instead. // Deprecated: use DialContextWithDialer and ListenPacketWithDialer instead.
@ -136,11 +149,13 @@ type ProxyAdapter interface {
// Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract.
Unwrap(metadata *Metadata, touch bool) Proxy Unwrap(metadata *Metadata, touch bool) Proxy
// Close releasing associated resources
Close() error
} }
type Group interface { type Group interface {
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
GetProxies(touch bool) []Proxy
Touch() Touch()
} }
@ -215,7 +230,10 @@ func (at AdapterType) String() string {
return "Tuic" return "Tuic"
case Ssh: case Ssh:
return "Ssh" return "Ssh"
case Mieru:
return "Mieru"
case AnyTLS:
return "AnyTLS"
case Relay: case Relay:
return "Relay" return "Relay"
case Selector: case Selector:

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