Compare commits

...

194 Commits

Author SHA1 Message Date
wwqgtxx
84086a6e6c chore: update dependencies 2025-08-27 17:14:48 +08:00
wwqgtxx
443200a51e chore: sync vless encryption code 2025-08-25 20:23:09 +08:00
wwqgtxx
aca0d97beb chore: sync vless encryption code 2025-08-25 11:31:21 +08:00
xishang0128
2605bf78f9 fix: add code signing for macOS executables during file copy 2025-08-25 11:31:21 +08:00
eWloYW8
d2395fb43a fix: allow disabling ALPN by setting an empty array (#2225) 2025-08-25 11:31:21 +08:00
wwqgtxx
e3d9a8e2fd fix: vision on vless encryption 2025-08-25 11:31:21 +08:00
wwqgtxx
1ae050ca3b chore: sync vless encryption code 2025-08-25 11:31:21 +08:00
wwqgtxx
7f38763e22 chore: update hkdf using 2025-08-23 20:13:11 +08:00
wwqgtxx
2a8831b0d0 chore: sync vless encryption code 2025-08-22 18:45:17 +08:00
wwqgtxx
cdf5e0c73e chore: rewrite vision client write 2025-08-22 11:28:17 +08:00
wwqgtxx
48f3ea8bc9 fix: buffer handle in vision server read 2025-08-22 11:28:05 +08:00
wwqgtxx
375e160368 fix: data loss in vision server read 2025-08-22 11:27:55 +08:00
wwqgtxx
b31664beeb chore: sync vless encryption code 2025-08-21 19:42:56 +08:00
wwqgtxx
7960bcae15 chore: code cleanup 2025-08-21 19:37:26 +08:00
wwqgtxx
664ddb8d55 chore: simplifying generator code 2025-08-21 16:02:17 +08:00
wwqgtxx
e4dfe09744 chore: output vless hash11 in generater 2025-08-21 11:25:41 +08:00
wwqgtxx
b56068ee1c chore: make vision server support splice 2025-08-21 11:17:36 +08:00
wwqgtxx
99e888c829 fix: missing WriterReplaceable for deadline.Conn 2025-08-21 10:46:55 +08:00
wwqgtxx
873d0deeaa chore: make XorConn replaceable for splice 2025-08-21 09:03:44 +08:00
wwqgtxx
7e0a77c99c chore: sync vless encryption code 2025-08-21 08:33:44 +08:00
wwqgtxx
5f09db2655 feat: support AmneziaWG v1.5 2025-08-20 15:42:04 +08:00
wwqgtxx
10174d281c chore: update wireguard-go 2025-08-20 14:50:19 +08:00
wwqgtxx
12c30acdda chore: cleanup vision code 2025-08-20 10:34:38 +08:00
wwqgtxx
2790481709 chore: update cast using in sing-vmess 2025-08-19 23:15:51 +08:00
wwqgtxx
182f60d424 chore: sync vless encryption code 2025-08-19 21:37:02 +08:00
wwqgtxx
930c70f065 doc: remind ordinary users that they should use tun in the top-level configuration 2025-08-19 16:56:35 +08:00
wwqgtxx
fc61715e4e chore: add handshake-mode for mieru 2025-08-19 10:16:59 +08:00
enfein
438be2d379 chore: update mieru version (#2215)
v3.19.0 optimized CPU consumption.

Tested: https://github.com/enfein/mieru/actions/runs/17014543289
2025-08-19 07:10:29 +08:00
wwqgtxx
4e20ed65f2 chore: sync vless encryption code 2025-08-18 23:08:30 +08:00
wwqgtxx
ce760fcf19 action: better patch file download 2025-08-18 10:23:43 +08:00
wwqgtxx
0f76fdf4c5 fix: vision on vless encryption 2025-08-18 09:34:20 +08:00
wwqgtxx
03f4513f61 chore: sync vless encryption code 2025-08-18 08:43:17 +08:00
wwqgtxx
26f603057f fix: 335d54e4 sync mistake 2025-08-18 08:40:52 +08:00
wwqgtxx
b481eca4a4 chore: allow vision with vless encryption 2025-08-17 16:14:20 +08:00
wwqgtxx
eb028b65fc chore: better reflect using in vision 2025-08-17 16:03:35 +08:00
wwqgtxx
48c1b1cdb2 chore: remove depend on lunixbochs/struc 2025-08-16 11:16:53 +08:00
wwqgtxx
76e40baebc chore: sync vless encryption code 2025-08-14 23:52:05 +08:00
wwqgtxx
946b4025df chore: code cleanup 2025-08-14 23:48:59 +08:00
wwqgtxx
089766b285 chore: update TypedValue in sing 2025-08-14 18:36:01 +08:00
wwqgtxx
b643388539 chore: sync vless encryption code 2025-08-14 18:31:56 +08:00
wwqgtxx
0836ec6ee3 chore: change time.Duration atomic using 2025-08-14 16:01:20 +08:00
wwqgtxx
eeb2ad8dae chore: add more test for TypedValue 2025-08-14 14:37:39 +08:00
wwqgtxx
71290b057f chore: reimplement TypedValue by atomic.Pointer 2025-08-14 10:57:56 +08:00
wwqgtxx
41b321dfe1 chore: sync vless encryption code 2025-08-14 09:57:20 +08:00
wwqgtxx
a18e99f966 chore: update dependencies 2025-08-14 09:55:26 +08:00
wwqgtxx
f90d0b954c chore: using atomic.Pointer in anytls 2025-08-14 00:51:55 +08:00
wwqgtxx
0408da2aee chore: sync vless encryption code 2025-08-13 20:50:46 +08:00
wwqgtxx
335d54e488 chore: sync vless encryption code 2025-08-13 19:50:53 +08:00
wwqgtxx
d11f9c895c chore: sync vless encryption code 2025-08-13 18:54:26 +08:00
wwqgtxx
e54ca7ceca chore: sync vless encryption code 2025-08-13 18:51:47 +08:00
wwqgtxx
ce82d49c25 chore: update golang to 1.25 2025-08-13 18:05:24 +08:00
wwqgtxx
8e6be1992b fix: h2mux client closed 2025-08-13 16:43:26 +08:00
wwqgtxx
0e9102daae chore: don't test h2mux for the inbound 2025-08-13 01:15:39 +08:00
wwqgtxx
46dccf26d1 chore: sync vless encryption code 2025-08-13 01:14:22 +08:00
wwqgtxx
854c6a13c3 chore: sync vless encryption code 2025-08-12 22:59:25 +08:00
wwqgtxx
b4c3bbf660 chore: sync vless encryption code 2025-08-12 20:06:08 +08:00
wwqgtxx
6c726d6436 chore: test different http data size for inbound 2025-08-12 15:54:39 +08:00
wwqgtxx
a0bdb861a9 chore: rebuild vless encryption string parsing 2025-08-12 08:46:44 +08:00
wwqgtxx
eca5a27774 fix: mlkem768 logging 2025-08-11 23:00:35 +08:00
wwqgtxx
16d95df100 chore: better wildcard test 2025-08-11 22:33:01 +08:00
wwqgtxx
9b90719ddd feat: support optional aes128xor layer for vless encryption 2025-08-11 20:57:23 +08:00
wwqgtxx
7392529677 chore: add a confused benchmark for wildcard 2025-08-11 18:13:34 +08:00
wwqgtxx
dc52c38179 fix: ? in DOMAIN-WILDCARD should match exactly one character
https://github.com/MetaCubeX/mihomo/issues/2204
2025-08-11 16:23:39 +08:00
wwqgtxx
d7999a32d3 chore: using named const value 2025-08-11 16:23:39 +08:00
wwqgtxx
b41ea05481 chore: add encryption to converter 2025-08-11 16:23:39 +08:00
wwqgtxx
e6fe895190 chore: sync code
3e19bf9233
2025-08-11 16:23:39 +08:00
wwqgtxx
adf553a958 fix: generate doc 2025-08-11 16:23:39 +08:00
wwqgtxx
2a915a5c94 fix: vless server close 2025-08-10 22:43:31 +08:00
wwqgtxx
1b0c72bfab feat: support vless encryption 2025-08-10 22:24:39 +08:00
wwqgtxx
e89af723cd fix: auto redirect panic 2025-08-01 21:02:59 +08:00
wwqgtxx
e8fddd85af fix: vless packetaddr not working 2025-07-31 11:39:06 +08:00
wwqgtxx
f04af734e3 chore: update quic-go to 0.54.0 2025-07-30 19:45:36 +08:00
wwqgtxx
00035302a1 chore: let /upgrade support channel and force as parameters in restful api
Leaving `channel` blank will automatically determine the channel. Other valid values are `alpha`/`release`.

Setting `force` to `true` will bypass the version check and force the update.
2025-07-30 17:50:11 +08:00
wwqgtxx
578e659bb9 chore: keep original file permissions when unpack in updater 2025-07-30 17:09:08 +08:00
wwqgtxx
0f1baeb935 fix: updater may not be able to overwrite files directly 2025-07-28 22:21:57 +08:00
wwqgtxx
16ff9e815b chore: code cleanup 2025-07-27 22:30:39 +08:00
wwqgtxx
5f1f296213 chore: add /cache/dns/flush to restful api 2025-07-27 12:30:33 +08:00
wwqgtxx
66fd5c9f0c chore: allow setting cache-max-size in dns section 2025-07-27 10:31:12 +08:00
wwqgtxx
c3a3009a8c chore: keep original file permissions when copyFile in updater 2025-07-26 22:10:47 +08:00
xishang0128
01cd7e2c0e chore: improve backup and replace logic in updater 2025-07-26 22:49:20 +09:00
wwqgtxx
deec7aafe5 chore: optimizing download in updater 2025-07-26 15:15:11 +08:00
wwqgtxx
a9b7e705f0 chore: optimizing copyFile in updater 2025-07-26 01:32:49 +08:00
xishang0128
fb043df1b6 chore: use canonical return value order 2025-07-25 23:38:35 +09:00
xishang0128
748b5df902 chore: keep original file permissions after update 2025-07-25 23:38:35 +09:00
wwqgtxx
8cbae59d55 chore: upgrade bbolt 2025-07-25 21:59:54 +08:00
wwqgtxx
a37440c81b fix: some downstream dependencies on the upgrader's output fields 2025-07-25 17:57:34 +08:00
wwqgtxx
dbb002a5ba action: add deb/rpm packages for GOAMD64 v1/2/3 2025-07-24 17:40:18 +08:00
wwqgtxx
1a84153213 chore: code cleanup 2025-07-24 15:40:32 +08:00
wwqgtxx
dfe6e0509b chore: rebuild core updater 2025-07-24 02:08:14 +08:00
xishang0128
b6dde7ded7 action: use a more standardized naming format while retaining some compatibility with the old format 2025-07-23 22:58:41 +09:00
wwqgtxx
9f1da11792 chore: use the compile-time GOAMD64 flag in the updater 2025-07-23 18:08:08 +08:00
白日梦主义
63ad95e10f fix: remove unconventional bits when unpacking for update_ui (#2178) 2025-07-22 22:45:20 +08:00
白日梦主义
b06ec5bef8 fix: add path safety check in file type providers (#2177) 2025-07-22 21:37:54 +08:00
wwqgtxx
d4fbffd8e8 chore: update utls to 1.8.0 2025-07-22 15:00:25 +08:00
wwqgtxx
305020175d fix: darwin system stack problem 2025-07-21 10:11:03 +08:00
wwqgtxx
79decdc253 fix: vision server crash 2025-07-20 15:18:01 +08:00
wwqgtxx
407c13b8a4 fix: hy2 server crash 2025-07-19 00:58:33 +08:00
wwqgtxx
d84b182be3 fix: darwin tun mixed stack not working 2025-07-18 11:29:39 +08:00
wwqgtxx
8f18d3f6db chore: add recvmsgx and sendmsgx config to tun
Only for advanced users, enabling `recvmsgx` under darwin can improve performance, but enabling `sendmsgx` may cause unknown problems, please use with caution.
2025-07-17 23:42:25 +08:00
wwqgtxx
b9260e06b8 chore: improve darwin tun performance 2025-07-17 22:11:57 +08:00
wwqgtxx
6337151207 chore: upgrade bbolt to 1.4.2 2025-07-15 22:09:51 +08:00
wwqgtxx
aa555ced5f chore: allow embedded xsync.Map to be lazily initialized 2025-07-15 17:33:36 +08:00
wwqgtxx
349b773b40 chore: upgrade and embed the xsync.Map to v4 2025-07-15 13:39:03 +08:00
wwqgtxx
300eb8b12a chore: rebuild rule parsing code 2025-07-14 10:35:33 +08:00
wwqgtxx
2b84dd3618 fix: regex in logic rules
https://github.com/MetaCubeX/mihomo/issues/2150
2025-07-07 16:16:16 +08:00
wwqgtxx
6a620ba287 chore: revert "chore: better dns batchExchange"
This reverts commit 55f626424f.

The previous changes resulted in a situation where no resolution results were found when multiple DNS servers were used concurrently, and the final resolution time was dragged down by the slowest server.
2025-07-05 23:05:49 +08:00
wwqgtxx
56c3462b76 chore: update quic-go to 0.53.0 2025-06-28 18:16:29 +08:00
wwqgtxx
6f4fe71e41 chore: update dependencies 2025-06-28 12:51:06 +08:00
enfein
ba3e7187a6 chore: update mieru to v3.16.1 (#2138)
Fix a bug that closed session can cause memory leak with bad timing.
2025-06-28 11:00:58 +08:00
JianGuo Wang
0d92b6724b fix: add base64 decoding for VLESS host in ConvertsV2Ray function (#2125) 2025-06-27 16:56:31 +08:00
ayanamist
241ae92bce feat: support DOMAIN-WILDCARD rule (#2124)
only support asterisk(*) and question mark(?)
2025-06-27 16:35:55 +08:00
phanium
91985c1ef8 chore: typo (#2127) 2025-06-26 07:45:46 +08:00
Leo
6a9d428991 chore: remove unused code (#2126) 2025-06-25 22:49:00 +08:00
wwqgtxx
765cbbcc01 fix: miss config in patch 2025-06-25 21:19:36 +08:00
wwqgtxx
5b975275f5 fix: incorrect checking of strings.Split return value
strings.Split will never return a slice of length 0 if sep is not empty, so any code that checks if the return value is of length 0 is incorrect and useless.
2025-06-25 16:20:37 +08:00
ayanamist
166392fe17 chore: sniffer replace domain only if domain is valid (#2122) 2025-06-24 21:44:26 +08:00
ayanamist
5c6aa433ca chore: unconditionally allow clients with passwords for password-free socks5 inbound (#2123) 2025-06-24 19:01:12 +08:00
xishang0128
2c55dc2557 action: fix run build on pull_request 2025-06-24 19:01:03 +08:00
wwqgtxx
56c0b088e8 doc: update path doc 2025-06-21 22:46:55 +08:00
Restia-Ashbell
5344e869a8 fix: ssr uri decode (#2116) 2025-06-21 12:19:13 +08:00
wwqgtxx
6cfaf15cbf fix: missing error return 2025-06-21 12:08:41 +08:00
wwqgtxx
31f0060b30 fix: chacha20 counter overflow
the implement it's a not safe chacha20 using but for compatible
2025-06-21 10:42:14 +08:00
wwqgtxx
c60750d549 chore: allow tun to skip the system ipv6 check when starting by environment variable SKIP_SYSTEM_IPV6_CHECK 2025-06-14 15:57:54 +08:00
wwqgtxx
ebf5918e94 fix: v2ray-plugin mux maybe not close underlay connection 2025-06-14 12:32:45 +08:00
riolurs
93ca18517c chore: converter support fingerprint for anytls 2025-06-13 23:05:06 +08:00
beck
32d447ce99 fix: convert https (#2102) 2025-06-12 17:10:09 +08:00
beck
617fef84ae feat: converter support anytls/socks/http (#2100) 2025-06-12 16:17:25 +08:00
wwqgtxx
d19199322d action: don't trigger cmfa update on pull request 2025-06-12 15:33:19 +08:00
wwqgtxx
87795e3a07 chore: add yaml marshal for common/atomic 2025-06-12 15:24:29 +08:00
wwqgtxx
85bb40aaf8 chore: add Int32Enum for common/atomic 2025-06-12 15:24:29 +08:00
wwqgtxx
082bcec281 chore: apply find process mode in direct/global mode 2025-06-12 00:27:51 +08:00
wwqgtxx
9283cb0f5f feat: add loopback-address support for tun 2025-06-11 17:45:28 +08:00
wwqgtxx
ae7967f662 chore: the resolve and findProcess behaviors of Logic and SubRules follow the order and needs of the internal rules 2025-06-10 20:11:50 +08:00
wwqgtxx
01f8f2db2f chore: cleanup allocator code 2025-06-10 10:54:08 +08:00
wwqgtxx
255ff5e977 chore: add rate limiting support for reality listener 2025-06-10 10:40:26 +08:00
wwqgtxx
939e4109d7 chore: write dns reply in single syscall 2025-06-07 00:38:39 +08:00
wwqgtxx
40587b62b8 feat: all dns client support skip-cert-verify params 2025-06-06 00:52:12 +08:00
wwqgtxx
85e6d25de5 feat: all dns client support ecs and ecs-override params 2025-06-06 00:45:58 +08:00
wwqgtxx
29a37f4f4b feat: all dns client support disable-ipv4 and disable-ipv6 params 2025-06-06 00:24:57 +08:00
wwqgtxx
2f9a3b3469 chore: cleanup code 2025-06-05 21:20:38 +08:00
wwqgtxx
40ea0ba098 fix: correct constructor for 2022-blake3-chacha8-poly1305 2025-06-05 13:47:26 +08:00
wwqgtxx
8d7f947a80 fix: TypedValue.CompareAndSwap
84aa7ff3bb
2025-06-05 13:43:30 +08:00
wwqgtxx
71a8705636 fix: remote dst parse 2025-05-31 22:57:05 +08:00
wwqgtxx
c0f452b540 chore: more unmap for 4in6 address 2025-05-29 10:14:06 +08:00
wwqgtxx
6c9abe16cc fix: vmess listener error 2025-05-28 21:33:44 +08:00
wwqgtxx
213d80c1e2 fix: quic sniffer should consider skipDomain 2025-05-28 10:06:53 +08:00
wwqgtxx
1db89da122 fix: quic sniffer should not replace domain when no valid host is read 2025-05-28 09:22:28 +08:00
wwqgtxx
689c58f661 chore: clear dstIP when overrideDest in sniffer 2025-05-27 22:47:21 +08:00
wwqgtxx
33590c4066 fix: destination should unmap before find interface 2025-05-27 18:26:35 +08:00
wwqgtxx
60ae9dce56 chore: recover log leval for preHandleMetadata 2025-05-27 18:10:44 +08:00
wwqgtxx
4741ac6702 fix: in-port not work with shadowsocks listener 2025-05-27 16:32:42 +08:00
wwqgtxx
ef3d7e4dd7 chore: remove unneeded dns resolve when proxydialer dial udp 2025-05-27 15:04:01 +08:00
wwqgtxx
a1c7881229 chore: rebuild udp dns resolve
The DNS resolution of the overall UDP part has been delayed to the connection initiation stage. During the rule matching process, it will only be triggered when the IP rule without no-resolve is matched.

For direct and wireguard outbound, the same logic as the TCP part will be followed, that is, when direct-nameserver (or DNS configured by wireguard) exists, the result of the matching process will be discarded and the domain name will be re-resolved. This re-resolution logic is only effective for fakeip.

For reject and DNS outbound, no resolution is required.

For other outbound, resolution will still be performed when the connection is initiated, and the domain name will not be sent directly to the remote server at present.
2025-05-27 10:45:26 +08:00
wwqgtxx
12e3952b74 chore: code cleanup 2025-05-26 12:33:24 +08:00
wwqgtxx
88419cbd12 chore: better parse remote dst 2025-05-26 01:12:35 +08:00
wwqgtxx
4ed830330e chore: remove confused code 2025-05-25 22:22:23 +08:00
wwqgtxx
3ed6ff9402 chore: export pipeDeadline 2025-05-25 22:07:29 +08:00
wwqgtxx
34de62d21d chore: better get localAddr 2025-05-24 23:19:38 +08:00
wwqgtxx
d2e255f257 fix: some error in tun 2025-05-24 22:23:10 +08:00
wwqgtxx
a0c46bb4b7 chore: remove the redundant layer of udpnat in sing-tun to reduce resource usage when processing udp 2025-05-24 15:57:49 +08:00
wwqgtxx
9e3bf14b1a chore: handle two interfaces have the same prefix but different address 2025-05-24 11:32:36 +08:00
wwqgtxx
28c387a9b6 chore: restore break change in sing-tun 2025-05-23 20:19:18 +08:00
wwqgtxx
15eda703b4 fix: hysteria2 panic 2025-05-23 20:12:38 +08:00
wwqgtxx
b1d12a15db chore: proxy's ech should fetch from proxy-nameserver 2025-05-22 17:42:40 +08:00
wwqgtxx
5a21bf3642 fix: listener close panic 2025-05-22 17:01:24 +08:00
wwqgtxx
199fb8fd5d chore: update quic-go to 0.52.0 2025-05-22 10:28:10 +08:00
wwqgtxx
fd959feff2 chore: update dependencies 2025-05-21 21:37:20 +08:00
wwqgtxx
d5a03901d2 fix: race in close grpc transport 2025-05-20 16:15:04 +08:00
wwqgtxx
257fead538 docs: update config.yaml follow 5cf0f18c 2025-05-20 11:08:42 +08:00
wwqgtxx
c489c5260b fix: hysteria2 hop ports init
https://github.com/MetaCubeX/mihomo/issues/2056
2025-05-20 10:56:14 +08:00
wwqgtxx
8f92b1de13 chore: simplify the single root decompression process 2025-05-20 09:48:05 +08:00
wwqgtxx
9f7a2a36c1 chore: unpack externalUI in a separate temporary directory to avoid malicious compressed packages from polluting workdir 2025-05-20 01:58:25 +08:00
wwqgtxx
a93479124c chore: stricter path checking when unpacking zip/tgz 2025-05-20 00:00:30 +08:00
wwqgtxx
ed42c4feb8 chore: disallow symlink in unzip 2025-05-19 23:42:39 +08:00
wwqgtxx
608ddb1b44 fix: external-ui-name must in local 2025-05-19 23:11:52 +08:00
wwqgtxx
d036d98128 fix: http server does not handle http2 logic correctly 2025-05-18 23:05:00 +08:00
wwqgtxx
d900c71214 fix: shadowtls v2 not work with X25519MLKEM768 2025-05-18 23:03:07 +08:00
wwqgtxx
1672750c47 chore: simplifying the old fingerprint processing method 2025-05-18 23:03:07 +08:00
wwqgtxx
41b57afb3f fix: grpc deadline implement 2025-05-18 23:03:07 +08:00
wwqgtxx
188372cb04 feat: add tls.ech-key for external-controller-tls 2025-05-17 21:21:02 +08:00
wwqgtxx
a1350d4985 feat: add ech-key for listeners 2025-05-17 20:50:21 +08:00
wwqgtxx
dc958e6a39 feat: add ech-opts for hysteria/hysteria2/tuic outbound 2025-05-17 18:41:39 +08:00
wwqgtxx
8a5f3b8909 chore: simplify port hop costs 2025-05-17 17:06:38 +08:00
wwqgtxx
c6d7ef8cb8 feat: add ech-opts for anytls/shadowsocks/trojan/vmess/vless outbound 2025-05-17 13:53:21 +08:00
wwqgtxx
bb8c47d83d fix: error typo 2025-05-15 18:07:55 +08:00
wwqgtxx
5cf0f18c29 feat: reality add support-x25519mlkem768, it only works with new version server 2025-05-15 14:54:43 +08:00
wwqgtxx
83213d493e chore: adjust min backoff from 1s to 10s 2025-05-14 21:51:18 +08:00
wwqgtxx
90ed01ed53 fix: backoff not reset when the file unchanged 2025-05-14 21:45:12 +08:00
wwqgtxx
f91a586da8 fix: inline proxy provider's healthcheck not work 2025-05-13 19:00:32 +08:00
wwqgtxx
266fb03838 chore: update dependencies 2025-05-13 12:09:38 +08:00
wwqgtxx
76e9607fd7 chore: move start healthcheck.process() from New to Initial in provider
avoid panic cause by build-in proxy have not set to tunnel
2025-05-13 01:12:06 +08:00
wwqgtxx
23e2d3a132 chore: rebuild provider load 2025-05-12 22:19:49 +08:00
wwqgtxx
6e35cf9399 fix: truncated UDP response in system dns
https://github.com/MetaCubeX/mihomo/issues/2031
2025-05-12 12:34:22 +08:00
wwqgtxx
2116640886 chore: the updateConfigs api also adds a check for SAFE_PATHS 2025-05-12 11:28:15 +08:00
251 changed files with 9523 additions and 3179 deletions

View File

@@ -29,14 +29,20 @@ jobs:
strategy:
matrix:
jobs:
- { goos: darwin, goarch: arm64, output: arm64 }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1 }
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2 }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3 }
- { goos: darwin, goarch: arm64, output: arm64 }
- { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386}
- { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible} # old style file name will be removed in next released
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64}
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1, debian: amd64, rpm: x86_64, pacman: x86_64, test: test }
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2, debian: amd64, rpm: x86_64, pacman: x86_64}
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3, debian: amd64, rpm: x86_64, pacman: x86_64}
- { goos: linux, goarch: arm64, output: arm64, debian: arm64, rpm: aarch64, pacman: aarch64}
- { goos: linux, goarch: arm, goarm: '5', output: armv5 }
- { goos: linux, goarch: arm, goarm: '6', output: armv6, debian: armel, rpm: armv6hl}
@@ -54,13 +60,19 @@ jobs:
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
- { goos: windows, goarch: '386', output: '386' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1 }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2 }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3 }
- { goos: windows, goarch: arm64, output: arm64 }
- { goos: freebsd, goarch: '386', output: '386' }
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
- { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-v1 }
- { goos: freebsd, goarch: amd64, goamd64: v2, output: amd64-v2 }
- { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64-v3 }
- { goos: freebsd, goarch: arm64, output: arm64 }
- { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' }
@@ -68,49 +80,70 @@ jobs:
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
# Go 1.24 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
- { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' }
# 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' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' }
# Go 1.22 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
- { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' }
# Go 1.21 can revert commit `9e4385` to work on Windows 7
# https://github.com/golang/go/issues/64622#issuecomment-1847475161
# (OR we can just use golang1.21.4 which unneeded any patch)
- { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go121, goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go121, goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go121, goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go121, goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go121, goversion: '1.21' }
# Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
- { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' }
# Go 1.24 is the last release that will run on macOS 11 Big Sur. Go 1.25 will require macOS 12 Monterey or later.
- { goos: darwin, goarch: arm64, output: arm64-go124, goversion: '1.24' }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' }
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' }
# Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later.
- { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' }
# Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
- { goos: darwin, goarch: arm64, output: arm64-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: v1, output: amd64-v1-go120, goversion: '1.20' }
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-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' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23', test: test }
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' }
# only for test
- { 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: v3, output: amd64-go120, goversion: '1.20' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20', test: test }
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' }
steps:
- uses: actions/checkout@v4
@@ -119,7 +152,7 @@ jobs:
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.25'
- name: Set up Go
if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }}
@@ -134,6 +167,25 @@ jobs:
sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local
echo "/usr/local/go/bin" >> $GITHUB_PATH
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# 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.25 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
# 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
@@ -144,8 +196,9 @@ jobs:
# 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 == '' }}
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.24' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -164,6 +217,7 @@ jobs:
- name: Revert Golang1.23 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.23' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -182,6 +236,7 @@ jobs:
- name: Revert Golang1.22 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.22' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -192,16 +247,18 @@ jobs:
- name: Revert Golang1.21 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.21' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
- name: Set variables
run: |
VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)"
VERSION="${VERSION//\//-}"
PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}"
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
PackageVersion="${VERSION#v}" >> $GITHUB_ENV
PackageVersion="${VERSION#v}"
fi
echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV

View File

@@ -24,6 +24,7 @@ jobs:
- 'ubuntu-24.04-arm' # arm64 linux
- 'macos-13' # amd64 macos
go-version:
- '1.25'
- '1.24'
- '1.23'
- '1.22'
@@ -47,6 +48,25 @@ jobs:
with:
go-version: ${{ matrix.go-version }}
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# 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.25 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
# 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
@@ -59,6 +79,7 @@ jobs:
- name: Revert Golang1.24 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.24' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -77,6 +98,7 @@ jobs:
- name: Revert Golang1.23 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.23' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -95,6 +117,7 @@ jobs:
- name: Revert Golang1.22 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.22' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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
@@ -105,6 +128,7 @@ jobs:
- name: Revert Golang1.21 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.21' }}
run: |
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
cd $(go env GOROOT)
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1

View File

@@ -10,9 +10,6 @@ on:
- Alpha
tags:
- "v*"
pull_request_target:
branches:
- Alpha
jobs:
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies

View File

@@ -17,11 +17,19 @@ GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.
-w -s -buildid='
PLATFORM_LIST = \
darwin-386 \
darwin-amd64-compatible \
darwin-amd64 \
darwin-amd64-v1 \
darwin-amd64-v2 \
darwin-amd64-v3 \
darwin-arm64 \
linux-386 \
linux-amd64-compatible \
linux-amd64 \
linux-amd64-v1 \
linux-amd64-v2 \
linux-amd64-v3 \
linux-armv5 \
linux-armv6 \
linux-armv7 \
@@ -43,37 +51,61 @@ WINDOWS_ARCH_LIST = \
windows-386 \
windows-amd64-compatible \
windows-amd64 \
windows-amd64-v1 \
windows-amd64-v2 \
windows-amd64-v3 \
windows-arm64 \
windows-arm32v7
all:linux-amd64 linux-arm64\
darwin-amd64 darwin-arm64\
windows-amd64 windows-arm64\
all:linux-amd64-v3 linux-arm64\
darwin-amd64-v3 darwin-arm64\
windows-amd64-v3 windows-arm64\
darwin-all: darwin-amd64 darwin-arm64
darwin-all: darwin-amd64-v3 darwin-arm64
docker:
GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-386:
GOARCH=386 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-compatible:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v1:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v2:
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-v3:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-compatible:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-compatible:
linux-amd64-v1:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v2:
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-v3:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@@ -125,12 +157,21 @@ freebsd-arm64:
windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-compatible:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-compatible:
windows-amd64-v1:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-v2:
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-v3:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe

View File

@@ -7,19 +7,17 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/queue"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/common/xsync"
"github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/puzpuzpuz/xsync/v3"
)
var UnifiedDelay = atomic.NewBool(false)
@@ -37,7 +35,7 @@ type Proxy struct {
C.ProxyAdapter
alive atomic.Bool
history *queue.Queue[C.DelayHistory]
extra *xsync.MapOf[string, *internalProxyState]
extra xsync.Map[string, *internalProxyState]
}
// Adapter implements C.Proxy
@@ -295,7 +293,7 @@ func NewProxy(adapter C.ProxyAdapter) *Proxy {
ProxyAdapter: adapter,
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
extra: xsync.NewMapOf[string, *internalProxyState]()}
}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@@ -316,15 +314,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return
}
}
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: uint16(uintPort),
}
err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port))
return
}

View File

@@ -4,7 +4,6 @@ import (
"net"
"net/http"
"net/netip"
"strconv"
"strings"
C "github.com/metacubex/mihomo/constant"
@@ -41,23 +40,8 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737)
host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: netip.Addr{},
DstPort: uint16Port,
}
ip, err := netip.ParseAddr(host)
if err == nil {
metadata.DstIP = ip
}
metadata := &C.Metadata{}
_ = metadata.SetRemoteAddress(net.JoinHostPort(host, port))
return metadata
}

View File

@@ -2,7 +2,6 @@ package outbound
import (
"context"
"errors"
"net"
"strconv"
"time"
@@ -10,7 +9,6 @@ import (
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"
@@ -28,19 +26,20 @@ type AnyTLS struct {
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"`
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"`
ECHOpts ECHOptions `proxy:"ech-opts,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) (_ C.Conn, err error) {
@@ -52,6 +51,10 @@ func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
}
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
// create tcp
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
if err != nil {
@@ -59,13 +62,6 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
}
// 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
}
@@ -115,12 +111,17 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
MinIdleSession: option.MinIdleSession,
}
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsConfig := &vmess.TLSConfig{
Host: option.SNI,
SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN,
FingerPrint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
ECH: echConfig,
}
if tlsConfig.Host == "" {
tlsConfig.Host = option.Server

View File

@@ -3,15 +3,16 @@ package outbound
import (
"context"
"encoding/json"
"fmt"
"net"
"runtime"
"strings"
"sync"
"syscall"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
)
@@ -19,6 +20,7 @@ import (
type ProxyAdapter interface {
C.ProxyAdapter
DialOptions() []dialer.Option
ResolveUDP(ctx context.Context, metadata *C.Metadata) error
}
type Base struct {
@@ -160,6 +162,17 @@ func (b *Base) DialOptions() (opts []dialer.Option) {
return opts
}
func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (b *Base) Close() error {
return nil
}
@@ -203,12 +216,21 @@ func NewBase(opt BaseOption) *Base {
type conn struct {
N.ExtendedConn
chain C.Chain
actualRemoteDestination string
chain C.Chain
adapterAddr string
}
func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination
if remoteAddr := c.RemoteAddr(); remoteAddr != nil {
m := C.Metadata{}
if err := m.SetRemoteAddr(remoteAddr); err == nil {
if m.Valid() {
return m.String()
}
}
}
host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
}
// Chains implements C.Connection
@@ -241,19 +263,25 @@ func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
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
}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, a.Addr()}
}
type packetConn struct {
N.EnhancePacketConn
chain C.Chain
adapterName string
connID string
actualRemoteDestination string
chain C.Chain
adapterName string
connID string
adapterAddr string
resolveUDP func(ctx context.Context, metadata *C.Metadata) error
}
func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
return c.resolveUDP(ctx, metadata)
}
func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination
host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
}
// Chains implements C.Connection
@@ -287,24 +315,12 @@ 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 ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
}
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
}
func parseRemoteDestination(addr string) string {
if dst, _, err := net.SplitHostPort(addr); err == nil {
return dst
} else {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
return dst
} else {
return ""
}
}
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
}
type AddRef interface {

View File

@@ -2,7 +2,8 @@ package outbound
import (
"context"
"errors"
"fmt"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/loopback"
"github.com/metacubex/mihomo/component/resolver"
@@ -38,13 +39,8 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
return nil, err
}
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err := d.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
if err != nil {
@@ -53,6 +49,17 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
}
func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
}

View File

@@ -3,6 +3,7 @@ package outbound
import (
"context"
"net"
"net/netip"
"time"
N "github.com/metacubex/mihomo/common/net"
@@ -31,6 +32,9 @@ func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, er
// ListenPacketContext implements C.ProxyAdapter
func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
if err := d.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
@@ -41,6 +45,13 @@ func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.
}, d), nil
}
func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2})
}
return nil
}
type dnsPacket struct {
data []byte
put func()

36
adapter/outbound/ech.go Normal file
View File

@@ -0,0 +1,36 @@
package outbound
import (
"context"
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/resolver"
)
type ECHOptions struct {
Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"`
Config string `proxy:"config,omitempty" obfs:"config,omitempty"`
}
func (o ECHOptions) Parse() (*ech.Config, error) {
if !o.Enable {
return nil, nil
}
echConfig := &ech.Config{}
if o.Config != "" {
list, err := base64.StdEncoding.DecodeString(o.Config)
if err != nil {
return nil, fmt.Errorf("base64 decode ech config string failed: %v", err)
}
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return list, nil
}
} else {
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver)
}
}
return echConfig, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
@@ -44,6 +45,9 @@ type Hysteria struct {
option *HysteriaOption
client *core.Client
tlsConfig *tlsC.Config
echConfig *ech.Config
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
@@ -56,6 +60,9 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
udpConn, err := h.client.DialUDP(h.genHdc(ctx))
if err != nil {
return nil, err
@@ -79,7 +86,15 @@ func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer {
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
},
remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddr(ctx, "udp", addr, h.prefer)
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer)
if err != nil {
return nil, err
}
err = h.echConfig.ClientHandle(ctx, h.tlsConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
},
}
}
@@ -93,30 +108,31 @@ func (h *Hysteria) ProxyInfo() C.ProxyInfo {
type HysteriaOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
Protocol string `proxy:"protocol,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
Up string `proxy:"up"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
Down string `proxy:"down"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
Auth string `proxy:"auth,omitempty"`
AuthString string `proxy:"auth-str,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
Protocol string `proxy:"protocol,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
Up string `proxy:"up"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
Down string `proxy:"down"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
Auth string `proxy:"auth,omitempty"`
AuthString string `proxy:"auth-str,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
}
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
@@ -156,11 +172,18 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
return nil, err
}
if len(option.ALPN) > 0 {
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
tlsConfig.NextProtos = option.ALPN
} else {
tlsConfig.NextProtos = []string{DefaultALPN}
}
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
@@ -215,7 +238,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
down = uint64(option.DownSpeed * mbpsToBps)
}
client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen,
)
@@ -233,8 +256,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
client: client,
option: &option,
client: client,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
}
return outbound, nil

View File

@@ -20,7 +20,6 @@ import (
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/quic-go"
"github.com/metacubex/randv2"
"github.com/metacubex/sing-quic/hysteria2"
M "github.com/metacubex/sing/common/metadata"
)
@@ -42,24 +41,25 @@ type Hysteria2 struct {
type Hysteria2Option struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
@@ -77,6 +77,9 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.
}
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := h.client.ListenPacket(ctx)
if err != nil {
return nil, err
@@ -150,10 +153,16 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
return nil, err
}
if len(option.ALPN) > 0 {
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
tlsConfig.NextProtos = option.ALPN
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.UdpMTU == 0 {
// "1200" from quic-go's MaxDatagramSize
// "-3" from quic-go's DatagramFrame.MaxDataLen
@@ -175,41 +184,46 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
ReceiveBPS: StringToBps(option.Down),
SalamanderPassword: salamanderPassword,
Password: option.Password,
TLSConfig: tlsC.UConfig(tlsConfig),
TLSConfig: tlsClientConfig,
QUICConfig: quicConfig,
UDPDisabled: false,
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
if err != nil {
return nil, err
}
err = echConfig.ClientHandle(ctx, tlsClientConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
},
}
var ranges utils.IntRanges[uint16]
var serverAddress []string
var serverPorts []uint16
if option.Ports != "" {
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
if err != nil {
return nil, err
}
ranges.Range(func(port uint16) bool {
serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port))))
serverPorts = append(serverPorts, port)
return true
})
if len(serverAddress) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
}
if len(serverPorts) > 0 {
if option.HopInterval == 0 {
option.HopInterval = defaultHopInterval
} else if option.HopInterval < minHopInterval {
option.HopInterval = minHopInterval
}
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
clientOptions.ServerPorts = serverPorts
}
}
if option.Port == 0 && len(serverAddress) == 0 {
if option.Port == 0 && len(serverPorts) == 0 {
return nil, errors.New("invalid port")
}

View File

@@ -28,15 +28,16 @@ type Mieru struct {
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"`
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"`
HandshakeMode string `proxy:"handshake-mode,omitempty"`
}
// DialContext implements C.ProxyAdapter
@@ -54,6 +55,9 @@ func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = m.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if err := m.ensureClientIsRunning(); err != nil {
return nil, err
}
@@ -242,6 +246,9 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
}
}
if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
}
return config, nil
}
@@ -291,6 +298,11 @@ func validateMieruOption(option MieruOption) error {
return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing)
}
}
if option.HandshakeMode != "" {
if _, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; !ok {
return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
}
}
return nil
}

View File

@@ -13,11 +13,14 @@ import (
type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
if o.PublicKey != "" {
config := new(tlsC.RealityConfig)
config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768
const x25519ScalarSize = 32
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)

View File

@@ -4,6 +4,7 @@ import (
"context"
"io"
"net"
"net/netip"
"time"
"github.com/metacubex/mihomo/common/buf"
@@ -29,9 +30,19 @@ func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := r.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
return newPacketConn(&nopPacketConn{}, r), nil
}
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.IPv4Unspecified()
}
return nil
}
func NewRejectWithOption(option RejectOption) *Reject {
return &Reject{
Base: &Base{

View File

@@ -2,7 +2,6 @@ package outbound
import (
"context"
"errors"
"fmt"
"net"
"strconv"
@@ -11,7 +10,6 @@ import (
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls"
@@ -64,6 +62,7 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -77,6 +76,7 @@ type gostObfsOption struct {
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -200,6 +200,9 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
return nil, err
}
}
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil {
return nil, err
@@ -228,15 +231,9 @@ func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
@@ -303,6 +300,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
}
v2rayOption.ECHConfig = echConfig
}
} else if option.Plugin == "gost-plugin" {
opts := gostObfsOption{Host: "bing.com", Mux: true}
@@ -325,6 +328,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
gostOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
}
gostOption.ECHConfig = echConfig
}
} else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode

View File

@@ -105,6 +105,9 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err
}
}
if err = ssr.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil {
return nil, err

View File

@@ -2,12 +2,10 @@ package outbound
import (
"context"
"errors"
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/log"
@@ -53,16 +51,9 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if s.onlyTcp {
return s.ProxyAdapter.ListenPacketContext(ctx, metadata)
}
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
if err != nil {
return nil, err

View File

@@ -127,6 +127,9 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return nil, err
}
}
if err = s.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil {
return nil, err

View File

@@ -109,6 +109,9 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err
}
}
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err)

View File

@@ -138,7 +138,7 @@ func NewSsh(option SshOption) (*Ssh, error) {
} else {
path := C.Path.Resolve(option.PrivateKey)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
b, err = os.ReadFile(path)
if err != nil {

View File

@@ -12,6 +12,7 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
@@ -32,6 +33,7 @@ type Trojan struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
ssCipher core.Cipher
}
@@ -48,6 +50,7 @@ type TrojanOption struct {
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
@@ -77,6 +80,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint,
ECHConfig: t.echConfig,
Headers: http.Header{},
}
@@ -91,7 +95,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
}
alpn := trojan.DefaultWebsocketALPN
if len(t.option.ALPN) != 0 {
if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
alpn = t.option.ALPN
}
@@ -110,12 +114,12 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
default:
// default tcp network
// handle TLS
alpn := trojan.DefaultALPN
if len(t.option.ALPN) != 0 {
if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
alpn = t.option.ALPN
}
c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{
@@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn,
ECH: t.echConfig,
Reality: t.realityConfig,
})
}
@@ -214,6 +219,10 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
// ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// grpc transport
@@ -245,6 +254,9 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
return nil, err
}
}
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@@ -266,12 +278,6 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
return C.ALLNet
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err
}
// SupportUOT implements C.ProxyAdapter
func (t *Trojan) SupportUOT() bool {
return true
@@ -321,6 +327,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err
}
t.echConfig, err = option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.SSOpts.Enabled {
if option.SSOpts.Password == "" {
return nil, errors.New("empty password")
@@ -365,7 +376,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{

View File

@@ -3,7 +3,6 @@ package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"math"
"net"
@@ -12,8 +11,8 @@ import (
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/tuic"
@@ -28,6 +27,9 @@ type Tuic struct {
*Base
option *TuicOption
client *tuic.PoolClient
tlsConfig *tlsC.Config
echConfig *ech.Config
}
type TuicOption struct {
@@ -48,18 +50,19 @@ type TuicOption struct {
DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
@@ -86,6 +89,10 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata
@@ -97,13 +104,6 @@ func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, meta
}
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
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())
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
@@ -135,6 +135,10 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
if err != nil {
return nil, nil, err
}
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
if err != nil {
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
@@ -249,6 +253,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion:
case 0:
@@ -268,7 +278,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
option: &option,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
}
clientMaxOpenStreams := int64(option.MaxOpenStreams)
@@ -285,7 +297,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsC.UConfig(tlsConfig),
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
@@ -305,7 +317,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsC.UConfig(tlsConfig),
TlsConfig: tlsClientConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,

View File

@@ -3,26 +3,23 @@ package outbound
import (
"context"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"sync"
"github.com/metacubex/mihomo/common/convert"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/vless"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/metacubex/mihomo/transport/vmess"
vmessSing "github.com/metacubex/sing-vmess"
@@ -30,22 +27,20 @@ import (
M "github.com/metacubex/sing/common/metadata"
)
const (
// max packet length
maxLength = 1024 << 3
)
type Vless struct {
*Base
client *vless.Client
option *VlessOption
encryption *encryption.ClientInstance
// for gun mux
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
}
type VlessOption struct {
@@ -61,7 +56,9 @@ type VlessOption struct {
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Encryption string `proxy:"encryption,omitempty"`
Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -88,6 +85,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{},
}
@@ -151,7 +149,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default:
// default tcp network
// handle TLS
@@ -170,6 +168,12 @@ func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if v.encryption != nil {
c, err = v.encryption.Handshake(c)
if err != nil {
return
}
}
if metadata.NetWork == C.UDP {
if v.option.PacketAddr {
metadata = &C.Metadata{
@@ -185,9 +189,6 @@ func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
}
}
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
if v.option.PacketAddr {
conn = packetaddr.NewBindConn(conn)
}
} else {
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false))
}
@@ -206,6 +207,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -272,13 +274,8 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// gun transport
@@ -310,13 +307,8 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
}
}
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -342,13 +334,8 @@ func (v *Vless) SupportWithDialer() C.NetWork {
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if v.option.XUDP {
@@ -363,12 +350,11 @@ func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada
), v), nil
} else if v.option.PacketAddr {
return newPacketConn(N.NewThreadSafePacketConn(
packetaddr.NewConn(&vlessPacketConn{
Conn: c, rAddr: metadata.UDPAddr(),
}, M.SocksaddrFromNet(metadata.UDPAddr())),
packetaddr.NewConn(v.client.PacketConn(c, metadata.UDPAddr()),
M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
}
return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil
return newPacketConn(N.NewThreadSafePacketConn(v.client.PacketConn(c, metadata.UDPAddr())), v), nil
}
// SupportUOT implements C.ProxyAdapter
@@ -419,98 +405,6 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
}
}
type vlessPacketConn struct {
net.Conn
rAddr net.Addr
remain int
mux sync.Mutex
cache [2]byte
}
func (c *vlessPacketConn) writePacket(payload []byte) (int, error) {
binary.BigEndian.PutUint16(c.cache[:], uint16(len(payload)))
if _, err := c.Conn.Write(c.cache[:]); err != nil {
return 0, err
}
return c.Conn.Write(payload)
}
func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
total := len(b)
if total == 0 {
return 0, nil
}
if total <= maxLength {
return c.writePacket(b)
}
offset := 0
for offset < total {
cursor := offset + maxLength
if cursor > total {
cursor = total
}
n, err := c.writePacket(b[offset:cursor])
if err != nil {
return offset + n, err
}
offset = cursor
if offset == total {
break
}
}
return total, nil
}
func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
c.mux.Lock()
defer c.mux.Unlock()
if c.remain > 0 {
length := len(b)
if c.remain < length {
length = c.remain
}
n, err := c.Conn.Read(b[:length])
if err != nil {
return 0, c.rAddr, err
}
c.remain -= n
return n, c.rAddr, nil
}
if _, err := c.Conn.Read(b[:2]); err != nil {
return 0, c.rAddr, err
}
total := int(binary.BigEndian.Uint16(b[:2]))
if total == 0 {
return 0, c.rAddr, nil
}
length := len(b)
if length > total {
length = total
}
if _, err := io.ReadFull(c.Conn, b[:length]); err != nil {
return 0, c.rAddr, errors.New("read packet error")
}
c.remain = total - length
return length, c.rAddr, nil
}
func NewVless(option VlessOption) (*Vless, error) {
var addons *vless.Addons
if option.Network != "ws" && len(option.Flow) >= 16 {
@@ -558,11 +452,21 @@ func NewVless(option VlessOption) (*Vless, error) {
option: &option,
}
v.encryption, err = encryption.NewClient(option.Encryption)
if err != nil {
return nil, err
}
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
@@ -611,7 +515,7 @@ func NewVless(option VlessOption) (*Vless, error) {
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
}
return v, nil

View File

@@ -15,8 +15,8 @@ import (
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
@@ -41,6 +41,7 @@ type Vmess struct {
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
echConfig *ech.Config
}
type VmessOption struct {
@@ -58,6 +59,7 @@ type VmessOption struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -109,6 +111,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{},
}
@@ -146,6 +149,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -195,7 +199,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default:
// handle TLS
if v.option.TLS {
@@ -205,6 +209,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
}
@@ -324,13 +329,8 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn
// gun transport
@@ -361,13 +361,8 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
}
}
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -407,13 +402,8 @@ func (v *Vmess) Close() error {
// ListenPacketOnStreamConn implements C.ProxyAdapter
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
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = v.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if pc, ok := c.(net.PacketConn); ok {
@@ -474,6 +464,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err
}
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
@@ -522,7 +517,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
}
return v, nil

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
@@ -88,15 +87,26 @@ type WireGuardPeerOption struct {
}
type AmneziaWGOption struct {
JC int `proxy:"jc"`
JMin int `proxy:"jmin"`
JMax int `proxy:"jmax"`
S1 int `proxy:"s1"`
S2 int `proxy:"s2"`
H1 uint32 `proxy:"h1"`
H2 uint32 `proxy:"h2"`
H3 uint32 `proxy:"h3"`
H4 uint32 `proxy:"h4"`
JC int `proxy:"jc,omitempty"`
JMin int `proxy:"jmin,omitempty"`
JMax int `proxy:"jmax,omitempty"`
S1 int `proxy:"s1,omitempty"`
S2 int `proxy:"s2,omitempty"`
H1 uint32 `proxy:"h1,omitempty"`
H2 uint32 `proxy:"h2,omitempty"`
H3 uint32 `proxy:"h3,omitempty"`
H4 uint32 `proxy:"h4,omitempty"`
// AmneziaWG v1.5
I1 string `proxy:"i1,omitempty"`
I2 string `proxy:"i2,omitempty"`
I3 string `proxy:"i3,omitempty"`
I4 string `proxy:"i4,omitempty"`
I5 string `proxy:"i5,omitempty"`
J1 string `proxy:"j1,omitempty"`
J2 string `proxy:"j2,omitempty"`
J3 string `proxy:"j3,omitempty"`
Itime int64 `proxy:"itime,omitempty"`
}
type wgSingErrorHandler struct {
@@ -387,15 +397,60 @@ func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, er
if !updateOnly {
ipcConf += "private_key=" + w.option.PrivateKey + "\n"
if w.option.AmneziaWGOption != nil {
ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n"
ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n"
ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n"
ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n"
ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n"
ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n"
ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n"
ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n"
ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n"
if w.option.AmneziaWGOption.JC != 0 {
ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n"
}
if w.option.AmneziaWGOption.JMin != 0 {
ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n"
}
if w.option.AmneziaWGOption.JMax != 0 {
ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n"
}
if w.option.AmneziaWGOption.S1 != 0 {
ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n"
}
if w.option.AmneziaWGOption.S2 != 0 {
ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n"
}
if w.option.AmneziaWGOption.H1 != 0 {
ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n"
}
if w.option.AmneziaWGOption.H2 != 0 {
ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n"
}
if w.option.AmneziaWGOption.H3 != 0 {
ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n"
}
if w.option.AmneziaWGOption.H4 != 0 {
ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n"
}
if w.option.AmneziaWGOption.I1 != "" {
ipcConf += "i1=" + w.option.AmneziaWGOption.I1 + "\n"
}
if w.option.AmneziaWGOption.I2 != "" {
ipcConf += "i2=" + w.option.AmneziaWGOption.I2 + "\n"
}
if w.option.AmneziaWGOption.I3 != "" {
ipcConf += "i3=" + w.option.AmneziaWGOption.I3 + "\n"
}
if w.option.AmneziaWGOption.I4 != "" {
ipcConf += "i4=" + w.option.AmneziaWGOption.I4 + "\n"
}
if w.option.AmneziaWGOption.I5 != "" {
ipcConf += "i5=" + w.option.AmneziaWGOption.I5 + "\n"
}
if w.option.AmneziaWGOption.J1 != "" {
ipcConf += "j1=" + w.option.AmneziaWGOption.J1 + "\n"
}
if w.option.AmneziaWGOption.J2 != "" {
ipcConf += "j2=" + w.option.AmneziaWGOption.J2 + "\n"
}
if w.option.AmneziaWGOption.J3 != "" {
ipcConf += "j3=" + w.option.AmneziaWGOption.J3 + "\n"
}
if w.option.AmneziaWGOption.Itime != 0 {
ipcConf += "itime=" + strconv.FormatInt(int64(w.option.AmneziaWGOption.Itime), 10) + "\n"
}
}
}
if len(w.option.Peers) > 0 {
@@ -520,16 +575,8 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if err = w.init(ctx); err != nil {
return nil, err
}
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
if err = w.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
if err != nil {
@@ -541,6 +588,21 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
return newPacketConn(pc, w), nil
}
func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
// IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true

View File

@@ -32,7 +32,6 @@ type HealthCheck struct {
url string
extra map[string]*extraOption
mu sync.Mutex
started atomic.Bool
proxies []C.Proxy
interval time.Duration
lazy bool
@@ -43,13 +42,8 @@ type HealthCheck struct {
}
func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(hc.interval)
hc.start()
go hc.check()
for {
select {
case <-ticker.C:
@@ -62,13 +56,12 @@ func (hc *HealthCheck) process() {
}
case <-hc.ctx.Done():
ticker.Stop()
hc.stop()
return
}
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
func (hc *HealthCheck) setProxies(proxies []C.Proxy) {
hc.proxies = proxies
}
@@ -105,10 +98,6 @@ func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
@@ -131,14 +120,6 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now())
}
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
}
func (hc *HealthCheck) check() {
if len(hc.proxies) == 0 {
return

View File

@@ -17,7 +17,6 @@ import (
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct {
@@ -109,13 +108,16 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
switch schema.Type {
case "file":
path := C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, C.Path.ErrNotSafePath(path)
}
vehicle = resource.NewFileVehicle(path)
case "http":
path := C.Path.GetPathByHash("proxies", schema.URL)
if schema.Path != "" {
path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
return nil, C.Path.ErrNotSafePath(path)
}
}
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)

View File

@@ -57,6 +57,13 @@ func (bp *baseProvider) Version() uint32 {
return bp.version
}
func (bp *baseProvider) Initial() error {
if bp.healthCheck.auto() {
go bp.healthCheck.process()
}
return nil
}
func (bp *baseProvider) HealthCheck() {
bp.healthCheck.check()
}
@@ -88,7 +95,7 @@ func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
bp.proxies = proxies
bp.version += 1
bp.healthCheck.setProxy(proxies)
bp.healthCheck.setProxies(proxies)
if bp.healthCheck.auto() {
go bp.healthCheck.check()
}
@@ -133,6 +140,9 @@ func (pp *proxySetProvider) Update() error {
}
func (pp *proxySetProvider) Initial() error {
if err := pp.baseProvider.Initial(); err != nil {
return err
}
_, err := pp.Fetcher.Initial()
if err != nil {
return err
@@ -162,10 +172,6 @@ func (pp *proxySetProvider) Close() error {
}
func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{
baseProvider: baseProvider{
name: name,
@@ -185,6 +191,8 @@ func NewProxySetProvider(name string, interval time.Duration, payload []map[stri
return nil, err
}
pd.proxies = proxies
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
}
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
@@ -234,10 +242,6 @@ 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()
@@ -245,10 +249,6 @@ func (ip *inlineProvider) Update() error {
}
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 {
@@ -258,6 +258,8 @@ func NewInlineProvider(name string, payload []map[string]any, parser resource.Pa
if err != nil {
return nil, err
}
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
ip := &inlineProvider{
baseProvider: baseProvider{
@@ -301,13 +303,6 @@ 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
}
@@ -317,10 +312,6 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
return nil, errors.New("provider need one proxy at least")
}
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{
baseProvider: baseProvider{
name: name,

63
common/atomic/enum.go Normal file
View File

@@ -0,0 +1,63 @@
package atomic
import (
"encoding/json"
"fmt"
"sync/atomic"
)
type Int32Enum[T ~int32] struct {
value atomic.Int32
}
func (i *Int32Enum[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(i.Load())
}
func (i *Int32Enum[T]) UnmarshalJSON(b []byte) error {
var v T
if err := json.Unmarshal(b, &v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32Enum[T]) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int32Enum[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v T
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32Enum[T]) String() string {
return fmt.Sprint(i.Load())
}
func (i *Int32Enum[T]) Store(v T) {
i.value.Store(int32(v))
}
func (i *Int32Enum[T]) Load() T {
return T(i.value.Load())
}
func (i *Int32Enum[T]) Swap(new T) T {
return T(i.value.Swap(int32(new)))
}
func (i *Int32Enum[T]) CompareAndSwap(old, new T) bool {
return i.value.CompareAndSwap(int32(old), int32(new))
}
func NewInt32Enum[T ~int32](v T) *Int32Enum[T] {
a := &Int32Enum[T]{}
a.Store(v)
return a
}

View File

@@ -29,6 +29,19 @@ func (i *Bool) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Bool) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Bool) UnmarshalYAML(unmarshal func(any) error) error {
var v bool
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Bool) String() string {
v := i.Load()
return strconv.FormatBool(v)
@@ -58,6 +71,19 @@ func (p *Pointer[T]) UnmarshalJSON(b []byte) error {
return nil
}
func (p *Pointer[T]) MarshalYAML() (any, error) {
return p.Load(), nil
}
func (p *Pointer[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v *T
if err := unmarshal(&v); err != nil {
return err
}
p.Store(v)
return nil
}
func (p *Pointer[T]) String() string {
return fmt.Sprint(p.Load())
}
@@ -84,6 +110,19 @@ func (i *Int32) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Int32) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int32) UnmarshalYAML(unmarshal func(any) error) error {
var v int32
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32) String() string {
v := i.Load()
return strconv.FormatInt(int64(v), 10)
@@ -111,6 +150,19 @@ func (i *Int64) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Int64) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int64) UnmarshalYAML(unmarshal func(any) error) error {
var v int64
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int64) String() string {
v := i.Load()
return strconv.FormatInt(int64(v), 10)
@@ -138,6 +190,19 @@ func (i *Uint32) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Uint32) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uint32) UnmarshalYAML(unmarshal func(any) error) error {
var v uint32
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uint32) String() string {
v := i.Load()
return strconv.FormatUint(uint64(v), 10)
@@ -165,6 +230,19 @@ func (i *Uint64) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Uint64) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uint64) UnmarshalYAML(unmarshal func(any) error) error {
var v uint64
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uint64) String() string {
v := i.Load()
return strconv.FormatUint(uint64(v), 10)
@@ -192,6 +270,19 @@ func (i *Uintptr) UnmarshalJSON(b []byte) error {
return nil
}
func (i *Uintptr) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uintptr) UnmarshalYAML(unmarshal func(any) error) error {
var v uintptr
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uintptr) String() string {
v := i.Load()
return strconv.FormatUint(uint64(v), 10)

View File

@@ -5,49 +5,50 @@ import (
"sync/atomic"
)
func DefaultValue[T any]() T {
var defaultValue T
return defaultValue
}
type TypedValue[T any] struct {
_ noCopy
value atomic.Value
value atomic.Pointer[T]
}
// tValue is a struct with determined type to resolve atomic.Value usages with interface types
// https://github.com/golang/go/issues/22550
//
// The intention to have an atomic value store for errors. However, running this code panics:
// panic: sync/atomic: store of inconsistently typed value into Value
// This is because atomic.Value requires that the underlying concrete type be the same (which is a reasonable expectation for its implementation).
// When going through the atomic.Value.Store method call, the fact that both these are of the error interface is lost.
type tValue[T any] struct {
value T
func (t *TypedValue[T]) Load() (v T) {
v, _ = t.LoadOk()
return
}
func (t *TypedValue[T]) Load() T {
func (t *TypedValue[T]) LoadOk() (v T, ok bool) {
value := t.value.Load()
if value == nil {
return DefaultValue[T]()
return
}
return value.(tValue[T]).value
return *value, true
}
func (t *TypedValue[T]) Store(value T) {
t.value.Store(tValue[T]{value})
t.value.Store(&value)
}
func (t *TypedValue[T]) Swap(new T) T {
old := t.value.Swap(tValue[T]{new})
func (t *TypedValue[T]) Swap(new T) (v T) {
old := t.value.Swap(&new)
if old == nil {
return DefaultValue[T]()
return
}
return old.(tValue[T]).value
return *old
}
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new})
for {
currentP := t.value.Load()
var currentValue T
if currentP != nil {
currentValue = *currentP
}
// Compare old and current via runtime equality check.
if any(currentValue) != any(old) {
return false
}
if t.value.CompareAndSwap(currentP, &new) {
return true
}
}
}
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
@@ -63,13 +64,20 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
return nil
}
func (t *TypedValue[T]) MarshalYAML() (any, error) {
return t.Load(), nil
}
func (t *TypedValue[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v T
if err := unmarshal(&v); err != nil {
return err
}
t.Store(v)
return nil
}
func NewTypedValue[T any](t T) (v TypedValue[T]) {
v.Store(t)
return
}
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}

169
common/atomic/value_test.go Normal file
View File

@@ -0,0 +1,169 @@
package atomic
import (
"io"
"os"
"testing"
)
func TestTypedValue(t *testing.T) {
{
var v TypedValue[int]
got, gotOk := v.LoadOk()
if got != 0 || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
}
v.Store(1)
got, gotOk = v.LoadOk()
if got != 1 || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
}
}
{
var v TypedValue[error]
got, gotOk := v.LoadOk()
if got != nil || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
}
v.Store(io.EOF)
got, gotOk = v.LoadOk()
if got != io.EOF || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
}
err := &os.PathError{}
v.Store(err)
got, gotOk = v.LoadOk()
if got != err || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
}
v.Store(nil)
got, gotOk = v.LoadOk()
if got != nil || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
}
}
{
e1, e2, e3 := io.EOF, &os.PathError{}, &os.PathError{}
var v TypedValue[error]
if v.CompareAndSwap(e1, e2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != nil {
t.Fatalf("Load = (%v), want (%v)", value, nil)
}
if v.CompareAndSwap(nil, e1) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != e1 {
t.Fatalf("Load = (%v), want (%v)", value, e1)
}
if v.CompareAndSwap(e2, e3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != e1 {
t.Fatalf("Load = (%v), want (%v)", value, e1)
}
if v.CompareAndSwap(e1, e2) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != e2 {
t.Fatalf("Load = (%v), want (%v)", value, e2)
}
if v.CompareAndSwap(e3, e2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != e2 {
t.Fatalf("Load = (%v), want (%v)", value, e2)
}
if v.CompareAndSwap(nil, e3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != e2 {
t.Fatalf("Load = (%v), want (%v)", value, e2)
}
}
{
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
var v TypedValue[chan struct{}]
if v.CompareAndSwap(c1, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != nil {
t.Fatalf("Load = (%v), want (%v)", value, nil)
}
if v.CompareAndSwap(nil, c1) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != c1 {
t.Fatalf("Load = (%v), want (%v)", value, c1)
}
if v.CompareAndSwap(c2, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c1 {
t.Fatalf("Load = (%v), want (%v)", value, c1)
}
if v.CompareAndSwap(c1, c2) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
if v.CompareAndSwap(c3, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
if v.CompareAndSwap(nil, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
}
{
c1, c2, c3 := &io.LimitedReader{}, &io.SectionReader{}, &io.SectionReader{}
var v TypedValue[io.Reader]
if v.CompareAndSwap(c1, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != nil {
t.Fatalf("Load = (%v), want (%v)", value, nil)
}
if v.CompareAndSwap(nil, c1) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != c1 {
t.Fatalf("Load = (%v), want (%v)", value, c1)
}
if v.CompareAndSwap(c2, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c1 {
t.Fatalf("Load = (%v), want (%v)", value, c1)
}
if v.CompareAndSwap(c1, c2) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
if v.CompareAndSwap(c3, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
if v.CompareAndSwap(nil, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if value := v.Load(); value != c2 {
t.Fatalf("Load = (%v), want (%v)", value, c2)
}
}
}

View File

@@ -14,6 +14,7 @@ var NewPacket = buf.NewPacket
var NewSize = buf.NewSize
var With = buf.With
var As = buf.As
var ReleaseMulti = buf.ReleaseMulti
var (
Must = common.Must

View File

@@ -2,6 +2,7 @@ package convert
import (
"encoding/base64"
"fmt"
"strings"
)
@@ -43,3 +44,22 @@ func decodeUrlSafe(data string) string {
}
return string(dcBuf)
}
func TryDecodeBase64(s string) (decoded []byte, err error) {
if len(s)%4 == 0 {
if decoded, err = base64.StdEncoding.DecodeString(s); err == nil {
return
}
if decoded, err = base64.URLEncoding.DecodeString(s); err == nil {
return
}
} else {
if decoded, err = base64.RawStdEncoding.DecodeString(s); err == nil {
return
}
if decoded, err = base64.RawURLEncoding.DecodeString(s); err == nil {
return
}
}
return nil, fmt.Errorf("invalid base64-encoded string")
}

View File

@@ -208,6 +208,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if err != nil {
continue
}
if decodedHost, err := tryDecodeBase64([]byte(urlVLess.Host)); err == nil {
urlVLess.Host = string(decodedHost)
}
query := urlVLess.Query()
vless := make(map[string]any, 20)
err = handleVShareLink(names, urlVLess, scheme, vless)
@@ -218,6 +221,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if flow := query.Get("flow"); flow != "" {
vless["flow"] = strings.ToLower(flow)
}
if encryption := query.Get("encryption"); encryption != "" {
vless["encryption"] = encryption
}
proxies = append(proxies, vless)
case "vmess":
@@ -456,12 +462,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := encRaw.DecodeString(body)
dcBuf, err := TryDecodeBase64(body)
if err != nil {
continue
}
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64param&protoparam=urlsafebase64param&remarks=urlsafebase64remarks&group=urlsafebase64group&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok {
@@ -490,7 +496,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam")
protocolParam := decodeUrlSafe(query.Get("protoparam"))
ssr := make(map[string]any, 20)
@@ -513,6 +519,101 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
}
proxies = append(proxies, ssr)
case "socks", "socks5", "socks5h", "http", "https":
link, err := url.Parse(line)
if err != nil {
continue
}
server := link.Hostname()
if server == "" {
continue
}
portStr := link.Port()
if portStr == "" {
continue
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
name := uniqueName(names, remarks)
encodeStr := link.User.String()
var username, password string
if encodeStr != "" {
decodeStr := string(DecodeBase64([]byte(encodeStr)))
splitStr := strings.Split(decodeStr, ":")
// todo: should use url.QueryUnescape ?
username = splitStr[0]
if len(splitStr) == 2 {
password = splitStr[1]
}
}
socks := make(map[string]any, 10)
socks["name"] = name
socks["type"] = func() string {
switch scheme {
case "socks", "socks5", "socks5h":
return "socks5"
case "http", "https":
return "http"
}
return scheme
}()
socks["server"] = server
socks["port"] = portStr
socks["username"] = username
socks["password"] = password
socks["skip-cert-verify"] = true
if scheme == "https" {
socks["tls"] = true
}
proxies = append(proxies, socks)
case "anytls":
// https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
link, err := url.Parse(line)
if err != nil {
continue
}
username := link.User.Username()
password, exist := link.User.Password()
if !exist {
password = username
}
query := link.Query()
server := link.Hostname()
if server == "" {
continue
}
portStr := link.Port()
if portStr == "" {
continue
}
insecure, sni := query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
fingerprint := query.Get("hpkp")
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
name := uniqueName(names, remarks)
anytls := make(map[string]any, 10)
anytls["name"] = name
anytls["type"] = "anytls"
anytls["server"] = server
anytls["port"] = portStr
anytls["username"] = username
anytls["password"] = password
anytls["sni"] = sni
anytls["fingerprint"] = fingerprint
anytls["skip-cert-verify"] = insecureBool
anytls["udp"] = true
proxies = append(proxies, anytls)
}
}

19
common/maphash/common.go Normal file
View File

@@ -0,0 +1,19 @@
package maphash
import "hash/maphash"
type Seed = maphash.Seed
func MakeSeed() Seed {
return maphash.MakeSeed()
}
type Hash = maphash.Hash
func Bytes(seed Seed, b []byte) uint64 {
return maphash.Bytes(seed, b)
}
func String(seed Seed, s string) uint64 {
return maphash.String(seed, s)
}

View File

@@ -0,0 +1,140 @@
//go:build !go1.24
package maphash
import "unsafe"
func Comparable[T comparable](s Seed, v T) uint64 {
return comparableHash(*(*seedTyp)(unsafe.Pointer(&s)), v)
}
func comparableHash[T comparable](seed seedTyp, v T) uint64 {
s := seed.s
var m map[T]struct{}
mTyp := iTypeOf(m)
var hasher func(unsafe.Pointer, uintptr) uintptr
hasher = (*iMapType)(unsafe.Pointer(mTyp)).Hasher
p := escape(unsafe.Pointer(&v))
if ptrSize == 8 {
return uint64(hasher(p, uintptr(s)))
}
lo := hasher(p, uintptr(s))
hi := hasher(p, uintptr(s>>32))
return uint64(hi)<<32 | uint64(lo)
}
// WriteComparable adds x to the data hashed by h.
func WriteComparable[T comparable](h *Hash, x T) {
// writeComparable (not in purego mode) directly operates on h.state
// without using h.buf. Mix in the buffer length so it won't
// commute with a buffered write, which either changes h.n or changes
// h.state.
hash := (*hashTyp)(unsafe.Pointer(h))
if hash.n != 0 {
hash.state.s = comparableHash(hash.state, hash.n)
}
hash.state.s = comparableHash(hash.state, x)
}
// go/src/hash/maphash/maphash.go
type hashTyp struct {
_ [0]func() // not comparable
seed seedTyp // initial seed used for this hash
state seedTyp // current hash of all flushed bytes
buf [128]byte // unflushed byte buffer
n int // number of unflushed bytes
}
type seedTyp struct {
s uint64
}
type iTFlag uint8
type iKind uint8
type iNameOff int32
// TypeOff is the offset to a type from moduledata.types. See resolveTypeOff in runtime.
type iTypeOff int32
type iType struct {
Size_ uintptr
PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
Hash uint32 // hash of type; avoids computation in hash tables
TFlag iTFlag // extra type information flags
Align_ uint8 // alignment of variable with this type
FieldAlign_ uint8 // alignment of struct field with this type
Kind_ iKind // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
Equal func(unsafe.Pointer, unsafe.Pointer) bool
// GCData stores the GC type data for the garbage collector.
// Normally, GCData points to a bitmask that describes the
// ptr/nonptr fields of the type. The bitmask will have at
// least PtrBytes/ptrSize bits.
// If the TFlagGCMaskOnDemand bit is set, GCData is instead a
// **byte and the pointer to the bitmask is one dereference away.
// The runtime will build the bitmask if needed.
// (See runtime/type.go:getGCMask.)
// Note: multiple types may have the same value of GCData,
// including when TFlagGCMaskOnDemand is set. The types will, of course,
// have the same pointer layout (but not necessarily the same size).
GCData *byte
Str iNameOff // string form
PtrToThis iTypeOff // type for pointer to this type, may be zero
}
type iMapType struct {
iType
Key *iType
Elem *iType
Group *iType // internal type representing a slot group
// function for hashing keys (ptr to key, seed) -> hash
Hasher func(unsafe.Pointer, uintptr) uintptr
}
func iTypeOf(a any) *iType {
eface := *(*iEmptyInterface)(unsafe.Pointer(&a))
// Types are either static (for compiler-created types) or
// heap-allocated but always reachable (for reflection-created
// types, held in the central map). So there is no need to
// escape types. noescape here help avoid unnecessary escape
// of v.
return (*iType)(noescape(unsafe.Pointer(eface.Type)))
}
type iEmptyInterface struct {
Type *iType
Data unsafe.Pointer
}
// noescape hides a pointer from escape analysis. noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
// USE CAREFULLY!
//
// nolint:all
//
//go:nosplit
//goland:noinspection ALL
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
var alwaysFalse bool
var escapeSink any
// escape forces any pointers in x to escape to the heap.
func escape[T any](x T) T {
if alwaysFalse {
escapeSink = x
}
return x
}
// ptrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant.
// It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit).
const ptrSize = 4 << (^uintptr(0) >> 63)

View File

@@ -0,0 +1,13 @@
//go:build go1.24
package maphash
import "hash/maphash"
func Comparable[T comparable](seed Seed, v T) uint64 {
return maphash.Comparable(seed, v)
}
func WriteComparable[T comparable](h *Hash, x T) {
maphash.WriteComparable(h, x)
}

View File

@@ -0,0 +1,532 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package maphash
import (
"bytes"
"fmt"
"hash"
"math"
"reflect"
"strings"
"testing"
"unsafe"
rand "github.com/metacubex/randv2"
)
func TestUnseededHash(t *testing.T) {
m := map[uint64]struct{}{}
for i := 0; i < 1000; i++ {
h := new(Hash)
m[h.Sum64()] = struct{}{}
}
if len(m) < 900 {
t.Errorf("empty hash not sufficiently random: got %d, want 1000", len(m))
}
}
func TestSeededHash(t *testing.T) {
s := MakeSeed()
m := map[uint64]struct{}{}
for i := 0; i < 1000; i++ {
h := new(Hash)
h.SetSeed(s)
m[h.Sum64()] = struct{}{}
}
if len(m) != 1 {
t.Errorf("seeded hash is random: got %d, want 1", len(m))
}
}
func TestHashGrouping(t *testing.T) {
b := bytes.Repeat([]byte("foo"), 100)
hh := make([]*Hash, 7)
for i := range hh {
hh[i] = new(Hash)
}
for _, h := range hh[1:] {
h.SetSeed(hh[0].Seed())
}
hh[0].Write(b)
hh[1].WriteString(string(b))
writeByte := func(h *Hash, b byte) {
err := h.WriteByte(b)
if err != nil {
t.Fatalf("WriteByte: %v", err)
}
}
writeSingleByte := func(h *Hash, b byte) {
_, err := h.Write([]byte{b})
if err != nil {
t.Fatalf("Write single byte: %v", err)
}
}
writeStringSingleByte := func(h *Hash, b byte) {
_, err := h.WriteString(string([]byte{b}))
if err != nil {
t.Fatalf("WriteString single byte: %v", err)
}
}
for i, x := range b {
writeByte(hh[2], x)
writeSingleByte(hh[3], x)
if i == 0 {
writeByte(hh[4], x)
} else {
writeSingleByte(hh[4], x)
}
writeStringSingleByte(hh[5], x)
if i == 0 {
writeByte(hh[6], x)
} else {
writeStringSingleByte(hh[6], x)
}
}
sum := hh[0].Sum64()
for i, h := range hh {
if sum != h.Sum64() {
t.Errorf("hash %d not identical to a single Write", i)
}
}
if sum1 := Bytes(hh[0].Seed(), b); sum1 != hh[0].Sum64() {
t.Errorf("hash using Bytes not identical to a single Write")
}
if sum1 := String(hh[0].Seed(), string(b)); sum1 != hh[0].Sum64() {
t.Errorf("hash using String not identical to a single Write")
}
}
func TestHashBytesVsString(t *testing.T) {
s := "foo"
b := []byte(s)
h1 := new(Hash)
h2 := new(Hash)
h2.SetSeed(h1.Seed())
n1, err1 := h1.WriteString(s)
if n1 != len(s) || err1 != nil {
t.Fatalf("WriteString(s) = %d, %v, want %d, nil", n1, err1, len(s))
}
n2, err2 := h2.Write(b)
if n2 != len(b) || err2 != nil {
t.Fatalf("Write(b) = %d, %v, want %d, nil", n2, err2, len(b))
}
if h1.Sum64() != h2.Sum64() {
t.Errorf("hash of string and bytes not identical")
}
}
func TestHashHighBytes(t *testing.T) {
// See issue 34925.
const N = 10
m := map[uint64]struct{}{}
for i := 0; i < N; i++ {
h := new(Hash)
h.WriteString("foo")
m[h.Sum64()>>32] = struct{}{}
}
if len(m) < N/2 {
t.Errorf("from %d seeds, wanted at least %d different hashes; got %d", N, N/2, len(m))
}
}
func TestRepeat(t *testing.T) {
h1 := new(Hash)
h1.WriteString("testing")
sum1 := h1.Sum64()
h1.Reset()
h1.WriteString("testing")
sum2 := h1.Sum64()
if sum1 != sum2 {
t.Errorf("different sum after resetting: %#x != %#x", sum1, sum2)
}
h2 := new(Hash)
h2.SetSeed(h1.Seed())
h2.WriteString("testing")
sum3 := h2.Sum64()
if sum1 != sum3 {
t.Errorf("different sum on the same seed: %#x != %#x", sum1, sum3)
}
}
func TestSeedFromSum64(t *testing.T) {
h1 := new(Hash)
h1.WriteString("foo")
x := h1.Sum64() // seed generated here
h2 := new(Hash)
h2.SetSeed(h1.Seed())
h2.WriteString("foo")
y := h2.Sum64()
if x != y {
t.Errorf("hashes don't match: want %x, got %x", x, y)
}
}
func TestSeedFromSeed(t *testing.T) {
h1 := new(Hash)
h1.WriteString("foo")
_ = h1.Seed() // seed generated here
x := h1.Sum64()
h2 := new(Hash)
h2.SetSeed(h1.Seed())
h2.WriteString("foo")
y := h2.Sum64()
if x != y {
t.Errorf("hashes don't match: want %x, got %x", x, y)
}
}
func TestSeedFromFlush(t *testing.T) {
b := make([]byte, 65)
h1 := new(Hash)
h1.Write(b) // seed generated here
x := h1.Sum64()
h2 := new(Hash)
h2.SetSeed(h1.Seed())
h2.Write(b)
y := h2.Sum64()
if x != y {
t.Errorf("hashes don't match: want %x, got %x", x, y)
}
}
func TestSeedFromReset(t *testing.T) {
h1 := new(Hash)
h1.WriteString("foo")
h1.Reset() // seed generated here
h1.WriteString("foo")
x := h1.Sum64()
h2 := new(Hash)
h2.SetSeed(h1.Seed())
h2.WriteString("foo")
y := h2.Sum64()
if x != y {
t.Errorf("hashes don't match: want %x, got %x", x, y)
}
}
func negativeZero[T float32 | float64]() T {
var f T
f = -f
return f
}
func TestComparable(t *testing.T) {
testComparable(t, int64(2))
testComparable(t, uint64(8))
testComparable(t, uintptr(12))
testComparable(t, any("s"))
testComparable(t, "s")
testComparable(t, true)
testComparable(t, new(float64))
testComparable(t, float64(9))
testComparable(t, complex128(9i+1))
testComparable(t, struct{}{})
testComparable(t, struct {
i int
u uint
b bool
f float64
p *int
a any
}{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1})
type S struct {
s string
}
s1 := S{s: heapStr(t)}
s2 := S{s: heapStr(t)}
if unsafe.StringData(s1.s) == unsafe.StringData(s2.s) {
t.Fatalf("unexpected two heapStr ptr equal")
}
if s1.s != s2.s {
t.Fatalf("unexpected two heapStr value not equal")
}
testComparable(t, s1, s2)
testComparable(t, s1.s, s2.s)
testComparable(t, float32(0), negativeZero[float32]())
testComparable(t, float64(0), negativeZero[float64]())
testComparableNoEqual(t, math.NaN(), math.NaN())
testComparableNoEqual(t, [2]string{"a", ""}, [2]string{"", "a"})
testComparableNoEqual(t, struct{ a, b string }{"foo", ""}, struct{ a, b string }{"", "foo"})
testComparableNoEqual(t, struct{ a, b any }{int(0), struct{}{}}, struct{ a, b any }{struct{}{}, int(0)})
}
func testComparableNoEqual[T comparable](t *testing.T, v1, v2 T) {
seed := MakeSeed()
if Comparable(seed, v1) == Comparable(seed, v2) {
t.Fatalf("Comparable(seed, %v) == Comparable(seed, %v)", v1, v2)
}
}
var heapStrValue = []byte("aTestString")
func heapStr(t *testing.T) string {
return string(heapStrValue)
}
func testComparable[T comparable](t *testing.T, v T, v2 ...T) {
t.Run(TypeFor[T]().String(), func(t *testing.T) {
var a, b T = v, v
if len(v2) != 0 {
b = v2[0]
}
var pa *T = &a
seed := MakeSeed()
if Comparable(seed, a) != Comparable(seed, b) {
t.Fatalf("Comparable(seed, %v) != Comparable(seed, %v)", a, b)
}
old := Comparable(seed, pa)
stackGrow(8192)
new := Comparable(seed, pa)
if old != new {
t.Fatal("Comparable(seed, ptr) != Comparable(seed, ptr)")
}
})
}
var use byte
//go:noinline
func stackGrow(dep int) {
if dep == 0 {
return
}
var local [1024]byte
// make sure local is allocated on the stack.
local[rand.Uint64()%1024] = byte(rand.Uint64())
use = local[rand.Uint64()%1024]
stackGrow(dep - 1)
}
func TestWriteComparable(t *testing.T) {
testWriteComparable(t, int64(2))
testWriteComparable(t, uint64(8))
testWriteComparable(t, uintptr(12))
testWriteComparable(t, any("s"))
testWriteComparable(t, "s")
testComparable(t, true)
testWriteComparable(t, new(float64))
testWriteComparable(t, float64(9))
testWriteComparable(t, complex128(9i+1))
testWriteComparable(t, struct{}{})
testWriteComparable(t, struct {
i int
u uint
b bool
f float64
p *int
a any
}{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1})
type S struct {
s string
}
s1 := S{s: heapStr(t)}
s2 := S{s: heapStr(t)}
if unsafe.StringData(s1.s) == unsafe.StringData(s2.s) {
t.Fatalf("unexpected two heapStr ptr equal")
}
if s1.s != s2.s {
t.Fatalf("unexpected two heapStr value not equal")
}
testWriteComparable(t, s1, s2)
testWriteComparable(t, s1.s, s2.s)
testWriteComparable(t, float32(0), negativeZero[float32]())
testWriteComparable(t, float64(0), negativeZero[float64]())
testWriteComparableNoEqual(t, math.NaN(), math.NaN())
testWriteComparableNoEqual(t, [2]string{"a", ""}, [2]string{"", "a"})
testWriteComparableNoEqual(t, struct{ a, b string }{"foo", ""}, struct{ a, b string }{"", "foo"})
testWriteComparableNoEqual(t, struct{ a, b any }{int(0), struct{}{}}, struct{ a, b any }{struct{}{}, int(0)})
}
func testWriteComparableNoEqual[T comparable](t *testing.T, v1, v2 T) {
seed := MakeSeed()
h1 := Hash{}
h2 := Hash{}
*(*Seed)(unsafe.Pointer(&h1)), *(*Seed)(unsafe.Pointer(&h2)) = seed, seed
WriteComparable(&h1, v1)
WriteComparable(&h2, v2)
if h1.Sum64() == h2.Sum64() {
t.Fatalf("WriteComparable(seed, %v) == WriteComparable(seed, %v)", v1, v2)
}
}
func testWriteComparable[T comparable](t *testing.T, v T, v2 ...T) {
t.Run(TypeFor[T]().String(), func(t *testing.T) {
var a, b T = v, v
if len(v2) != 0 {
b = v2[0]
}
var pa *T = &a
h1 := Hash{}
h2 := Hash{}
*(*Seed)(unsafe.Pointer(&h1)) = MakeSeed()
h2 = h1
WriteComparable(&h1, a)
WriteComparable(&h2, b)
if h1.Sum64() != h2.Sum64() {
t.Fatalf("WriteComparable(h, %v) != WriteComparable(h, %v)", a, b)
}
WriteComparable(&h1, pa)
old := h1.Sum64()
stackGrow(8192)
WriteComparable(&h2, pa)
new := h2.Sum64()
if old != new {
t.Fatal("WriteComparable(seed, ptr) != WriteComparable(seed, ptr)")
}
})
}
func TestComparableShouldPanic(t *testing.T) {
s := []byte("s")
a := any(s)
defer func() {
e := recover()
err, ok := e.(error)
if !ok {
t.Fatalf("Comaparable(any([]byte)) should panic")
}
want := "hash of unhashable type []uint8"
if s := err.Error(); !strings.Contains(s, want) {
t.Fatalf("want %s, got %s", want, s)
}
}()
Comparable(MakeSeed(), a)
}
func TestWriteComparableNoncommute(t *testing.T) {
seed := MakeSeed()
var h1, h2 Hash
h1.SetSeed(seed)
h2.SetSeed(seed)
h1.WriteString("abc")
WriteComparable(&h1, 123)
WriteComparable(&h2, 123)
h2.WriteString("abc")
if h1.Sum64() == h2.Sum64() {
t.Errorf("WriteComparable and WriteString unexpectedly commute")
}
}
func TestComparableAllocations(t *testing.T) {
t.Skip("test broken in old golang version")
seed := MakeSeed()
x := heapStr(t)
allocs := testing.AllocsPerRun(10, func() {
s := "s" + x
Comparable(seed, s)
})
if allocs > 0 {
t.Errorf("got %v allocs, want 0", allocs)
}
type S struct {
a int
b string
}
allocs = testing.AllocsPerRun(10, func() {
s := S{123, "s" + x}
Comparable(seed, s)
})
if allocs > 0 {
t.Errorf("got %v allocs, want 0", allocs)
}
}
// Make sure a Hash implements the hash.Hash and hash.Hash64 interfaces.
var _ hash.Hash = &Hash{}
var _ hash.Hash64 = &Hash{}
func benchmarkSize(b *testing.B, size int) {
h := &Hash{}
buf := make([]byte, size)
s := string(buf)
b.Run("Write", func(b *testing.B) {
b.SetBytes(int64(size))
for i := 0; i < b.N; i++ {
h.Reset()
h.Write(buf)
h.Sum64()
}
})
b.Run("Bytes", func(b *testing.B) {
b.SetBytes(int64(size))
seed := h.Seed()
for i := 0; i < b.N; i++ {
Bytes(seed, buf)
}
})
b.Run("String", func(b *testing.B) {
b.SetBytes(int64(size))
seed := h.Seed()
for i := 0; i < b.N; i++ {
String(seed, s)
}
})
}
func BenchmarkHash(b *testing.B) {
sizes := []int{4, 8, 16, 32, 64, 256, 320, 1024, 4096, 16384}
for _, size := range sizes {
b.Run(fmt.Sprint("n=", size), func(b *testing.B) {
benchmarkSize(b, size)
})
}
}
func benchmarkComparable[T comparable](b *testing.B, v T) {
b.Run(TypeFor[T]().String(), func(b *testing.B) {
seed := MakeSeed()
for i := 0; i < b.N; i++ {
Comparable(seed, v)
}
})
}
func BenchmarkComparable(b *testing.B) {
type testStruct struct {
i int
u uint
b bool
f float64
p *int
a any
}
benchmarkComparable(b, int64(2))
benchmarkComparable(b, uint64(8))
benchmarkComparable(b, uintptr(12))
benchmarkComparable(b, any("s"))
benchmarkComparable(b, "s")
benchmarkComparable(b, true)
benchmarkComparable(b, new(float64))
benchmarkComparable(b, float64(9))
benchmarkComparable(b, complex128(9i+1))
benchmarkComparable(b, struct{}{})
benchmarkComparable(b, testStruct{i: 9, u: 1, b: true, f: 9.9, p: new(int), a: 1})
}
// TypeFor returns the [Type] that represents the type argument T.
func TypeFor[T any]() reflect.Type {
var v T
if t := reflect.TypeOf(v); t != nil {
return t // optimize for T being a non-interface kind
}
return reflect.TypeOf((*T)(nil)).Elem() // only for an interface kind
}

View File

@@ -20,7 +20,7 @@ type connReadResult struct {
type Conn struct {
network.ExtendedConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
pipeDeadline PipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan *connReadResult
@@ -34,7 +34,7 @@ func IsConn(conn any) bool {
func NewConn(conn net.Conn) *Conn {
c := &Conn{
ExtendedConn: bufio.NewExtendedConn(conn),
pipeDeadline: makePipeDeadline(),
pipeDeadline: MakePipeDeadline(),
resultCh: make(chan *connReadResult, 1),
}
c.resultCh <- nil
@@ -58,7 +58,7 @@ func (c *Conn) Read(p []byte) (n int, err error) {
c.resultCh <- nil
break
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return 0, os.ErrDeadlineExceeded
}
@@ -104,7 +104,7 @@ func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) {
c.resultCh <- nil
break
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return os.ErrDeadlineExceeded
}
@@ -130,7 +130,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.ExtendedConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
c.pipeDeadline.Set(t)
return nil
}
@@ -149,6 +149,10 @@ func (c *Conn) ReaderReplaceable() bool {
return c.disablePipe.Load() || c.deadline.Load().IsZero()
}
func (c *Conn) WriterReplaceable() bool {
return true
}
func (c *Conn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -19,7 +19,7 @@ type readResult struct {
type NetPacketConn struct {
net.PacketConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
pipeDeadline PipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan any
@@ -28,7 +28,7 @@ type NetPacketConn struct {
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
npc := &NetPacketConn{
PacketConn: pc,
pipeDeadline: makePipeDeadline(),
pipeDeadline: MakePipeDeadline(),
resultCh: make(chan any, 1),
}
npc.resultCh <- nil
@@ -83,7 +83,7 @@ FOR:
c.resultCh <- nil
break FOR
}
case <-c.pipeDeadline.wait():
case <-c.pipeDeadline.Wait():
return 0, nil, os.ErrDeadlineExceeded
}
}
@@ -122,7 +122,7 @@ func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
return c.PacketConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
c.pipeDeadline.Set(t)
return nil
}

View File

@@ -52,7 +52,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return nil, nil, nil, os.ErrDeadlineExceeded
}
}

View File

@@ -69,7 +69,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
@@ -146,7 +146,7 @@ FOR:
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
case <-c.netPacketConn.pipeDeadline.Wait():
return nil, M.Socksaddr{}, os.ErrDeadlineExceeded
}
}

View File

@@ -9,24 +9,24 @@ import (
"time"
)
// pipeDeadline is an abstraction for handling timeouts.
type pipeDeadline struct {
// PipeDeadline is an abstraction for handling timeouts.
type PipeDeadline struct {
mu sync.Mutex // Guards timer and cancel
timer *time.Timer
cancel chan struct{} // Must be non-nil
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
func MakePipeDeadline() PipeDeadline {
return PipeDeadline{cancel: make(chan struct{})}
}
// set sets the point in time when the deadline will time out.
// Set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future.
//
// A zero value for t prevents timeout.
func (d *pipeDeadline) set(t time.Time) {
func (d *PipeDeadline) Set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
@@ -61,8 +61,8 @@ func (d *pipeDeadline) set(t time.Time) {
}
}
// wait returns a channel that is closed when the deadline is exceeded.
func (d *pipeDeadline) wait() chan struct{} {
// Wait returns a channel that is closed when the deadline is exceeded.
func (d *PipeDeadline) Wait() chan struct{} {
d.mu.Lock()
defer d.mu.Unlock()
return d.cancel

View File

@@ -33,8 +33,8 @@ type pipe struct {
localDone chan struct{}
remoteDone <-chan struct{}
readDeadline pipeDeadline
writeDeadline pipeDeadline
readDeadline PipeDeadline
writeDeadline PipeDeadline
readWaitOptions N.ReadWaitOptions
}
@@ -56,15 +56,15 @@ func Pipe() (net.Conn, net.Conn) {
rdRx: cb1, rdTx: cn1,
wrTx: cb2, wrRx: cn2,
localDone: done1, remoteDone: done2,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
readDeadline: MakePipeDeadline(),
writeDeadline: MakePipeDeadline(),
}
p2 := &pipe{
rdRx: cb2, rdTx: cn2,
wrTx: cb1, wrRx: cn1,
localDone: done2, remoteDone: done1,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
readDeadline: MakePipeDeadline(),
writeDeadline: MakePipeDeadline(),
}
return p1, p2
}
@@ -86,7 +86,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return 0, io.EOF
case isClosedChan(p.readDeadline.wait()):
case isClosedChan(p.readDeadline.Wait()):
return 0, os.ErrDeadlineExceeded
}
@@ -99,7 +99,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case <-p.remoteDone:
return 0, io.EOF
case <-p.readDeadline.wait():
case <-p.readDeadline.Wait():
return 0, os.ErrDeadlineExceeded
}
}
@@ -118,7 +118,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return 0, io.ErrClosedPipe
case isClosedChan(p.writeDeadline.wait()):
case isClosedChan(p.writeDeadline.Wait()):
return 0, os.ErrDeadlineExceeded
}
@@ -134,7 +134,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return n, io.ErrClosedPipe
case <-p.remoteDone:
return n, io.ErrClosedPipe
case <-p.writeDeadline.wait():
case <-p.writeDeadline.Wait():
return n, os.ErrDeadlineExceeded
}
}
@@ -145,8 +145,8 @@ func (p *pipe) SetDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.readDeadline.set(t)
p.writeDeadline.set(t)
p.readDeadline.Set(t)
p.writeDeadline.Set(t)
return nil
}
@@ -154,7 +154,7 @@ func (p *pipe) SetReadDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.readDeadline.set(t)
p.readDeadline.Set(t)
return nil
}
@@ -162,7 +162,7 @@ func (p *pipe) SetWriteDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe
}
p.writeDeadline.set(t)
p.writeDeadline.Set(t)
return nil
}
@@ -192,7 +192,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe
case isClosedChan(p.remoteDone):
return nil, io.EOF
case isClosedChan(p.readDeadline.wait()):
case isClosedChan(p.readDeadline.Wait()):
return nil, os.ErrDeadlineExceeded
}
select {
@@ -211,7 +211,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe
case <-p.remoteDone:
return nil, io.EOF
case <-p.readDeadline.wait():
case <-p.readDeadline.Wait():
return nil, os.ErrDeadlineExceeded
}
}

View File

@@ -34,8 +34,8 @@ func (l *handleContextListener) init() {
}
}
}()
if c, err := l.handle(l.ctx, c); err == nil {
l.conns <- c
if conn, err := l.handle(l.ctx, c); err == nil {
l.conns <- conn
} else {
// handle failed, close the underlying connection.
_ = c.Close()

View File

@@ -22,6 +22,10 @@ type ExtendedReader = network.ExtendedReader
var WriteBuffer = bufio.WriteBuffer
type ReadWaitOptions = network.ReadWaitOptions
var NewReadWaitOptions = network.NewReadWaitOptions
func NewDeadlineConn(conn net.Conn) ExtendedConn {
if deadline.IsPipe(conn) || deadline.IsPipe(network.UnwrapReader(conn)) {
return NewExtendedConn(conn) // pipe always have correctly deadline implement

View File

@@ -8,18 +8,23 @@ import (
"sync"
)
var defaultAllocator = NewAllocator()
var DefaultAllocator = NewAllocator()
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct {
type Allocator interface {
Get(size int) []byte
Put(buf []byte) error
}
// defaultAllocator for incoming frames, optimized to prevent overwriting after zeroing
type defaultAllocator struct {
buffers [11]sync.Pool
}
// NewAllocator initiates a []byte allocator for frames less than 65536 bytes,
// the waste(memory fragmentation) of space allocation is guaranteed to be
// no more than 50%.
func NewAllocator() *Allocator {
return &Allocator{
func NewAllocator() Allocator {
return &defaultAllocator{
buffers: [...]sync.Pool{ // 64B -> 64K
{New: func() any { return new([1 << 6]byte) }},
{New: func() any { return new([1 << 7]byte) }},
@@ -37,7 +42,7 @@ func NewAllocator() *Allocator {
}
// Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte {
func (alloc *defaultAllocator) Get(size int) []byte {
switch {
case size < 0:
panic("alloc.Get: len out of range")
@@ -87,7 +92,7 @@ func (alloc *Allocator) Get(size int) []byte {
// Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error {
func (alloc *defaultAllocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 {
return nil
}

View File

@@ -3,13 +3,12 @@
package pool
const (
// RelayBufferSize using for tcp
// io.Copy default buffer size is 32 KiB
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 16 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// UDPBufferSize using for udp
// Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 8 * 1024
)

View File

@@ -3,13 +3,12 @@
package pool
const (
// RelayBufferSize using for tcp
// io.Copy default buffer size is 32 KiB
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024
RelayBufferSize = 32 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU
// UDPBufferSize using for udp
// Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024
)

View File

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

View File

@@ -3,5 +3,5 @@ package pool
import "github.com/metacubex/sing/common/buf"
func init() {
buf.DefaultAllocator = defaultAllocator
buf.DefaultAllocator = DefaultAllocator
}

View File

@@ -239,13 +239,15 @@ func (n *num) UnmarshalText(text []byte) (err error) {
func TestStructure_TextUnmarshaller(t *testing.T) {
rawMap := map[string]any{
"num": "255",
"num_p": "127",
"num": "255",
"num_p": "127",
"num_arr": []string{"1", "2", "3"},
}
s := &struct {
Num num `test:"num"`
NumP *num `test:"num_p"`
Num num `test:"num"`
NumP *num `test:"num_p"`
NumArr []num `test:"num_arr"`
}{}
err := decoder.Decode(rawMap, s)
@@ -253,6 +255,7 @@ func TestStructure_TextUnmarshaller(t *testing.T) {
assert.Equal(t, 255, s.Num.a)
assert.NotNil(t, s.NumP)
assert.Equal(t, s.NumP.a, 127)
assert.Equal(t, s.NumArr, []num{{1}, {2}, {3}})
// test WeaklyTypedInput
rawMap["num"] = 256

926
common/xsync/map.go Normal file
View File

@@ -0,0 +1,926 @@
package xsync
// copy and modified from https://github.com/puzpuzpuz/xsync/blob/v4.1.0/map.go
// which is licensed under Apache v2.
//
// mihomo modified:
// 1. parallel Map resize has been removed to decrease the memory using.
// 2. the zero Map is ready for use.
import (
"fmt"
"math"
"math/bits"
"strings"
"sync"
"sync/atomic"
"unsafe"
"github.com/metacubex/mihomo/common/maphash"
)
const (
// number of Map entries per bucket; 5 entries lead to size of 64B
// (one cache line) on 64-bit machines
entriesPerMapBucket = 5
// threshold fraction of table occupation to start a table shrinking
// when deleting the last entry in a bucket chain
mapShrinkFraction = 128
// map load factor to trigger a table resize during insertion;
// a map holds up to mapLoadFactor*entriesPerMapBucket*mapTableLen
// key-value pairs (this is a soft limit)
mapLoadFactor = 0.75
// minimal table size, i.e. number of buckets; thus, minimal map
// capacity can be calculated as entriesPerMapBucket*defaultMinMapTableLen
defaultMinMapTableLen = 32
// minimum counter stripes to use
minMapCounterLen = 8
// maximum counter stripes to use; stands for around 4KB of memory
maxMapCounterLen = 32
defaultMeta uint64 = 0x8080808080808080
metaMask uint64 = 0xffffffffff
defaultMetaMasked uint64 = defaultMeta & metaMask
emptyMetaSlot uint8 = 0x80
)
type mapResizeHint int
const (
mapGrowHint mapResizeHint = 0
mapShrinkHint mapResizeHint = 1
mapClearHint mapResizeHint = 2
)
type ComputeOp int
const (
// CancelOp signals to Compute to not do anything as a result
// of executing the lambda. If the entry was not present in
// the map, nothing happens, and if it was present, the
// returned value is ignored.
CancelOp ComputeOp = iota
// UpdateOp signals to Compute to update the entry to the
// value returned by the lambda, creating it if necessary.
UpdateOp
// DeleteOp signals to Compute to always delete the entry
// from the map.
DeleteOp
)
type loadOp int
const (
noLoadOp loadOp = iota
loadOrComputeOp
loadAndDeleteOp
)
// Map is like a Go map[K]V but is safe for concurrent
// use by multiple goroutines without additional locking or
// coordination. It follows the interface of sync.Map with
// a number of valuable extensions like Compute or Size.
//
// A Map must not be copied after first use.
//
// Map uses a modified version of Cache-Line Hash Table (CLHT)
// data structure: https://github.com/LPD-EPFL/CLHT
//
// CLHT is built around idea to organize the hash table in
// cache-line-sized buckets, so that on all modern CPUs update
// operations complete with at most one cache-line transfer.
// Also, Get operations involve no write to memory, as well as no
// mutexes or any other sort of locks. Due to this design, in all
// considered scenarios Map outperforms sync.Map.
//
// Map also borrows ideas from Java's j.u.c.ConcurrentHashMap
// (immutable K/V pair structs instead of atomic snapshots)
// and C++'s absl::flat_hash_map (meta memory and SWAR-based
// lookups).
type Map[K comparable, V any] struct {
initOnce sync.Once
totalGrowths atomic.Int64
totalShrinks atomic.Int64
resizing atomic.Bool // resize in progress flag
resizeMu sync.Mutex // only used along with resizeCond
resizeCond sync.Cond // used to wake up resize waiters (concurrent modifications)
table atomic.Pointer[mapTable[K, V]]
minTableLen int
growOnly bool
}
type mapTable[K comparable, V any] struct {
buckets []bucketPadded[K, V]
// striped counter for number of table entries;
// used to determine if a table shrinking is needed
// occupies min(buckets_memory/1024, 64KB) of memory
size []counterStripe
seed maphash.Seed
}
type counterStripe struct {
c int64
// Padding to prevent false sharing.
_ [cacheLineSize - 8]byte
}
// bucketPadded is a CL-sized map bucket holding up to
// entriesPerMapBucket entries.
type bucketPadded[K comparable, V any] struct {
//lint:ignore U1000 ensure each bucket takes two cache lines on both 32 and 64-bit archs
pad [cacheLineSize - unsafe.Sizeof(bucket[K, V]{})]byte
bucket[K, V]
}
type bucket[K comparable, V any] struct {
meta atomic.Uint64
entries [entriesPerMapBucket]atomic.Pointer[entry[K, V]] // *entry
next atomic.Pointer[bucketPadded[K, V]] // *bucketPadded
mu sync.Mutex
}
// entry is an immutable map entry.
type entry[K comparable, V any] struct {
key K
value V
}
// MapConfig defines configurable Map options.
type MapConfig struct {
sizeHint int
growOnly bool
}
// WithPresize configures new Map instance with capacity enough
// to hold sizeHint entries. The capacity is treated as the minimal
// capacity meaning that the underlying hash table will never shrink
// to a smaller capacity. If sizeHint is zero or negative, the value
// is ignored.
func WithPresize(sizeHint int) func(*MapConfig) {
return func(c *MapConfig) {
c.sizeHint = sizeHint
}
}
// WithGrowOnly configures new Map instance to be grow-only.
// This means that the underlying hash table grows in capacity when
// new keys are added, but does not shrink when keys are deleted.
// The only exception to this rule is the Clear method which
// shrinks the hash table back to the initial capacity.
func WithGrowOnly() func(*MapConfig) {
return func(c *MapConfig) {
c.growOnly = true
}
}
// NewMap creates a new Map instance configured with the given
// options.
func NewMap[K comparable, V any](options ...func(*MapConfig)) *Map[K, V] {
c := &MapConfig{}
for _, o := range options {
o(c)
}
m := &Map[K, V]{}
if c.sizeHint > defaultMinMapTableLen*entriesPerMapBucket {
tableLen := nextPowOf2(uint32((float64(c.sizeHint) / entriesPerMapBucket) / mapLoadFactor))
m.minTableLen = int(tableLen)
}
m.growOnly = c.growOnly
return m
}
func (m *Map[K, V]) init() {
if m.minTableLen == 0 {
m.minTableLen = defaultMinMapTableLen
}
m.resizeCond = *sync.NewCond(&m.resizeMu)
table := newMapTable[K, V](m.minTableLen)
m.minTableLen = len(table.buckets)
m.table.Store(table)
}
func newMapTable[K comparable, V any](minTableLen int) *mapTable[K, V] {
buckets := make([]bucketPadded[K, V], minTableLen)
for i := range buckets {
buckets[i].meta.Store(defaultMeta)
}
counterLen := minTableLen >> 10
if counterLen < minMapCounterLen {
counterLen = minMapCounterLen
} else if counterLen > maxMapCounterLen {
counterLen = maxMapCounterLen
}
counter := make([]counterStripe, counterLen)
t := &mapTable[K, V]{
buckets: buckets,
size: counter,
seed: maphash.MakeSeed(),
}
return t
}
// ToPlainMap returns a native map with a copy of xsync Map's
// contents. The copied xsync Map should not be modified while
// this call is made. If the copied Map is modified, the copying
// behavior is the same as in the Range method.
func ToPlainMap[K comparable, V any](m *Map[K, V]) map[K]V {
pm := make(map[K]V)
if m != nil {
m.Range(func(key K, value V) bool {
pm[key] = value
return true
})
}
return pm
}
// Load returns the value stored in the map for a key, or zero value
// of type V if no value is present.
// The ok result indicates whether value was found in the map.
func (m *Map[K, V]) Load(key K) (value V, ok bool) {
m.initOnce.Do(m.init)
table := m.table.Load()
hash := maphash.Comparable(table.seed, key)
h1 := h1(hash)
h2w := broadcast(h2(hash))
bidx := uint64(len(table.buckets)-1) & h1
b := &table.buckets[bidx]
for {
metaw := b.meta.Load()
markedw := markZeroBytes(metaw^h2w) & metaMask
for markedw != 0 {
idx := firstMarkedByteIndex(markedw)
e := b.entries[idx].Load()
if e != nil {
if e.key == key {
return e.value, true
}
}
markedw &= markedw - 1
}
b = b.next.Load()
if b == nil {
return
}
}
}
// Store sets the value for a key.
func (m *Map[K, V]) Store(key K, value V) {
m.doCompute(
key,
func(V, bool) (V, ComputeOp) {
return value, UpdateOp
},
noLoadOp,
false,
)
}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
return m.doCompute(
key,
func(oldValue V, loaded bool) (V, ComputeOp) {
if loaded {
return oldValue, CancelOp
}
return value, UpdateOp
},
loadOrComputeOp,
false,
)
}
// LoadAndStore returns the existing value for the key if present,
// while setting the new value for the key.
// It stores the new value and returns the existing one, if present.
// The loaded result is true if the existing value was loaded,
// false otherwise.
func (m *Map[K, V]) LoadAndStore(key K, value V) (actual V, loaded bool) {
return m.doCompute(
key,
func(V, bool) (V, ComputeOp) {
return value, UpdateOp
},
noLoadOp,
false,
)
}
// LoadOrCompute returns the existing value for the key if
// present. Otherwise, it tries to compute the value using the
// provided function and, if successful, stores and returns
// the computed value. The loaded result is true if the value was
// loaded, or false if computed. If valueFn returns true as the
// cancel value, the computation is cancelled and the zero value
// for type V is returned.
//
// This call locks a hash table bucket while the compute function
// is executed. It means that modifications on other entries in
// the bucket will be blocked until the valueFn executes. Consider
// this when the function includes long-running operations.
func (m *Map[K, V]) LoadOrCompute(
key K,
valueFn func() (newValue V, cancel bool),
) (value V, loaded bool) {
return m.doCompute(
key,
func(oldValue V, loaded bool) (V, ComputeOp) {
if loaded {
return oldValue, CancelOp
}
newValue, c := valueFn()
if !c {
return newValue, UpdateOp
}
return oldValue, CancelOp
},
loadOrComputeOp,
false,
)
}
// Compute either sets the computed new value for the key,
// deletes the value for the key, or does nothing, based on
// the returned [ComputeOp]. When the op returned by valueFn
// is [UpdateOp], the value is updated to the new value. If
// it is [DeleteOp], the entry is removed from the map
// altogether. And finally, if the op is [CancelOp] then the
// entry is left as-is. In other words, if it did not already
// exist, it is not created, and if it did exist, it is not
// updated. This is useful to synchronously execute some
// operation on the value without incurring the cost of
// updating the map every time. The ok result indicates
// whether the entry is present in the map after the compute
// operation. The actual result contains the value of the map
// if a corresponding entry is present, or the zero value
// otherwise. See the example for a few use cases.
//
// This call locks a hash table bucket while the compute function
// is executed. It means that modifications on other entries in
// the bucket will be blocked until the valueFn executes. Consider
// this when the function includes long-running operations.
func (m *Map[K, V]) Compute(
key K,
valueFn func(oldValue V, loaded bool) (newValue V, op ComputeOp),
) (actual V, ok bool) {
return m.doCompute(key, valueFn, noLoadOp, true)
}
// LoadAndDelete deletes the value for a key, returning the previous
// value if any. The loaded result reports whether the key was
// present.
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
return m.doCompute(
key,
func(value V, loaded bool) (V, ComputeOp) {
return value, DeleteOp
},
loadAndDeleteOp,
false,
)
}
// Delete deletes the value for a key.
func (m *Map[K, V]) Delete(key K) {
m.LoadAndDelete(key)
}
func (m *Map[K, V]) doCompute(
key K,
valueFn func(oldValue V, loaded bool) (V, ComputeOp),
loadOp loadOp,
computeOnly bool,
) (V, bool) {
m.initOnce.Do(m.init)
for {
compute_attempt:
var (
emptyb *bucketPadded[K, V]
emptyidx int
)
table := m.table.Load()
tableLen := len(table.buckets)
hash := maphash.Comparable(table.seed, key)
h1 := h1(hash)
h2 := h2(hash)
h2w := broadcast(h2)
bidx := uint64(len(table.buckets)-1) & h1
rootb := &table.buckets[bidx]
if loadOp != noLoadOp {
b := rootb
load:
for {
metaw := b.meta.Load()
markedw := markZeroBytes(metaw^h2w) & metaMask
for markedw != 0 {
idx := firstMarkedByteIndex(markedw)
e := b.entries[idx].Load()
if e != nil {
if e.key == key {
if loadOp == loadOrComputeOp {
return e.value, true
}
break load
}
}
markedw &= markedw - 1
}
b = b.next.Load()
if b == nil {
if loadOp == loadAndDeleteOp {
return *new(V), false
}
break load
}
}
}
rootb.mu.Lock()
// The following two checks must go in reverse to what's
// in the resize method.
if m.resizeInProgress() {
// Resize is in progress. Wait, then go for another attempt.
rootb.mu.Unlock()
m.waitForResize()
goto compute_attempt
}
if m.newerTableExists(table) {
// Someone resized the table. Go for another attempt.
rootb.mu.Unlock()
goto compute_attempt
}
b := rootb
for {
metaw := b.meta.Load()
markedw := markZeroBytes(metaw^h2w) & metaMask
for markedw != 0 {
idx := firstMarkedByteIndex(markedw)
e := b.entries[idx].Load()
if e != nil {
if e.key == key {
// In-place update/delete.
// We get a copy of the value via an interface{} on each call,
// thus the live value pointers are unique. Otherwise atomic
// snapshot won't be correct in case of multiple Store calls
// using the same value.
oldv := e.value
newv, op := valueFn(oldv, true)
switch op {
case DeleteOp:
// Deletion.
// First we update the hash, then the entry.
newmetaw := setByte(metaw, emptyMetaSlot, idx)
b.meta.Store(newmetaw)
b.entries[idx].Store(nil)
rootb.mu.Unlock()
table.addSize(bidx, -1)
// Might need to shrink the table if we left bucket empty.
if newmetaw == defaultMeta {
m.resize(table, mapShrinkHint)
}
return oldv, !computeOnly
case UpdateOp:
newe := new(entry[K, V])
newe.key = key
newe.value = newv
b.entries[idx].Store(newe)
case CancelOp:
newv = oldv
}
rootb.mu.Unlock()
if computeOnly {
// Compute expects the new value to be returned.
return newv, true
}
// LoadAndStore expects the old value to be returned.
return oldv, true
}
}
markedw &= markedw - 1
}
if emptyb == nil {
// Search for empty entries (up to 5 per bucket).
emptyw := metaw & defaultMetaMasked
if emptyw != 0 {
idx := firstMarkedByteIndex(emptyw)
emptyb = b
emptyidx = idx
}
}
if b.next.Load() == nil {
if emptyb != nil {
// Insertion into an existing bucket.
var zeroV V
newValue, op := valueFn(zeroV, false)
switch op {
case DeleteOp, CancelOp:
rootb.mu.Unlock()
return zeroV, false
default:
newe := new(entry[K, V])
newe.key = key
newe.value = newValue
// First we update meta, then the entry.
emptyb.meta.Store(setByte(emptyb.meta.Load(), h2, emptyidx))
emptyb.entries[emptyidx].Store(newe)
rootb.mu.Unlock()
table.addSize(bidx, 1)
return newValue, computeOnly
}
}
growThreshold := float64(tableLen) * entriesPerMapBucket * mapLoadFactor
if table.sumSize() > int64(growThreshold) {
// Need to grow the table. Then go for another attempt.
rootb.mu.Unlock()
m.resize(table, mapGrowHint)
goto compute_attempt
}
// Insertion into a new bucket.
var zeroV V
newValue, op := valueFn(zeroV, false)
switch op {
case DeleteOp, CancelOp:
rootb.mu.Unlock()
return newValue, false
default:
// Create and append a bucket.
newb := new(bucketPadded[K, V])
newb.meta.Store(setByte(defaultMeta, h2, 0))
newe := new(entry[K, V])
newe.key = key
newe.value = newValue
newb.entries[0].Store(newe)
b.next.Store(newb)
rootb.mu.Unlock()
table.addSize(bidx, 1)
return newValue, computeOnly
}
}
b = b.next.Load()
}
}
}
func (m *Map[K, V]) newerTableExists(table *mapTable[K, V]) bool {
return table != m.table.Load()
}
func (m *Map[K, V]) resizeInProgress() bool {
return m.resizing.Load()
}
func (m *Map[K, V]) waitForResize() {
m.resizeMu.Lock()
for m.resizeInProgress() {
m.resizeCond.Wait()
}
m.resizeMu.Unlock()
}
func (m *Map[K, V]) resize(knownTable *mapTable[K, V], hint mapResizeHint) {
knownTableLen := len(knownTable.buckets)
// Fast path for shrink attempts.
if hint == mapShrinkHint {
if m.growOnly ||
m.minTableLen == knownTableLen ||
knownTable.sumSize() > int64((knownTableLen*entriesPerMapBucket)/mapShrinkFraction) {
return
}
}
// Slow path.
if !m.resizing.CompareAndSwap(false, true) {
// Someone else started resize. Wait for it to finish.
m.waitForResize()
return
}
var newTable *mapTable[K, V]
table := m.table.Load()
tableLen := len(table.buckets)
switch hint {
case mapGrowHint:
// Grow the table with factor of 2.
m.totalGrowths.Add(1)
newTable = newMapTable[K, V](tableLen << 1)
case mapShrinkHint:
shrinkThreshold := int64((tableLen * entriesPerMapBucket) / mapShrinkFraction)
if tableLen > m.minTableLen && table.sumSize() <= shrinkThreshold {
// Shrink the table with factor of 2.
m.totalShrinks.Add(1)
newTable = newMapTable[K, V](tableLen >> 1)
} else {
// No need to shrink. Wake up all waiters and give up.
m.resizeMu.Lock()
m.resizing.Store(false)
m.resizeCond.Broadcast()
m.resizeMu.Unlock()
return
}
case mapClearHint:
newTable = newMapTable[K, V](m.minTableLen)
default:
panic(fmt.Sprintf("unexpected resize hint: %d", hint))
}
// Copy the data only if we're not clearing the map.
if hint != mapClearHint {
for i := 0; i < tableLen; i++ {
copied := copyBucket(&table.buckets[i], newTable)
newTable.addSizePlain(uint64(i), copied)
}
}
// Publish the new table and wake up all waiters.
m.table.Store(newTable)
m.resizeMu.Lock()
m.resizing.Store(false)
m.resizeCond.Broadcast()
m.resizeMu.Unlock()
}
func copyBucket[K comparable, V any](
b *bucketPadded[K, V],
destTable *mapTable[K, V],
) (copied int) {
rootb := b
rootb.mu.Lock()
for {
for i := 0; i < entriesPerMapBucket; i++ {
if e := b.entries[i].Load(); e != nil {
hash := maphash.Comparable(destTable.seed, e.key)
bidx := uint64(len(destTable.buckets)-1) & h1(hash)
destb := &destTable.buckets[bidx]
appendToBucket(h2(hash), b.entries[i].Load(), destb)
copied++
}
}
if next := b.next.Load(); next == nil {
rootb.mu.Unlock()
return
} else {
b = next
}
}
}
// Range calls f sequentially for each key and value present in the
// map. If f returns false, range stops the iteration.
//
// Range does not necessarily correspond to any consistent snapshot
// of the Map's contents: no key will be visited more than once, but
// if the value for any key is stored or deleted concurrently, Range
// may reflect any mapping for that key from any point during the
// Range call.
//
// It is safe to modify the map while iterating it, including entry
// creation, modification and deletion. However, the concurrent
// modification rule apply, i.e. the changes may be not reflected
// in the subsequently iterated entries.
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
m.initOnce.Do(m.init)
// Pre-allocate array big enough to fit entries for most hash tables.
bentries := make([]*entry[K, V], 0, 16*entriesPerMapBucket)
table := m.table.Load()
for i := range table.buckets {
rootb := &table.buckets[i]
b := rootb
// Prevent concurrent modifications and copy all entries into
// the intermediate slice.
rootb.mu.Lock()
for {
for i := 0; i < entriesPerMapBucket; i++ {
if entry := b.entries[i].Load(); entry != nil {
bentries = append(bentries, entry)
}
}
if next := b.next.Load(); next == nil {
rootb.mu.Unlock()
break
} else {
b = next
}
}
// Call the function for all copied entries.
for j, e := range bentries {
if !f(e.key, e.value) {
return
}
// Remove the reference to avoid preventing the copied
// entries from being GCed until this method finishes.
bentries[j] = nil
}
bentries = bentries[:0]
}
}
// Clear deletes all keys and values currently stored in the map.
func (m *Map[K, V]) Clear() {
m.initOnce.Do(m.init)
m.resize(m.table.Load(), mapClearHint)
}
// Size returns current size of the map.
func (m *Map[K, V]) Size() int {
m.initOnce.Do(m.init)
return int(m.table.Load().sumSize())
}
func appendToBucket[K comparable, V any](h2 uint8, e *entry[K, V], b *bucketPadded[K, V]) {
for {
for i := 0; i < entriesPerMapBucket; i++ {
if b.entries[i].Load() == nil {
b.meta.Store(setByte(b.meta.Load(), h2, i))
b.entries[i].Store(e)
return
}
}
if next := b.next.Load(); next == nil {
newb := new(bucketPadded[K, V])
newb.meta.Store(setByte(defaultMeta, h2, 0))
newb.entries[0].Store(e)
b.next.Store(newb)
return
} else {
b = next
}
}
}
func (table *mapTable[K, V]) addSize(bucketIdx uint64, delta int) {
cidx := uint64(len(table.size)-1) & bucketIdx
atomic.AddInt64(&table.size[cidx].c, int64(delta))
}
func (table *mapTable[K, V]) addSizePlain(bucketIdx uint64, delta int) {
cidx := uint64(len(table.size)-1) & bucketIdx
table.size[cidx].c += int64(delta)
}
func (table *mapTable[K, V]) sumSize() int64 {
sum := int64(0)
for i := range table.size {
sum += atomic.LoadInt64(&table.size[i].c)
}
return sum
}
func h1(h uint64) uint64 {
return h >> 7
}
func h2(h uint64) uint8 {
return uint8(h & 0x7f)
}
// MapStats is Map statistics.
//
// Warning: map statistics are intented to be used for diagnostic
// purposes, not for production code. This means that breaking changes
// may be introduced into this struct even between minor releases.
type MapStats struct {
// RootBuckets is the number of root buckets in the hash table.
// Each bucket holds a few entries.
RootBuckets int
// TotalBuckets is the total number of buckets in the hash table,
// including root and their chained buckets. Each bucket holds
// a few entries.
TotalBuckets int
// EmptyBuckets is the number of buckets that hold no entries.
EmptyBuckets int
// Capacity is the Map capacity, i.e. the total number of
// entries that all buckets can physically hold. This number
// does not consider the load factor.
Capacity int
// Size is the exact number of entries stored in the map.
Size int
// Counter is the number of entries stored in the map according
// to the internal atomic counter. In case of concurrent map
// modifications this number may be different from Size.
Counter int
// CounterLen is the number of internal atomic counter stripes.
// This number may grow with the map capacity to improve
// multithreaded scalability.
CounterLen int
// MinEntries is the minimum number of entries per a chain of
// buckets, i.e. a root bucket and its chained buckets.
MinEntries int
// MinEntries is the maximum number of entries per a chain of
// buckets, i.e. a root bucket and its chained buckets.
MaxEntries int
// TotalGrowths is the number of times the hash table grew.
TotalGrowths int64
// TotalGrowths is the number of times the hash table shrinked.
TotalShrinks int64
}
// ToString returns string representation of map stats.
func (s *MapStats) ToString() string {
var sb strings.Builder
sb.WriteString("MapStats{\n")
sb.WriteString(fmt.Sprintf("RootBuckets: %d\n", s.RootBuckets))
sb.WriteString(fmt.Sprintf("TotalBuckets: %d\n", s.TotalBuckets))
sb.WriteString(fmt.Sprintf("EmptyBuckets: %d\n", s.EmptyBuckets))
sb.WriteString(fmt.Sprintf("Capacity: %d\n", s.Capacity))
sb.WriteString(fmt.Sprintf("Size: %d\n", s.Size))
sb.WriteString(fmt.Sprintf("Counter: %d\n", s.Counter))
sb.WriteString(fmt.Sprintf("CounterLen: %d\n", s.CounterLen))
sb.WriteString(fmt.Sprintf("MinEntries: %d\n", s.MinEntries))
sb.WriteString(fmt.Sprintf("MaxEntries: %d\n", s.MaxEntries))
sb.WriteString(fmt.Sprintf("TotalGrowths: %d\n", s.TotalGrowths))
sb.WriteString(fmt.Sprintf("TotalShrinks: %d\n", s.TotalShrinks))
sb.WriteString("}\n")
return sb.String()
}
// Stats returns statistics for the Map. Just like other map
// methods, this one is thread-safe. Yet it's an O(N) operation,
// so it should be used only for diagnostics or debugging purposes.
func (m *Map[K, V]) Stats() MapStats {
m.initOnce.Do(m.init)
stats := MapStats{
TotalGrowths: m.totalGrowths.Load(),
TotalShrinks: m.totalShrinks.Load(),
MinEntries: math.MaxInt32,
}
table := m.table.Load()
stats.RootBuckets = len(table.buckets)
stats.Counter = int(table.sumSize())
stats.CounterLen = len(table.size)
for i := range table.buckets {
nentries := 0
b := &table.buckets[i]
stats.TotalBuckets++
for {
nentriesLocal := 0
stats.Capacity += entriesPerMapBucket
for i := 0; i < entriesPerMapBucket; i++ {
if b.entries[i].Load() != nil {
stats.Size++
nentriesLocal++
}
}
nentries += nentriesLocal
if nentriesLocal == 0 {
stats.EmptyBuckets++
}
if next := b.next.Load(); next == nil {
break
} else {
b = next
}
stats.TotalBuckets++
}
if nentries < stats.MinEntries {
stats.MinEntries = nentries
}
if nentries > stats.MaxEntries {
stats.MaxEntries = nentries
}
}
return stats
}
const (
// cacheLineSize is used in paddings to prevent false sharing;
// 64B are used instead of 128B as a compromise between
// memory footprint and performance; 128B usage may give ~30%
// improvement on NUMA machines.
cacheLineSize = 64
)
// nextPowOf2 computes the next highest power of 2 of 32-bit v.
// Source: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
func nextPowOf2(v uint32) uint32 {
if v == 0 {
return 1
}
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
v++
return v
}
func broadcast(b uint8) uint64 {
return 0x101010101010101 * uint64(b)
}
func firstMarkedByteIndex(w uint64) int {
return bits.TrailingZeros64(w) >> 3
}
// SWAR byte search: may produce false positives, e.g. for 0x0100,
// so make sure to double-check bytes found by this function.
func markZeroBytes(w uint64) uint64 {
return ((w - 0x0101010101010101) & (^w) & 0x8080808080808080)
}
func setByte(w uint64, b uint8, idx int) uint64 {
shift := idx << 3
return (w &^ (0xff << shift)) | (uint64(b) << shift)
}

28
common/xsync/map_extra.go Normal file
View File

@@ -0,0 +1,28 @@
package xsync
// LoadOrStoreFn returns the existing value for the key if
// present. Otherwise, it tries to compute the value using the
// provided function and, if successful, stores and returns
// the computed value. The loaded result is true if the value was
// loaded, or false if computed.
//
// This call locks a hash table bucket while the compute function
// is executed. It means that modifications on other entries in
// the bucket will be blocked until the valueFn executes. Consider
// this when the function includes long-running operations.
//
// Recovery this API and renamed from xsync/v3's LoadOrCompute.
// We unneeded support no-op (cancel) compute operation, it will only add complexity to existing code.
func (m *Map[K, V]) LoadOrStoreFn(key K, valueFn func() V) (actual V, loaded bool) {
return m.doCompute(
key,
func(oldValue V, loaded bool) (V, ComputeOp) {
if loaded {
return oldValue, CancelOp
}
return valueFn(), UpdateOp
},
loadOrComputeOp,
false,
)
}

View File

@@ -0,0 +1,49 @@
package xsync
import (
"strconv"
"testing"
)
func TestMapOfLoadOrStoreFn(t *testing.T) {
const numEntries = 1000
m := NewMap[string, int]()
for i := 0; i < numEntries; i++ {
v, loaded := m.LoadOrStoreFn(strconv.Itoa(i), func() int {
return i
})
if loaded {
t.Fatalf("value not computed for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
for i := 0; i < numEntries; i++ {
v, loaded := m.LoadOrStoreFn(strconv.Itoa(i), func() int {
return i
})
if !loaded {
t.Fatalf("value not loaded for %d", i)
}
if v != i {
t.Fatalf("values do not match for %d: %v", i, v)
}
}
}
func TestMapOfLoadOrStoreFn_FunctionCalledOnce(t *testing.T) {
m := NewMap[int, int]()
for i := 0; i < 100; {
m.LoadOrStoreFn(i, func() (v int) {
v, i = i, i+1
return v
})
}
m.Range(func(k, v int) bool {
if k != v {
t.Fatalf("%dth key is not equal to value %d", k, v)
}
return true
})
}

1732
common/xsync/map_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -41,3 +41,11 @@ func NewAuthenticator(users []AuthUser) Authenticator {
}
return au
}
var AlwaysValid Authenticator = alwaysValid{}
type alwaysValid struct{}
func (alwaysValid) Verify(string, string) bool { return true }
func (alwaysValid) Users() []string { return nil }

View File

@@ -83,7 +83,7 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error)
if len(customCA) > 0 {
path := C.Path.Resolve(customCA)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
certificate, err = os.ReadFile(path)
if err != nil {

View File

@@ -17,6 +17,7 @@ import (
type Path interface {
Resolve(path string) string
IsSafePath(path string) bool
ErrNotSafePath(path string) error
}
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
@@ -42,10 +43,12 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate,
certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey)
var loadErr error
if path.IsSafePath(certificate) && path.IsSafePath(privateKey) {
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
if !path.IsSafePath(certificate) {
loadErr = path.ErrNotSafePath(certificate)
} else if !path.IsSafePath(privateKey) {
loadErr = path.ErrNotSafePath(privateKey)
} else {
loadErr = fmt.Errorf("path is not subpath of home directory")
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
}
if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@@ -73,7 +73,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil {
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap())
}
}
if rAddrPort.Addr().Unmap().IsLoopback() {

31
component/ech/ech.go Normal file
View File

@@ -0,0 +1,31 @@
package ech
import (
"context"
"fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
)
type Config struct {
GetEncryptedClientHelloConfigList func(ctx context.Context, serverName string) ([]byte, error)
}
func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) {
if cfg == nil {
return nil
}
echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName)
if err != nil {
return fmt.Errorf("resolve ECH config error: %w", err)
}
tlsConfig.EncryptedClientHelloConfigList = echConfigList
if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 {
tlsConfig.MinVersion = tlsC.VersionTLS13
}
if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 {
tlsConfig.MaxVersion = tlsC.VersionTLS13
}
return nil
}

143
component/ech/key.go Normal file
View File

@@ -0,0 +1,143 @@
package ech
import (
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/metacubex/mihomo/component/ca"
tlsC "github.com/metacubex/mihomo/component/tls"
"golang.org/x/crypto/cryptobyte"
)
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS.
// We need this so that when we insert them into ECHConfigs the ordering
// is stable.
var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
for _, aeadID := range sortedSupportedAEADs {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.BytesOrPanic()
}
func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigList := builder.BytesOrPanic()
builder2 := cryptobyte.NewBuilder(nil)
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigKeys := builder2.BytesOrPanic()
configBase64 = base64.StdEncoding.EncodeToString(echConfigList)
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys}))
return
}
func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) {
var keys []tlsC.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tlsC.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, errors.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, errors.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, errors.New("empty ECH keys")
}
return keys, nil
}
func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error {
if key == "" {
return nil
}
painTextErr := loadECHKey([]byte(key), tlsConfig)
if painTextErr == nil {
return nil
}
key = path.Resolve(key)
var loadErr error
if !path.IsSafePath(key) {
loadErr = path.ErrNotSafePath(key)
} else {
var echKey []byte
echKey, loadErr = os.ReadFile(key)
if loadErr == nil {
loadErr = loadECHKey(echKey, tlsConfig)
}
}
if loadErr != nil {
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
return nil
}
func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error {
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return errors.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return fmt.Errorf("parse ECH keys: %w", err)
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
}

View File

@@ -1,37 +0,0 @@
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

@@ -1,97 +0,0 @@
// 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

@@ -0,0 +1,73 @@
package generator
import (
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/transport/vless/encryption"
"github.com/gofrs/uuid/v5"
)
func Main(args []string) {
if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair/vless-mlkem768/vless-x25519")
}
switch args[0] {
case "uuid":
newUUID, err := uuid.NewV4()
if err != nil {
panic(err)
}
fmt.Println(newUUID.String())
case "reality-keypair":
privateKey, err := GenX25519PrivateKey()
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey.Bytes()))
fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(privateKey.PublicKey().Bytes()))
case "wg-keypair":
privateKey, err := GenX25519PrivateKey()
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + base64.StdEncoding.EncodeToString(privateKey.Bytes()))
fmt.Println("PublicKey: " + base64.StdEncoding.EncodeToString(privateKey.PublicKey().Bytes()))
case "ech-keypair":
if len(args) < 2 {
panic("Using: generate ech-keypair <plain_server_name>")
}
configBase64, keyPem, err := ech.GenECHConfig(args[1])
if err != nil {
panic(err)
}
fmt.Println("Config:", configBase64)
fmt.Println("Key:", keyPem)
case "vless-mlkem768":
var seed string
if len(args) > 1 {
seed = args[1]
}
seedBase64, clientBase64, hash32Base64, err := encryption.GenMLKEM768(seed)
if err != nil {
panic(err)
}
fmt.Println("Seed: " + seedBase64)
fmt.Println("Client: " + clientBase64)
fmt.Println("Hash32: " + hash32Base64)
case "vless-x25519":
var privateKey string
if len(args) > 1 {
privateKey = args[1]
}
privateKeyBase64, passwordBase64, hash32Base64, err := encryption.GenX25519(privateKey)
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + privateKeyBase64)
fmt.Println("Password: " + passwordBase64)
fmt.Println("Hash32: " + hash32Base64)
}
}

View File

@@ -0,0 +1,27 @@
package generator
import (
"crypto/ecdh"
"crypto/rand"
)
const X25519KeySize = 32
func GenX25519PrivateKey() (*ecdh.PrivateKey, error) {
var privateKey [X25519KeySize]byte
_, err := rand.Read(privateKey[:])
if err != nil {
return nil, err
}
// Avoid generating equivalent X25519 private keys
// https://github.com/XTLS/Xray-core/pull/1747
//
// Modify random bytes using algorithm described at:
// https://cr.yp.to/ecdh.html.
privateKey[0] &= 248
privateKey[31] &= 127
privateKey[31] |= 64
return ecdh.X25519().NewPrivateKey(privateKey[:])
}

View File

@@ -1,7 +1,6 @@
package geodata
import (
"errors"
"fmt"
"strings"
@@ -76,13 +75,13 @@ func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) {
if countryCode[0] == '!' {
not = true
countryCode = countryCode[1:]
if countryCode == "" {
return nil, fmt.Errorf("country code could not be empty")
}
}
countryCode = strings.ToLower(countryCode)
parts := strings.Split(countryCode, "@")
if len(parts) == 0 {
return nil, errors.New("empty rule")
}
listName := strings.TrimSpace(parts[0])
attrVal := parts[1:]
attrs := parseAttrs(attrVal)

View File

@@ -26,8 +26,9 @@ var (
)
type ifaceCache struct {
ifMap map[string]*Interface
ifTable bart.Table[*Interface]
ifMapByName map[string]*Interface
ifMapByAddr map[netip.Addr]*Interface
ifTable bart.Table[*Interface]
}
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
@@ -40,7 +41,8 @@ func getCache() (*ifaceCache, error) {
}
cache := &ifaceCache{
ifMap: make(map[string]*Interface),
ifMapByName: make(map[string]*Interface),
ifMapByAddr: make(map[netip.Addr]*Interface),
}
for _, iface := range ifaces {
@@ -78,12 +80,13 @@ func getCache() (*ifaceCache, error) {
Flags: iface.Flags,
Addresses: ipNets,
}
cache.ifMap[iface.Name] = ifaceObj
cache.ifMapByName[iface.Name] = ifaceObj
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
for _, prefix := range ipNets {
cache.ifMapByAddr[prefix.Addr()] = ifaceObj
cache.ifTable.Insert(prefix, ifaceObj)
}
}
@@ -98,7 +101,7 @@ func Interfaces() (map[string]*Interface, error) {
if err != nil {
return nil, err
}
return cache.ifMap, nil
return cache.ifMapByName, nil
}
func ResolveInterface(name string) (*Interface, error) {
@@ -120,6 +123,11 @@ func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
if err != nil {
return nil, err
}
// maybe two interfaces have the same prefix but different address
// so direct check address equal before do a route lookup (longest prefix match)
if iface, ok := cache.ifMapByAddr[addr]; ok {
return iface, nil
}
iface, ok := cache.ifTable.Lookup(addr)
if !ok {
return nil, ErrIfaceNotFound
@@ -133,7 +141,8 @@ func IsLocalIp(addr netip.Addr) (bool, error) {
if err != nil {
return false, err
}
return cache.ifTable.Contains(addr), nil
_, ok := cache.ifMapByAddr[addr]
return ok, nil
}
func FlushCache() {

View File

@@ -10,27 +10,27 @@ import (
)
var (
keepAliveIdle = atomic.NewTypedValue[time.Duration](0 * time.Second)
keepAliveInterval = atomic.NewTypedValue[time.Duration](0 * time.Second)
keepAliveIdle = atomic.NewInt64(0)
keepAliveInterval = atomic.NewInt64(0)
disableKeepAlive = atomic.NewBool(false)
SetDisableKeepAliveCallback = utils.NewCallback[bool]()
)
func SetKeepAliveIdle(t time.Duration) {
keepAliveIdle.Store(t)
keepAliveIdle.Store(int64(t))
}
func SetKeepAliveInterval(t time.Duration) {
keepAliveInterval.Store(t)
keepAliveInterval.Store(int64(t))
}
func KeepAliveIdle() time.Duration {
return keepAliveIdle.Load()
return time.Duration(keepAliveIdle.Load())
}
func KeepAliveInterval() time.Duration {
return keepAliveInterval.Load()
return time.Duration(keepAliveInterval.Load())
}
func SetDisableKeepAlive(disable bool) {

View File

@@ -8,11 +8,10 @@ import (
"strconv"
"github.com/metacubex/mihomo/common/callback"
"github.com/metacubex/mihomo/common/xsync"
"github.com/metacubex/mihomo/component/iface"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
"github.com/puzpuzpuz/xsync/v3"
)
var disableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR"))
@@ -26,22 +25,19 @@ func init() {
var ErrReject = errors.New("reject loopback connection")
type Detector struct {
connMap *xsync.MapOf[netip.AddrPort, struct{}]
packetConnMap *xsync.MapOf[uint16, struct{}]
connMap xsync.Map[netip.AddrPort, struct{}]
packetConnMap xsync.Map[uint16, struct{}]
}
func NewDetector() *Detector {
if disableLoopBackDetector {
return nil
}
return &Detector{
connMap: xsync.NewMapOf[netip.AddrPort, struct{}](),
packetConnMap: xsync.NewMapOf[uint16, struct{}](),
}
return &Detector{}
}
func (l *Detector) NewConn(conn C.Conn) C.Conn {
if l == nil || l.connMap == nil {
if l == nil {
return conn
}
metadata := C.Metadata{}
@@ -59,7 +55,7 @@ func (l *Detector) NewConn(conn C.Conn) C.Conn {
}
func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn {
if l == nil || l.packetConnMap == nil {
if l == nil {
return conn
}
metadata := C.Metadata{}
@@ -78,7 +74,7 @@ func (l *Detector) NewPacketConn(conn C.PacketConn) C.PacketConn {
}
func (l *Detector) CheckConn(metadata *C.Metadata) error {
if l == nil || l.connMap == nil {
if l == nil {
return nil
}
connAddr := metadata.SourceAddrPort()
@@ -92,7 +88,7 @@ func (l *Detector) CheckConn(metadata *C.Metadata) error {
}
func (l *Detector) CheckPacketConn(metadata *C.Metadata) error {
if l == nil || l.packetConnMap == nil {
if l == nil {
return nil
}
connAddr := metadata.SourceAddrPort()

View File

@@ -4,27 +4,24 @@ import (
"net"
"sync"
"github.com/metacubex/mihomo/common/xsync"
C "github.com/metacubex/mihomo/constant"
"github.com/puzpuzpuz/xsync/v3"
)
type Table struct {
mapping *xsync.MapOf[string, *entry]
mapping xsync.Map[string, *entry]
}
type entry struct {
PacketSender C.PacketSender
LocalUDPConnMap *xsync.MapOf[string, *net.UDPConn]
LocalLockMap *xsync.MapOf[string, *sync.Cond]
LocalUDPConnMap xsync.Map[string, *net.UDPConn]
LocalLockMap xsync.Map[string, *sync.Cond]
}
func (t *Table) GetOrCreate(key string, maker func() C.PacketSender) (C.PacketSender, bool) {
item, loaded := t.mapping.LoadOrCompute(key, func() *entry {
item, loaded := t.mapping.LoadOrStoreFn(key, func() *entry {
return &entry{
PacketSender: maker(),
LocalUDPConnMap: xsync.NewMapOf[string, *net.UDPConn](),
LocalLockMap: xsync.NewMapOf[string, *sync.Cond](),
PacketSender: maker(),
}
})
return item.PacketSender, loaded
@@ -68,7 +65,7 @@ func (t *Table) GetOrCreateLockForLocalConn(lAddr, key string) (*sync.Cond, bool
if !loaded {
return nil, false
}
item, loaded := entry.LocalLockMap.LoadOrCompute(key, makeLock)
item, loaded := entry.LocalLockMap.LoadOrStoreFn(key, makeLock)
return item, loaded
}
@@ -98,7 +95,5 @@ func makeLock() *sync.Cond {
// New return *Cache
func New() *Table {
return &Table{
mapping: xsync.NewMapOf[string, *entry](),
}
return &Table{}
}

View File

@@ -1,57 +1,52 @@
package process
import (
"encoding/json"
"errors"
"strings"
)
const (
FindProcessAlways = "always"
FindProcessStrict = "strict"
FindProcessOff = "off"
FindProcessStrict FindProcessMode = iota
FindProcessAlways
FindProcessOff
)
var (
validModes = map[string]struct{}{
FindProcessAlways: {},
FindProcessOff: {},
FindProcessStrict: {},
validModes = map[string]FindProcessMode{
FindProcessStrict.String(): FindProcessStrict,
FindProcessAlways.String(): FindProcessAlways,
FindProcessOff.String(): FindProcessOff,
}
)
type FindProcessMode string
type FindProcessMode int32
func (m FindProcessMode) Always() bool {
return m == FindProcessAlways
}
func (m FindProcessMode) Off() bool {
return m == FindProcessOff
}
func (m *FindProcessMode) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
return m.Set(tp)
}
func (m *FindProcessMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
return m.Set(tp)
// UnmarshalText unserialize FindProcessMode
func (m *FindProcessMode) UnmarshalText(data []byte) error {
return m.Set(string(data))
}
func (m *FindProcessMode) Set(value string) error {
mode := strings.ToLower(value)
_, exist := validModes[mode]
mode, exist := validModes[strings.ToLower(value)]
if !exist {
return errors.New("invalid find process mode")
}
*m = FindProcessMode(mode)
*m = mode
return nil
}
// MarshalText serialize FindProcessMode
func (m FindProcessMode) MarshalText() ([]byte, error) {
return []byte(m.String()), nil
}
func (m FindProcessMode) String() string {
switch m {
case FindProcessAlways:
return "always"
case FindProcessOff:
return "off"
default:
return "strict"
}
}

View File

@@ -2,7 +2,6 @@ package proxydialer
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
@@ -10,7 +9,6 @@ import (
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
@@ -40,17 +38,16 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
return nil, err
}
if strings.Contains(network, "udp") { // using in wireguard outbound
if !currentMeta.Resolved() {
ip, err := resolver.ResolveIP(ctx, currentMeta.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
currentMeta.DstIP = ip
}
pc, err := p.listenPacket(ctx, currentMeta)
if err != nil {
return nil, err
}
if !currentMeta.Resolved() { // should not happen, maybe by a wrongly implemented proxy, but we can handle this (:
err = pc.ResolveUDP(ctx, currentMeta)
if err != nil {
return nil, err
}
}
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
}
var conn C.Conn

View File

@@ -77,7 +77,7 @@ func NewHostValue(value any) (HostValue, error) {
isDomain = false
for _, str := range valueArr {
if ip, err := netip.ParseAddr(str); err == nil {
ips = append(ips, ip)
ips = append(ips, ip.Unmap())
} else {
return HostValue{}, err
}
@@ -85,7 +85,7 @@ func NewHostValue(value any) (HostValue, error) {
} else if len(valueArr) == 1 {
host := valueArr[0]
if ip, err := netip.ParseAddr(host); err == nil {
ips = append(ips, ip)
ips = append(ips, ip.Unmap())
isDomain = false
} else {
domain = host
@@ -113,7 +113,7 @@ func NewHostValueByDomain(domain string) (HostValue, error) {
domain = strings.Trim(domain, ".")
item := strings.Split(domain, ".")
if len(item) < 2 {
return HostValue{}, errors.New("invaild domain")
return HostValue{}, errors.New("invalid domain")
}
return HostValue{
IsDomain: true,

View File

@@ -46,17 +46,24 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration)
ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout)
defer cancel()
inData := buff[:n]
msg, err := relayDnsPacket(ctx, inData, buff, 0)
outBuff := buff[2:]
msg, err := relayDnsPacket(ctx, inData, outBuff, 0)
if err != nil {
return err
}
err = binary.Write(conn, binary.BigEndian, uint16(len(msg)))
if err != nil {
return err
if &msg[0] == &outBuff[0] { // msg is still in the buff
binary.BigEndian.PutUint16(buff[:2], uint16(len(msg)))
outBuff = buff[:2+len(msg)]
} else { // buff not big enough (WTF???)
newBuff := pool.Get(len(msg) + 2)
defer pool.Put(newBuff)
binary.BigEndian.PutUint16(newBuff[:2], uint16(len(msg)))
copy(newBuff[2:], msg)
outBuff = newBuff
}
_, err = conn.Write(msg)
_, err = conn.Write(outBuff)
if err != nil {
return err
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"net/netip"
"strings"
"time"
"github.com/metacubex/mihomo/common/utils"
@@ -49,6 +48,7 @@ type Resolver interface {
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error)
ResolveECH(ctx context.Context, host string) ([]byte, error)
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
Invalid() bool
ClearCache()
@@ -67,7 +67,8 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net
ip, err := netip.ParseAddr(host)
if err == nil {
if ip.Is4() || ip.Is4In6() {
ip = ip.Unmap()
if ip.Is4() {
return []netip.Addr{ip}, nil
}
return []netip.Addr{}, ErrIPVersion
@@ -116,7 +117,8 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net
}
if ip, err := netip.ParseAddr(host); err == nil {
if strings.Contains(host, ":") {
ip = ip.Unmap()
if ip.Is6() {
return []netip.Addr{ip}, nil
}
return nil, ErrIPVersion
@@ -165,6 +167,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
return []netip.Addr{ip}, nil
}
@@ -216,10 +219,29 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
}
func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) {
if r != nil && r.Invalid() {
return r.ResolveECH(ctx, host)
}
return SystemResolver.ResolveECH(ctx, host)
}
func ResolveECH(ctx context.Context, host string) ([]byte, error) {
return ResolveECHWithResolver(ctx, host, DefaultResolver)
}
func ClearCache() {
if DefaultResolver != nil {
go DefaultResolver.ClearCache()
}
go SystemResolver.ClearCache() // SystemResolver unneeded check nil
}
func ResetConnection() {
if DefaultResolver != nil {
go DefaultResolver.ResetConnection()
}
go SystemResolver.ResetConnection() // SystemResolver unneeded check nil
}
func SortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) {

View File

@@ -105,6 +105,7 @@ func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (
_ = os.Chtimes(f.vehicle.Path(), now, now)
}
f.updatedAt = now
f.backoff.Reset() // no error, reset backoff
return lo.Empty[V](), true, nil
}
@@ -220,6 +221,10 @@ func (f *Fetcher[V]) updateWithLog() {
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
ctx, cancel := context.WithCancel(context.Background())
minBackoff := 10 * time.Second
if interval < minBackoff {
minBackoff = interval
}
return &Fetcher[V]{
ctx: ctx,
ctxCancel: cancel,
@@ -231,7 +236,7 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
backoff: slowdown.Backoff{
Factor: 2,
Jitter: false,
Min: time.Second,
Min: minBackoff,
Max: interval,
},
}

View File

@@ -6,6 +6,8 @@ import (
"net/netip"
"time"
"github.com/metacubex/sing/common/metadata"
"github.com/metacubex/mihomo/common/lru"
N "github.com/metacubex/mihomo/common/net"
C "github.com/metacubex/mihomo/constant"
@@ -72,8 +74,16 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
overrideDest := config.OverrideDest
if inWhitelist {
replaceDomain := func(metadata *C.Metadata, host string) {
if sd.domainCanReplace(host) {
replaceDomain(metadata, host, overrideDest)
} else {
log.Debugln("[Sniffer] Skip sni[%s]", host)
}
}
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
return wrapable.WrapperSender(packetSender, overrideDest)
return wrapable.WrapperSender(packetSender, replaceDomain)
}
host, err := current.SniffData(packet.Data())
@@ -81,7 +91,7 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
continue
}
replaceDomain(metadata, host, overrideDest)
replaceDomain(metadata, host)
return packetSender
}
}
@@ -128,11 +138,9 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
return false
}
for _, matcher := range sd.skipDomain {
if matcher.MatchDomain(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return false
}
if !sd.domainCanReplace(host) {
log.Debugln("[Sniffer] Skip sni[%s]", host)
return false
}
sd.skipList.Delete(dst)
@@ -152,10 +160,23 @@ func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
metadata.RemoteAddress(),
metadata.Host, host)
metadata.Host = host
metadata.DstIP = netip.Addr{}
}
metadata.DNSMode = C.DNSNormal
}
func (sd *Dispatcher) domainCanReplace(host string) bool {
if host == "." || !metadata.IsDomainName(host) {
return false
}
for _, matcher := range sd.skipDomain {
if matcher.MatchDomain(host) {
return false
}
}
return true
}
func (sd *Dispatcher) Enable() bool {
return sd != nil && sd.enable
}

View File

@@ -74,24 +74,25 @@ func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
return "", ErrorUnsupportedSniffer
}
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender {
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, replaceDomain sniffer.ReplaceDomain) constant.PacketSender {
return &quicPacketSender{
sender: packetSender,
chClose: make(chan struct{}),
override: override,
PacketSender: packetSender,
replaceDomain: replaceDomain,
chClose: make(chan struct{}),
}
}
var _ constant.PacketSender = (*quicPacketSender)(nil)
type quicPacketSender struct {
lock sync.RWMutex
ranges utils.IntRanges[uint64]
buffer []byte
result string
override bool
lock sync.RWMutex
ranges utils.IntRanges[uint64]
buffer []byte
result *string
sender constant.PacketSender
replaceDomain sniffer.ReplaceDomain
constant.PacketSender
chClose chan struct{}
closed bool
@@ -100,7 +101,7 @@ type quicPacketSender struct {
// 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)
defer q.PacketSender.Send(current)
q.lock.RLock()
if q.closed {
@@ -116,29 +117,27 @@ func (q *quicPacketSender) Send(current constant.PacketAdapter) {
}
}
// 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 {
// DoSniff wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) DoSniff(metadata *constant.Metadata) error {
select {
case <-q.chClose:
q.lock.RLock()
replaceDomain(data, q.result, q.override)
if q.result != nil {
host := *q.result
q.replaceDomain(metadata, host)
}
q.lock.RUnlock()
break
case <-time.After(quicWaitConn):
q.close()
}
return q.sender.ResolveUDP(data)
return q.PacketSender.DoSniff(metadata)
}
// Close stop the Process loop
func (q *quicPacketSender) Close() {
q.sender.Close()
q.PacketSender.Close()
q.close()
}
@@ -433,7 +432,7 @@ func (q *quicPacketSender) tryAssemble() error {
}
q.lock.Lock()
q.result = *domain
q.result = domain
q.closeLocked()
q.lock.Unlock()

View File

@@ -12,7 +12,7 @@ import (
)
type fakeSender struct {
resultCh chan *constant.Metadata
constant.PacketSender
}
var _ constant.PacketSender = (*fakeSender)(nil)
@@ -22,18 +22,7 @@ func (e *fakeSender) Send(packet constant.PacketAdapter) {
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")
}
func (e *fakeSender) DoSniff(metadata *constant.Metadata) error { return nil }
type fakeUDPPacket struct {
data []byte
@@ -78,23 +67,28 @@ func asPacket(data string) constant.PacketAdapter {
return pktAdp
}
func testQuicSniffer(data []string, async bool) (string, error) {
const fakeHost = "fake.host.com"
func testQuicSniffer(data []string, async bool) (string, string, error) {
q, err := NewQuicSniffer(SnifferConfig{})
if err != nil {
return "", err
return "", "", err
}
resultCh := make(chan *constant.Metadata, 1)
emptySender := &fakeSender{resultCh: resultCh}
emptySender := &fakeSender{}
sender := q.WrapperSender(emptySender, true)
sender := q.WrapperSender(emptySender, func(metadata *constant.Metadata, host string) {
replaceDomain(metadata, host, true)
})
go func() {
meta := constant.Metadata{}
err = sender.ResolveUDP(&meta)
meta := constant.Metadata{Host: fakeHost}
err := sender.DoSniff(&meta)
if err != nil {
panic(err)
}
resultCh <- &meta
}()
for _, d := range data {
@@ -106,14 +100,15 @@ func testQuicSniffer(data []string, async bool) (string, error) {
}
meta := <-resultCh
return meta.SniffHost, nil
return meta.SniffHost, meta.Host, nil
}
func TestQuicHeaders(t *testing.T) {
cases := []struct {
input []string
domain string
input []string
domain string
invalid bool
}{
//Normal domain quic sniff
{
@@ -171,16 +166,31 @@ func TestQuicHeaders(t *testing.T) {
},
domain: "www.google.com",
},
// invalid packet
{
input: []string{"00000000000000000000"},
invalid: true,
},
}
for _, test := range cases {
data, err := testQuicSniffer(test.input, true)
data, host, err := testQuicSniffer(test.input, true)
assert.NoError(t, err)
assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
data, err = testQuicSniffer(test.input, false)
data, host, err = testQuicSniffer(test.input, false)
assert.NoError(t, err)
assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
}
}

View File

@@ -0,0 +1,70 @@
package tls
import (
"context"
"net"
"net/http"
"runtime/debug"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/log"
"golang.org/x/net/http2"
)
func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration {
var ret time.Duration
for _, v := range [...]time.Duration{
s.ReadHeaderTimeout,
s.ReadTimeout,
s.WriteTimeout,
} {
if v <= 0 {
continue
}
if ret == 0 || v < ret {
ret = v
}
}
return ret
}
// NewListenerForHttps returns a net.Listener for (*http.Server).Serve()
// the "func (c *conn) serve(ctx context.Context)" in http\server.go
// only do tls handshake and check NegotiatedProtocol with std's *tls.Conn
// so we do the same logic to let http2 (not h2c) work fine
func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener {
http2Server := &http2.Server{}
_ = http2.ConfigureServer(httpServer, http2Server)
return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) {
c := Server(conn, tlsConfig)
tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer)
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
_ = conn.SetReadDeadline(dl)
_ = conn.SetWriteDeadline(dl)
}
err := c.HandshakeContext(ctx)
if err != nil {
return nil, err
}
// Restore Conn-level deadlines.
if tlsTO > 0 {
_ = conn.SetReadDeadline(time.Time{})
_ = conn.SetWriteDeadline(time.Time{})
}
if c.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
http2Server.ServeConn(c, &http2.ServeConnOpts{BaseConfig: httpServer})
return nil, net.ErrClosed
}
return c, nil
}, func(a any) {
stack := debug.Stack()
log.Errorln("https server panic: %s\n%s", a, stack)
})
}

View File

@@ -26,7 +26,6 @@ import (
utls "github.com/metacubex/utls"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
"golang.org/x/exp/slices"
"golang.org/x/net/http2"
)
@@ -35,9 +34,11 @@ const RealityMaxShortIDLen = 8
type RealityConfig struct {
PublicKey *ecdh.PublicKey
ShortID [RealityMaxShortIDLen]byte
SupportX25519MLKEM768 bool
}
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *Config, realityConfig *RealityConfig) (net.Conn, error) {
for retry := 0; ; retry++ {
verifier := &realityVerifier{
serverName: tlsConfig.ServerName,
@@ -48,39 +49,18 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
SessionTicketsDisabled: true,
VerifyPeerCertificate: verifier.VerifyPeerCertificate,
}
clientID := utls.ClientHelloID{
Client: fingerprint.Client,
Version: fingerprint.Version,
Seed: fingerprint.Seed,
if !realityConfig.SupportX25519MLKEM768 && fingerprint == HelloChrome_Auto {
fingerprint = HelloChrome_120 // old reality server doesn't work with X25519MLKEM768
}
uConn := utls.UClient(conn, uConfig, clientID)
uConn := utls.UClient(conn, uConfig, fingerprint)
verifier.UConn = uConn
err := uConn.BuildHandshakeState()
if err != nil {
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
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
for i := range rawSessionID { // https://github.com/golang/go/issues/5373
@@ -105,6 +85,9 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
continue // retry
}
ecdheKey := keyShareKeys.Ecdhe
if ecdheKey == nil {
ecdheKey = keyShareKeys.MlkemEcdhe
}
if ecdheKey == nil {
// WTF???
if retry > 2 {
@@ -144,7 +127,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
if !verifier.verified {
go realityClientFallback(uConn, uConfig.ServerName, clientID)
go realityClientFallback(uConn, uConfig.ServerName, fingerprint)
return nil, errors.New("REALITY authentication failed")
}
@@ -187,6 +170,7 @@ type realityVerifier struct {
//var pOffset = utils.MustOK(reflect.TypeOf((*utls.Conn)(nil)).Elem().FieldByName("peerCertificates")).Offset
func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
log.Debugln("REALITY localAddr: %v is using X25519MLKEM768 for TLS' communication: %v", c.RemoteAddr(), c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768)
//p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates")
//certs := *(*[]*x509.Certificate)(unsafe.Add(unsafe.Pointer(c.Conn), pOffset))
certs := c.Conn.PeerCertificates()

View File

@@ -16,6 +16,7 @@ type Conn = utls.Conn
type UConn = utls.UConn
type UClientHelloID = utls.ClientHelloID
const VersionTLS12 = utls.VersionTLS12
const VersionTLS13 = utls.VersionTLS13
func Client(c net.Conn, config *utls.Config) *Conn {
@@ -26,6 +27,14 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn
return utls.UClient(c, config, fingerprint)
}
func Server(c net.Conn, config *utls.Config) *Conn {
return utls.Server(c, config)
}
func NewListener(inner net.Listener, config *Config) net.Listener {
return utls.NewListener(inner, config)
}
func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) {
if len(clientFingerprint) == 0 {
clientFingerprint = globalFingerprint
@@ -65,21 +74,26 @@ var randomFingerprint = once.OnceValue(func() UClientHelloID {
return fingerprint
})
var HelloChrome_Auto = utls.HelloChrome_Auto
var HelloChrome_120 = utls.HelloChrome_120 // special fingerprint for some old protocols doesn't work with HelloChrome_Auto
var fingerprints = map[string]UClientHelloID{
"chrome": utls.HelloChrome_Auto,
"chrome": utls.HelloChrome_Auto,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
// deprecated fingerprints should not be used
"chrome_psk": utls.HelloChrome_100_PSK,
"chrome_psk_shuffle": utls.HelloChrome_106_Shuffle,
"chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf,
"chrome_pq": utls.HelloChrome_115_PQ,
"chrome_pq_psk": utls.HelloChrome_115_PQ_PSK,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
"randomized": utls.HelloRandomized,
}
@@ -93,7 +107,9 @@ func init() {
fingerprints["randomized"] = randomized
}
func UCertificates(it tls.Certificate) utls.Certificate {
type Certificate = utls.Certificate
func UCertificate(it tls.Certificate) utls.Certificate {
return utls.Certificate{
Certificate: it.Certificate,
PrivateKey: it.PrivateKey,
@@ -106,13 +122,15 @@ func UCertificates(it tls.Certificate) utls.Certificate {
}
}
type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
type Config = utls.Config
func UConfig(config *tls.Config) *utls.Config {
return &utls.Config{
Rand: config.Rand,
Time: config.Time,
Certificates: utils.Map(config.Certificates, UCertificates),
Certificates: utils.Map(config.Certificates, UCertificate),
VerifyPeerCertificate: config.VerifyPeerCertificate,
RootCAs: config.RootCAs,
NextProtos: config.NextProtos,

View File

@@ -17,74 +17,99 @@ import (
mihomoHttp "github.com/metacubex/mihomo/component/http"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
"github.com/metacubex/mihomo/log"
"github.com/klauspost/cpuid/v2"
)
const (
baseReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/"
versionReleaseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"
baseAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/"
versionAlphaURL = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt"
// MaxPackageFileSize is a maximum package file length in bytes. The largest
// package whose size is limited by this constant currently has the size of
// approximately 32 MiB.
MaxPackageFileSize = 32 * 1024 * 1024
)
const (
ReleaseChannel = "release"
AlphaChannel = "alpha"
)
// CoreUpdater is the mihomo updater.
// modify from https://github.com/AdguardTeam/AdGuardHome/blob/595484e0b3fb4c457f9bb727a6b94faa78a66c5f/internal/updater/updater.go
// Updater is the mihomo updater.
var (
goarm string
gomips string
amd64Compatible string
workDir string
// mu protects all fields below.
type CoreUpdater struct {
mu sync.Mutex
}
currentExeName string // 当前可执行文件
updateDir string // 更新目录
packageName string // 更新压缩文件
backupDir string // 备份目录
backupExeName string // 备份文件名
updateExeName string // 更新后的可执行文件
var DefaultCoreUpdater = CoreUpdater{}
baseURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/mihomo"
versionURL string = "https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt"
packageURL string
latestVersion string
)
func init() {
if runtime.GOARCH == "amd64" && cpuid.CPU.X64Level() < 3 {
amd64Compatible = "-compatible"
}
if !strings.HasPrefix(C.Version, "alpha") {
baseURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/mihomo"
versionURL = "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt"
func (u *CoreUpdater) CoreBaseName() string {
switch runtime.GOARCH {
case "arm":
// mihomo-linux-armv5
return fmt.Sprintf("mihomo-%s-%sv%s", runtime.GOOS, runtime.GOARCH, features.GOARM)
case "arm64":
if runtime.GOOS == "android" {
// mihomo-android-arm64-v8
return fmt.Sprintf("mihomo-%s-%s-v8", runtime.GOOS, runtime.GOARCH)
} else {
// mihomo-linux-arm64
return fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH)
}
case "mips", "mipsle":
// mihomo-linux-mips-hardfloat
return fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOMIPS)
case "amd64":
// mihomo-linux-amd64-v1
return fmt.Sprintf("mihomo-%s-%s-%s", runtime.GOOS, runtime.GOARCH, features.GOAMD64)
default:
// mihomo-linux-386
// mihomo-linux-mips64
// mihomo-linux-riscv64
// mihomo-linux-s390x
return fmt.Sprintf("mihomo-%s-%s", runtime.GOOS, runtime.GOARCH)
}
}
type updateError struct {
Message string
}
func (u *CoreUpdater) Update(currentExePath string, channel string, force bool) (err error) {
u.mu.Lock()
defer u.mu.Unlock()
func (e *updateError) Error() string {
return fmt.Sprintf("update error: %s", e.Message)
}
// Update performs the auto-updater. It returns an error if the updater failed.
// If firstRun is true, it assumes the configuration file doesn't exist.
func UpdateCore(execPath string) (err error) {
mu.Lock()
defer mu.Unlock()
latestVersion, err = getLatestVersion()
info, err := os.Stat(currentExePath)
if err != nil {
return err
return fmt.Errorf("check currentExePath %q: %w", currentExePath, err)
}
baseURL := baseAlphaURL
versionURL := versionAlphaURL
switch strings.ToLower(channel) {
case ReleaseChannel:
baseURL = baseReleaseURL
versionURL = versionReleaseURL
case AlphaChannel:
break
default: // auto
if !strings.HasPrefix(C.Version, "alpha") {
baseURL = baseReleaseURL
versionURL = versionReleaseURL
}
}
latestVersion, err := u.getLatestVersion(versionURL)
if err != nil {
return fmt.Errorf("get latest version: %w", err)
}
log.Infoln("current version %s, latest version %s", C.Version, latestVersion)
if latestVersion == C.Version {
err := &updateError{Message: "already using latest version"}
return err
if latestVersion == C.Version && !force {
// don't change this output, some downstream dependencies on the upgrader's output fields
return fmt.Errorf("update error: already using latest version %s", C.Version)
}
updateDownloadURL()
defer func() {
if err != nil {
log.Errorln("updater: failed: %v", err)
@@ -93,31 +118,49 @@ func UpdateCore(execPath string) (err error) {
}
}()
workDir = filepath.Dir(execPath)
// ---- prepare ----
mihomoBaseName := u.CoreBaseName()
packageName := mihomoBaseName + "-" + latestVersion
if runtime.GOOS == "windows" {
packageName = packageName + ".zip"
} else {
packageName = packageName + ".gz"
}
packageURL := baseURL + packageName
log.Infoln("updater: updating using url: %s", packageURL)
err = prepare(execPath)
workDir := filepath.Dir(currentExePath)
backupDir := filepath.Join(workDir, "meta-backup")
updateDir := filepath.Join(workDir, "meta-update")
packagePath := filepath.Join(updateDir, packageName)
//log.Infoln(packagePath)
updateExeName := mihomoBaseName
if runtime.GOOS == "windows" {
updateExeName = updateExeName + ".exe"
}
log.Infoln("updateExeName: %s", updateExeName)
updateExePath := filepath.Join(updateDir, updateExeName)
backupExePath := filepath.Join(backupDir, filepath.Base(currentExePath))
defer u.clean(updateDir)
err = u.download(updateDir, packagePath, packageURL)
if err != nil {
return fmt.Errorf("preparing: %w", err)
return fmt.Errorf("downloading: %w", err)
}
defer clean()
err = downloadPackageFile()
if err != nil {
return fmt.Errorf("downloading package file: %w", err)
}
err = unpack()
err = u.unpack(updateDir, packagePath, info.Mode())
if err != nil {
return fmt.Errorf("unpacking: %w", err)
}
err = backup()
err = u.backup(currentExePath, backupExePath, backupDir)
if err != nil {
return fmt.Errorf("backuping: %w", err)
}
err = replace()
err = u.copyFile(updateExePath, currentExePath)
if err != nil {
return fmt.Errorf("replacing: %w", err)
}
@@ -125,116 +168,30 @@ func UpdateCore(execPath string) (err error) {
return nil
}
// prepare fills all necessary fields in Updater object.
func prepare(exePath string) (err error) {
updateDir = filepath.Join(workDir, "meta-update")
currentExeName = exePath
_, pkgNameOnly := filepath.Split(packageURL)
if pkgNameOnly == "" {
return fmt.Errorf("invalid PackageURL: %q", packageURL)
}
packageName = filepath.Join(updateDir, pkgNameOnly)
//log.Infoln(packageName)
backupDir = filepath.Join(workDir, "meta-backup")
if runtime.GOOS == "windows" {
updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe"
} else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" {
updateExeName = "mihomo-android-arm64-v8"
} else {
updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible
}
log.Infoln("updateExeName: %s ", updateExeName)
backupExeName = filepath.Join(backupDir, filepath.Base(exePath))
updateExeName = filepath.Join(updateDir, updateExeName)
log.Infoln(
"updater: updating using url: %s",
packageURL,
)
currentExeName = exePath
_, err = os.Stat(currentExeName)
func (u *CoreUpdater) getLatestVersion(versionURL string) (version string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil)
if err != nil {
return fmt.Errorf("checking %q: %w", currentExeName, err)
return "", err
}
return nil
}
// unpack extracts the files from the downloaded archive.
func unpack() error {
var err error
_, pkgNameOnly := filepath.Split(packageURL)
log.Infoln("updater: unpacking package")
if strings.HasSuffix(pkgNameOnly, ".zip") {
_, err = zipFileUnpack(packageName, updateDir)
if err != nil {
return fmt.Errorf(".zip unpack failed: %w", err)
defer func() {
closeErr := resp.Body.Close()
if closeErr != nil && err == nil {
err = closeErr
}
}()
} else if strings.HasSuffix(pkgNameOnly, ".gz") {
_, err = gzFileUnpack(packageName, updateDir)
if err != nil {
return fmt.Errorf(".gz unpack failed: %w", err)
}
} else {
return fmt.Errorf("unknown package extension")
}
return nil
}
// backup makes a backup of the current executable file
func backup() (err error) {
log.Infoln("updater: backing up current ExecFile:%s to %s", currentExeName, backupExeName)
_ = os.Mkdir(backupDir, 0o755)
err = os.Rename(currentExeName, backupExeName)
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
return "", err
}
return nil
content := strings.TrimRight(string(body), "\n")
return content, nil
}
// replace moves the current executable with the updated one
func replace() error {
var err error
log.Infoln("replacing: %s to %s", updateExeName, currentExeName)
if runtime.GOOS == "windows" {
// rename fails with "File in use" error
err = copyFile(updateExeName, currentExeName)
} else {
err = os.Rename(updateExeName, currentExeName)
}
if err != nil {
return err
}
log.Infoln("updater: renamed: %s to %s", updateExeName, currentExeName)
return nil
}
// clean removes the temporary directory itself and all it's contents.
func clean() {
_ = os.RemoveAll(updateDir)
}
// MaxPackageFileSize is a maximum package file length in bytes. The largest
// package whose size is limited by this constant currently has the size of
// approximately 32 MiB.
const MaxPackageFileSize = 32 * 1024 * 1024
// Download package file and save it to disk
func downloadPackageFile() (err error) {
// download package file and save it to disk
func (u *CoreUpdater) download(updateDir, packagePath, packageURL string) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil)
@@ -249,38 +206,95 @@ func downloadPackageFile() (err error) {
}
}()
var r io.Reader
r, err = LimitReader(resp.Body, MaxPackageFileSize)
if err != nil {
return fmt.Errorf("http request failed: %w", err)
}
log.Debugln("updater: reading http body")
// This use of ReadAll is now safe, because we limited body's Reader.
body, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("io.ReadAll() failed: %w", err)
}
log.Debugln("updateDir %s", updateDir)
err = os.Mkdir(updateDir, 0o755)
if err != nil {
return fmt.Errorf("mkdir error: %w", err)
}
log.Debugln("updater: saving package to file %s", packageName)
err = os.WriteFile(packageName, body, 0o644)
log.Debugln("updater: saving package to file %s", packagePath)
// Create the output file
wc, err := os.OpenFile(packagePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
return fmt.Errorf("os.WriteFile() failed: %w", err)
return fmt.Errorf("os.OpenFile(%s): %w", packagePath, err)
}
defer func() {
closeErr := wc.Close()
if closeErr != nil && err == nil {
err = closeErr
}
}()
log.Debugln("updater: reading http body")
// This use of io.Copy is now safe, because we limited body's Reader.
n, err := io.Copy(wc, io.LimitReader(resp.Body, MaxPackageFileSize))
if err != nil {
return fmt.Errorf("io.Copy(): %w", err)
}
if n == MaxPackageFileSize {
// Use whether n is equal to MaxPackageFileSize to determine whether the limit has been reached.
// It is also possible that the size of the downloaded file is exactly the same as the maximum limit,
// but we should not consider this too rare situation.
return fmt.Errorf("attempted to read more than %d bytes", MaxPackageFileSize)
}
log.Debugln("updater: downloaded package to file %s", packagePath)
return nil
}
// unpack extracts the files from the downloaded archive.
func (u *CoreUpdater) unpack(updateDir, packagePath string, fileMode os.FileMode) error {
log.Infoln("updater: unpacking package")
if strings.HasSuffix(packagePath, ".zip") {
_, err := u.zipFileUnpack(packagePath, updateDir, fileMode)
if err != nil {
return fmt.Errorf(".zip unpack failed: %w", err)
}
} else if strings.HasSuffix(packagePath, ".gz") {
_, err := u.gzFileUnpack(packagePath, updateDir, fileMode)
if err != nil {
return fmt.Errorf(".gz unpack failed: %w", err)
}
} else {
return fmt.Errorf("unknown package extension")
}
return nil
}
// backup creates a backup of the current executable file.
func (u *CoreUpdater) backup(currentExePath, backupExePath, backupDir string) (err error) {
log.Infoln("updater: backing up current ExecFile:%s to %s", currentExePath, backupExePath)
_ = os.Mkdir(backupDir, 0o755)
// On Windows, since the running executable cannot be overwritten or deleted, it uses os.Rename to move the file to the backup path.
// On other platforms, it copies the file to the backup path, preserving the original file and its permissions.
// The backup directory is created if it does not exist.
if runtime.GOOS == "windows" {
err = os.Rename(currentExePath, backupExePath)
} else {
err = u.copyFile(currentExePath, backupExePath)
}
if err != nil {
return err
}
return nil
}
// clean removes the temporary directory itself and all it's contents.
func (u *CoreUpdater) clean(updateDir string) {
_ = os.RemoveAll(updateDir)
}
// Unpack a single .gz file to the specified directory
// Existing files are overwritten
// All files are created inside outDir, subdirectories are not created
// Return the output file name
func gzFileUnpack(gzfile, outDir string) (string, error) {
func (u *CoreUpdater) gzFileUnpack(gzfile, outDir string, fileMode os.FileMode) (outputName string, err error) {
f, err := os.Open(gzfile)
if err != nil {
return "", fmt.Errorf("os.Open(): %w", err)
@@ -312,14 +326,10 @@ func gzFileUnpack(gzfile, outDir string) (string, error) {
originalName = strings.TrimSuffix(originalName, ".gz")
}
outputName := filepath.Join(outDir, originalName)
outputName = filepath.Join(outDir, originalName)
// Create the output file
wc, err := os.OpenFile(
outputName,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
0o755,
)
wc, err := os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return "", fmt.Errorf("os.OpenFile(%s): %w", outputName, err)
}
@@ -344,7 +354,7 @@ func gzFileUnpack(gzfile, outDir string) (string, error) {
// Existing files are overwritten
// All files are created inside 'outDir', subdirectories are not created
// Return the output file name
func zipFileUnpack(zipfile, outDir string) (string, error) {
func (u *CoreUpdater) zipFileUnpack(zipfile, outDir string, fileMode os.FileMode) (outputName string, err error) {
zrc, err := zip.OpenReader(zipfile)
if err != nil {
return "", fmt.Errorf("zip.OpenReader(): %w", err)
@@ -376,14 +386,14 @@ func zipFileUnpack(zipfile, outDir string) (string, error) {
}()
fi := zf.FileInfo()
name := fi.Name()
outputName := filepath.Join(outDir, name)
outputName = filepath.Join(outDir, name)
if fi.IsDir() {
return "", fmt.Errorf("the target file is a directory")
}
var wc io.WriteCloser
wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
wc, err = os.OpenFile(outputName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return "", fmt.Errorf("os.OpenFile(): %w", err)
}
@@ -403,97 +413,60 @@ func zipFileUnpack(zipfile, outDir string) (string, error) {
}
// Copy file on disk
func copyFile(src, dst string) error {
d, e := os.ReadFile(src)
if e != nil {
return e
}
e = os.WriteFile(dst, d, 0o644)
if e != nil {
return e
}
return nil
}
func getLatestVersion() (version string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil)
func (u *CoreUpdater) copyFile(src, dst string) (err error) {
rc, err := os.Open(src)
if err != nil {
return "", fmt.Errorf("get Latest Version fail: %w", err)
return fmt.Errorf("os.Open(%s): %w", src, err)
}
defer func() {
closeErr := resp.Body.Close()
closeErr := rc.Close()
if closeErr != nil && err == nil {
err = closeErr
}
}()
body, err := io.ReadAll(resp.Body)
info, err := rc.Stat()
if err != nil {
return "", fmt.Errorf("get Latest Version fail: %w", err)
return fmt.Errorf("rc.Stat(): %w", err)
}
content := strings.TrimRight(string(body), "\n")
return content, nil
}
func updateDownloadURL() {
var middle string
if runtime.GOARCH == "arm" && probeGoARM() {
//-linux-armv7-alpha-e552b54.gz
middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion)
} else if runtime.GOARCH == "arm64" {
//-linux-arm64-alpha-e552b54.gz
if runtime.GOOS == "android" {
middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion)
} else {
middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion)
// Create the output file
// If the file does not exist, creates it with permissions perm (before umask);
// otherwise truncates it before writing, without changing permissions.
wc, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
// On some file system (such as Android's /data) maybe return error: "text file busy"
// Let's delete the target file and recreate it
err = os.Remove(dst)
if err != nil {
return fmt.Errorf("os.Remove(%s): %w", dst, err)
}
wc, err = os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil {
return fmt.Errorf("os.OpenFile(%s): %w", dst, err)
}
} else if isMIPS(runtime.GOARCH) && gomips != "" {
middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion)
} else {
middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, amd64Compatible, latestVersion)
}
if runtime.GOOS == "windows" {
middle += ".zip"
} else {
middle += ".gz"
}
packageURL = baseURL + middle
//log.Infoln(packageURL)
}
defer func() {
closeErr := wc.Close()
if closeErr != nil && err == nil {
err = closeErr
}
}()
// isMIPS returns true if arch is any MIPS architecture.
func isMIPS(arch string) (ok bool) {
switch arch {
case
"mips",
"mips64",
"mips64le",
"mipsle":
return true
default:
return false
}
}
// linux only
func probeGoARM() (ok bool) {
cmd := exec.Command("cat", "/proc/cpuinfo")
output, err := cmd.Output()
_, err = io.Copy(wc, rc)
if err != nil {
log.Errorln("probe goarm error:%s", err)
return false
return fmt.Errorf("io.Copy(): %w", err)
}
cpuInfo := string(output)
if strings.Contains(cpuInfo, "vfpv3") || strings.Contains(cpuInfo, "vfpv4") {
goarm = "v7"
} else if strings.Contains(cpuInfo, "vfp") {
goarm = "v6"
} else {
goarm = "v5"
if runtime.GOOS == "darwin" {
err = exec.Command("/usr/bin/codesign", "--sign", "-", dst).Run()
if err != nil {
log.Warnln("codesign failed: %v", err)
}
}
return true
log.Infoln("updater: copy: %s to %s", src, dst)
return nil
}

View File

@@ -0,0 +1,10 @@
package updater
import (
"fmt"
"testing"
)
func TestCoreBaseName(t *testing.T) {
fmt.Println("Core base name =", DefaultCoreUpdater.CoreBaseName())
}

View File

@@ -212,7 +212,7 @@ func UpdateGeoDatabases() error {
return nil
}
func getUpdateTime() (err error, time time.Time) {
func getUpdateTime() (time time.Time, err error) {
filesToCheck := []string{
C.Path.GeoIP(),
C.Path.MMDB(),
@@ -224,7 +224,7 @@ func getUpdateTime() (err error, time time.Time) {
var fileInfo os.FileInfo
fileInfo, err = os.Stat(file)
if err == nil {
return nil, fileInfo.ModTime()
return fileInfo.ModTime(), nil
}
}
@@ -241,7 +241,7 @@ func RegisterGeoUpdater() {
ticker := time.NewTicker(time.Duration(updateInterval) * time.Hour)
defer ticker.Stop()
err, lastUpdate := getUpdateTime()
lastUpdate, err := getUpdateTime()
if err != nil {
log.Errorln("[GEO] Get GEO database update time error: %s", err.Error())
return

View File

@@ -3,6 +3,7 @@ package updater
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
@@ -32,6 +33,17 @@ const (
typeTarGzip
)
func (t compressionType) String() string {
switch t {
case typeZip:
return "zip"
case typeTarGzip:
return "tar.gz"
default:
return "unknown"
}
}
var DefaultUiUpdater = &UIUpdater{}
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
@@ -99,48 +111,38 @@ func detectFileType(data []byte) compressionType {
}
func (u *UIUpdater) downloadUI() error {
err := u.prepareUIPath()
if err != nil {
return fmt.Errorf("prepare UI path failed: %w", err)
}
data, err := downloadForBytes(u.externalUIURL)
if err != nil {
return fmt.Errorf("can't download file: %w", err)
}
fileType := detectFileType(data)
if fileType == typeUnknown {
return fmt.Errorf("unknown or unsupported file type")
tmpDir := C.Path.Resolve("downloadUI.tmp")
defer os.RemoveAll(tmpDir)
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
log.Debugln("extractedFolder: %s", tmpDir)
err = extract(data, tmpDir)
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
}
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 {
return fmt.Errorf("can't save compressed file: %w", err)
}
defer os.Remove(saved)
err = cleanup(u.externalUIPath)
log.Debugln("cleanupFolder: %s", u.externalUIPath)
err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("cleanup exist file error: %w", err)
}
}
extractedFolder, err := extract(saved, C.Path.HomeDir())
err = u.prepareUIPath()
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
return fmt.Errorf("prepare UI path failed: %w", err)
}
err = os.Rename(extractedFolder, u.externalUIPath)
log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
if err != nil {
return fmt.Errorf("rename UI folder failed: %w", err)
return fmt.Errorf("move UI folder failed: %w", err)
}
return nil
}
@@ -155,228 +157,106 @@ func (u *UIUpdater) prepareUIPath() error {
return nil
}
func unzip(src, dest string) (string, error) {
r, err := zip.OpenReader(src)
func unzip(data []byte, dest string) error {
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return "", err
return err
}
defer r.Close()
// check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File {
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)
}
fpath := filepath.Join(dest, f.Name)
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid file path: %s", fpath)
if !inDest(fpath, dest) {
return fmt.Errorf("invalid file path: %s", fpath)
}
if f.FileInfo().IsDir() {
info := f.FileInfo()
if info.IsDir() {
os.MkdirAll(fpath, os.ModePerm)
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
if info.Mode()&os.ModeSymlink != 0 {
continue // disallow symlink
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
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, f.Mode().Perm())
if err != nil {
return "", err
return err
}
rc, err := f.Open()
if err != nil {
return "", err
return err
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return "", err
return err
}
}
return extractedFolder, nil
return nil
}
func untgz(src, dest string) (string, error) {
file, err := os.Open(src)
func untgz(data []byte, dest string) error {
gzr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
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
return err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
fpath := filepath.Join(dest, header.Name)
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)
if !inDest(fpath, dest) {
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
return err
}
case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err
return err
}
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode).Perm())
if err != nil {
return "", err
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return "", err
return err
}
outFile.Close()
}
}
return extractedFolder, nil
return 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)
func extract(data []byte, dest string) error {
fileType := detectFileType(data)
log.Debugln("compression Type: %s", fileType)
switch fileType {
case typeZip:
return unzip(data, dest)
case typeTarGzip:
return untgz(data, dest)
default:
return "", fmt.Errorf("unsupported file format: %s", src)
return fmt.Errorf("unknown or unsupported file type")
}
}
@@ -398,22 +278,49 @@ func cleanTarPath(path string) string {
}
func cleanup(root string) error {
if _, err := os.Stat(root); os.IsNotExist(err) {
return nil
dirEntryList, err := os.ReadDir(root)
if err != nil {
return err
}
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
for _, dirEntry := range dirEntryList {
err = os.RemoveAll(filepath.Join(root, dirEntry.Name()))
if err != nil {
return err
}
if info.IsDir() {
if err := os.RemoveAll(path); err != nil {
return err
}
} else {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}
return nil
}
func moveDir(src string, dst string) error {
dirEntryList, err := os.ReadDir(src)
if err != nil {
return err
}
if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
src = filepath.Join(src, dirEntryList[0].Name())
log.Debugln("match the singleRoot: %s", src)
dirEntryList, err = os.ReadDir(src)
if err != nil {
return err
}
}
for _, dirEntry := range dirEntryList {
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
if err != nil {
return err
}
}
return nil
}
func inDest(fpath, dest string) bool {
if rel, err := filepath.Rel(dest, fpath); err == nil {
if filepath.IsLocal(rel) {
return true
}
}
return false
}

View File

@@ -2,15 +2,12 @@ package updater
import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
mihomoHttp "github.com/metacubex/mihomo/component/http"
"golang.org/x/exp/constraints"
)
const defaultHttpTimeout = time.Second * 90
@@ -30,62 +27,3 @@ func downloadForBytes(url string) ([]byte, error) {
func saveFile(bytes []byte, path string) error {
return os.WriteFile(path, bytes, 0o644)
}
// LimitReachedError records the limit and the operation that caused it.
type LimitReachedError struct {
Limit int64
}
// Error implements the [error] interface for *LimitReachedError.
//
// TODO(a.garipov): Think about error string format.
func (lre *LimitReachedError) Error() string {
return fmt.Sprintf("attempted to read more than %d bytes", lre.Limit)
}
// limitedReader is a wrapper for [io.Reader] limiting the input and dealing
// with errors package.
type limitedReader struct {
r io.Reader
limit int64
n int64
}
// Read implements the [io.Reader] interface.
func (lr *limitedReader) Read(p []byte) (n int, err error) {
if lr.n == 0 {
return 0, &LimitReachedError{
Limit: lr.limit,
}
}
p = p[:Min(lr.n, int64(len(p)))]
n, err = lr.r.Read(p)
lr.n -= int64(n)
return n, err
}
// LimitReader wraps Reader to make it's Reader stop with ErrLimitReached after
// n bytes read.
func LimitReader(r io.Reader, n int64) (limited io.Reader, err error) {
if n < 0 {
return nil, &updateError{Message: "limit must be non-negative"}
}
return &limitedReader{
r: r,
limit: n,
n: n,
}, nil
}
// Min returns the smaller of x or y.
func Min[T constraints.Integer | ~string](x, y T) (res T) {
if x < y {
return x
}
return y
}

View File

@@ -0,0 +1,84 @@
// Package wildcard modified IGLOU-EU/go-wildcard to support:
//
// `*` matches zero or more characters
// `?` matches exactly one character
//
// The original go-wildcard library used `.` to match exactly one character, and `?` to match zero or one character.
// `.` is a valid delimiter in domain name matching and should not be used as a wildcard.
// The `?` matching logic strictly matches only one character in most scenarios.
// So, the `?` matching logic in the original go-wildcard library has been removed and its wildcard `.` has been replaced with `?`.
package wildcard
// copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21
// which is licensed under OpenBSD's ISC-style license.
// Copyright (c) 2023 Iglou.eu contact@iglou.eu Copyright (c) 2023 Adrien Kara adrien@iglou.eu
func Match(pattern, s string) bool {
if pattern == "" {
return s == pattern
}
if pattern == "*" || s == pattern {
return true
}
return matchByString(pattern, s)
}
func matchByString(pattern, s string) bool {
var patternIndex, sIndex, lastStar int
patternLen := len(pattern)
sLen := len(s)
star := -1
Loop:
if sIndex >= sLen {
goto checkPattern
}
if patternIndex >= patternLen {
if star != -1 {
patternIndex = star + 1
lastStar++
sIndex = lastStar
goto Loop
}
return false
}
switch pattern[patternIndex] {
case '?':
// It matches any single character. So, we don't need to check anything.
case '*':
// '*' matches zero or more characters. Store its position and increment the pattern index.
star = patternIndex
lastStar = sIndex
patternIndex++
goto Loop
default:
// If the characters don't match, check if there was a previous '*' to backtrack.
if pattern[patternIndex] != s[sIndex] {
if star != -1 {
patternIndex = star + 1
lastStar++
sIndex = lastStar
goto Loop
}
return false
}
}
patternIndex++
sIndex++
goto Loop
// Check if the remaining pattern characters are '*', which can match the end of the string.
checkPattern:
if patternIndex < patternLen {
if pattern[patternIndex] == '*' {
patternIndex++
goto checkPattern
}
}
return patternIndex == patternLen
}

View File

@@ -0,0 +1,192 @@
package wildcard
/*
* copy and modified from https://github.com/IGLOU-EU/go-wildcard/tree/ce22b7af48e487517a492d3727d9386492043e21
*
* Copyright (c) 2023 Iglou.eu <contact@iglou.eu>
* Copyright (c) 2023 Adrien Kara <adrien@iglou.eu>
*
* Licensed under the BSD 3-Clause License,
*/
import (
"testing"
)
// TestMatch validates the logic of wild card matching,
// it need to support '*', '?' and only validate for byte comparison
// over string, not rune or grapheme cluster
func TestMatch(t *testing.T) {
cases := []struct {
s string
pattern string
result bool
}{
{"", "", true},
{"", "*", true},
{"", "**", true},
{"", "?", false},
{"", "?*", false},
{"", "*?", false},
{"a", "", false},
{"a", "a", true},
{"a", "*", true},
{"a", "**", true},
{"a", "?", true},
{"a", "?*", true},
{"a", "*?", true},
{"match the exact string", "match the exact string", true},
{"do not match a different string", "this is a different string", false},
{"Match The Exact String WITH DIFFERENT CASE", "Match The Exact String WITH DIFFERENT CASE", true},
{"do not match a different string WITH DIFFERENT CASE", "this is a different string WITH DIFFERENT CASE", false},
{"Do Not Match The Exact String With Different Case", "do not match the exact string with different case", false},
{"match an emoji 😃", "match an emoji 😃", true},
{"do not match because of different emoji 😃", "do not match because of different emoji 😄", false},
{"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", true},
{"🌅☕️📰👨‍💼👩‍💼🏢🖥️💼💻📊📈📉👨‍👩‍👧‍👦🍝🕰️💪🏋️‍♂️🏋️‍♀️🏋️‍♂️💼🚴‍♂️🚴‍♀️🚴‍♂️🛀💤🌃", "🦌🐇🦡🐿️🌲🌳🏰🌳🌲🌞🌧️❄️🌬️⛈️🔥🎄🎅🎁🎉🎊🥳👨‍👩‍👧‍👦💏👪💖👩‍💼🛀", false},
{"match a string with a *", "match a string *", true},
{"match a string with a * at the beginning", "* at the beginning", true},
{"match a string with two *", "match * with *", true},
{"do not match a string with extra and a *", "do not match a string * with more", false},
{"match a string with a ?", "match ? string with a ?", true},
{"match a string with a ? at the beginning", "?atch a string with a ? at the beginning", true},
{"match a string with two ?", "match a ??ring with two ?", true},
{"do not match a string with extra ?", "do not match a string with extra ??", false},
{"abc.edf.hjg", "abc.edf.hjg", true},
{"abc.edf.hjg", "ab.cedf.hjg", false},
{"abc.edf.hjg", "abc.edfh.jg", false},
{"abc.edf.hjg", "abc.edf.hjq", false},
{"abc.edf.hjg", "abc.*.hjg", true},
{"abc.edf.hjg", "abc.*.hjq", false},
{"abc.edf.hjg", "abc*hjg", true},
{"abc.edf.hjg", "abc*hjq", false},
{"abc.edf.hjg", "a*g", true},
{"abc.edf.hjg", "a*q", false},
{"abc.edf.hjg", "ab?.edf.hjg", true},
{"abc.edf.hjg", "?b?.edf.hjg", true},
{"abc.edf.hjg", "??c.edf.hjg", true},
{"abc.edf.hjg", "a??.edf.hjg", true},
{"abc.edf.hjg", "ab??.edf.hjg", false},
{"abc.edf.hjg", "??.edf.hjg", false},
}
for i, c := range cases {
t.Run(c.s, func(t *testing.T) {
result := Match(c.pattern, c.s)
if c.result != result {
t.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s)
}
})
}
}
func match(pattern, name string) bool { // https://research.swtch.com/glob
px := 0
nx := 0
nextPx := 0
nextNx := 0
for px < len(pattern) || nx < len(name) {
if px < len(pattern) {
c := pattern[px]
switch c {
default: // ordinary character
if nx < len(name) && name[nx] == c {
px++
nx++
continue
}
case '?': // single-character wildcard
if nx < len(name) {
px++
nx++
continue
}
case '*': // zero-or-more-character wildcard
// Try to match at nx.
// If that doesn't work out,
// restart at nx+1 next.
nextPx = px
nextNx = nx + 1
px++
continue
}
}
// Mismatch. Maybe restart.
if 0 < nextNx && nextNx <= len(name) {
px = nextPx
nx = nextNx
continue
}
return false
}
// Matched all of pattern to all of name. Success.
return true
}
func FuzzMatch(f *testing.F) {
f.Fuzz(func(t *testing.T, pattern, name string) {
result1 := Match(pattern, name)
result2 := match(pattern, name)
if result1 != result2 {
t.Fatalf("Match failed for pattern `%s` and name `%s`", pattern, name)
}
})
}
func BenchmarkMatch(b *testing.B) {
cases := []struct {
s string
pattern string
result bool
}{
{"abc.edf.hjg", "abc.edf.hjg", true},
{"abc.edf.hjg", "ab.cedf.hjg", false},
{"abc.edf.hjg", "abc.edfh.jg", false},
{"abc.edf.hjg", "abc.edf.hjq", false},
{"abc.edf.hjg", "abc.*.hjg", true},
{"abc.edf.hjg", "abc.*.hjq", false},
{"abc.edf.hjg", "abc*hjg", true},
{"abc.edf.hjg", "abc*hjq", false},
{"abc.edf.hjg", "a*g", true},
{"abc.edf.hjg", "a*q", false},
{"abc.edf.hjg", "ab?.edf.hjg", true},
{"abc.edf.hjg", "?b?.edf.hjg", true},
{"abc.edf.hjg", "??c.edf.hjg", true},
{"abc.edf.hjg", "a??.edf.hjg", true},
{"abc.edf.hjg", "ab??.edf.hjg", false},
{"abc.edf.hjg", "??.edf.hjg", false},
{"r4.cdn-aa-wow-this-is-long-a1.video-yajusenpai1145141919810-oh-hell-yeah-this-is-also-very-long-and-sukka-the-fox-has-a-very-big-fluffy-fox-tail-ao-wu-ao-wu-regex-and-wildcard-both-might-have-deadly-back-tracing-issue-be-careful-or-use-linear-matching.com", "*.cdn-*-*.video**.com", true},
}
b.Run("Match", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, c := range cases {
result := Match(c.pattern, c.s)
if c.result != result {
b.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s)
}
}
}
})
b.Run("match", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, c := range cases {
result := match(c.pattern, c.s)
if c.result != result {
b.Errorf("Test %d: Expected `%v`, found `%v`; With Pattern: `%s` and String: `%s`", i+1, c.result, result, c.pattern, c.s)
}
}
}
})
}

View File

@@ -1,12 +1,12 @@
package config
import (
"container/list"
"errors"
"fmt"
"net"
"net/netip"
"net/url"
"path/filepath"
"strings"
"time"
_ "unsafe"
@@ -156,6 +156,7 @@ type DNS struct {
EnhancedMode C.DNSMode
DefaultNameserver []dns.NameServer
CacheAlgorithm string
CacheMaxSize int
FakeIPRange *fakeip.Pool
Hosts *trie.DomainTrie[resolver.HostValue]
NameServerPolicy []dns.Policy
@@ -174,6 +175,7 @@ type Profile struct {
type TLS struct {
Certificate string
PrivateKey string
EchKey string
CustomTrustCert []string
}
@@ -222,6 +224,7 @@ type RawDNS struct {
FakeIPFilterMode C.FilterMode `yaml:"fake-ip-filter-mode" json:"fake-ip-filter-mode"`
DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"`
CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"`
CacheMaxSize int `yaml:"cache-max-size" json:"cache-max-size"`
NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"`
ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"`
DirectNameServer []string `yaml:"direct-nameserver" json:"direct-nameserver"`
@@ -268,6 +271,7 @@ type RawTun struct {
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -294,6 +298,10 @@ type RawTun struct {
Inet6RouteAddress []netip.Prefix `yaml:"inet6-route-address" json:"inet6-route-address,omitempty"`
Inet4RouteExcludeAddress []netip.Prefix `yaml:"inet4-route-exclude-address" json:"inet4-route-exclude-address,omitempty"`
Inet6RouteExcludeAddress []netip.Prefix `yaml:"inet6-route-exclude-address" json:"inet6-route-exclude-address,omitempty"`
// darwin special config
RecvMsgX bool `yaml:"recvmsgx" json:"recvmsgx,omitempty"`
SendMsgX bool `yaml:"sendmsgx" json:"sendmsgx,omitempty"`
}
type RawTuicServer struct {
@@ -360,6 +368,7 @@ type RawSniffingConfig struct {
type RawTLS struct {
Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
}
@@ -510,6 +519,8 @@ func DefaultRawConfig() *RawConfig {
AutoRoute: true,
AutoDetectInterface: true,
Inet6Address: []netip.Prefix{netip.MustParsePrefix("fdfe:dcba:9876::1/126")},
RecvMsgX: true,
SendMsgX: false, // In the current implementation, if enabled, the kernel may freeze during multi-thread downloads, so it is disabled by default.
},
TuicServer: RawTuicServer{
Enable: false,
@@ -755,7 +766,10 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
func parseController(cfg *RawConfig) (*Controller, error) {
if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path)
return nil, C.Path.ErrNotSafePath(path)
}
if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) {
return nil, fmt.Errorf("external UI name is not local: %s", uiName)
}
return &Controller{
ExternalController: cfg.ExternalController,
@@ -814,6 +828,7 @@ func parseTLS(cfg *RawConfig) (*TLS, error) {
return &TLS{
Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
CustomTrustCert: cfg.TLS.CustomTrustCert,
}, nil
}
@@ -830,8 +845,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
AllProxies []string
hasGlobal bool
)
proxiesList := list.New()
groupsList := list.New()
proxies["DIRECT"] = adapter.NewProxy(outbound.NewDirect())
proxies["REJECT"] = adapter.NewProxy(outbound.NewReject())
@@ -853,7 +866,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
proxies[proxy.Name()] = proxy
proxyList = append(proxyList, proxy.Name())
AllProxies = append(AllProxies, proxy.Name())
proxiesList.PushBack(mapping)
}
// keep the original order of ProxyGroups in config file
@@ -866,7 +878,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
hasGlobal = true
}
proxyList = append(proxyList, groupName)
groupsList.PushBack(mapping)
}
// check if any loop exists and sort the ProxyGroups
@@ -1032,46 +1043,20 @@ func parseRules(rulesConfig []string, proxies map[string]C.Proxy, ruleProviders
// parse rules
for idx, line := range rulesConfig {
rule := trimArr(strings.Split(line, ","))
var (
payload string
target string
params []string
ruleName = strings.ToUpper(rule[0])
)
l := len(rule)
if ruleName == "NOT" || ruleName == "OR" || ruleName == "AND" || ruleName == "SUB-RULE" || ruleName == "DOMAIN-REGEX" || ruleName == "PROCESS-NAME-REGEX" || ruleName == "PROCESS-PATH-REGEX" {
target = rule[l-1]
payload = strings.Join(rule[1:l-1], ",")
} else {
if l < 2 {
return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line)
}
if l < 4 {
rule = append(rule, make([]string, 4-l)...)
}
if ruleName == "MATCH" {
l = 2
}
if l >= 3 {
l = 3
payload = rule[1]
}
target = rule[l-1]
params = rule[l:]
tp, payload, target, params := RC.ParseRulePayload(line, true)
if target == "" {
return nil, fmt.Errorf("%s[%d] [%s] error: format invalid", format, idx, line)
}
if _, ok := proxies[target]; !ok {
if ruleName != "SUB-RULE" {
if tp != "SUB-RULE" {
return nil, fmt.Errorf("%s[%d] [%s] error: proxy [%s] not found", format, idx, line, target)
} else if _, ok = subRules[target]; !ok {
return nil, fmt.Errorf("%s[%d] [%s] error: sub-rule [%s] not found", format, idx, line, target)
}
}
params = trimArr(params)
parsed, parseErr := R.ParseRule(ruleName, payload, target, params, subRules)
parsed, parseErr := R.ParseRule(tp, payload, target, params, subRules)
if parseErr != nil {
return nil, fmt.Errorf("%s[%d] [%s] error: %s", format, idx, line, parseErr.Error())
}
@@ -1161,10 +1146,19 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
}
proxyName := u.Fragment
var proxyName string
params := map[string]string{}
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.SplitN(s, "=", 2)
switch len(arr) {
case 1:
proxyName = arr[0]
case 2:
params[arr[0]] = arr[1]
}
}
var addr, dnsNetType string
params := map[string]string{}
switch u.Scheme {
case "udp":
addr, err = hostWithDefaultPort(u.Host, "53")
@@ -1182,23 +1176,8 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
addr, err = hostWithDefaultPort(u.Host, "80")
}
if err == nil {
proxyName = ""
clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User}
addr = clearURL.String()
if len(u.Fragment) != 0 {
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.Split(s, "=")
if len(arr) == 0 {
continue
} else if len(arr) == 1 {
proxyName = arr[0]
} else if len(arr) == 2 {
params[arr[0]] = arr[1]
} else {
params[arr[0]] = strings.Join(arr[1:], "=")
}
}
}
}
case "quic":
addr, err = hostWithDefaultPort(u.Host, "853")
@@ -1375,6 +1354,8 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
IPv6: cfg.IPv6,
UseSystemHosts: cfg.UseSystemHosts,
EnhancedMode: cfg.EnhancedMode,
CacheAlgorithm: cfg.CacheAlgorithm,
CacheMaxSize: cfg.CacheMaxSize,
}
var err error
if dnsCfg.NameServer, err = parseNameServer(cfg.NameServer, cfg.RespectRules, cfg.PreferH3); err != nil {
@@ -1508,12 +1489,6 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul
dnsCfg.Hosts = hosts
}
if cfg.CacheAlgorithm == "" || cfg.CacheAlgorithm == "lru" {
dnsCfg.CacheAlgorithm = "lru"
} else {
dnsCfg.CacheAlgorithm = "arc"
}
return dnsCfg, nil
}
@@ -1556,6 +1531,7 @@ func parseTun(rawTun RawTun, general *General) error {
AutoRedirect: rawTun.AutoRedirect,
AutoRedirectInputMark: rawTun.AutoRedirectInputMark,
AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark,
LoopbackAddress: rawTun.LoopbackAddress,
StrictRoute: rawTun.StrictRoute,
RouteAddress: rawTun.RouteAddress,
RouteAddressSet: rawTun.RouteAddressSet,
@@ -1582,6 +1558,9 @@ func parseTun(rawTun RawTun, general *General) error {
Inet6RouteAddress: rawTun.Inet6RouteAddress,
Inet4RouteExcludeAddress: rawTun.Inet4RouteExcludeAddress,
Inet6RouteExcludeAddress: rawTun.Inet6RouteExcludeAddress,
RecvMsgX: rawTun.RecvMsgX,
SendMsgX: rawTun.SendMsgX,
}
return nil

View File

@@ -4,19 +4,13 @@ import (
"fmt"
"net"
"net/netip"
"strings"
"os"
"strconv"
"github.com/metacubex/mihomo/adapter/outboundgroup"
"github.com/metacubex/mihomo/common/structure"
)
func trimArr(arr []string) (r []string) {
for _, e := range arr {
r = append(r, strings.Trim(e, " "))
}
return
}
// Check if ProxyGroups form DAG(Directed Acyclic Graph), and sort all ProxyGroups by dependency order.
// Meanwhile, record the original index in the config file.
// If loop is detected, return an error with location of loop.
@@ -150,6 +144,9 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error {
}
func verifyIP6() bool {
if skip, _ := strconv.ParseBool(os.Getenv("SKIP_SYSTEM_IPV6_CHECK")); skip {
return true
}
if iAddrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range iAddrs {
if prefix, err := netip.ParsePrefix(addr.String()); err == nil {

View File

@@ -92,8 +92,7 @@ type Conn interface {
type PacketConn interface {
N.EnhancePacketConn
Connection
// Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed
// WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error)
ResolveUDP(ctx context.Context, metadata *Metadata) error
}
type Dialer interface {
@@ -319,10 +318,15 @@ type PacketSender interface {
Send(PacketAdapter)
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
Process(PacketConn, WriteBackProxy)
// ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved
ResolveUDP(*Metadata) error
// Close stop the Process loop
Close()
// DoSniff will blocking after sniffer work done
DoSniff(*Metadata) error
// AddMapping add a destination NAT record
AddMapping(originMetadata *Metadata, metadata *Metadata)
// RestoreReadFrom restore destination NAT for ReadFrom
// the implement must ensure returned netip.Add is valid (or just return input addr)
RestoreReadFrom(addr netip.Addr) netip.Addr
}
type NatTable interface {

View File

@@ -1,7 +1,6 @@
package constant
import (
"encoding/json"
"errors"
"strings"
)
@@ -22,44 +21,6 @@ const (
type DNSMode int
// UnmarshalYAML unserialize EnhancedMode with yaml
func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := DNSModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalYAML serialize EnhancedMode with yaml
func (e DNSMode) MarshalYAML() (any, error) {
return e.String(), nil
}
// UnmarshalJSON unserialize EnhancedMode with json
func (e *DNSMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
mode, exist := DNSModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalJSON serialize EnhancedMode with json
func (e DNSMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
// UnmarshalText unserialize EnhancedMode
func (e *DNSMode) UnmarshalText(data []byte) error {
mode, exist := DNSModeMapping[strings.ToLower(string(data))]
@@ -157,40 +118,6 @@ func (e FilterMode) String() string {
}
}
func (e FilterMode) MarshalYAML() (interface{}, error) {
return e.String(), nil
}
func (e *FilterMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := FilterModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
func (e FilterMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
func (e *FilterMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
mode, exist := FilterModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
func (e FilterMode) MarshalText() ([]byte, error) {
return []byte(e.String()), nil
}

View File

@@ -0,0 +1,24 @@
package features
import "runtime/debug"
var (
GOARM string
GOMIPS string
GOAMD64 string
)
func init() {
if info, ok := debug.ReadBuildInfo(); ok {
for _, bs := range info.Settings {
switch bs.Key {
case "GOARM":
GOARM = bs.Value
case "GOMIPS":
GOMIPS = bs.Value
case "GOAMD64":
GOAMD64 = bs.Value
}
}
}
}

View File

@@ -261,6 +261,11 @@ func (m *Metadata) Pure() *Metadata {
return m
}
func (m *Metadata) Clone() *Metadata {
copyM := *m
return &copyM
}
func (m *Metadata) AddrPort() netip.AddrPort {
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
}

View File

@@ -1,6 +1,7 @@
package constant
import (
"fmt"
"os"
P "path"
"path/filepath"
@@ -87,10 +88,8 @@ func (p *path) IsSafePath(path string) bool {
if p.allowUnsafePath || features.CMFA {
return true
}
homedir := p.HomeDir()
path = p.Resolve(path)
safePaths := append([]string{homedir}, p.safePaths...) // add homedir to safePaths
for _, safePath := range safePaths {
for _, safePath := range p.SafePaths() {
if rel, err := filepath.Rel(safePath, path); err == nil {
if filepath.IsLocal(rel) {
return true
@@ -100,6 +99,23 @@ func (p *path) IsSafePath(path string) bool {
return false
}
func (p *path) SafePaths() []string {
return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths
}
func (p *path) ErrNotSafePath(path string) error {
return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()}
}
type ErrNotSafePath struct {
Path string
SafePaths []string
}
func (e ErrNotSafePath) Error() string {
return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths)
}
func (p *path) GetPathByHash(prefix, name string) string {
hash := utils.MakeHash([]byte(name))
filename := hash.String()

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