Compare commits

...

923 Commits

Author SHA1 Message Date
66f108bf24 Fix Vision read 2023-02-25 10:28:38 +08:00
c5444a03ac Fix short buffer 2023-02-24 21:19:57 +08:00
9aacfe11e7 Update to the latest Alpha branch 2023-02-24 21:06:24 +08:00
11e0bbebf4 PREVIEW 2023-02-24 20:47:07 +08:00
fc58f80cc8 DRAFT 3 2023-02-24 20:47:07 +08:00
abced62f4d DRAFT 2 2023-02-24 20:47:07 +08:00
4f27911659 DRAFT 1 2023-02-24 20:46:59 +08:00
5bfad04b41 fix: checkTunName mistake 2023-02-24 14:58:01 +08:00
880664c6ab fix: tunnel's inboundTFO missing 2023-02-24 14:19:50 +08:00
8f0c61ed14 fix: tuic missing routing mark 2023-02-24 14:02:20 +08:00
7d524668e0 chore: support TFO for outbounds 2023-02-24 13:53:44 +08:00
75680c5866 chore: use early conn to support real ws 0-rtt 2023-02-24 09:54:54 +08:00
a1d008e6f0 chore: add pprof api, when log-level is debug 2023-02-23 23:30:53 +08:00
d5d62a4ffd chore: change internal tcp traffic type 2023-02-23 20:26:25 +08:00
b72bd5bb37 chore: adjust the configuration loading order 2023-02-23 14:13:27 +08:00
7fecd20a1d chore: adjust the configuration loading order 2023-02-22 23:45:18 +08:00
f586f22ce3 fix: incorrect time to set interface name 2023-02-22 21:08:08 +08:00
21848d6bf1 chore: code cleanup 2023-02-22 19:43:32 +08:00
28c57c4144 chore: Update dependencies 2023-02-22 19:35:43 +08:00
4a6ebff473 fix: add "dns resolve failed" error in dialer 2023-02-22 19:14:11 +08:00
5c8d955f61 chore: better windows bind error handle 2023-02-22 13:41:33 +08:00
baaf509637 chore: using sing-shadowtls to support shadowtls v1/2/3 2023-02-21 21:58:37 +08:00
db3e1b9ed5 feat: add sni field for tuic 2023-02-19 16:20:30 +08:00
1a1e3345f4 chore: reset tunName in macos when it isn't startWith "utun" 2023-02-19 10:10:27 +08:00
527fc2790b chore: combine workflows 2023-02-19 01:23:06 +08:00
cd7d9fc4f5 fix: socks5 serialize error #376 2023-02-18 17:18:58 +08:00
a61685ce01 fix: disable header protection in vmess server 2023-02-18 16:42:54 +08:00
b9e63d3f7d fix: ensure return a nil interface not an interface with nil value 2023-02-18 14:16:03 +08:00
cc3a9dd553 fix: websocket headroom 2023-02-18 13:58:08 +08:00
6a89cc15c3 chore: Considering remove GOAMD64=v2 of linux-amd64-compatible
close https://github.com/MetaCubeX/Clash.Meta/issues/391
2023-02-18 13:32:26 +08:00
fc50392ec7 chore: cleanup natTable's api 2023-02-18 13:16:07 +08:00
59cd89a9c9 fix: parsing ipv6 doh error 2023-02-17 23:30:38 +08:00
d6ff5f7d96 style: run go fmt on every .go file (#392) 2023-02-17 16:31:37 +08:00
8e4dfbd10d feat: introduce a new robust approach to handle tproxy udp. (#389) 2023-02-17 16:31:15 +08:00
b2d1cea759 fix: RoundRobin strategy of load balance when called multiple times (#390) 2023-02-17 16:31:00 +08:00
6fe1766c83 chore: add log 2023-02-17 13:48:29 +08:00
e59c35a308 fix issue #357.
Copy from upstream.
2023-02-16 21:14:27 +08:00
b50071ed37 chore: better log time 2023-02-15 22:39:28 +08:00
28c7de6185 fix: avoid modifying the request message id 2023-02-14 21:09:37 +08:00
6fb4ebba15 chore: Allow 0-RTT in Tuic server
refers to: https://github.com/quic-go/quic-go/pull/3635
2023-02-13 23:52:15 +08:00
d00d83abd4 fix: tun udp with 4in6 ip 2023-02-13 22:06:09 +08:00
e6d16e458f chore: update gvisor 2023-02-13 20:50:11 +08:00
ae42d35184 chore: support golang1.20's dialer.ControlContext 2023-02-13 11:14:19 +08:00
ce8929d153 chore: better bind in windows 2023-02-13 10:14:59 +08:00
cc2a775271 feat: Converter support uTLS fingerprint field 2023-02-11 16:40:01 +08:00
83d719cf79 fix: VLESS handshake write 2023-02-11 15:13:17 +08:00
4643b5835e chore: setting sniffHost value 2023-02-10 13:01:53 +08:00
a991bf9045 fix: missing sniffhost field in RESTful API 2023-02-10 12:48:02 +08:00
3fd3d83029 feat: Attempts to send request with first payload on VLESS 2023-02-10 10:03:37 +08:00
24419551a9 chore: update tfo-go for golang1.20 2023-02-08 13:10:44 +08:00
c83eb2e0c9 chore: adjust log 2023-02-07 21:29:40 +08:00
929b1675e3 chore: avoid repeated wrapper 2023-02-07 21:29:40 +08:00
db54b438e6 chore: do not use extra pointer in UClient 2023-02-07 17:51:37 +08:00
967254d9ca chore: move global-utls-client snippets to components\tls 2023-02-07 17:24:14 +08:00
2d806df9b9 fix: sniff domain don't match geosite when override-destination value is false 2023-02-07 15:59:44 +08:00
4fe798ec3b chore: update sing-vmess 2023-02-07 15:10:36 +08:00
3555ff5f4e chore: update docs/config.yml 2023-02-07 13:19:19 +08:00
05ca819823 feat: add global-client-fingerprint.
* Available: "chrome","firefox","safari","ios","random","none".
* global-client-fingerprint will NOT overwrite the proxy's client-fingerprint setting when "client-fingerprint: none".
2023-02-07 01:26:56 +08:00
c8b8b60b93 chore: override-destination default value is true 2023-02-06 17:48:49 +08:00
287986d524 Update README.md 2023-02-05 23:36:08 +08:00
4c25f5e73b feat: Update utls support.
* client-fingerprint is used to apply Utls for modifying ClientHello, it accepts "chrome","firefox","safari","ios","random" options.
* Utls is currently support TLS transport in TCP/grpc/WS/HTTP for VLESS/Vmess and trojan.
2023-02-05 17:34:37 +08:00
cbc217e80a fix: Converter Shadowsocks password parse 2023-02-04 16:58:17 +08:00
fe348e89c5 chore: add nameserver-policy demo 2023-02-03 21:41:26 +08:00
e1e1984d3e feat: nameserver policy support multiple server 2023-02-03 21:40:05 +08:00
99662b616f fix: tuic listener config name 2023-02-02 21:48:20 +08:00
857d6e419f fix: Parse CC fail in tuic. 2023-02-02 21:29:12 +08:00
a298b9ea01 chore: fix mips atomic panic 2023-02-02 21:03:24 +08:00
61097d0826 chore: update to golang1.20 2023-02-02 15:39:57 +08:00
2ee0f634e6 feat: Add utls for modifying client's fingerprint.
Currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan-grpc.
2023-02-01 22:36:05 +08:00
61b3b4f775 fix: Handle error earlier in DialContextWithDialer.
chore: Fix typo.
2023-01-31 18:05:46 +08:00
dc4b9753d3 Merge pull request #360 from tgNotHouse/Alpha
fix: get tlsconfig err not handle, return nil pointer #358
2023-01-31 15:40:44 +08:00
f1ef6c2096 fix: get tlsconfig err not handle, return nil pointer 2023-01-31 15:26:18 +08:00
872c915cf7 Chore: Add images for wiki 2023-01-30 21:19:46 +08:00
fb9f09c97f Update README.md 2023-01-30 20:39:13 +08:00
884db8a8b5 chore: add patch for debug api,better workflow. 2023-01-30 20:19:44 +08:00
ee21b7bc37 chore: update gvisor 2023-01-29 22:30:40 +08:00
32c53b9584 chore: dns log error 2023-01-29 11:03:39 +08:00
4e5f3fbe84 Merge pull request #356 from kunish/Alpha
Chore: Remove missing image link, mention Yacd-meta in README.md
2023-01-29 00:24:37 +08:00
2ce193877b docs(README.md): remove missing image link, mention Yacd-meta 2023-01-29 00:17:33 +08:00
e52d599326 chore: better dns log 2023-01-28 22:33:03 +08:00
2cf66f41cb fix: parse error 2023-01-28 16:09:14 +08:00
a06b387acc adjust: VLESS enable XUDP by default 2023-01-28 14:58:52 +08:00
03520e0d6f Fix: dns api panic on disable dns section (#2498) 2023-01-28 00:55:30 +08:00
a6a72a5b54 Feature: add dns query json api 2023-01-28 00:55:24 +08:00
85db58aeb5 chore: update config.yaml 2023-01-28 00:32:17 +08:00
596bf32caa chore: adjust keyword for geosite-based nameserver policy 2023-01-28 00:19:58 +08:00
2b2644a76f chore: restful api display xudp for VLESS and VMess 2023-01-28 00:07:20 +08:00
02684a868f feature: geosite-based nameserver policy 2023-01-27 23:40:53 +08:00
1924b308fd chore: clear code 2023-01-27 17:10:15 +08:00
0d62e42c50 chore: better parsing pure UDP DNS 2023-01-27 17:02:58 +08:00
d3193cf8b7 Chore: Better parsing pure IPv6 UDP DNS 2023-01-27 15:08:05 +08:00
f7538568c0 Chore: Change default latency test url to HTTPS. 2023-01-27 13:41:23 +08:00
4629ecb8ee Chore: Add GEO data url configuration. 2023-01-27 13:27:39 +08:00
5bcea37d59 chore: better parse udp dns 2023-01-27 13:07:52 +08:00
6decaef050 fix: sub-rule condition don't work 2023-01-27 12:38:15 +08:00
248578086f feat: Converter support WS early data parameters 2023-01-27 11:31:58 +08:00
87553c6aa0 Update config.yaml 2023-01-26 23:19:33 +08:00
a2aa267e43 chore: update workflows docker 2023-01-25 20:53:39 +08:00
a563e9375e chore: better source address 2023-01-25 13:00:18 +08:00
9a4be1fbec Chore: Action ignore docs/**,README.md when push. 2023-01-24 21:56:17 +08:00
80f48518ca Chore: Update config.yaml 2023-01-24 21:50:21 +08:00
16c4b55e31 Chore: Decrease the default MaxUdpRelayPacketSize to 1252 to avoid the relay UDP exceeding the size of the QUIC's datagram.
ClientMaxOpenStreams now follows the config.yaml option.
2023-01-24 21:48:31 +08:00
023a96a6d3 make ConvertsV2Ray more robust (#349)
* make ConvertsV2Ray more  robust
* add log
* fix
2023-01-24 16:34:52 +08:00
39394e49ae chore: update config.yaml 2023-01-23 14:51:25 +08:00
b54ddc3aa9 chore: update config.yaml 2023-01-23 14:19:13 +08:00
97537bd185 chore: update config.yaml 2023-01-23 14:14:18 +08:00
1225173a43 chore: update config.yaml 2023-01-23 14:12:53 +08:00
096bb8d439 feat: add override-destination for sniffer 2023-01-23 14:08:11 +08:00
df1f6e2b99 feat: better config for sniffer 2023-01-23 13:16:25 +08:00
d1f5bef25d chore: better log 2023-01-23 11:17:30 +08:00
d426db43ec chore: adjust log 2023-01-23 11:14:45 +08:00
3bace07948 fix: ipv6 logic 2023-01-21 22:31:07 +08:00
24e31d0a40 Chore: Update paths-ignore 2023-01-21 14:42:48 +08:00
fb623c0929 chore: Correct the decision of enabling find process 2023-01-21 14:27:09 +08:00
4f641ce12d fix: ShadowTLS header use array instead 2023-01-20 17:35:49 +08:00
8cd1e40fb3 Update README.md 2023-01-20 17:13:32 +08:00
8a7027e8d6 Fix: Remove EnableProcess from config.go and enable-process from config.yaml.
Fix: FindProcess is now enabled by default when the rule set contains process-name rules.
2023-01-20 16:29:08 +08:00
5bbf73e3b5 chore: new Random TLS KeyPair when empty input 2023-01-18 12:06:36 +08:00
106a58779d chore: update quic-go 2023-01-17 22:06:21 +08:00
fa5b5ca02d fix: tcpTracker's upload 2023-01-17 21:36:16 +08:00
ba6163574e chore: better parseAddr 2023-01-17 15:41:51 +08:00
37eca8af24 fix: tuic server's MaxIncomingStreams 2023-01-17 14:25:19 +08:00
421c91a58c chore: update docker.yaml and Makefile docker 2023-01-17 12:43:51 +08:00
c90bf1c6e2 chore: Update const type 2023-01-17 12:33:15 +08:00
5b1de296af chore: Update dependencies 2023-01-17 12:26:31 +08:00
f4414566d3 fix: tuic server's SetCongestionController 2023-01-17 10:41:51 +08:00
db4f3eda55 fix: Add CC for TUIC server 2023-01-17 01:08:30 +08:00
f3b76df13b chore: Update BBR config
chore: Adjust workflow
2023-01-16 21:50:02 +08:00
bb79272020 chore: better workflow 2023-01-16 16:44:47 +08:00
926ef9e33d feat: gRPC gun implement extended writer 2023-01-16 15:54:20 +08:00
ead21f37d7 chore: better workflow 2023-01-16 15:09:25 +08:00
49a2602329 fix: add Upstream to refconn 2023-01-16 13:26:30 +08:00
e88bddc24f fix: addr panic 2023-01-16 12:47:22 +08:00
a5821e5785 fix: add ReaderReplaceable to BufferedConn, avoid buffered data lost 2023-01-16 12:28:30 +08:00
4e4d741075 chore: code cleanup 2023-01-16 12:11:34 +08:00
bec66e9e69 adjust: Improve WebSocket mask 2023-01-16 11:42:10 +08:00
50832aab47 chore: decrease direct depend on the sing package 2023-01-16 10:50:31 +08:00
643fdd0bce chore: tuic decrease unneeded copy 2023-01-16 09:55:06 +08:00
d1565bb46f refactor: Implement extended IO 2023-01-16 09:42:03 +08:00
8fa66c13a9 chore: better workflow 2023-01-15 21:51:33 +08:00
c0ffa06b95 chore: Update dependencies 2023-01-15 15:04:58 +08:00
3b53f5bca3 chore: better workflow 2023-01-15 15:04:27 +08:00
2c80155c6f Update Makefile
add CGO support for release build
add release.sh
2023-01-15 02:08:46 +08:00
8a9b3b3d59 fix: config parse error 2023-01-14 22:34:54 +08:00
5dd691aa95 fix: ss converter cipher missing 2023-01-14 21:37:10 +08:00
27ceae580a chore: update config.yaml 2023-01-14 21:34:26 +08:00
b6b6413d04 refactor: replace experimental.fingerprints with custom-certificates and Change the fingerprint verification logic to SSL pinning 2023-01-14 21:08:06 +08:00
2095f4f670 chore: update gitignore 2023-01-14 18:10:22 +08:00
606e8948c0 Fix: TLS defaults to true for h2/grpc networks 2023-01-14 16:20:58 +08:00
3b6fc1c496 chore: adjust the case of Program names and HttpRequest UA 2023-01-14 16:17:10 +08:00
f96bf65557 chore: Refine process code 2023-01-14 16:16:59 +08:00
804cff8c55 fix: skip-cert-verify is true by default (#333)
* fix: skip-cert-verify is true by default

* fix: format

* fix: typo

Co-authored-by: 3andero <3andero@github.com>
Co-authored-by: Hellojack <106379370+H1JK@users.noreply.github.com>
2023-01-13 09:55:01 +08:00
633b9c0426 chore: adjust Dockerfile 2023-01-12 02:13:22 +08:00
7d6991dd65 chore: adjust makefile 2023-01-12 01:31:38 +08:00
95247154d6 Fix: Deprecate TCPMSS (#336)
* 修改 DefaultTCPMSS 为 MaxDatagramSize
修改 MaxDatagramSize 的值提高 TUIC 的上传速度
2023-01-12 00:53:42 +08:00
be6142aa43 feat: VLESS support packet encodings (#334)
* adjust: Do not use XTLS on H2 connections

* feat: VLESS support XUDP fullcone NAT

* fix: VLESS with PacketAddr does not work

* fix: VLESS XUDP crash
2023-01-11 22:01:15 +08:00
0069513780 chore: shadowtls don't depend on trojan's code 2023-01-11 10:19:30 +08:00
0c9a23a53c fix: dns cache index out of range 2023-01-11 09:54:07 +08:00
0035fc2313 Update prerelease.yml 2023-01-11 00:50:04 +08:00
6f62d4d5c1 chore: update config.yaml 2023-01-11 00:28:21 +08:00
51f9b34a7c feat: Support ShadowTLS v2 as Shadowsocks plugin (#330) 2023-01-11 00:13:48 +08:00
337be9124f chore: clean code 2023-01-11 00:01:28 +08:00
dd4e4d7559 chore: ss2022 converter method verify 2023-01-10 21:55:36 +08:00
0f29c267be fix: Converter VMess XUDP not enabled by default when using v2rayN style share link 2023-01-10 20:47:58 +08:00
d38ceb78c9 chore: Refine converter packet encoding parse 2023-01-10 18:25:05 +08:00
0c354c748a fix: ss2022 converter password decode error 2023-01-10 18:13:18 +08:00
3a8e7c8899 chore: vemss converter xudp is true by default 2023-01-10 18:10:21 +08:00
261b8a1d06 fix: vmess udp 2023-01-10 13:21:32 +08:00
01d8b224db fix: vless RoutingMark bind 2023-01-09 23:15:17 +08:00
e9a7e104c0 fix: geoip mmdb/geodata init 2023-01-09 21:12:13 +08:00
b4503908df fix #322: add option general.find-process-mode, user can turn off findProcess feature in router
findProcess slow down connection due to repeat call to FindProcessName in router environment
this option has 3 values: always, strict, off
- always, equal to enable-process: true. Just try to merge all process related option into one
- strict, as default value, behavior remains unchanged
- off, turn off findProcess, useful in router environment
2023-01-09 19:48:39 +08:00
fd48c6df8a chore: Fix fmt in #321
Replace all double spaces to tabs due to Go fmt proposal.
2023-01-07 12:24:28 +08:00
cd7134e309 Merge pull request #321 from ag2s20150909/Alpha
proxy-provider and proxy-groups support exclude node by node type
2023-01-06 11:58:12 +08:00
5fa6777239 fix: Process rule is not work in classical rule-set 2023-01-04 21:18:07 +08:00
908d0b0007 Merge pull request #1 from ag2s20150909/fixConverter
fix converter error
2023-01-03 22:36:38 +08:00
8e6989758e fix converter error 2023-01-03 22:33:29 +08:00
29b72df14c proxy-groups support exclude node by node type 2023-01-03 21:47:57 +08:00
f100a33d98 proxy-provider support exclude node by node type 2023-01-03 21:27:07 +08:00
7a64c432b1 Merge branch 'dev' of https://github.com/Dreamacro/clash into Alpha 2023-01-01 13:03:30 +08:00
2301b909d2 Fix: immediately update provider when modtime too old 2022-12-31 16:32:30 +08:00
89680de12b fix: only wrap with SyscallConn() when currentConn implements syscall.Conn 2022-12-25 13:29:25 +08:00
a03af85a6b fix: trying to let hysteria's port hopping work 2022-12-23 11:00:55 +08:00
fbca37c42b Feature: REDIRECT support IPv6 (#2473) 2022-12-22 19:25:30 +08:00
4a57917783 Chore: skip cache acme challenge dns msg (#2469) 2022-12-22 13:30:23 +08:00
cdc7d449a6 Fix: safeConnClose not working (#2463) 2022-12-22 12:42:38 +08:00
daf0b23805 fix: some safeConnClose forget using original 2022-12-22 12:31:45 +08:00
d8ac82be36 Fix: broken build badge (#2470) 2022-12-22 12:09:24 +08:00
a6c144038b Chore: improve redir getorigdst 2022-12-22 12:00:56 +08:00
980454beb2 chore: cleanup code 2022-12-22 09:53:25 +08:00
63922f86a2 chore: linux ipv6 REDIRECT (#311) 2022-12-22 08:45:11 +08:00
22414ce399 chore: relay support tuic 2022-12-20 00:11:02 +08:00
7496d9c114 chore: rebuild relay 2022-12-19 21:34:07 +08:00
c63dd62ed2 chore: support relay native udp when using ss and ssr protocol 2022-12-19 17:02:04 +08:00
ff01d845b4 fix: try to fix gvisor panic 2022-12-17 11:03:37 +08:00
57592ee840 chore: better safeConnClose 2022-12-16 22:16:02 +08:00
432c4c2cf1 Merge pull request #302 from oluceps/fix-nix
Fix nix build fail
2022-12-15 13:54:45 +08:00
98b7377643 fix: spec Go 119 for nix build 2022-12-15 13:25:56 +08:00
287ec7e851 chore: update flake lock 2022-12-15 13:25:18 +08:00
8a2d1ec5a7 chore: better structure decoder 2022-12-13 21:13:31 +08:00
afb2364ca2 chore: wireguard's reserved support base64 input 2022-12-13 20:40:07 +08:00
9711390c18 fix: check conn is nil in safeConnClose 2022-12-13 18:09:19 +08:00
bffb0573a6 fix: safeConnClose not working 2022-12-13 13:20:40 +08:00
17cbbb5bf0 chore: split dns's dialContext and listenPacket 2022-12-13 12:38:46 +08:00
b3b5f17e03 chore: cleanup doh/doq's code 2022-12-13 11:23:34 +08:00
88acf8e098 fix: fix bindIfaceToListenConfig() in windows force bind to an ipv4 address 2022-12-13 11:18:32 +08:00
f87144f84b chore: add persistent-keepalive for wireguard 2022-12-13 08:35:01 +08:00
1333f1fd47 fix: fix wireguard outbound not work with ipv6 server 2022-12-13 08:23:17 +08:00
8fa6bd1743 fix: fix wireguard outbound not work with the sniffer 2022-12-11 23:53:53 +08:00
02d3468516 chore: Android version supports child processes following the main process rules 2022-12-11 18:24:55 +08:00
f657ac97f6 fix: add an unmap before is6 2022-12-11 15:48:37 +08:00
57dfaf135d fix: hysteria ipv6 outbound 2022-12-11 13:41:44 +08:00
9df42d7b98 fix: issue #292 2022-12-11 09:25:46 +08:00
b5928c36a3 fix: tunnel panic 2022-12-11 08:59:57 +08:00
910e7fed97 fix: parse DoH url 2022-12-08 12:46:46 +08:00
a9839abd4c fix: tun create panic 2022-12-08 11:29:39 +08:00
78c7b6259c chore: update config.yaml 2022-12-08 09:59:04 +08:00
a6f7e1472b fix: let doh/dot server follow hosts and can remotely resolve itself ip 2022-12-07 20:01:44 +08:00
e03fcd24dd fix: let any type of dns server can use remote proxy 2022-12-07 13:00:45 +08:00
cd99b2e795 fix: config code merge 2022-12-06 10:13:05 +08:00
b5b06ea49c fix: config crash 2022-12-06 09:04:30 +08:00
f7fb5840cf fix: reorder metadata 2022-12-06 08:23:30 +08:00
3b96d54369 chore: cleanup rules/logic code 2022-12-05 23:51:38 +08:00
f390b9cf2f fix: inbound nil pointer 2022-12-05 23:10:47 +08:00
1c65a2c1b4 chore: cleanup rule parse code 2022-12-05 22:29:52 +08:00
2d2b75a4bf doc: update tor listeners' tun 2022-12-05 21:32:08 +08:00
dcbe25c3ae chore: add log 2022-12-05 19:48:54 +08:00
46d23d9b86 chore: all listeners support specialProxy 2022-12-05 17:53:54 +08:00
fd9c4cbfa5 chore: listeners support tun 2022-12-05 17:43:50 +08:00
5c410b8df4 chore: listeners support tunnel 2022-12-05 17:03:12 +08:00
8c58d8a8ad fix: remove unneed listener's start 2022-12-05 16:29:50 +08:00
a0a2eb2106 chore: update dependencies 2022-12-05 11:03:28 +08:00
b7d976796a chore: listeners support shadowsocks/vmess 2022-12-05 10:12:53 +08:00
2e22c712af chore: rebuild add adapter/inbound.Addition to simply Listener.New apis 2022-12-05 00:20:50 +08:00
c7f83d3ff1 chore: listeners support tuic 2022-12-04 23:05:13 +08:00
62474e0ed6 fix: correct C.Metadata 2022-12-04 22:08:20 +08:00
62226e8b3d chore: rebuild InboundListener 2022-12-04 21:53:13 +08:00
8144373725 chore: support skip the same config Listeners' Close and Listen 2022-12-04 17:20:24 +08:00
e9d8dd09ac fix: close linstener 2022-12-04 15:15:23 +08:00
6fc62da7ae chore: change C.PacketAdapter from a struct to an interface 2022-12-04 14:37:52 +08:00
4f75201a98 feat: add linsters 2022-12-04 13:37:14 +08:00
ba884c29bd refactor: adjust config 2022-12-03 14:14:15 +08:00
2fe271f19f feat: add tls port for RESTful api and external controller 2022-12-03 12:25:10 +08:00
cf5709aab1 fix: a temporary solution for error reporting when enabling tun for devices that do not have an ipv6 environment. 2022-12-03 00:10:50 +08:00
654cdf3d5b chore: adjust fakeip test 2022-12-02 20:21:31 +08:00
6c79d9e63b chore: fixed v2fly v4 version for test 2022-12-02 20:21:15 +08:00
0aefa3be85 fix: remove cyclic dependent to make tuic's Finalizer work 2022-12-02 16:56:17 +08:00
bc5ab3120f fix: use same strClone function 2022-11-30 20:38:03 +08:00
df8e129fc6 chore: optimize DomainTrie for only one child 2022-11-30 19:42:05 +08:00
84caee94af chore: decrease DomainTrie's memory use 2022-11-30 18:50:46 +08:00
1d9e320087 test: cleanup the test code 2022-11-30 08:58:53 +08:00
2a3c4c1a33 fix: ensure pool's dial pass to client 2022-11-29 09:23:28 +08:00
8c0fbb3665 chore: restful api display fast-open for tuic and hysteria 2022-11-29 00:56:27 +08:00
9ea09b2b94 fix: tuic protocol error 2022-11-29 00:42:26 +08:00
db81db5363 Chore: allow tunnels don't set special proxy 2022-11-28 20:31:32 +08:00
e715ccbdd5 chore: update docs 2022-11-28 20:12:09 +08:00
bc94c50783 chore: update dependencies 2022-11-28 20:09:24 +08:00
b4b9ef2362 Merge branch 'dev' of https://github.com/Dreamacro/clash into Alpha 2022-11-28 20:04:56 +08:00
dd6f7e3701 fix: structure decode first do strict match 2022-11-28 19:11:55 +08:00
01e382285d chore: tuic-server support restful api patch 2022-11-28 18:53:09 +08:00
4b1d4a3e20 chore: tuic-server support heartbeat command 2022-11-28 18:28:22 +08:00
562819e3ca chore: tuic-server support disassociate command 2022-11-28 18:18:51 +08:00
551283c16e chore: add tuic-server listener 2022-11-28 17:09:25 +08:00
cd53e2d4a7 fix: set RequestTimeout in earlyConn 2022-11-27 16:38:41 +08:00
a58234f0cd chore: support KeyReplacer in Structure Decoder 2022-11-27 13:44:38 +08:00
c8d7243b5b fix: hop-interval of tysteria is optional too 2022-11-27 13:07:27 +08:00
6b1ca7b07c fix: ports of hysteria is optional 2022-11-27 12:52:14 +08:00
b80e7c3c92 fix: tuic client use pool-based bytes.Buffer 2022-11-27 12:06:04 +08:00
0da09c5ddd fix: quicStreamPacketConn's close 2022-11-27 11:42:43 +08:00
17c081a40c add support for hysteria udp port hopping (#269)
* add support for hysteria udp port hopping

* add ports field for hysteria

* change method for udp connection

Co-authored-by: geoleonsh <geoleonsh@live.com>
2022-11-27 11:09:56 +08:00
0647cee02a chore: update quic-go 2022-11-27 10:37:10 +08:00
423850a7aa chore: allow to set max-open-streams for tuic 2022-11-27 09:38:20 +08:00
896d30b151 chore: rebuild tuic client's code 2022-11-26 23:53:59 +08:00
495fd191f2 chore: clear config field name (be compatible with old field name) 2022-11-26 21:35:47 +08:00
ae76daf393 chore: tuic add fast-open support 2022-11-26 21:14:56 +08:00
8056b5573b chore: update github action 2022-11-26 20:11:43 +08:00
516623cbbb chore: update demo 2022-11-26 19:48:16 +08:00
a5ae2e891c feat: support fast_open for hysteria, and unified parameter naming 2022-11-26 19:37:32 +08:00
90b40a8e5a Fix: drop UDP packet which mismatched destination for VMess (#2410)
Co-authored-by: SUN Sizhe <sunsizhe@cmi.chinamobile.com>
2022-11-26 11:27:24 +08:00
7f40645934 chore: tuic use a udp pool too and auto close when outbound is garbage collected 2022-11-25 22:42:28 +08:00
6c204d2b77 chroe: wrong variable 2022-11-25 20:53:08 +08:00
ed988dcdc5 Chore: update dependencies 2022-11-25 20:42:28 +08:00
7b44cde4bd chore: tuic use a simple client pool 2022-11-25 20:14:05 +08:00
c7bad89af3 fix: tuic better stream close 2022-11-25 19:14:09 +08:00
21a91e88a1 fix: tuic set MaxOpenStreams 2022-11-25 18:32:30 +08:00
76d2838721 chore: split tuic's tcp and udp client 2022-11-25 17:15:45 +08:00
9b1fe9f466 fix: tuic stream close 2022-11-25 16:06:56 +08:00
9976800a35 fix: tuic err handle 2022-11-25 13:03:36 +08:00
f542351404 chore: tuic add max_udp_relay_packet_size 2022-11-25 12:43:23 +08:00
a13dedb6e4 fix: tuic panic 2022-11-25 12:10:33 +08:00
d47ce79a24 chore: better tuic conn close 2022-11-25 11:32:52 +08:00
cce42b4b83 fix: prefer ipv6 not working 2022-11-25 11:12:22 +08:00
142d17ebad fix: don't close tuic when read timeout 2022-11-25 11:04:28 +08:00
30ca59dab7 fix: tuic typo 2022-11-25 10:45:06 +08:00
c89b1f0e96 chore: tuic add cubic,new_reno,bbr congestion_controller 2022-11-25 10:33:37 +08:00
59bd11a3a7 chore: add tuic outbound
close #133
2022-11-25 08:08:14 +08:00
3880c3c1be chore: add retry in tunnel dial 2022-11-24 12:32:35 +08:00
efa4b9e0b8 Fix: lint warning 2022-11-22 21:01:51 +08:00
8c6e205c5a Fix: tunnel proxy match 2022-11-22 19:16:08 +08:00
d478728cb7 fix: geosite match 2022-11-21 10:33:42 +08:00
5b07d7b776 Feature: add tunnels 2022-11-20 21:30:55 +08:00
18d62c4a17 fix: catch context.DeadlineExceeded too 2022-11-19 23:16:20 +08:00
02830e0ad6 fix: adjust log 2022-11-19 23:07:49 +08:00
6d89bddf29 fix: better error return 2022-11-19 23:06:27 +08:00
dbbd499349 fix: better error check 2022-11-19 23:03:14 +08:00
d3562ce394 fix: DoH recreate the connection multiple times 2022-11-19 22:48:04 +08:00
d5973cf8a6 chore: Adjust error of dialer 2022-11-19 10:57:33 +08:00
1d3cc36eef chore: return context error 2022-11-19 10:50:13 +08:00
8fcfecbed1 chore: Adjust the return 2022-11-19 10:47:03 +08:00
7c1b878c3f fix: resolver's ctx 2022-11-19 10:45:44 +08:00
4ea4221380 fix: rollback batchExchange's code 2022-11-19 10:35:45 +08:00
b8b3c9ef9f fix: DoH/DoQ doesn't use context 2022-11-19 10:31:50 +08:00
f00dc69bb6 fix: doh use NewRequestWithContext and batchExchange don't wait cancel finish 2022-11-19 09:43:31 +08:00
23f286f24e fix: auto retry in exchangeWithoutCache 2022-11-19 08:51:00 +08:00
16f8f77f5d fix: better wireguard error handle 2022-11-18 19:40:39 +08:00
dfc0ec995c fix: wireguard handle conn is nil 2022-11-18 19:32:12 +08:00
8b848b62bb fix: reset timeout in exchangeWithoutCache's singleflight 2022-11-18 18:02:46 +08:00
2dc62024fe chore: support old chacha20 2022-11-16 18:37:14 +08:00
994e85425f fix: resolver's defer 2022-11-16 17:53:52 +08:00
1880a485f8 chore: better tfo inbound code 2022-11-16 10:43:16 +08:00
03645fb235 fix: correct the go.mod 2022-11-16 10:18:42 +08:00
eb8431255d fix: sing-shadowsocks serverConn.Write return (0,nil) when p isn't empty 2022-11-16 10:18:42 +08:00
e5a81b6c35 fix: don't use ReadOnceFrom in sing-shadowsocks 2022-11-16 10:18:42 +08:00
0eecd11fdc ss and vmess inbound add tfo 2022-11-16 10:18:10 +08:00
9c8e39827f update tfo to v2 2022-11-16 10:18:10 +08:00
586dec5ba3 Merge commit '2a8e1778ad1d7b507e432f659407d76dffcaacb8' into Alpha 2022-11-14 20:17:52 +08:00
6db7c800d5 fix: DoQ and HTTP/3 over proxy 2022-11-14 20:17:12 +08:00
2a8e1778ad chore: sync tunnel code 2022-11-12 21:42:45 +08:00
a3425c0e78 chore: sync dns code 2022-11-12 21:31:07 +08:00
7300c917dc fix: build error 2022-11-12 20:59:29 +08:00
dc3e144b6a Merge branch 'dev' of https://github.com/Dreamacro/clash into Alpha 2022-11-12 20:43:48 +08:00
75d339392b chore: better dns background fetch retrying 2022-11-12 18:29:19 +08:00
901a47318d chore: always pass context when resolve dns 2022-11-12 13:18:36 +08:00
dbadf37823 chore: update listeners config 2022-11-12 12:36:59 +08:00
3321ac95ca fix: cleanup import 2022-11-12 12:31:44 +08:00
c0bd4af120 chore: update dns config 2022-11-12 12:12:31 +08:00
d78b2b1cfb Merge pull request #256 from Skimmle/Alpha
featrue: DoH and DoQ are implemented using AdGuardTeam/dnsProxy
2022-11-12 11:19:04 +08:00
3e20912339 featrue: DoH and DoQ are implemented using AdGuardTeam/dnsProxy, DoH support perfer and force http3 2022-11-12 11:14:51 +08:00
b2d7149a95 chore: support IN-PORT rule 2022-11-11 23:36:06 +08:00
64be213b66 code cleanup 2022-11-11 22:48:44 +08:00
68b28ed530 chore: shadowsocks listener support old cipher 2022-11-11 22:44:44 +08:00
3eacce9a66 chore: add vmess, shadowsocks, tcptun and udptun listener 2022-11-11 20:56:08 +08:00
6dadc2357a chore: remove AddrType on Metadata 2022-11-11 09:19:50 +08:00
698d8ca701 Update README.md 2022-11-11 04:05:13 +08:00
1a4b00c70e fix: update sing-vmess 2022-11-10 21:23:52 +08:00
64552fbd00 fix: when host's ip in fakeip's range, don't send to remote server 2022-11-10 21:08:06 +08:00
7c8d8f56e1 Update docs/config.yaml 2022-11-09 22:55:32 +08:00
93ada8989f Update README.md 2022-11-09 19:55:30 +08:00
4b4c3dc41e fix: small-case import name 2022-11-09 19:42:56 +08:00
b699fb046b fix: wireguard's dns resolve 2022-11-09 19:35:03 +08:00
ae08d13de4 chore: support wireguard outbound 2022-11-09 18:44:06 +08:00
1d784231b0 fix: exclude-filter not work when filter is empty 2022-11-09 08:41:30 +08:00
5fd79890e7 chore: add exclude-filter to ProxyGroup 2022-11-09 08:06:37 +08:00
53b2a480ef fix: subscriptionInfo api 2022-11-08 22:30:50 +08:00
943137de3b Merge pull request #250 from Skimmle/Alpha
chore: upgrade dependencies
2022-11-08 15:58:21 +08:00
2d3aad573e chore: upgrade dependencies 2022-11-08 15:50:01 +08:00
409cd4f6a1 fix: subscriptionInfo api
fix: subscriptionInfo api

fix: subscriptionInfo api
2022-11-08 07:59:08 +08:00
bd526ad0a1 chore: adjust tun config
Update config.yaml

chore: adjust tun demo
2022-11-07 18:33:27 +08:00
4673d2093a fix: context import 2022-11-06 08:43:39 +08:00
94a765ee31 fix: avoid choose ZeroTier's tap to defaultInterface 2022-11-05 20:51:28 +08:00
dcd2417fce feat: subscriptionInfo 2022-11-05 19:39:17 +08:00
4c5853e5e7 feat: Converter Shadowsocks UoT support 2022-11-05 06:41:07 +00:00
52f4cb599a fix: pool_test.go 2022-11-05 13:08:50 +08:00
90f6cc233c fix: correct yaml config name 2022-11-04 17:38:24 +08:00
de264c42a8 Chore: update test dependencies 2022-11-04 13:31:20 +08:00
c2469162fb Chore: update dependencies 2022-11-04 13:28:51 +08:00
19b7c7f52a Fix: a shared fastSingle.Do() may cause providers untouched (#2378) 2022-11-04 13:11:01 +08:00
e20d01a679 chore: try to let tun's restful patch api work 2022-11-04 08:52:30 +08:00
9a5c0a4b6d chore: better tun config passing 2022-11-03 18:56:03 +08:00
1b0d09068b fix: RESTful API empty tun device name 2022-11-03 18:04:37 +08:00
3373b62b02 fix: try to support android hotspot when using tun 2022-11-03 12:58:21 +08:00
508e257543 fix: RESTful API sniffingEnable 2022-11-03 00:31:31 +08:00
7b0cd14b00 chore: netlink duplicate contains 2022-11-02 23:58:51 +08:00
22fb219ad8 chore: trie.DomainTrie will not depend on zero value 2022-11-02 22:28:18 +08:00
c34c5ff1f9 build: fix golang build cache 2022-11-02 22:13:54 +08:00
4e5bdec13a Fix: amd64 macOS Ventura process name match 2022-11-02 11:43:43 +08:00
fbd43d9947 Fix: macOS Ventura process name match 2022-11-02 11:38:31 +08:00
5dab89c9ec fix: group filter add not matched proxies at the end 2022-10-31 21:50:30 +08:00
2a24effac0 chore: better UrlTest's torch 2022-10-31 16:58:29 +08:00
972d3f1d39 fix: UrlTest's torch not work
close #232
2022-10-31 16:45:14 +08:00
a7aa5fd523 adjust: add some log for healthcheck debug 2022-10-31 16:04:50 +08:00
b9d8b69889 fix: lazy check 2022-10-30 23:08:18 +08:00
dedb9122df chore: support multi filter in GroupBase too 2022-10-30 22:30:54 +08:00
0e5bf0c27e chore: support multi filter like subconverter in ProxyProvider and add exclude-filter to ProxyProvider 2022-10-30 21:04:33 +08:00
a46436f61a chore: parse user's hosts before remoteDial 2022-10-29 09:03:00 +08:00
6106adc6a5 fix: hysteria converter 2022-10-27 18:37:27 +08:00
c8bc11d61d Fix: amd64 macOS Ventura process name match 2022-10-27 15:36:09 +08:00
f29b54898f Fix: macOS Ventura process name match 2022-10-27 11:25:18 +08:00
fc693bc257 chore: SUB-RULE 2022-10-23 16:54:50 +08:00
bba5c2cc8a Update config.yaml 2022-10-21 01:21:15 +08:00
0fb0e490f8 fix: when connection refused active health test 2022-10-16 13:12:49 +08:00
023e3d0c41 chore: add parse-pure-ip in sniffer 2022-10-14 08:42:28 +08:00
c11a359761 chore: retrying for "Cannot create a file when that file already exists." 2022-10-14 08:27:34 +08:00
0da49bd92b chore: add force-dns-mapping in sniffer 2022-10-14 07:46:33 +08:00
b9ef713dd7 chore: add sing-tun's custom route support 2022-10-13 19:26:14 +08:00
4948f3f213 chore: Cache and skip multiple failed addresses 2022-10-11 21:35:26 +08:00
7b1427b843 fix: set default tun udp timeout to 5 minutes 2022-10-10 22:10:36 +08:00
77a3c1c3ae fix: tun stack shown 2022-10-10 19:02:57 +08:00
2c236387b7 fix: flush default interface when tun config hasn't change 2022-10-10 09:32:42 +08:00
66e5136ba0 fix: correct sing-tun's rAddr 2022-10-09 13:16:13 +08:00
f748e3632d fix: fakeip pool test 2022-10-09 11:07:24 +08:00
90688b238a fix: try let fakeip mode get real destination ip 2022-10-09 10:48:26 +08:00
986c91b5c8 Merge pull request #207 from oluceps/add-tags
add: with_gvisor tag for nix build
2022-10-08 15:47:48 +08:00
8c13426492 add: with_gvisor tag for nix build 2022-10-08 15:40:41 +08:00
fefd9b7427 Merge pull request #206 from oluceps/update-sha256
Update vendorSha256
2022-10-08 15:06:29 +08:00
8a55208c62 chore: update vendorSha256 due to dependencies change 2022-10-08 13:11:25 +08:00
1f8b54a92d fix: don't set auto detect interface with tun name 2022-10-07 16:54:08 +08:00
8d74a86bf1 fix: macos's tunName 2022-10-07 06:57:03 +08:00
6c82e98bbc chore: fix sing-tun's BuildAndroidRules 2022-10-06 22:18:49 +08:00
94246104b8 chore: use sing-tun to replace old tun_adapter 2022-10-06 19:23:38 +08:00
347e5e9606 fix: dns tcp hijack not working 2022-10-05 13:29:10 +08:00
e4138c3e1e chore: add description 2022-10-04 22:16:03 +08:00
f2b5ae6894 Merge remote-tracking branch 'meta/Alpha' into Alpha 2022-10-03 22:41:40 +08:00
0a89107b8b fix: global fingerprints load failed 2022-10-03 22:41:24 +08:00
2a323f77ce Merge pull request #203 from oluceps/Alpha
add: current version and BuildTime for nix build
2022-10-03 13:19:04 +08:00
59edcf33bd feat: Add VMess global padding support 2022-10-02 22:42:33 +08:00
35506e179a chore: Unify config field name style 2022-10-02 21:46:01 +08:00
6ce3805719 feat: Converter support packet encodings for VMess 2022-10-02 21:10:29 +08:00
c1a82f2fae chore: fix doc 2022-10-02 20:54:51 +08:00
4f9478a336 chore: adjust doc 2022-10-02 20:53:52 +08:00
e1dc2681f1 chore: adjust doc 2022-10-02 20:48:50 +08:00
ce77c3fd8b chore: add SUB-RULE demo 2022-10-02 20:47:43 +08:00
9f1194056a Merge branch 'udp' into Alpha 2022-10-02 20:28:44 +08:00
b82c9ba190 chore: remove buffer for relay 2022-10-02 20:28:31 +08:00
6857b05039 chore: pure udp metadata 2022-10-02 20:08:41 +08:00
abbbcb02c0 Merge branch 'Alpha' into dev 2022-10-02 20:02:39 +08:00
ec6144250d add: current version and BuildTime for nix build 2022-10-02 18:29:20 +08:00
89d1222b8f rm: default.nix 2022-10-02 18:20:26 +08:00
de4985a9b6 Merge pull request #202 from oluceps/refactor_flake
Refactor flake
2022-10-02 02:23:57 +08:00
6dd8cf6c0a refactor: multi-platform support for nix build 2022-10-02 02:15:37 +08:00
6b2eae36f2 adjust: gvisor version 2022-10-02 01:00:07 +08:00
13445d815b add: flake auto track latest commit 2022-10-02 00:18:35 +08:00
e2d71abecd Fix: handle parse socks5 udp address properly (#2220)
(cherry picked from commit bec4df7b12)
2022-10-01 23:45:06 +08:00
ddf1c74091 adjust: routes for windows 2022-10-01 23:30:41 +08:00
1bcc916807 chore: upgrade dependencies 2022-10-01 23:09:23 +08:00
c06b48af0e Merge pull request #201 from oluceps/add_flake
add: flake.nix and other required files for nix build
2022-10-01 22:44:20 +08:00
7e9549c05a add: flake.nix and other required files for nix build 2022-10-01 22:34:39 +08:00
Pan
3e2b08f9d0 Chore: upgrade go.mod go version to 1.19 (#2331) 2022-09-29 11:47:30 +08:00
fb85691fb9 Fix: uncorrect README link (#2325) 2022-09-27 14:22:21 +08:00
1684756b79 Merge pull request #191 from StashNetworks/patch-1
Chore: compatible with Stash hysteria config
2022-09-21 23:54:40 +08:00
88e4a9a755 Chore: compatible with Stash hysteria config 2022-09-21 23:42:33 +08:00
d411394482 Chore: rename linux-armv8 to linux-arm64, windows-arm32v7 to windows-armv7 2022-09-21 21:18:24 +08:00
827d5289bc Refactor: improve Dockerfile (#2246) 2022-09-21 21:09:11 +08:00
b133bc58f0 fix: upgrade sing-vmess to let xudp work 2022-09-21 19:06:13 +08:00
82c9a1a2bb chore: add xudp support for vmess 2022-09-19 18:26:43 +08:00
e1ec4a2502 fix: wrong host shown when using uot 2022-09-19 17:37:16 +08:00
b6dc539105 Merge pull request #189 from sjtuross/Alpha
Add iptables package to docker
2022-09-19 00:07:30 +08:00
6e8d8befb8 docker: add iptables package 2022-09-18 23:19:25 +08:00
6995e98181 Refactor: linux process resolving (#2305) 2022-09-18 12:53:51 +08:00
4597ed49cf fix: adjust sub_rule to logic package, and fix not rule failed 2022-09-11 16:19:42 +08:00
ef2f8317c7 Fix: wechat protocol is not working if no obfs string is configured 2022-09-11 15:24:56 +08:00
9b89ff9f2d feat: support sub-rule, eg.
rules:
  - SUB-RULE,(AND,((NETWORK,TCP),(DOMAIN-KEYWORD,google))),TEST2
  - SUB-RULE,(GEOIP,!CN),TEST1
  - MATCH,DIRECT

sub-rules:
  TEST2:
    - MATCH,Proxy
  TEST1:
    - RULE-SET,Local,DIRECT,no-resolve
    - GEOSITE,CN,Domestic
    - GEOIP,CN,Domestic
    - MATCH,Proxy
2022-09-06 17:30:35 +08:00
4f291fa513 Chore: show the source ip in log (#2284)
Co-authored-by: Li Feng <fengli@smartx.com>
2022-09-02 16:59:00 +08:00
22b9befbda Fix: fake ip pool offset calculate (#2281) 2022-09-01 11:33:47 +08:00
a9694fcdc0 chore: update doc 2022-08-30 15:59:52 +08:00
d823dde43c chore: update doc 2022-08-29 13:07:38 +08:00
d69e0bce4a fix: resolve ip of udp proxy error 2022-08-29 13:04:48 +08:00
7f197ede51 fix: hysteria udp crash 2022-08-29 12:10:46 +08:00
af97922e94 fix: no main result conn, will fail 2022-08-28 20:26:13 +08:00
db94dc76b4 fix: udp default resolve ip 2022-08-28 15:57:10 +08:00
99effb051b feat: add ip-version param 2022-08-28 13:41:43 +08:00
425b6e0dc0 Chore: update README (#2276) 2022-08-27 12:16:25 +08:00
2516169f61 Chore: update dependencies 2022-08-26 21:18:16 +08:00
a3281712e2 Chore: reduce dhcp dns client cost 2022-08-24 21:36:19 +08:00
bf079742cb Clean: use go 1.19 Appendf 2022-08-24 20:21:06 +08:00
98f4f4d6c4 chore: log error 2022-08-23 20:12:28 +08:00
42e489e199 Merge remote-tracking branch 'origin/Alpha' into Alpha
# Conflicts:
#	component/sniffer/http_sniffer.go
2022-08-22 23:22:26 +08:00
d3b88d1b4f fix: ebpf support 2022-08-22 23:17:41 +08:00
ec318f1cc5 Chore: the default sniffing is changed to a standard port, and the sniffing result is only used for this connection. 2022-08-21 08:43:57 +08:00
6cfae6919f Merge pull request #167 from H1JK/Alpha
feat: Update Converter
2022-08-20 02:32:44 +08:00
b23a071050 feat: Converter VMessAEAD share link standard support 2022-08-19 22:00:22 +08:00
732e82e3d0 fix: Converter VMess security field typo 2022-08-19 21:17:44 +08:00
fecbc7a091 chore: Clean converter code and add doc 2022-08-19 19:46:50 +08:00
4b39362039 chore: Skip initial "lan" rules that load geoip 2022-08-17 12:42:33 +08:00
835cab58cf fix: http sniffer skip ip 2022-08-17 12:41:36 +08:00
6e058f8581 Chore: remove old cache implementation 2022-08-17 11:43:20 +08:00
9317dd610b chore: Skip initial "lan" rules that load geoip 2022-08-17 00:33:03 +08:00
8dc56b56ad fix: http sniffer skip ip 2022-08-16 22:59:53 +08:00
4611fbfe0c chore: disable tcp_test 2022-08-15 15:52:03 +08:00
b725c91b05 chore: clean code 2022-08-15 15:46:07 +08:00
482062376e fix: Temporarily delete marking node alive as false when error occurs 2022-08-13 18:25:28 +08:00
32fc990c68 fix: Unhandled dns resolve failure error 2022-08-13 16:47:24 +08:00
3946d771e5 Feature: sync missing resolver logic from premium, but still net.IP on opensource 2022-08-13 13:07:35 +08:00
5940f62794 Chore: http2 should use DialTLSContext and some tls handshake should with context 2022-08-13 12:35:39 +08:00
71cad51e8f Fix: satisfy RFC4343 - DNS case insensitivity (#2260) 2022-08-12 13:47:51 +08:00
dfeb901417 Fix: no_gviosr tags 2022-08-12 12:53:11 +08:00
02933ae568 Fix: nil pointer 2022-08-12 12:49:35 +08:00
4ca2d4146b Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-08-12 03:36:15 +08:00
5d97a7f9ca Chore: clean code 2022-08-12 03:35:49 +08:00
6eab1f158a Fixed: gViosr func 2022-08-12 03:34:59 +08:00
65a289e16f Chore: clean code 2022-08-12 03:04:58 +08:00
637707e58f Chore: Migration 1.19 2022-08-12 00:07:13 +08:00
95e602bf3b Chore: gVisor use bufferv2 2022-08-11 23:47:45 +08:00
4ac192f520 Chore: update badges 2022-08-11 23:45:50 +08:00
473d0f74bd fix: remove extra and the actual original IDNA domain name is no longer stored, for reduce memory 2022-08-11 21:50:16 +08:00
93ea1248e3 Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-08-08 10:28:18 +08:00
97270dcbe0 rm EBpf tun && disable android ebpf 2022-08-08 10:21:16 +08:00
50105f0559 Migration: go1.19 2022-08-07 21:45:50 +08:00
50cc274b2e Merge pull request #151 from H1JK/update-converter2
fix: Converter error when VMess `aid` field not exists
2022-08-07 20:47:32 +08:00
3867329ef3 fix: Converter error when VMess aid field not exists 2022-08-07 20:43:11 +08:00
6648793e40 Chore: reenable latest golangci-lint 2022-08-05 10:52:36 +08:00
2899a126fc fix filepath undefined 2022-08-02 17:13:10 +08:00
5391425123 Merge branch 'dev' into Alpha 2022-08-01 22:13:46 +08:00
1e7af0bbc7 fix: repeat set http status 2022-08-01 22:12:36 +08:00
dd67a8c8ba Merge remote-tracking branch 'origin/Alpha' into Alpha 2022-08-01 18:07:09 +08:00
28ba9c5efa revert 9be70f67ca 2022-08-01 18:06:09 +08:00
bb413ece49 Merge pull request #144 from zhudan/ebpf
support ebpf
2022-08-01 17:20:14 +08:00
31f4d20477 support ebpf 2022-07-29 09:08:35 +08:00
95e3a88608 Chore: update bug_report.yml (#2240) 2022-07-28 20:27:53 +08:00
bec4df7b12 Fix: handle parse socks5 udp address properly (#2220) 2022-07-25 12:44:00 +08:00
93400cf44d Fix: ALPN should on DoH instead of DoT (#2232) 2022-07-25 12:41:22 +08:00
57a15088c2 update config demo 2022-07-25 09:27:31 +08:00
be6d55e5c4 Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-07-24 01:50:16 +08:00
09419d88af fix process code 2022-07-24 01:50:10 +08:00
9c4bae6f23 fix process code 2022-07-24 01:38:00 +08:00
b74320008a Merge remote-tracking branch 'Meta/Alpha' into Alpha 2022-07-24 01:37:06 +08:00
35b87e79a7 fix process code 2022-07-24 01:37:01 +08:00
9be70f67ca fix process code 2022-07-24 01:34:22 +08:00
bb86098235 fix process code 2022-07-24 01:32:22 +08:00
dec32da262 clean code 2022-07-24 01:07:30 +08:00
a33e511c12 Fix: macOS udp find process should use unspecified fallback 2022-07-24 00:10:01 +08:00
d71a2ce61e Fix: fakeip udp should not replace with another ip 2022-07-24 00:02:45 +08:00
133bb2319f Chore: load balance hash need to have fallback strategy 2022-07-23 23:59:13 +08:00
7d84a47683 Chore: load balance hash need to have fallback strategy 2022-07-23 23:51:42 +08:00
38e6b81d07 Merge pull request #129 from zhudan/Alpha
入站增加TFO支持(默认不开启)
2022-07-22 06:32:34 -04:00
143c5de51d inbound tfo 2022-07-22 15:16:09 +08:00
3e424dea7b refactor: DoH use fragment setting params 2022-07-21 21:40:28 +08:00
a794819869 Chore: upgrade actions and fixed golangci-lint version 2022-07-21 15:15:14 +08:00
b0fd50453a fix: DoT-ALPN error 2022-07-21 13:57:06 +08:00
fe3ad3724c fix: resolver error handling exception 2022-07-21 09:02:58 +08:00
e1c6142851 fix: pure ip resolve 2022-07-20 22:59:04 +08:00
6e7002dbd3 chore: clean code 2022-07-20 17:15:19 +08:00
6a4063af0d refactor: optimize nodes caching 2022-07-20 08:53:54 +08:00
6b636c051a chore: Adjust the falling logic 2022-07-16 19:52:51 +08:00
9a035d3c51 fix: no_gvisor compile failed for target linux 2022-07-16 19:35:52 +08:00
850c52d07c chore: log level should be setting after launched 2022-07-16 13:33:27 +08:00
10f8f5dd7b chore: Increase idle timeout and add keep alive period 2022-07-15 21:57:50 +08:00
4e272ff066 fix: DoH retry HTTP/3 2022-07-15 21:54:57 +08:00
a73e690172 fix: DoQ closes udp immediately. 2022-07-15 21:54:02 +08:00
be8d63ba8f Fix: macOS udp find process should use unspecified fallback 2022-07-15 17:00:41 +08:00
3b90e18047 Chore: update test dependencies 2022-07-15 16:07:18 +08:00
947d9d4560 chore: clean up code 2022-07-13 22:27:49 +08:00
179bc6ecdf chore: clean up code 2022-07-13 22:20:14 +08:00
fbabcfce94 fix: CA params convert to fingerprint 2022-07-12 14:32:34 +08:00
3a92ad47e7 fix: default nameserver cannot use doh of pure IP 2022-07-12 13:05:59 +08:00
92a20a5362 chore: tcp conn error text 2022-07-11 22:29:35 +08:00
9565b5194c chore: remove log 2022-07-11 22:18:24 +08:00
23b2f3b971 Merge branch 'provider' into Alpha 2022-07-11 22:17:56 +08:00
f93dd6052e fix: default nameserver cannot use non-standard port of doh 2022-07-11 22:05:37 +08:00
80df572b18 refactor: Unified provider loading resources 2022-07-11 21:30:34 +08:00
0c64d7e56a chore: fingerprint style 2022-07-11 13:44:27 +08:00
a8ce283727 feat: add fingerprint param 2022-07-11 13:42:28 +08:00
ab8e9e7d7a fix: skip-cert-verify not work 2022-07-11 12:37:27 +08:00
dbce268692 feat: Prepare to specify the fingerprint function 2022-07-10 21:56:33 +08:00
fef9f95e65 feat: add fingerprint for tls verify 2022-07-10 20:44:24 +08:00
f0952b55d0 Fix: query string parse on ws-opts (#2213) 2022-07-10 14:56:34 +08:00
8c7c8f4374 Chore: update dependencies 2022-07-07 22:15:50 +08:00
60e1947ed2 chore: upgrade dependencies for hysteria 2022-07-07 12:49:52 +08:00
5b35822945 Merge branch 'Alpha' into dev 2022-07-07 12:23:09 +08:00
0a76876764 fix: h3 of doh fall back logic 2022-07-06 21:25:25 +08:00
e382496e4c Merge branch 'h3' into Alpha 2022-07-06 20:54:10 +08:00
0c91a4e0f3 refactor: h3 for doh 2022-07-06 20:53:34 +08:00
65a8e8f59c Fix: process rule type (#2206) 2022-07-06 13:44:04 +08:00
5497adaba1 Fix: fakeip udp should not replace with another ip 2022-07-05 21:09:29 +08:00
56fae0b1f5 chore: reduce wrapper 2022-07-05 21:00:41 +08:00
aaf08dadff Change: remove AddrType on Metadata (#2199) 2022-07-05 20:26:43 +08:00
557297ac9a Chore: load balance hash need to have fallback strategy 2022-07-04 21:36:33 +08:00
baee951657 fix: close idle connections 2022-07-04 20:38:07 +08:00
253dc24e40 chore: clash.mini hack. 2022-07-04 18:53:24 +08:00
503b1efd8a fix: close transport with doh of h3 2022-07-03 23:01:49 +08:00
50f2ecbcb0 chore: upgrade dependencies 2022-07-03 22:58:03 +08:00
97e158989f Merge branch 'h3' into Alpha
# Conflicts:
#	go.mod
2022-07-03 22:55:26 +08:00
e732fbb414 chore: add prefer-h3 into config.yaml 2022-07-03 22:53:49 +08:00
f8a168e64d Merge branch 'hy' into Alpha 2022-07-03 22:51:33 +08:00
e599621a32 fix: resolve ipv6 error in hysteria 2022-07-03 22:51:20 +08:00
59ab2083aa feat: try h3 connect DOH, failed will fall back h2; turn on with dns.prefer-h3: true 2022-07-03 21:59:47 +08:00
2d7c4eda66 Merge branch 'hy' into Alpha 2022-07-03 18:27:14 +08:00
3cc1870aee chore: embed hysteria, clean irrelevant codes, code from https://github.com/HyNetwork/hysteria 2022-07-03 18:26:46 +08:00
8eec86232c chore: add config.yaml demo 2022-07-02 13:44:04 +08:00
77a1e3a653 Chore: cleanup bind mark code 2022-06-30 17:27:57 +08:00
27e1d6cdae Chore: cleanup code 2022-06-30 17:12:06 +08:00
91c22b16bf Fix: proxy provider filter validation (#2198) 2022-06-30 17:08:53 +08:00
fc5c9b931b Fix: try to unmap lAddr on tproxy udp listener 2022-06-29 23:36:45 +08:00
8ce9737f3d Update dependencies 2022-06-28 08:15:03 +08:00
6b44178108 Fix concurrency vmess udp write 2022-06-28 08:12:56 +08:00
6664547f43 chore: upgrade dependencies 2022-06-26 22:37:59 +08:00
10383e2701 Merge branch 'dev' into Alpha 2022-06-26 21:53:03 +08:00
f4b9f2965f fix: hysteria dial use external context 2022-06-26 21:52:22 +08:00
2ba933d16a chore: hysteria params verify 2022-06-25 12:43:47 +08:00
669961e496 fix: proxy provider force update on init 2022-06-25 12:42:52 +08:00
f979491013 fix: tcp concurrent force close when context done 2022-06-25 09:16:53 +08:00
0d55b28805 chore: dns interface name 2022-06-25 09:16:51 +08:00
9c70e649ca fix: disable doq skip verify cert 2022-06-25 09:16:49 +08:00
8c079bf5bc fix: tcp concurrent force close when context done 2022-06-25 09:16:28 +08:00
2cdf4a0532 chore: RESTful test group use request context 2022-06-25 08:53:11 +08:00
4ba34ce672 chore: healthcheck only once check at same time 2022-06-25 08:53:04 +08:00
637f1b5aed ClashX hack. (#102) 2022-06-24 20:08:33 +08:00
56a87125e0 Merge remote-tracking branch 'Meta/Alpha' into Alpha
# Conflicts:
#	common/convert/converter.go
2022-06-23 00:55:34 +08:00
6fedc8d942 fix: Converter for password of ss2022
fix: Converter for password of ss2022

fixup! fix: Converter for password of ss2022 and ws

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

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

26
.github/release.sh vendored Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
FILENAMES=$(ls)
for FILENAME in $FILENAMES
do
if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then
gzip -S ".gz" $FILENAME
elif [[ $FILENAME =~ ".exe" ]];then
zip -m ${FILENAME%.*}.zip $FILENAME
else echo "skip $FILENAME"
fi
done
FILENAMES=$(ls)
for FILENAME in $FILENAMES
do
if [[ $FILENAME =~ ".zip" ]];then
echo "rename $FILENAME"
mv $FILENAME ${FILENAME%.*}-${VERSION}.zip
elif [[ $FILENAME =~ ".gz" ]];then
echo "rename $FILENAME"
mv $FILENAME ${FILENAME%.*}-${VERSION}.gz
else
echo "skip $FILENAME"
fi
done

26
.github/rename-cgo.sh vendored Normal file
View File

@ -0,0 +1,26 @@
#!/bin/bash
FILENAMES=$(ls)
for FILENAME in $FILENAMES
do
if [[ $FILENAME =~ "darwin-10.16-arm64" ]];then
echo "rename darwin-10.16-arm64 $FILENAME"
mv $FILENAME clash.meta-darwin-arm64-cgo
elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then
echo "rename darwin-10.16-amd64 $FILENAME"
mv $FILENAME clash.meta-darwin-amd64-cgo
elif [[ $FILENAME =~ "windows-4.0-386" ]];then
echo "rename windows 386 $FILENAME"
mv $FILENAME clash.meta-windows-386-cgo.exe
elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then
echo "rename windows amd64 $FILENAME"
mv $FILENAME clash.meta-windows-amd64-cgo.exe
elif [[ $FILENAME =~ "linux" ]];then
echo "rename linux $FILENAME"
mv $FILENAME $FILENAME-cgo
elif [[ $FILENAME =~ "android" ]];then
echo "rename android $FILENAME"
mv $FILENAME $FILENAME-cgo
else echo "skip $FILENAME"
fi
done

View File

@ -1,71 +0,0 @@
name: Alpha
on: [push]
jobs:
Feature-build:
if: ${{ !contains(github.event.head_commit.message, '[Skip CI]') }}
runs-on: ubuntu-latest
steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Cache go module
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# - name: Get dependencies, run test
# run: |
# go test ./...
- name: Build
if: success()
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j releases
- name: Delete current release assets
uses: andreaswilli/delete-release-assets-action@v2.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: alpha
deleteOnlyFromDrafts: false
- name: Tag Repo
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Alpha
uses: softprops/action-gh-release@v1
if: ${{ env.GIT_BRANCH != 'Meta' && success() }}
with:
tag: ${{ github.ref }}
tag_name: alpha
files: bin/*
prerelease: true
- name: send telegram message on push
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TTELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_TOKEN }}
message: |
${{ github.actor }} created commit:
Commit message: ${{ github.event.commits[0].message }}
Repository: ${{ github.repository }}
See changes: https://github.com/${{ github.repository }}/commit/${{github.sha}}

View File

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

325
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,325 @@
name: Build
on:
workflow_dispatch:
push:
paths-ignore:
- "docs/**"
- "README.md"
branches:
- Alpha
- Beta
- Meta
tags:
- "v*"
pull_request_target:
branches:
- Alpha
- Beta
- Meta
env:
REGISTRY: docker.io
jobs:
Build:
permissions: write-all
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- {
type: "WithoutCGO",
target: "linux-amd64 linux-amd64-compatible",
id: "1",
}
- {
type: "WithoutCGO",
target: "linux-armv5 linux-armv6 linux-armv7",
id: "2",
}
- {
type: "WithoutCGO",
target: "linux-arm64 linux-mips64 linux-mips64le",
id: "3",
}
- {
type: "WithoutCGO",
target: "linux-mips-softfloat linux-mips-hardfloat linux-mipsle-softfloat linux-mipsle-hardfloat",
id: "4",
}
- {
type: "WithoutCGO",
target: "freebsd-386 freebsd-amd64 freebsd-arm64",
id: "5",
}
- {
type: "WithoutCGO",
target: "windows-amd64-compatible windows-amd64 windows-386",
id: "6",
}
- {
type: "WithoutCGO",
target: "windows-arm64 windows-arm32v7",
id: "7",
}
- {
type: "WithoutCGO",
target: "darwin-amd64 darwin-arm64 android-arm64",
id: "8",
}
- { type: "WithCGO", target: "windows/*", id: "1" }
- { type: "WithCGO", target: "linux/386", id: "2" }
- { type: "WithCGO", target: "linux/amd64", id: "3" }
- { type: "WithCGO", target: "linux/arm64,linux/riscv64", id: "4" }
- { type: "WithCGO", target: "linux/arm,", id: "5" }
- { type: "WithCGO", target: "linux/arm-6,linux/arm-7", id: "6" }
- { type: "WithCGO", target: "linux/mips,linux/mipsle", id: "7" }
- { type: "WithCGO", target: "linux/mips64", id: "8" }
- { type: "WithCGO", target: "linux/mips64le", id: "9" }
- { type: "WithCGO", target: "darwin-10.16/*", id: "10" }
- { type: "WithCGO", target: "android", id: "11" }
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set variables
run: echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Alpha'}}
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Beta'}}
run: echo "VERSION=beta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Meta'}}
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='' || github.ref_type=='tag'}}
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV
shell: bash
- name: Set ENV
run: |
echo "NAME=clash.meta" >> $GITHUB_ENV
echo "REPO=${{ github.repository }}" >> $GITHUB_ENV
echo "ShortSHA=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
echo "BUILDTIME=$(date -u)" >> $GITHUB_ENV
echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set ENV
run: |
echo "TAGS=with_gvisor,with_lwip" >> $GITHUB_ENV
echo "LDFLAGS=-X 'github.com/Dreamacro/clash/constant.Version=${VERSION}' -X 'github.com/Dreamacro/clash/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" >> $GITHUB_ENV
shell: bash
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: "1.20"
check-latest: true
- name: Test
if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }}
run: |
go test ./...
- name: Build WithoutCGO
if: ${{ matrix.job.type=='WithoutCGO' }}
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j$(($(nproc) + 1)) ${{ matrix.job.target }}
- uses: nttld/setup-ndk@v1
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
id: setup-ndk
with:
ndk-version: r25b
add-to-path: false
local-cache: true
- name: Build Android
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir bin
CC=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang
CGO_ENABLED=1 CC=${CC} GOARCH=arm64 GOOS=android go build -tags ${TAGS} -trimpath -ldflags "${LDFLAGS}" -o bin/${NAME}-android-arm64
- name: Set up xgo
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
- name: Build by xgo
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir bin
xgo --targets="${{ matrix.job.target }}" --tags="${TAGS}" -ldflags="${LDFLAGS}" --out bin/${NAME} ./
- name: Rename
if: ${{ matrix.job.type=='WithCGO' }}
run: |
cd bin
ls -la
cp ../.github/rename-cgo.sh ./
bash ./rename-cgo.sh
rm ./rename-cgo.sh
ls -la
cd ..
- name: Zip
if: ${{ success() }}
run: |
cd bin
ls -la
chmod +x *
cp ../.github/release.sh ./
bash ./release.sh
rm ./release.sh
ls -la
cd ..
- uses: actions/upload-artifact@v3
if: ${{ success() }}
with:
name: artifact
path: bin/
Upload-Prerelease:
permissions: write-all
if: ${{ github.ref_type=='branch' }}
needs: [ Build ]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Delete current release assets
uses: andreaswilli/delete-release-assets-action@v2.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: Prerelease-${{ github.ref_name }}
deleteOnlyFromDrafts: false
- name: Tag Repo
uses: richardsimko/update-tag@v1.0.6
with:
tag_name: Prerelease-${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Prerelease
uses: softprops/action-gh-release@v1
if: ${{ success() }}
with:
tag: ${{ github.ref_name }}
tag_name: Prerelease-${{ github.ref_name }}
files: bin/*
prerelease: true
generate_release_notes: true
Upload-Release:
permissions: write-all
if: ${{ github.ref_type=='tag' }}
needs: [ Build ]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Upload Release
uses: softprops/action-gh-release@v1
if: ${{ success() }}
with:
tag: ${{ github.ref_name }}
tag_name: ${{ github.ref_name }}
files: bin/*
generate_release_notes: true
Docker:
permissions: write-all
needs: [ Build ]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
- name: Show files
run: |
ls .
ls bin/
- name: Log into registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: |
linux/386
linux/amd64
linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ vendor
# test suite # test suite
test/config/cache* test/config/cache*
/output /output
/.vscode .vscode/
.fleet/

View File

@ -8,9 +8,10 @@ linters:
linters-settings: linters-settings:
gci: gci:
custom-order: true
sections: sections:
- standard - standard
- prefix(github.com/Dreamacro/clash) - prefix(github.com/Dreamacro/clash)
- default - default
staticcheck: staticcheck:
go: '1.18' go: '1.19'

View File

@ -1,18 +1,25 @@
FROM golang:alpine as builder FROM alpine:latest as builder
RUN apk add --no-cache make git && \ RUN apk add --no-cache gzip && \
wget -O /Country.mmdb https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb mkdir /clash-config && \
WORKDIR /clash-src wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
COPY --from=tonistiigi/xx:golang / / wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
COPY . /clash-src wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
RUN go mod download && \
make docker && \
mv ./bin/clash-docker /clash
COPY docker/file-name.sh /clash/file-name.sh
WORKDIR /clash
COPY bin/ bin/
RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \
FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && \
mv bin/$FILE_NAME clash.gz && gzip -d clash.gz && echo "$FILE_NAME" > /clash-config/test
FROM alpine:latest FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/Dreamacro/clash" LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta"
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates tzdata iptables
COPY --from=builder /Country.mmdb /root/.config/clash/
COPY --from=builder /clash / VOLUME ["/root/.config/clash/"]
ENTRYPOINT ["/clash"]
COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /clash/clash /clash
RUN chmod +x /clash
ENTRYPOINT [ "/clash" ]

View File

@ -1,20 +1,26 @@
NAME=Clash.Meta NAME=clash.meta
BINDIR=bin BINDIR=bin
BRANCH=$(shell git rev-parse --abbrev-ref HEAD) BRANCH=$(shell git branch --show-current)
ifeq ($(BRANCH),Alpha)
VERSION=alpha-$(shell git rev-parse --short HEAD) VERSION=alpha-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),Beta)
VERSION=beta-$(shell git rev-parse --short HEAD)
else ifeq ($(BRANCH),)
VERSION=$(shell git describe --tags)
else
VERSION=$(shell git rev-parse --short HEAD)
endif
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid=' -w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64v1 \ darwin-amd64 \
darwin-amd64v2 \
darwin-amd64v3 \
darwin-arm64 \ darwin-arm64 \
linux-amd64v1 \ linux-amd64-compatible \
linux-amd64v2 \ linux-amd64 \
linux-amd64v3 \
linux-armv5 \ linux-armv5 \
linux-armv6 \ linux-armv6 \
linux-armv7 \ linux-armv7 \
@ -32,9 +38,8 @@ PLATFORM_LIST = \
WINDOWS_ARCH_LIST = \ WINDOWS_ARCH_LIST = \
windows-386 \ windows-386 \
windows-amd64v1 \ windows-amd64-compatible \
windows-amd64v2 \ windows-amd64 \
windows-amd64v3 \
windows-arm64 \ windows-arm64 \
windows-arm32v7 windows-arm32v7
@ -42,16 +47,16 @@ all:linux-amd64 linux-arm64\
darwin-amd64 darwin-arm64\ darwin-amd64 darwin-arm64\
windows-amd64 windows-arm64\ windows-amd64 windows-arm64\
docker:
$(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v3: darwin-all: darwin-amd64 darwin-arm64
docker:
GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v2: darwin-amd64-compatible:
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64v1:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64: darwin-arm64:
@ -60,13 +65,10 @@ darwin-arm64:
linux-386: linux-386:
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v3: linux-amd64:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v2: linux-amd64-compatible:
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64v1:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64: linux-arm64:
@ -114,13 +116,10 @@ freebsd-arm64:
windows-386: windows-386:
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v3: windows-amd64:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v2: windows-amd64-compatible:
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64v1:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64: windows-arm64:
@ -151,3 +150,11 @@ lint:
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*
CLANG ?= clang-14
CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)
ebpf: export BPF_CLANG := $(CLANG)
ebpf: export BPF_CFLAGS := $(CFLAGS)
ebpf:
cd component/ebpf/ && go generate ./...

138
README.md
View File

@ -29,12 +29,42 @@
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`. - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller - Comprehensive HTTP RESTful API controller
## Getting Started ## Wiki
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
## Advanced usage for this branch Documentation and configuring examples are available on [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki) and [Clash.Meta Wiki](https://docs.metacubex.one/).
### DNS configuration ## Build
You should install [golang](https://go.dev) first.
Then get the source code of Clash.Meta:
```shell
git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download
```
If you can't visit github,you should set proxy first:
```shell
go env -w GOPROXY=https://goproxy.io,direct
```
Now you can build it:
```shell
go build
```
If you need gvisor for tun stack, build with:
```shell
go build -tags with_gvisor
```
<!-- ## Advanced usage of this fork -->
<!-- ### DNS configuration
Support `geosite` with `fallback-filter`. Support `geosite` with `fallback-filter`.
@ -44,7 +74,6 @@ Support resolve ip with a `Proxy Tunnel`.
```yaml ```yaml
proxy-groups: proxy-groups:
- name: DNS - name: DNS
type: url-test type: url-test
use: use:
@ -53,6 +82,7 @@ proxy-groups:
interval: 180 interval: 180
lazy: true lazy: true
``` ```
```yaml ```yaml
dns: dns:
enable: true enable: true
@ -68,8 +98,8 @@ dns:
- https://doh.pub/dns-query - https://doh.pub/dns-query
- tls://223.5.5.5:853 - tls://223.5.5.5:853
fallback: fallback:
- 'https://1.0.0.1/dns-query#DNS' # append the proxy adapter name or group name to the end of DNS URL with '#' prefix. - "https://1.0.0.1/dns-query#DNS" # append the proxy adapter name or group name to the end of DNS URL with '#' prefix.
- 'tls://8.8.4.4:853#DNS' - "tls://8.8.4.4:853#DNS"
fallback-filter: fallback-filter:
geoip: false geoip: false
geosite: geosite:
@ -90,21 +120,23 @@ Built-in [Wintun](https://www.wintun.net) driver.
# Enable the TUN listener # Enable the TUN listener
tun: tun:
enable: true enable: true
stack: gvisor # only gvisor stack: system # system/gvisor
dns-hijack: dns-hijack:
- 0.0.0.0:53 # additional dns server listen on TUN - 0.0.0.0:53 # additional dns server listen on TUN
auto-route: true # auto set global route auto-route: true # auto set global route
``` ```
### Rules configuration ### Rules configuration
- Support rule `GEOSITE`. - Support rule `GEOSITE`.
- Support rule-providers `RULE-SET`. - Support rule-providers `RULE-SET`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`. - Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules. - Support `network` condition for all rules.
- Support source IPCIDR condition for all rules, just append to the end. - Support source IPCIDR condition for all rules, just append to the end.
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat. - The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
```yaml ```yaml
rules: rules:
# network(tcp/udp) condition for all rules # network(tcp/udp) condition for all rules
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp - DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp - DOMAIN-SUFFIX,bilibili.com,REJECT,udp
@ -133,7 +165,6 @@ rules:
- MATCH,PROXY - MATCH,PROXY
``` ```
### Proxies configuration ### Proxies configuration
Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node) Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node)
@ -142,18 +173,17 @@ Support `Policy Group Filter`
```yaml ```yaml
proxy-groups: proxy-groups:
- name: 🚀 HK Group - name: 🚀 HK Group
type: select type: select
use: use:
- ALL - ALL
filter: 'HK' filter: "HK"
- name: 🚀 US Group - name: 🚀 US Group
type: select type: select
use: use:
- ALL - ALL
filter: 'US' filter: "US"
proxy-providers: proxy-providers:
ALL: ALL:
@ -165,14 +195,12 @@ proxy-providers:
enable: true enable: true
interval: 600 interval: 600
url: http://www.gstatic.com/generate_204 url: http://www.gstatic.com/generate_204
``` ```
Support outbound transport protocol `VLESS`. Support outbound transport protocol `VLESS`.
The XTLS support (TCP/UDP) transport by the XRAY-CORE. The XTLS support (TCP/UDP) transport by the XRAY-CORE.
```yaml ```yaml
proxies: proxies:
- name: "vless" - name: "vless"
@ -212,8 +240,46 @@ proxies:
grpc-service-name: grpcname grpc-service-name: grpcname
``` ```
Support outbound transport protocol `Wireguard`
```yaml
proxies:
- name: "wg"
type: wireguard
server: 162.159.192.1
port: 2480
ip: 172.16.0.2
ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5
private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=
public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=
udp: true
```
Support outbound transport protocol `Tuic`
```yaml
proxies:
- name: "tuic"
server: www.example.com
port: 10443
type: tuic
token: TOKEN
# ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server'
# heartbeat-interval: 10000
# alpn: [h3]
# disable-sni: true
reduce-rtt: true
# request-timeout: 8000
udp-relay-mode: native # Available: "native", "quic". Default: "native"
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
# max-udp-relay-packet-size: 1500
# fast-open: true
# skip-cert-verify: true
``` -->
### IPTABLES configuration ### IPTABLES configuration
Work on Linux OS who's supported `iptables`
Work on Linux OS which supported `iptables`
```yaml ```yaml
# Enable the TPROXY listener # Enable the TPROXY listener
@ -224,17 +290,15 @@ iptables:
inbound-interface: eth0 # detect the inbound interface, default is 'lo' inbound-interface: eth0 # detect the inbound interface, default is 'lo'
``` ```
### General installation guide for Linux ### General installation guide for Linux
+ Create user given name `clash-meta`
+ Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases) - Create user given name `clash-meta`
+ Rename executable file to `Clash-Meta` and move to `/usr/local/bin/` - Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
+ Create folder `/etc/Clash-Meta/` as working directory
- Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
- Create folder `/etc/Clash-Meta/` as working directory
Run Meta Kernel by user `clash-meta` as a daemon. Run Meta Kernel by user `clash-meta` as a daemon.
@ -251,8 +315,8 @@ User=clash-meta
Group=clash-meta Group=clash-meta
LimitNPROC=500 LimitNPROC=500
LimitNOFILE=1000000 LimitNOFILE=1000000
CapabilityBoundingSet=cap_net_admin CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=cap_net_admin AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
Restart=always Restart=always
ExecStartPre=/usr/bin/sleep 1s ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
@ -260,10 +324,13 @@ ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
``` ```
Launch clashd on system startup with: Launch clashd on system startup with:
```shell ```shell
$ systemctl enable Clash-Meta $ systemctl enable Clash-Meta
``` ```
Launch clashd immediately with: Launch clashd immediately with:
```shell ```shell
@ -274,22 +341,29 @@ $ systemctl start Clash-Meta
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`. Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard). To display process name in GUI please use [Razord-meta](https://github.com/MetaCubeX/Razord-meta).
![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png) ### Dashboard
We also made a custom fork of yacd provide better support for this project, check it out at [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta)
## Development ## Development
If you want to build an application that uses clash as a library, check out the If you want to build an application that uses clash as a library, check out the
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library) the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Debugging
Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug API.
## Credits ## Credits
* [Dreamacro/clash](https://github.com/Dreamacro/clash) - [Dreamacro/clash](https://github.com/Dreamacro/clash)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) - [SagerNet/sing-box](https://github.com/SagerNet/sing-box)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) - [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) - [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [yaling888/clash-plus-pro](https://github.com/yaling888/clash) - [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
- [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
## License ## License

View File

@ -4,15 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/Dreamacro/clash/common/queue" "github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"net"
"net/http"
"net/netip"
"net/url"
"time"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@ -40,11 +39,6 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
return conn, err return conn, err
} }
@ -58,15 +52,14 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err return pc, err
} }
// DelayHistory implements C.Proxy // DelayHistory implements C.Proxy
func (p *Proxy) DelayHistory() []C.DelayHistory { func (p *Proxy) DelayHistory() []C.DelayHistory {
queue := p.history.Copy() queueM := p.history.Copy()
histories := []C.DelayHistory{} histories := []C.DelayHistory{}
for _, item := range queue { for _, item := range queueM {
histories = append(histories, item) histories = append(histories, item)
} }
return histories return histories
@ -95,10 +88,12 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
} }
mapping := map[string]any{} mapping := map[string]any{}
json.Unmarshal(inner, &mapping) _ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP()
mapping["tfo"] = p.SupportTFO()
return json.Marshal(mapping) return json.Marshal(mapping)
} }
@ -129,7 +124,9 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
if err != nil { if err != nil {
return return
} }
defer instance.Close() defer func() {
_ = instance.Close()
}()
req, err := http.NewRequest(http.MethodHead, url, nil) req, err := http.NewRequest(http.MethodHead, url, nil)
if err != nil { if err != nil {
@ -138,7 +135,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
req = req.WithContext(ctx) req = req.WithContext(ctx)
transport := &http.Transport{ transport := &http.Transport{
Dial: func(string, string) (net.Conn, error) { DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil return instance, nil
}, },
// from http.DefaultTransport // from http.DefaultTransport
@ -149,26 +146,32 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
} }
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second,
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
} }
_ = resp.Body.Close()
if unifiedDelay { if unifiedDelay {
start = time.Now() second := time.Now()
resp, err = client.Do(req) resp, err = client.Do(req)
if err != nil { if err == nil {
return _ = resp.Body.Close()
start = second
} }
} }
resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
@ -197,9 +200,8 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
} }
addr = C.Metadata{ addr = C.Metadata{
AddrType: C.AtypDomainName,
Host: u.Hostname(), Host: u.Hostname(),
DstIP: nil, DstIP: netip.Addr{},
DstPort: port, DstPort: port,
} }
return return

View File

@ -0,0 +1,29 @@
package inbound
import (
C "github.com/Dreamacro/clash/constant"
)
type Addition func(metadata *C.Metadata)
func (a Addition) Apply(metadata *C.Metadata) {
a(metadata)
}
func WithInName(name string) Addition {
return func(metadata *C.Metadata) {
metadata.InName = name
}
}
func WithSpecialRules(specialRules string) Addition {
return func(metadata *C.Metadata) {
metadata.SpecialRules = specialRules
}
}
func WithSpecialProxy(specialProxy string) Addition {
return func(metadata *C.Metadata) {
metadata.SpecialProxy = specialProxy
}
}

View File

@ -9,13 +9,20 @@ import (
) )
// NewHTTP receive normal http request and return HTTPContext // NewHTTP receive normal http request and return HTTPContext
func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext { func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn, additions ...Addition) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.HTTP metadata.Type = C.HTTP
if ip, port, err := parseAddr(source.String()); err == nil { for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(source); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

View File

@ -9,12 +9,19 @@ import (
) )
// NewHTTPS receive CONNECT request and return ConnContext // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext { func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) *context.ConnContext {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPCONNECT metadata.Type = C.HTTPS
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil { for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
} }

26
adapter/inbound/listen.go Normal file
View File

@ -0,0 +1,26 @@
package inbound
import (
"context"
"net"
"github.com/sagernet/tfo-go"
)
var (
lc = tfo.ListenConfig{
DisableTFO: true,
}
)
func SetTfo(open bool) {
lc.DisableTFO = !open
}
func ListenContext(ctx context.Context, network, address string) (net.Listener, error) {
return lc.Listen(ctx, network, address)
}
func Listen(network, address string) (net.Listener, error) {
return ListenContext(context.Background(), network, address)
}

View File

@ -17,17 +17,26 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
} }
// NewPacket is PacketAdapter generator // NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter { func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.UDP metadata.NetWork = C.UDP
metadata.Type = source metadata.Type = source
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil { for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(packet.LocalAddr()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if p, ok := packet.(C.UDPPacketInAddr); ok {
if ip, port, err := parseAddr(p.InAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
}
return &PacketAdapter{ return &PacketAdapter{
UDPPacket: packet, packet,
metadata: metadata, metadata,
} }
} }

View File

@ -2,6 +2,7 @@ package inbound
import ( import (
"net" "net"
"net/netip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context" "github.com/Dreamacro/clash/context"
@ -9,17 +10,21 @@ import (
) )
// NewSocket receive TCP inbound and return ConnContext // NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext { func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
remoteAddr := conn.RemoteAddr() for _, addition := range additions {
// Filter when net.Addr interface is nil addition.Apply(metadata)
if remoteAddr != nil { }
if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
metadata.SrcIP = ip metadata.SrcIP = ip
metadata.SrcPort = port metadata.SrcPort = port
} }
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
} }
return context.NewConnContext(conn, metadata) return context.NewConnContext(conn, metadata)
@ -29,18 +34,14 @@ func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
metadata := &C.Metadata{} metadata := &C.Metadata{}
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.INNER metadata.Type = C.INNER
metadata.DNSMode = C.DNSMapping metadata.DNSMode = C.DNSNormal
metadata.Host = host metadata.Host = host
metadata.AddrType = C.AtypDomainName
metadata.Process = C.ClashName metadata.Process = C.ClashName
if ip, port, err := parseAddr(dst); err == nil { if h, port, err := net.SplitHostPort(dst); err == nil {
metadata.DstPort = port metadata.DstPort = port
if host == "" { if host == "" {
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip metadata.DstIP = ip
if ip.To4() == nil {
metadata.AddrType = C.AtypIPv6
} else {
metadata.AddrType = C.AtypIPv4
} }
} }
} }

View File

@ -1,19 +1,20 @@
package inbound package inbound
import ( import (
"errors"
"net" "net"
"net/http" "net/http"
"net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/common/nnip"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
func parseSocksAddr(target socks5.Addr) *C.Metadata { func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{ metadata := &C.Metadata{}
AddrType: int(target[0]),
}
switch target[0] { switch target[0] {
case socks5.AtypDomainName: case socks5.AtypDomainName:
@ -21,12 +22,11 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4: case socks5.AtypIPv4:
ip := net.IP(target[1 : 1+net.IPv4len]) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstIP = ip
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
ip := net.IP(target[1 : 1+net.IPv6len]) ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip metadata.DstIP = ip6.Unmap()
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
@ -45,32 +45,36 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
metadata := &C.Metadata{ metadata := &C.Metadata{
NetWork: C.TCP, NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host, Host: host,
DstIP: nil, DstIP: netip.Addr{},
DstPort: port, DstPort: port,
} }
ip := net.ParseIP(host) ip, err := netip.ParseAddr(host)
if ip != nil { if err == nil {
switch {
case ip.To4() == nil:
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip metadata.DstIP = ip
} }
return metadata return metadata
} }
func parseAddr(addr string) (net.IP, string, error) { func parseAddr(addr net.Addr) (netip.Addr, string, error) {
host, port, err := net.SplitHostPort(addr) // Filter when net.Addr interface is nil
if addr == nil {
return netip.Addr{}, "", errors.New("nil addr")
}
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
ip, port, err := parseAddr(rawAddr.RawAddr())
if err == nil {
return ip, port, err
}
}
addrStr := addr.String()
host, port, err := net.SplitHostPort(addrStr)
if err != nil { if err != nil {
return nil, "", err return netip.Addr{}, "", err
} }
ip := net.ParseIP(host) ip, err := netip.ParseAddr(host)
return ip, port, nil return ip, port, err
} }

View File

@ -5,9 +5,13 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"net" "net"
"strings"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
) )
type Base struct { type Base struct {
@ -16,7 +20,11 @@ type Base struct {
iface string iface string
tp C.AdapterType tp C.AdapterType
udp bool udp bool
xudp bool
tfo bool
rmark int rmark int
id string
prefer C.DNSPrefer
} }
// Name implements C.ProxyAdapter // Name implements C.ProxyAdapter
@ -24,6 +32,20 @@ func (b *Base) Name() string {
return b.name return b.name
} }
// Id implements C.ProxyAdapter
func (b *Base) Id() string {
if b.id == "" {
id, err := uuid.NewV6()
if err != nil {
b.id = b.name
} else {
b.id = id.String()
}
}
return b.id
}
// Type implements C.ProxyAdapter // Type implements C.ProxyAdapter
func (b *Base) Type() C.AdapterType { func (b *Base) Type() C.AdapterType {
return b.tp return b.tp
@ -34,20 +56,55 @@ func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support") return c, errors.New("no support")
} }
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return nil, errors.New("no support")
}
// DialContextWithDialer implements C.ProxyAdapter
func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
return nil, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, errors.New("no support") return nil, errors.New("no support")
} }
// ListenPacketWithDialer implements C.ProxyAdapter
func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, errors.New("no support")
}
// SupportWithDialer implements C.ProxyAdapter
func (b *Base) SupportWithDialer() bool {
return false
}
// SupportUOT implements C.ProxyAdapter
func (b *Base) SupportUOT() bool {
return false
}
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
func (b *Base) SupportUDP() bool { func (b *Base) SupportUDP() bool {
return b.udp return b.udp
} }
// SupportXUDP implements C.ProxyAdapter
func (b *Base) SupportXUDP() bool {
return b.xudp
}
// SupportTFO implements C.ProxyAdapter
func (b *Base) SupportTFO() bool {
return b.tfo
}
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) { func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{ return json.Marshal(map[string]string{
"type": b.Type().String(), "type": b.Type().String(),
"id": b.Id(),
}) })
} }
@ -57,7 +114,7 @@ func (b *Base) Addr() string {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy { func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return nil return nil
} }
@ -71,12 +128,30 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithRoutingMark(b.rmark)) opts = append(opts, dialer.WithRoutingMark(b.rmark))
} }
switch b.prefer {
case C.IPv4Only:
opts = append(opts, dialer.WithOnlySingleStack(true))
case C.IPv6Only:
opts = append(opts, dialer.WithOnlySingleStack(false))
case C.IPv4Prefer:
opts = append(opts, dialer.WithPreferIPv4())
case C.IPv6Prefer:
opts = append(opts, dialer.WithPreferIPv6())
default:
}
if b.tfo {
opts = append(opts, dialer.WithTFO(true))
}
return opts return opts
} }
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"`
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
} }
type BaseOption struct { type BaseOption struct {
@ -84,8 +159,11 @@ type BaseOption struct {
Addr string Addr string
Type C.AdapterType Type C.AdapterType
UDP bool UDP bool
XUDP bool
TFO bool
Interface string Interface string
RoutingMark int RoutingMark int
Prefer C.DNSPrefer
} }
func NewBase(opt BaseOption) *Base { func NewBase(opt BaseOption) *Base {
@ -94,14 +172,22 @@ func NewBase(opt BaseOption) *Base {
addr: opt.Addr, addr: opt.Addr,
tp: opt.Type, tp: opt.Type,
udp: opt.UDP, udp: opt.UDP,
xudp: opt.XUDP,
tfo: opt.TFO,
iface: opt.Interface, iface: opt.Interface,
rmark: opt.RoutingMark, rmark: opt.RoutingMark,
prefer: opt.Prefer,
} }
} }
type conn struct { type conn struct {
net.Conn N.ExtendedConn
chain C.Chain chain C.Chain
actualRemoteDestination string
}
func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination
} }
// Chains implements C.Connection // Chains implements C.Connection
@ -114,13 +200,22 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) c.chain = append(c.chain, a.Name())
} }
func (c *conn) Upstream() any {
return c.ExtendedConn
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
return &conn{c, []string{a.Name()}} return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
} }
type packetConn struct { type packetConn struct {
net.PacketConn net.PacketConn
chain C.Chain chain C.Chain
actualRemoteDestination string
}
func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination
} }
// Chains implements C.Connection // Chains implements C.Connection
@ -134,5 +229,17 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
} }
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
return &packetConn{pc, []string{a.Name()}} return &packetConn{pc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
}
func parseRemoteDestination(addr string) string {
if dst, _, err := net.SplitHostPort(addr); err == nil {
return dst
} else {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
return dst
} else {
return ""
}
}
} }

View File

@ -5,6 +5,7 @@ import (
"net" "net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -14,7 +15,7 @@ type Direct struct {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithDirect()) opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -25,8 +26,8 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
opts = append(opts, dialer.WithDirect()) opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...) pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -43,6 +44,7 @@ func NewDirect() *Direct {
name: "DIRECT", name: "DIRECT",
tp: C.Direct, tp: C.Direct,
udp: true, udp: true,
prefer: C.DualStack,
}, },
} }
} }
@ -53,16 +55,7 @@ func NewCompatible() *Direct {
name: "COMPATIBLE", name: "COMPATIBLE",
tp: C.Compatible, tp: C.Compatible,
udp: true, udp: true,
}, prefer: C.DualStack,
}
}
func NewPass() *Direct {
return &Direct{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
}, },
} }
} }

View File

@ -14,6 +14,7 @@ import (
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
@ -35,6 +36,7 @@ type HttpOption struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
} }
@ -42,7 +44,9 @@ type HttpOption struct {
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil { if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig) cc := tls.Client(c, h.tlsConfig)
err := cc.Handshake() ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
@ -57,13 +61,20 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...) return h.DialContextWithDialer(ctx, dialer.NewDialer(h.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", h.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = h.StreamConn(c, metadata) c, err = h.StreamConn(c, metadata)
if err != nil { if err != nil {
@ -73,6 +84,11 @@ func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di
return NewConn(c, h), nil return NewConn(c, h), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (h *Http) SupportWithDialer() bool {
return true
}
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
addr := metadata.RemoteAddress() addr := metadata.RemoteAddress()
req := &http.Request{ req := &http.Request{
@ -126,16 +142,26 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
} }
func NewHttp(option HttpOption) *Http { func NewHttp(option HttpOption) (*Http, error) {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
sni := option.Server sni := option.Server
if option.SNI != "" { if option.SNI != "" {
sni = option.SNI sni = option.SNI
} }
tlsConfig = &tls.Config{ if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni, ServerName: sni,
})
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni,
}, option.Fingerprint); err != nil {
return nil, err
}
} }
} }
@ -144,12 +170,14 @@ func NewHttp(option HttpOption) *Http {
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
option: &option, option: &option,
} }, nil
} }

View File

@ -0,0 +1,344 @@
package outbound
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"net"
"net/netip"
"os"
"regexp"
"strconv"
"time"
"github.com/metacubex/quic-go"
"github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
"github.com/Dreamacro/clash/transport/hysteria/core"
"github.com/Dreamacro/clash/transport/hysteria/obfs"
"github.com/Dreamacro/clash/transport/hysteria/pmtud_fix"
"github.com/Dreamacro/clash/transport/hysteria/transport"
)
const (
mbpsToBps = 125000
DefaultStreamReceiveWindow = 15728640 // 15 MB/s
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
DefaultALPN = "hysteria"
DefaultProtocol = "udp"
DefaultHopInterval = 10
)
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct {
*Base
client *core.Client
}
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
hdc := hyDialerWithContext{
ctx: context.Background(),
hyDialer: func(network string) (net.PacketConn, error) {
return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...)
},
remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
},
}
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc)
if err != nil {
return nil, err
}
return NewConn(tcpConn, h), nil
}
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
hdc := hyDialerWithContext{
ctx: context.Background(),
hyDialer: func(network string) (net.PacketConn, error) {
return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...)
},
remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
},
}
udpConn, err := h.client.DialUDP(&hdc)
if err != nil {
return nil, err
}
return newPacketConn(&hyPacketConn{udpConn}, h), nil
}
type HysteriaOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,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"`
}
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64
up = stringToBps(c.Up)
if up == 0 {
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up)
}
down = stringToBps(c.Down)
if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down)
}
return up, down, nil
}
func NewHysteria(option HysteriaOption) (*Hysteria, error) {
clientTransport := &transport.ClientTransport{
Dialer: &net.Dialer{
Timeout: 8 * time.Second,
},
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
ports := option.Ports
serverName := option.Server
if option.SNI != "" {
serverName = option.SNI
}
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
var bs []byte
var err error
if len(option.CustomCA) > 0 {
bs, err = os.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
}
} else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString)
}
if len(bs) > 0 {
block, _ := pem.Decode(bs)
if block == nil {
return nil, fmt.Errorf("CA cert is not PEM")
}
fpBytes := sha256.Sum256(block.Bytes)
if len(option.Fingerprint) == 0 {
option.Fingerprint = hex.EncodeToString(fpBytes[:])
}
}
if len(option.Fingerprint) != 0 {
var err error
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
}
if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN
} else {
tlsConfig.NextProtos = []string{DefaultALPN}
}
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
KeepAlivePeriod: 10 * time.Second,
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
EnableDatagrams: true,
}
if option.ObfsProtocol != "" {
option.Protocol = option.ObfsProtocol
}
if option.Protocol == "" {
option.Protocol = DefaultProtocol
}
if option.HopInterval == 0 {
option.HopInterval = DefaultHopInterval
}
hopInterval := time.Duration(int64(option.HopInterval)) * time.Second
if option.ReceiveWindow == 0 {
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
}
if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
}
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
}
var auth = []byte(option.AuthString)
if option.Auth != "" {
auth, err = base64.StdEncoding.DecodeString(option.Auth)
if err != nil {
return nil, err
}
}
var obfuscator obfs.Obfuscator
if len(option.Obfs) > 0 {
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
}
up, down, err := option.Speed()
if err != nil {
return nil, err
}
if option.UpSpeed != 0 {
up = uint64(option.UpSpeed * mbpsToBps)
}
if option.DownSpeed != 0 {
down = uint64(option.DownSpeed * mbpsToBps)
}
client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen,
)
if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
}
return &Hysteria{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Hysteria,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
client: client,
}, nil
}
func stringToBps(s string) uint64 {
if s == "" {
return 0
}
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return stringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
}
type hyPacketConn struct {
core.UDPConn
}
func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
b, addrStr, err := c.UDPConn.ReadFrom()
if err != nil {
return
}
n = copy(p, b)
addr = M.ParseSocksaddr(addrStr).UDPAddr()
return
}
func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
if err != nil {
return
}
n = len(p)
return
}
type hyDialerWithContext struct {
hyDialer func(network string) (net.PacketConn, error)
ctx context.Context
remoteAddr func(host string) (net.Addr, error)
}
func (h *hyDialerWithContext) ListenPacket(rAddr net.Addr) (net.PacketConn, error) {
network := "udp"
if addrPort, err := netip.ParseAddrPort(rAddr.String()); err == nil {
network = dialer.ParseNetwork(network, addrPort.Addr())
}
return h.hyDialer(network)
}
func (h *hyDialerWithContext) Context() context.Context {
return h.ctx
}
func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) {
return h.remoteAddr(host)
}

View File

@ -30,6 +30,18 @@ func NewReject() *Reject {
name: "REJECT", name: "REJECT",
tp: C.Reject, tp: C.Reject,
udp: true, udp: true,
prefer: C.DualStack,
},
}
}
func NewPass() *Reject {
return &Reject{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
prefer: C.DualStack,
}, },
} }
} }
@ -41,6 +53,9 @@ func (rw *nopConn) Read(b []byte) (int, error) {
} }
func (rw *nopConn) Write(b []byte) (int, error) { func (rw *nopConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
return 0, io.EOF return 0, io.EOF
} }

View File

@ -11,20 +11,27 @@ import (
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
"github.com/Dreamacro/go-shadowsocks2/core" shadowsocks "github.com/metacubex/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
) )
type ShadowSocks struct { type ShadowSocks struct {
*Base *Base
cipher core.Cipher method shadowsocks.Method
option *ShadowSocksOption
// obfs // obfs
obfsMode string obfsMode string
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option v2rayOption *v2rayObfs.Option
shadowTLSOption *shadowtls.ShadowTLSOption
} }
type ShadowSocksOption struct { type ShadowSocksOption struct {
@ -37,6 +44,7 @@ type ShadowSocksOption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@ -49,13 +57,38 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
} }
type shadowTLSOption struct {
Password string `obfs:"password"`
Host string `obfs:"host"`
Fingerprint string `obfs:"fingerprint,omitempty"`
ClientFingerprint string `obfs:"client-fingerprint,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Version int `obfs:"version,omitempty"`
}
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch ss.obfsMode {
case shadowtls.Mode:
// fix tls handshake not timeout
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
var err error
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
if err != nil {
return nil, err
}
}
return ss.streamConn(c, metadata)
}
func (ss *ShadowSocks) streamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch ss.obfsMode { switch ss.obfsMode {
case "tls": case "tls":
c = obfs.NewTLSObfs(c, ss.obfsOption.Host) c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
@ -69,53 +102,96 @@ func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, e
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
} }
c = ss.cipher.StreamConn(c) if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
_, err := c.Write(serializesSocksAddr(metadata)) return ss.method.DialEarlyConn(c, M.ParseSocksaddr(uot.UOTMagicAddress+":443")), nil
return c, err }
return ss.method.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata) switch ss.obfsMode {
case shadowtls.Mode:
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
if err != nil {
return nil, err
}
}
c, err = ss.streamConn(c, metadata)
return NewConn(c, ss), err return NewConn(c, ss), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
return newPacketConn(uot.NewClientConn(tcpConn), ss), nil
}
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
addr, err := resolveUDPAddr("udp", ss.addr) pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
return newPacketConn(pc, ss), nil
}
pc = ss.cipher.PacketConn(pc) // SupportWithDialer implements C.ProxyAdapter
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil func (ss *ShadowSocks) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
return newPacketConn(uot.NewClientConn(c), ss), nil
}
return nil, errors.New("no support")
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
} }
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
password := option.Password
ciph, err := core.PickCipher(cipher, nil, password)
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
} }
var v2rayOption *v2rayObfs.Option var v2rayOption *v2rayObfs.Option
var obfsOption *simpleObfsOption var obfsOption *simpleObfsOption
var shadowTLSOpt *shadowtls.ShadowTLSOption
obfsMode := "" obfsMode := ""
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
@ -151,6 +227,23 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
} }
} else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode
opt := &shadowTLSOption{
Version: 2,
}
if err := decoder.Decode(option.PluginOpts, opt); err != nil {
return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err)
}
shadowTLSOpt = &shadowtls.ShadowTLSOption{
Password: opt.Password,
Host: opt.Host,
Fingerprint: opt.Fingerprint,
ClientFingerprint: opt.ClientFingerprint,
SkipCertVerify: opt.SkipCertVerify,
Version: opt.Version,
}
} }
return &ShadowSocks{ return &ShadowSocks{
@ -159,14 +252,18 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr: addr, addr: addr,
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
cipher: ciph, method: method,
option: &option,
obfsMode: obfsMode, obfsMode: obfsMode,
v2rayOption: v2rayOption, v2rayOption: v2rayOption,
obfsOption: obfsOption, obfsOption: obfsOption,
shadowTLSOption: shadowTLSOpt,
}, nil }, nil
} }

View File

@ -8,12 +8,11 @@ import (
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/ssr/obfs" "github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol" "github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/Dreamacro/go-shadowsocks2/core"
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
) )
type ShadowSocksR struct { type ShadowSocksR struct {
@ -61,13 +60,20 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...) return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ssr.StreamConn(c, metadata) c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err return NewConn(c, ssr), err
@ -75,14 +81,18 @@ func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata,
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...) return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
addr, err := resolveUDPAddr("udp", ssr.addr) pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
@ -91,7 +101,18 @@ func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Me
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) SupportWithDialer() bool {
return true
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility
// https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" {
option.Cipher = "dummy"
}
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
cipher := option.Cipher cipher := option.Cipher
password := option.Password password := option.Password
@ -103,13 +124,14 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
ivSize int ivSize int
key []byte key []byte
) )
if option.Cipher == "dummy" { if option.Cipher == "dummy" {
ivSize = 0 ivSize = 0
key = core.Kdf(option.Password, 16) key = core.Kdf(option.Password, 16)
} else { } else {
ciph, ok := coreCiph.(*core.StreamCipher) ciph, ok := coreCiph.(*core.StreamCipher)
if !ok { if !ok {
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher) return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
} }
ivSize = ciph.IVSize() ivSize = ciph.IVSize()
key = ciph.Key key = ciph.Key
@ -141,8 +163,10 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
addr: addr, addr: addr,
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
cipher: coreCiph, cipher: coreCiph,
obfs: obfs, obfs: obfs,

View File

@ -53,6 +53,10 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP {
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version) err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err return c, err
@ -74,13 +78,20 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return NewConn(c, s), err return NewConn(c, s), err
} }
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...) return s.DialContextWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = s.StreamConn(c, metadata) c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err return NewConn(c, s), err
@ -88,7 +99,12 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...) return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -104,6 +120,16 @@ func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return newPacketConn(pc, s), nil return newPacketConn(pc, s), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (s *Snell) SupportWithDialer() bool {
return true
}
// SupportUOT implements C.ProxyAdapter
func (s *Snell) SupportUOT() bool {
return true
}
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk) psk := []byte(option.Psk)
@ -141,8 +167,10 @@ func NewSnell(option SnellOption) (*Snell, error) {
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,

View File

@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
) )
@ -33,13 +34,16 @@ type Socks5Option struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
} }
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
err := cc.Handshake() ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err := cc.HandshakeContext(ctx)
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
@ -61,13 +65,20 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = ss.StreamConn(c, metadata) c, err = ss.StreamConn(c, metadata)
if err != nil { if err != nil {
@ -77,6 +88,11 @@ func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ..
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (ss *Socks5) SupportWithDialer() bool {
return true
}
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
@ -87,11 +103,15 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
err = cc.Handshake() ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
defer cancel()
err = cc.HandshakeContext(ctx)
c = cc c = cc
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c) tcpKeepAlive(c)
var user *socks5.User var user *socks5.User
@ -108,7 +128,21 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return return
} }
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...) // Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return return
} }
@ -121,30 +155,25 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc.Close() pc.Close()
}() }()
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
} }
func NewSocks5(option Socks5Option) *Socks5 { func NewSocks5(option Socks5Option) (*Socks5, error) {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: option.Server, ServerName: option.Server,
} }
if len(option.Fingerprint) == 0 {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
}
}
} }
return &Socks5{ return &Socks5{
@ -153,15 +182,17 @@ func NewSocks5(option Socks5Option) *Socks5 {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tls: option.TLS, tls: option.TLS,
skipCertVerify: option.SkipCertVerify, skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
} }, nil
} }
type socksPacketConn struct { type socksPacketConn struct {

View File

@ -8,13 +8,13 @@ import (
"net/http" "net/http"
"strconv" "strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan" "github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"golang.org/x/net/http2"
) )
type Trojan struct { type Trojan struct {
@ -25,7 +25,7 @@ type Trojan struct {
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *http2.Transport transport *gun.TransportWrap
} }
type TrojanOption struct { type TrojanOption struct {
@ -37,12 +37,14 @@ type TrojanOption struct {
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"` Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"` FlowShow bool `proxy:"flow-show,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) { func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
@ -75,6 +77,11 @@ func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
t.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
if t.transport != nil { if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else { } else {
@ -90,8 +97,12 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
return nil, err return nil, err
} }
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)) if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err return c, err
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return N.NewExtendedConn(c), err
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -116,14 +127,20 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return NewConn(c, t), nil return NewConn(c, t), nil
} }
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) // DialContextWithDialer implements C.ProxyAdapter
func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.StreamConn(c, metadata) c, err = t.StreamConn(c, metadata)
if err != nil { if err != nil {
@ -143,19 +160,34 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
} else { safeConnClose(c, err)
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...) }(c)
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil {
return nil, err
}
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c) tcpKeepAlive(c)
c, err = t.plainStream(c) c, err = t.plainStream(c)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata)) err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
@ -166,6 +198,22 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
// SupportWithDialer implements C.ProxyAdapter
func (t *Trojan) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
// SupportUOT implements C.ProxyAdapter
func (t *Trojan) SupportUOT() bool {
return true
}
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
@ -175,9 +223,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: option.Server, ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow, FlowShow: option.FlowShow,
Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
} }
if option.Network != "ws" && len(option.Flow) >= 16 { switch option.Network {
case "", "tcp":
if len(option.Flow) >= 16 {
option.Flow = option.Flow[:16] option.Flow = option.Flow[:16]
switch option.Flow { switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS: case vless.XRO, vless.XRD, vless.XRS:
@ -186,6 +238,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
} }
}
if option.SNI != "" { if option.SNI != "" {
tOption.ServerName = option.SNI tOption.ServerName = option.SNI
@ -197,8 +250,10 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
addr: addr, addr: addr,
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
instance: trojan.New(tOption), instance: trojan.New(tOption),
option: &option, option: &option,
@ -221,11 +276,16 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName, ServerName: tOption.ServerName,
} }
if t.option.Flow != "" { if len(option.Fingerprint) == 0 {
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else { } else {
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig) var err error
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
return nil, err
} }
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint)
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{

251
adapter/outbound/tuic.go Normal file
View File

@ -0,0 +1,251 @@
package outbound
import (
"context"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"encoding/pem"
"fmt"
"math"
"net"
"os"
"strconv"
"time"
"github.com/metacubex/quic-go"
"github.com/Dreamacro/clash/component/dialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/tuic"
)
type Tuic struct {
*Base
client *tuic.PoolClient
}
type TuicOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Token string `proxy:"token"`
Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
RequestTimeout int `proxy:"request-timeout,omitempty"`
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
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"`
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"`
SNI string `proxy:"sni,omitempty"`
}
// DialContext implements C.ProxyAdapter
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
}
return NewConn(conn, t), err
}
// ListenPacketContext implements C.ProxyAdapter
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
}
return newPacketConn(pc, t), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (t *Tuic) SupportWithDialer() bool {
return true
}
func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (pc net.PacketConn, addr net.Addr, err error) {
return t.dialWithDialer(ctx, dialer.NewDialer(opts...))
}
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) {
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer)
if err != nil {
return nil, nil, err
}
addr = udpAddr
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, nil, err
}
return
}
func NewTuic(option TuicOption) (*Tuic, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
if option.SNI != "" {
tlsConfig.ServerName = option.SNI
}
var bs []byte
var err error
if len(option.CustomCA) > 0 {
bs, err = os.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
}
} else if option.CustomCAString != "" {
bs = []byte(option.CustomCAString)
}
if len(bs) > 0 {
block, _ := pem.Decode(bs)
if block == nil {
return nil, fmt.Errorf("CA cert is not PEM")
}
fpBytes := sha256.Sum256(block.Bytes)
if len(option.Fingerprint) == 0 {
option.Fingerprint = hex.EncodeToString(fpBytes[:])
}
}
if len(option.Fingerprint) != 0 {
var err error
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
} else {
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
}
if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN
} else {
tlsConfig.NextProtos = []string{"h3"}
}
if option.RequestTimeout == 0 {
option.RequestTimeout = 8000
}
if option.HeartbeatInterval <= 0 {
option.HeartbeatInterval = 10000
}
if option.UdpRelayMode != "quic" {
option.UdpRelayMode = "native"
}
if option.MaxUdpRelayPacketSize == 0 {
option.MaxUdpRelayPacketSize = 1252
}
if option.MaxOpenStreams == 0 {
option.MaxOpenStreams = 100
}
// ensure server's incoming stream can handle correctly, increase to 1.1x
quicMaxOpenStreams := int64(option.MaxOpenStreams)
quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0))
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxIncomingStreams: quicMaxOpenStreams,
MaxIncomingUniStreams: quicMaxOpenStreams,
KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond,
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
EnableDatagrams: true,
}
if option.ReceiveWindowConn == 0 {
quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10
quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow
}
if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10
quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow
}
if len(option.Ip) > 0 {
addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port))
}
host := option.Server
if option.DisableSni {
host = ""
tlsConfig.ServerName = ""
}
tkn := tuic.GenTKN(option.Token)
t := &Tuic{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Tuic,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
}
clientMaxOpenStreams := int64(option.MaxOpenStreams)
// to avoid tuic's "too many open streams", decrease to 0.9x
if clientMaxOpenStreams == 100 {
clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0))
}
if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1
}
clientOption := &tuic.ClientOption{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Host: host,
Token: tkn,
UdpRelayMode: option.UdpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
}
t.client = tuic.NewPoolClient(clientOption)
return t, nil
}

View File

@ -2,9 +2,11 @@ package outbound
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
xtls "github.com/xtls/go" xtls "github.com/xtls/go"
"net" "net"
"net/netip"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -22,8 +24,8 @@ var (
func tcpKeepAlive(c net.Conn) { func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok { if tcp, ok := c.(*net.TCPConn); ok {
tcp.SetKeepAlive(true) _ = tcp.SetKeepAlive(true)
tcp.SetKeepAlivePeriod(30 * time.Second) _ = tcp.SetKeepAlivePeriod(30 * time.Second)
} }
} }
@ -43,31 +45,88 @@ func getClientXSessionCache() xtls.ClientSessionCache {
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
aType := uint8(metadata.AddrType) addrType := metadata.AddrType()
aType := uint8(addrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch metadata.AddrType { switch addrType {
case socks5.AtypDomainName: case socks5.AtypDomainName:
len := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
buf = [][]byte{{aType, len}, host, port} buf = [][]byte{{aType, lenM}, host, port}
case socks5.AtypIPv4: case socks5.AtypIPv4:
host := metadata.DstIP.To4() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{aType}, host, port}
case socks5.AtypIPv6: case socks5.AtypIPv6:
host := metadata.DstIP.To16() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{aType}, host, port}
} }
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
} }
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) { func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ip, err := resolver.ResolveProxyServerHost(host) ip, err := resolver.ResolveProxyServerHost(ctx, host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip netip.Addr
var fallback netip.Addr
switch prefer {
case C.IPv4Only:
ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host)
case C.IPv6Only:
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host)
case C.IPv6Prefer:
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is6() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
default:
// C.IPv4Prefer, C.DualStack and other
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is4() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
}
if !ip.IsValid() && fallback.IsValid() {
ip = fallback
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -75,7 +134,7 @@ func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
} }
func safeConnClose(c net.Conn, err error) { func safeConnClose(c net.Conn, err error) {
if err != nil { if err != nil && c != nil {
c.Close() _ = c.Close()
} }
} }

View File

@ -12,14 +12,19 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vless" "github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess" "github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2" vmessSing "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
) )
const ( const (
@ -35,7 +40,7 @@ type Vless struct {
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *http2.Transport transport *gun.TransportWrap
} }
type VlessOption struct { type VlessOption struct {
@ -48,6 +53,9 @@ type VlessOption struct {
FlowShow bool `proxy:"flow-show,omitempty"` FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@ -56,11 +64,18 @@ type VlessOption struct {
WSPath string `proxy:"ws-path,omitempty"` WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
@ -71,28 +86,44 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
header.Add(key, value) wsOpts.Headers.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{ tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if len(v.option.Fingerprint) == 0 {
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
if host := wsOpts.Headers.Get("Host"); host == "" {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
}
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
@ -123,11 +154,7 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c, err = vmess.StreamH2Conn(c, h2Opts) c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
if v.isXTLSEnabled() {
c, err = gun.StreamGunWithXTLSConn(c, v.gunTLSConfig, v.gunConfig)
} else {
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
}
default: default:
// default tcp network // default tcp network
// handle TLS And XTLS // handle TLS And XTLS
@ -138,20 +165,17 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return nil, err return nil, err
} }
return v.client.StreamConn(c, parseVlessAddr(metadata)) return v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
} }
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
if v.isXTLSEnabled() { if v.isLegacyXTLSEnabled() && !isH2 {
xtlsOpts := vless.XTLSConfig{ xtlsOpts := vless.XTLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
} Fingerprint: v.option.Fingerprint,
if isH2 {
xtlsOpts.NextProtos = []string{"h2"}
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
@ -164,6 +188,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
} }
if isH2 { if isH2 {
@ -180,8 +206,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
return conn, nil return conn, nil
} }
func (v *Vless) isXTLSEnabled() bool { func (v *Vless) isLegacyXTLSEnabled() bool {
return v.client.Addons != nil return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -192,32 +218,43 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) // DialContextWithDialer implements C.ProxyAdapter
func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
return NewConn(c, v), err return NewConn(c, v), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr // vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host) ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
@ -231,17 +268,55 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) if v.option.PacketAddr {
packetAddrMetadata := *metadata // make a copy
packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress
packetAddrMetadata.DstPort = "443"
c, err = v.client.StreamConn(c, parseVlessAddr(&packetAddrMetadata, false))
} else { } else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
}
if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err)
}
return v.ListenPacketOnStreamConn(c, metadata)
}
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, 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
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
if v.option.PacketAddr {
packetAddrMetadata := *metadata // make a copy
packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress
packetAddrMetadata.DstPort = "443"
c, err = v.StreamConn(c, &packetAddrMetadata)
} else {
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
} }
@ -249,26 +324,52 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
return v.ListenPacketOnStreamConn(c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter
func (v *Vless) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if v.option.XUDP {
return newPacketConn(&threadSafePacketConn{
PacketConn: vmessSing.NewXUDPConn(c, M.ParseSocksaddr(metadata.RemoteAddress())),
}, v), nil
} else if v.option.PacketAddr {
return newPacketConn(&threadSafePacketConn{
PacketConn: packetaddr.NewConn(&vlessPacketConn{
Conn: c, rAddr: metadata.UDPAddr(),
}, M.ParseSocksaddr(metadata.RemoteAddress())),
}, v), nil
}
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { // SupportUOT implements C.ProxyAdapter
func (v *Vless) SupportUOT() bool {
return true
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
var addrType byte var addrType byte
var addr []byte var addr []byte
switch metadata.AddrType { switch metadata.AddrType() {
case C.AtypIPv4: case socks5.AtypIPv4:
addrType = byte(vless.AtypIPv4) addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6: case socks5.AtypIPv6:
addrType = byte(vless.AtypIPv6) addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16()) copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName: case socks5.AtypDomainName:
addrType = byte(vless.AtypDomainName) addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host)) addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host)) copy(addr[1:], metadata.Host)
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
@ -276,7 +377,8 @@ func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
Addr: addr, Addr: addr,
Port: uint(port), Port: uint16(port),
Mux: metadata.NetWork == C.UDP && xudp,
} }
} }
@ -377,7 +479,7 @@ func NewVless(option VlessOption) (*Vless, error) {
if option.Network != "ws" && len(option.Flow) >= 16 { if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16] option.Flow = option.Flow[:16]
switch option.Flow { switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS: case vless.XRO, vless.XRD, vless.XRS, vless.XRV:
addons = &vless.Addons{ addons = &vless.Addons{
Flow: option.Flow, Flow: option.Flow,
} }
@ -386,6 +488,16 @@ func NewVless(option VlessOption) (*Vless, error) {
} }
} }
switch option.PacketEncoding {
case "packetaddr", "packet":
option.PacketAddr = true
option.XUDP = false
default: // https://github.com/XTLS/Xray-core/pull/1567#issuecomment-1407305458
if !option.PacketAddr {
option.XUDP = true
}
}
client, err := vless.NewClient(option.UUID, addons, option.FlowShow) client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
if err != nil { if err != nil {
return nil, err return nil, err
@ -397,7 +509,11 @@ func NewVless(option VlessOption) (*Vless, error) {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless, tp: C.Vless,
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
@ -421,11 +537,12 @@ func NewVless(option VlessOption) (*Vless, error) {
gunConfig := &gun.Config{ gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName, ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
} }
tlsConfig := &tls.Config{ tlsConfig := tlsC.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
} })
if v.option.ServerName == "" { if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
@ -435,11 +552,9 @@ func NewVless(option VlessOption) (*Vless, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
if v.isXTLSEnabled() {
v.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint)
} else {
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
} }
return v, nil return v, nil

View File

@ -9,16 +9,22 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun" "github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/vmess" clashVMess "github.com/Dreamacro/clash/transport/vmess"
"golang.org/x/net/http2" vmess "github.com/sagernet/sing-vmess"
"github.com/sagernet/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
) )
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
type Vmess struct { type Vmess struct {
*Base *Base
client *vmess.Client client *vmess.Client
@ -27,7 +33,7 @@ type Vmess struct {
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *http2.Transport transport *gun.TransportWrap
} }
type VmessOption struct { type VmessOption struct {
@ -42,15 +48,18 @@ type VmessOption struct {
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
// TODO: compatible with VMESS WS older version configurations XUDP bool `proxy:"xudp,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"`
WSPath string `proxy:"ws-path,omitempty"` GlobalPadding bool `proxy:"global-padding,omitempty"`
AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
type HTTPOptions struct { type HTTPOptions struct {
@ -78,121 +87,139 @@ type WSOptions struct {
// StreamConn implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error var err error
switch v.option.Network {
case "ws": if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
if v.option.WSOpts.Path == "" { v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
v.option.WSOpts.Path = v.option.WSPath
}
if len(v.option.WSOpts.Headers) == 0 {
v.option.WSOpts.Headers = v.option.WSHeaders
} }
switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{ wsOpts := &clashVMess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
header.Add(key, value) wsOpts.Headers.Add(key, value)
} }
wsOpts.Headers = header
} }
if v.option.TLS { if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{ tlsConfig := &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
if len(v.option.Fingerprint) == 0 {
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
} else {
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
return nil, err
}
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} }
c, err = vmess.StreamWebsocketConn(c, wsOpts) c, err = clashVMess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{ tlsOpts := &clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(c, tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
httpOpts := &vmess.HTTPConfig{ httpOpts := &clashVMess.HTTPConfig{
Host: host, Host: host,
Method: v.option.HTTPOpts.Method, Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path, Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers, Headers: v.option.HTTPOpts.Headers,
} }
c = vmess.StreamHTTPConn(c, httpOpts) c = clashVMess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := vmess.TLSConfig{ tlsOpts := clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"}, NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, &tlsOpts) c, err = clashVMess.StreamTLSConn(c, &tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
h2Opts := &vmess.H2Config{ h2Opts := &clashVMess.H2Config{
Hosts: v.option.HTTP2Opts.Host, Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = vmess.StreamH2Conn(c, h2Opts) c, err = clashVMess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &vmess.TLSConfig{ tlsOpts := &clashVMess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = vmess.StreamTLSConn(c, tlsOpts) c, err = clashVMess.StreamTLSConn(c, tlsOpts)
} }
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
if metadata.NetWork == C.UDP {
return v.client.StreamConn(c, parseVmessAddr(metadata)) if v.option.XUDP {
return v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
} else {
return v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
}
} else {
return v.client.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
}
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
@ -203,22 +230,30 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) // DialContextWithDialer implements C.ProxyAdapter
func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c) tcpKeepAlive(c)
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata) c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
@ -226,15 +261,22 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(metadata.Host) ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
if v.option.PacketAddr {
_metadata := *metadata // make a copy
metadata = &_metadata
metadata.Host = packetaddr.SeqPacketMagicAddress
metadata.DstPort = "443"
}
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && len(opts) == 0 {
@ -242,45 +284,99 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer safeConnClose(c, err) defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.client.StreamConn(c, parseVmessAddr(metadata)) if v.option.XUDP {
c = v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} else { } else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) c = v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConn(c, metadata)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
return v.ListenPacketOnStreamConn(c, metadata)
}
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, 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
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return v.ListenPacketOnStreamConn(c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter
func (v *Vmess) SupportWithDialer() bool {
return true
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if v.option.PacketAddr {
return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindConn(c)}, v), nil
} else if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
// SupportUOT implements C.ProxyAdapter
func (v *Vmess) SupportUOT() bool {
return true
}
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
client, err := vmess.NewClient(vmess.Config{ var options []vmess.ClientOption
UUID: option.UUID, if option.GlobalPadding {
AlterID: uint16(option.AlterID), options = append(options, vmess.ClientWithGlobalPadding())
Security: security, }
HostName: option.Server, if option.AuthenticatedLength {
Port: strconv.Itoa(option.Port), options = append(options, vmess.ClientWithAuthenticatedLength())
IsAead: option.AlterID == 0, }
}) client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch option.PacketEncoding {
case "packetaddr", "packet":
option.PacketAddr = true
case "xudp":
option.XUDP = true
}
if option.XUDP {
option.PacketAddr = false
}
switch option.Network { switch option.Network {
case "h2", "grpc": case "h2", "grpc":
if !option.TLS { if !option.TLS {
return nil, fmt.Errorf("TLS must be true with h2/grpc network") option.TLS = true
} }
} }
@ -290,8 +386,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
iface: option.Interface, iface: option.Interface,
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
@ -315,6 +414,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
gunConfig := &gun.Config{ gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName, ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
@ -329,46 +429,40 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint)
}
return v, nil return v, nil
} }
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr { type threadSafePacketConn struct {
var addrType byte net.PacketConn
var addr []byte access sync.Mutex
switch metadata.AddrType { }
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.To4())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.To16())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16) func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return &vmess.DstAddr{ c.access.Lock()
UDP: metadata.NetWork == C.UDP, defer c.access.Unlock()
AddrType: addrType, return c.PacketConn.WriteTo(b, addr)
Addr: addr,
Port: uint(port),
}
} }
type vmessPacketConn struct { type vmessPacketConn struct {
net.Conn net.Conn
rAddr net.Addr rAddr net.Addr
access sync.Mutex
} }
// WriteTo implments C.PacketConn.WriteTo
// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not.
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
allowedAddr := uc.rAddr.(*net.UDPAddr)
destAddr := addr.(*net.UDPAddr)
if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) {
return 0, ErrUDPRemoteAddrMismatch
}
uc.access.Lock()
defer uc.access.Unlock()
return uc.Conn.Write(b) return uc.Conn.Write(b)
} }

View File

@ -0,0 +1,258 @@
package outbound
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"runtime"
"strconv"
"strings"
"sync"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
wireguard "github.com/metacubex/sing-wireguard"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/device"
)
type WireGuard struct {
*Base
bind *wireguard.ClientBind
device *device.Device
tunDevice wireguard.Device
dialer *wgDialer
startOnce sync.Once
startErr error
}
type WireGuardOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Ip string `proxy:"ip,omitempty"`
Ipv6 string `proxy:"ipv6,omitempty"`
PrivateKey string `proxy:"private-key"`
PublicKey string `proxy:"public-key"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"`
Workers int `proxy:"workers,omitempty"`
MTU int `proxy:"mtu,omitempty"`
UDP bool `proxy:"udp,omitempty"`
PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"`
}
type wgDialer struct {
options []dialer.Option
}
func (d *wgDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return dialer.DialContext(ctx, network, destination.String(), d.options...)
}
func (d *wgDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", destination.Addr), "", d.options...)
}
func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
outbound := &WireGuard{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.WireGuard,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
dialer: &wgDialer{},
}
runtime.SetFinalizer(outbound, closeWireGuard)
var reserved [3]uint8
if len(option.Reserved) > 0 {
if len(option.Reserved) != 3 {
return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved))
}
reserved[0] = uint8(option.Reserved[0])
reserved[1] = uint8(option.Reserved[1])
reserved[2] = uint8(option.Reserved[2])
}
peerAddr := M.ParseSocksaddrHostPort(option.Server, uint16(option.Port))
outbound.bind = wireguard.NewClientBind(context.Background(), outbound.dialer, peerAddr, reserved)
localPrefixes := make([]netip.Prefix, 0, 2)
if len(option.Ip) > 0 {
if !strings.Contains(option.Ip, "/") {
option.Ip = option.Ip + "/32"
}
if prefix, err := netip.ParsePrefix(option.Ip); err == nil {
localPrefixes = append(localPrefixes, prefix)
} else {
return nil, E.Cause(err, "ip address parse error")
}
}
if len(option.Ipv6) > 0 {
if !strings.Contains(option.Ipv6, "/") {
option.Ipv6 = option.Ipv6 + "/128"
}
if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil {
localPrefixes = append(localPrefixes, prefix)
} else {
return nil, E.Cause(err, "ipv6 address parse error")
}
}
if len(localPrefixes) == 0 {
return nil, E.New("missing local address")
}
var privateKey, peerPublicKey, preSharedKey string
{
bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey)
if err != nil {
return nil, E.Cause(err, "decode private key")
}
privateKey = hex.EncodeToString(bytes)
}
{
bytes, err := base64.StdEncoding.DecodeString(option.PublicKey)
if err != nil {
return nil, E.Cause(err, "decode peer public key")
}
peerPublicKey = hex.EncodeToString(bytes)
}
if option.PreSharedKey != "" {
bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey)
if err != nil {
return nil, E.Cause(err, "decode pre shared key")
}
preSharedKey = hex.EncodeToString(bytes)
}
ipcConf := "private_key=" + privateKey
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + peerAddr.String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
var has4, has6 bool
for _, address := range localPrefixes {
if address.Addr().Is4() {
has4 = true
} else {
has6 = true
}
}
if has4 {
ipcConf += "\nallowed_ip=0.0.0.0/0"
}
if has6 {
ipcConf += "\nallowed_ip=::/0"
}
if option.PersistentKeepalive != 0 {
ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive)
}
mtu := option.MTU
if mtu == 0 {
mtu = 1408
}
var err error
outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu))
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
},
Errorf: func(format string, args ...interface{}) {
log.SingLogger.Error(fmt.Sprintf(strings.ToLower(format), args...))
},
}, option.Workers)
if debug.Enabled {
log.SingLogger.Trace("created wireguard ipc conf: \n", ipcConf)
}
err = outbound.device.IpcSet(ipcConf)
if err != nil {
return nil, E.Cause(err, "setup wireguard")
}
//err = outbound.tunDevice.Start()
return outbound, nil
}
func closeWireGuard(w *WireGuard) {
if w.device != nil {
w.device.Close()
}
_ = common.Close(w.tunDevice)
}
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
w.dialer.options = opts
var conn net.Conn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
})
if w.startErr != nil {
return nil, w.startErr
}
if !metadata.Resolved() {
var addrs []netip.Addr
addrs, err = resolver.LookupIP(ctx, metadata.Host)
if err != nil {
return nil, err
}
conn, err = N.DialSerial(ctx, w.tunDevice, "tcp", M.ParseSocksaddr(metadata.RemoteAddress()), addrs)
} else {
port, _ := strconv.Atoi(metadata.DstPort)
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)))
}
if err != nil {
return nil, err
}
if conn == nil {
return nil, E.New("conn is nil")
}
return NewConn(CN.NewRefConn(conn, w), w), nil
}
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
w.dialer.options = opts
var pc net.PacketConn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
})
if w.startErr != nil {
return nil, w.startErr
}
if err != nil {
return nil, err
}
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
port, _ := strconv.Atoi(metadata.DstPort)
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)))
if err != nil {
return nil, err
}
if pc == nil {
return nil, E.New("packetConn is nil")
}
return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil
}

View File

@ -1,55 +0,0 @@
package outboundgroup
import (
"github.com/Dreamacro/clash/tunnel"
"github.com/dlclark/regexp2"
"time"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
const (
defaultGetProxiesDuration = time.Second * 5
)
func getProvidersProxies(providers []provider.ProxyProvider, touch bool, filter string) []C.Proxy {
proxies := []C.Proxy{}
for _, provider := range providers {
if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
}
var filterReg *regexp2.Regexp = nil
var matchedProxies []C.Proxy
if len(filter) > 0 {
//filterReg = regexp.MustCompile(filter)
filterReg = regexp2.MustCompile(filter, 0)
for _, p := range proxies {
if p.Type() < 8 {
matchedProxies = append(matchedProxies, p)
}
//if filterReg.MatchString(p.Name()) {
if mat, _ := filterReg.FindStringMatch(p.Name()); mat != nil {
matchedProxies = append(matchedProxies, p)
}
}
if len(matchedProxies) > 0 {
return matchedProxies
} else {
return append([]C.Proxy{}, tunnel.Proxies()["COMPATIBLE"])
}
} else {
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
} else {
return proxies
}
}
}

View File

@ -3,25 +3,21 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log" "errors"
"go.uber.org/atomic"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/callback"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
type Fallback struct { type Fallback struct {
*outbound.Base *GroupBase
disableUDP bool disableUDP bool
filter string testUrl string
single *singledo.Single selected string
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
@ -35,10 +31,19 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...) c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else { } else {
f.onDialFailed() f.onDialFailed(proxy.Type(), err)
}
c = &callback.FirstWriteCallBackConn{
Conn: c,
Callback: func(err error) {
if err == nil {
f.onDialSuccess()
} else {
f.onDialFailed(proxy.Type(), err)
}
},
} }
return c, err return c, err
@ -50,41 +55,11 @@ func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata
pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...) pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
f.onDialFailed()
} }
return pc, err return pc, err
} }
func (f *Fallback) onDialFailed() {
if f.failedTime.Load() == -1 {
log.Warnln("%s first failed", f.Name())
now := time.Now().UnixMilli()
f.failedTime.Store(now)
f.failedTimes.Store(1)
} else {
if f.failedTime.Load()-time.Now().UnixMilli() > 5*time.Second.Milliseconds() {
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
} else {
failedCount := f.failedTimes.Inc()
log.Warnln("%s failed count: %d", f.Name(), failedCount)
if failedCount >= 5 {
log.Warnln("because %s failed multiple times, active health check", f.Name())
for _, proxyProvider := range f.providers {
go proxyProvider.HealthCheck()
}
f.failedTimes.Store(-1)
f.failedTime.Store(-1)
}
}
}
}
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
func (f *Fallback) SupportUDP() bool { func (f *Fallback) SupportUDP() bool {
if f.disableUDP { if f.disableUDP {
@ -98,7 +73,7 @@ func (f *Fallback) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) { func (f *Fallback) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range f.proxies(false) { for _, proxy := range f.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -109,43 +84,70 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
proxy := f.findAliveProxy(true) proxy := f.findAliveProxy(touch)
return proxy return proxy
} }
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (any, error) {
return getProvidersProxies(f.providers, touch, f.filter), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch) proxies := f.GetProxies(touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if len(f.selected) == 0 {
if proxy.Alive() { if proxy.Alive() {
return proxy return proxy
} }
} else {
if proxy.Name() == f.selected {
if proxy.Alive() {
return proxy
} else {
f.selected = ""
}
}
}
} }
return proxies[0] return proxies[0]
} }
func (f *Fallback) Set(name string) error {
var p C.Proxy
for _, proxy := range f.GetProxies(false) {
if proxy.Name() == name {
p = proxy
break
}
}
if p == nil {
return errors.New("proxy not exist")
}
f.selected = name
if !p.Alive() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
_, _ = p.URLTest(ctx, f.testUrl)
}
return nil
}
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{ return &Fallback{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name, Name: option.Name,
Type: C.Fallback, Type: C.Fallback,
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
filter: option.Filter, testUrl: option.URL,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
} }

View File

@ -0,0 +1,293 @@
package outboundgroup
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
"github.com/dlclark/regexp2"
"go.uber.org/atomic"
)
type GroupBase struct {
*outbound.Base
filterRegs []*regexp2.Regexp
excludeFilterReg *regexp2.Regexp
excludeTypeArray []string
providers []provider.ProxyProvider
failedTestMux sync.Mutex
failedTimes int
failedTime time.Time
failedTesting *atomic.Bool
proxies [][]C.Proxy
versions []atomic.Uint32
}
type GroupBaseOption struct {
outbound.BaseOption
filter string
excludeFilter string
excludeType string
providers []provider.ProxyProvider
}
func NewGroupBase(opt GroupBaseOption) *GroupBase {
var excludeFilterReg *regexp2.Regexp
if opt.excludeFilter != "" {
excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, 0)
}
var excludeTypeArray []string
if opt.excludeType != "" {
excludeTypeArray = strings.Split(opt.excludeType, "|")
}
var filterRegs []*regexp2.Regexp
if opt.filter != "" {
for _, filter := range strings.Split(opt.filter, "`") {
filterReg := regexp2.MustCompile(filter, 0)
filterRegs = append(filterRegs, filterReg)
}
}
gb := &GroupBase{
Base: outbound.NewBase(opt.BaseOption),
filterRegs: filterRegs,
excludeFilterReg: excludeFilterReg,
excludeTypeArray: excludeTypeArray,
providers: opt.providers,
failedTesting: atomic.NewBool(false),
}
gb.proxies = make([][]C.Proxy, len(opt.providers))
gb.versions = make([]atomic.Uint32, len(opt.providers))
return gb
}
func (gb *GroupBase) Touch() {
for _, pd := range gb.providers {
pd.Touch()
}
}
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
var proxies []C.Proxy
if len(gb.filterRegs) == 0 {
for _, pd := range gb.providers {
if touch {
pd.Touch()
}
proxies = append(proxies, pd.Proxies()...)
}
} else {
for i, pd := range gb.providers {
if touch {
pd.Touch()
}
if pd.VehicleType() == types.Compatible {
gb.versions[i].Store(pd.Version())
gb.proxies[i] = pd.Proxies()
continue
}
version := gb.versions[i].Load()
if version != pd.Version() && gb.versions[i].CompareAndSwap(version, pd.Version()) {
var (
proxies []C.Proxy
newProxies []C.Proxy
)
proxies = pd.Proxies()
proxiesSet := map[string]struct{}{}
for _, filterReg := range gb.filterRegs {
for _, p := range proxies {
name := p.Name()
if mat, _ := filterReg.FindStringMatch(name); mat != nil {
if _, ok := proxiesSet[name]; !ok {
proxiesSet[name] = struct{}{}
newProxies = append(newProxies, p)
}
}
}
}
gb.proxies[i] = newProxies
}
}
for _, p := range gb.proxies {
proxies = append(proxies, p...)
}
}
if len(proxies) == 0 {
return append(proxies, tunnel.Proxies()["COMPATIBLE"])
}
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
var newProxies []C.Proxy
proxiesSet := map[string]struct{}{}
for _, filterReg := range gb.filterRegs {
for _, p := range proxies {
name := p.Name()
if mat, _ := filterReg.FindStringMatch(name); mat != nil {
if _, ok := proxiesSet[name]; !ok {
proxiesSet[name] = struct{}{}
newProxies = append(newProxies, p)
}
}
}
}
for _, p := range proxies { // add not matched proxies at the end
name := p.Name()
if _, ok := proxiesSet[name]; !ok {
proxiesSet[name] = struct{}{}
newProxies = append(newProxies, p)
}
}
proxies = newProxies
}
if gb.excludeTypeArray != nil {
var newProxies []C.Proxy
for _, p := range proxies {
mType := p.Type().String()
flag := false
for i := range gb.excludeTypeArray {
if strings.EqualFold(mType, gb.excludeTypeArray[i]) {
flag = true
break
}
}
if flag {
continue
}
newProxies = append(newProxies, p)
}
proxies = newProxies
}
if gb.excludeFilterReg != nil {
var newProxies []C.Proxy
for _, p := range proxies {
name := p.Name()
if mat, _ := gb.excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
newProxies = append(newProxies, p)
}
proxies = newProxies
}
return proxies
}
func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
var wg sync.WaitGroup
var lock sync.Mutex
mp := map[string]uint16{}
proxies := gb.GetProxies(false)
for _, proxy := range proxies {
proxy := proxy
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url)
if err == nil {
lock.Lock()
mp[proxy.Name()] = delay
lock.Unlock()
}
wg.Done()
}()
}
wg.Wait()
if len(mp) == 0 {
return mp, fmt.Errorf("get delay: all proxies timeout")
} else {
return mp, nil
}
}
func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) {
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass {
return
}
if strings.Contains(err.Error(), "connection refused") {
go gb.healthCheck()
return
}
go func() {
gb.failedTestMux.Lock()
defer gb.failedTestMux.Unlock()
gb.failedTimes++
if gb.failedTimes == 1 {
log.Debugln("ProxyGroup: %s first failed", gb.Name())
gb.failedTime = time.Now()
} else {
if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
gb.failedTimes = 0
return
}
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes() {
log.Warnln("because %s failed multiple times, active health check", gb.Name())
gb.healthCheck()
}
}
}()
}
func (gb *GroupBase) healthCheck() {
if gb.failedTesting.Load() {
return
}
gb.failedTesting.Store(true)
wg := sync.WaitGroup{}
for _, proxyProvider := range gb.providers {
wg.Add(1)
proxyProvider := proxyProvider
go func() {
defer wg.Done()
proxyProvider.HealthCheck()
}()
}
wg.Wait()
gb.failedTesting.Store(false)
gb.failedTimes = 0
}
func (gb *GroupBase) failedIntervalTime() int64 {
return 5 * time.Second.Milliseconds()
}
func (gb *GroupBase) onDialSuccess() {
if !gb.failedTesting.Load() {
gb.failedTimes = 0
}
}
func (gb *GroupBase) maxFailedTimes() int {
return 5
}
func (gb *GroupBase) failedTimeoutInterval() time.Duration {
return 5 * time.Second
}

View File

@ -6,10 +6,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/callback"
"github.com/Dreamacro/clash/common/murmur3" "github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
@ -20,26 +22,25 @@ import (
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct { type LoadBalance struct {
*outbound.Base *GroupBase
disableUDP bool disableUDP bool
single *singledo.Single
filter string
providers []provider.ProxyProvider
strategyFn strategyFn strategyFn strategyFn
} }
var errStrategy = errors.New("unsupported strategy") var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string { func parseStrategy(config map[string]any) string {
if elm, ok := config["strategy"]; ok { if strategy, ok := config["strategy"].(string); ok {
if strategy, ok := elm.(string); ok {
return strategy return strategy
} }
}
return "consistent-hashing" return "consistent-hashing"
} }
func getKey(metadata *C.Metadata) string { func getKey(metadata *C.Metadata) string {
if metadata == nil {
return ""
}
if metadata.Host != "" { if metadata.Host != "" {
// ip host // ip host
if ip := net.ParseIP(metadata.Host); ip != nil { if ip := net.ParseIP(metadata.Host); ip != nil {
@ -51,13 +52,23 @@ func getKey(metadata *C.Metadata) string {
} }
} }
if metadata.DstIP == nil { if !metadata.DstIP.IsValid() {
return "" return ""
} }
return metadata.DstIP.String() return metadata.DstIP.String()
} }
func getKeyWithSrcAndDst(metadata *C.Metadata) string {
dst := getKey(metadata)
src := ""
if metadata != nil {
src = metadata.SrcIP.String()
}
return fmt.Sprintf("%s%s", src, dst)
}
func jumpHash(key uint64, buckets int32) int32 { func jumpHash(key uint64, buckets int32) int32 {
var b, j int64 var b, j int64
@ -72,15 +83,25 @@ func jumpHash(key uint64, buckets int32) int32 {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
defer func() { proxy := lb.Unwrap(metadata, true)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(lb) c.AppendToChains(lb)
} else {
lb.onDialFailed(proxy.Type(), err)
} }
}()
proxy := lb.Unwrap(metadata) c = &callback.FirstWriteCallBackConn{
Conn: c,
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...) Callback: func(err error) {
if err == nil {
lb.onDialSuccess()
} else {
lb.onDialFailed(proxy.Type(), err)
}
},
}
return return
} }
@ -92,7 +113,7 @@ func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Meta
} }
}() }()
proxy := lb.Unwrap(metadata) proxy := lb.Unwrap(metadata, true)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...) return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
} }
@ -102,11 +123,20 @@ func (lb *LoadBalance) SupportUDP() bool {
} }
func strategyRoundRobin() strategyFn { func strategyRoundRobin() strategyFn {
flag := true
idx := 0 idx := 0
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy { return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
length := len(proxies) length := len(proxies)
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
idx = (idx + 1) % length flag = !flag
if flag {
idx = (idx - 1) % length
} else {
idx = (idx + 2) % length
}
if idx < 0 {
idx = idx + length
}
proxy := proxies[idx] proxy := proxies[idx]
if proxy.Alive() { if proxy.Alive() {
return proxy return proxy
@ -130,28 +160,62 @@ func strategyConsistentHashing() strategyFn {
} }
} }
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return proxies[0]
}
}
func strategyStickySessions() strategyFn {
ttl := time.Minute * 10
maxRetry := 5
lruCache := cache.New[uint64, int](
cache.WithAge[uint64, int](int64(ttl.Seconds())),
cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
length := len(proxies)
idx, has := lruCache.Get(key)
if !has {
idx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
}
nowIdx := idx
for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx]
if proxy.Alive() {
if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx)
}
return proxy
} else {
nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
}
}
lruCache.Delete(key)
lruCache.Set(key, 0)
return proxies[0] return proxies[0]
} }
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy { func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
proxies := lb.proxies(true) proxies := lb.GetProxies(touch)
return lb.strategyFn(proxies, metadata) return lb.strategyFn(proxies, metadata)
} }
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (any, error) {
return getProvidersProxies(lb.providers, touch, lb.filter), nil
})
return elm.([]C.Proxy)
}
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (lb *LoadBalance) MarshalJSON() ([]byte, error) { func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all := []string{} var all []string
for _, proxy := range lb.proxies(false) { for _, proxy := range lb.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -167,20 +231,25 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
strategyFn = strategyConsistentHashing() strategyFn = strategyConsistentHashing()
case "round-robin": case "round-robin":
strategyFn = strategyRoundRobin() strategyFn = strategyRoundRobin()
case "sticky-sessions":
strategyFn = strategyStickySessions()
default: default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy) return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
} }
return &LoadBalance{ return &LoadBalance{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name, Name: option.Name,
Type: C.LoadBalance, Type: C.LoadBalance,
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
filter: option.Filter,
}, nil }, nil
} }

View File

@ -30,6 +30,8 @@ type GroupCommonOption struct {
Lazy bool `group:"lazy,omitempty"` Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"` Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
} }
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
@ -75,8 +77,12 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
providers = append(providers, pd) providers = append(providers, pd)
providersMap[groupName] = pd providersMap[groupName] = pd
} else { } else {
if groupOption.URL == "" || groupOption.Interval == 0 { if groupOption.URL == "" {
return nil, errMissHealthCheck groupOption.URL = "https://cp.cloudflare.com/generate_204"
}
if groupOption.Interval == 0 {
groupOption.Interval = 300
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)

View File

@ -3,30 +3,54 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "net"
"net/netip"
"strings"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/singledo" N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/constant/provider"
) )
type Relay struct { type Relay struct {
*outbound.Base *GroupBase
single *singledo.Single }
providers []provider.ProxyProvider
filter string type proxyDialer struct {
proxy C.Proxy
dialer C.Dialer
}
func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
currentMeta, err := addrToMetadata(address)
if err != nil {
return nil, err
}
if strings.Contains(network, "udp") { // should not support this operation
currentMeta.NetWork = C.UDP
pc, err := p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
if err != nil {
return nil, err
}
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
}
return p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta)
}
func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
currentMeta, err := addrToMetadata(rAddrPort.String())
if err != nil {
return nil, err
}
currentMeta.NetWork = C.UDP
return p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
var proxies []C.Proxy proxies, chainProxies := r.proxies(metadata, true)
for _, proxy := range r.proxies(metadata, true) {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
proxies = append(proxies, proxy)
}
}
switch len(proxies) { switch len(proxies) {
case 0: case 0:
@ -34,43 +58,88 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
case 1: case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...) return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
var d C.Dialer
first := proxies[0] d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
last := proxies[len(proxies)-1] for _, proxy := range proxies[:len(proxies)-1] {
d = proxyDialer{
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...) proxy: proxy,
if err != nil { dialer: d,
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
tcpKeepAlive(c) }
last := proxies[len(proxies)-1]
var currentMeta *C.Metadata conn, err := last.DialContextWithDialer(ctx, d, metadata)
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err = first.StreamConn(c, currentMeta) for i := len(chainProxies) - 2; i >= 0; i-- {
conn.AppendToChains(chainProxies[i])
}
conn.AppendToChains(r)
return conn, nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
proxies, chainProxies := r.proxies(metadata, true)
switch len(proxies) {
case 0:
return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1:
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var d C.Dialer
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
for _, proxy := range proxies[:len(proxies)-1] {
d = proxyDialer{
proxy: proxy,
dialer: d,
}
}
last := proxies[len(proxies)-1]
pc, err := last.ListenPacketWithDialer(ctx, d, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err) return nil, err
} }
first = proxy for i := len(chainProxies) - 2; i >= 0; i-- {
pc.AppendToChains(chainProxies[i])
} }
c, err = last.StreamConn(c, metadata) pc.AppendToChains(r)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
return outbound.NewConn(c, r), nil return pc, nil
}
// SupportUDP implements C.ProxyAdapter
func (r *Relay) SupportUDP() bool {
proxies, _ := r.proxies(nil, false)
if len(proxies) == 0 { // C.Direct
return true
}
for i := len(proxies) - 1; i >= 0; i-- {
proxy := proxies[i]
if !proxy.SupportUDP() {
return false
}
if proxy.SupportUOT() {
return true
}
if !proxy.SupportWithDialer() {
return false
}
}
return true
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range r.rawProxies(false) { for _, proxy := range r.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -79,38 +148,51 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
}) })
} }
func (r *Relay) rawProxies(touch bool) []C.Proxy { func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
elm, _, _ := r.single.Do(func() (any, error) { rawProxies := r.GetProxies(touch)
return getProvidersProxies(r.providers, touch, r.filter), nil
})
return elm.([]C.Proxy) var proxies []C.Proxy
var chainProxies []C.Proxy
var targetProxies []C.Proxy
for n, proxy := range rawProxies {
proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy)
subproxy := proxy.Unwrap(metadata, touch)
for subproxy != nil {
chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata, touch)
}
}
for _, proxy := range proxies {
if proxy.Type() != C.Direct && proxy.Type() != C.Compatible {
targetProxies = append(targetProxies, proxy)
}
}
return targetProxies, chainProxies
} }
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy { func (r *Relay) Addr() string {
proxies := r.rawProxies(touch) proxies, _ := r.proxies(nil, true)
return proxies[len(proxies)-1].Addr()
for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata)
for subproxy != nil {
proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata)
}
}
return proxies
} }
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
return &Relay{ return &Relay{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name, Name: option.Name,
Type: C.Relay, Type: C.Relay,
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
},
"",
"",
"",
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration),
providers: providers,
filter: option.Filter,
} }
} }

View File

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

View File

@ -3,11 +3,10 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/Dreamacro/clash/log"
"go.uber.org/atomic"
"time" "time"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/callback"
"github.com/Dreamacro/clash/common/singledo" "github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
@ -23,16 +22,11 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
} }
type URLTest struct { type URLTest struct {
*outbound.Base *GroupBase
tolerance uint16 tolerance uint16
disableUDP bool disableUDP bool
fastNode C.Proxy fastNode C.Proxy
filter string fastSingle *singledo.Single[C.Proxy]
single *singledo.Single
fastSingle *singledo.Single
providers []provider.ProxyProvider
failedTimes *atomic.Int32
failedTime *atomic.Int64
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
@ -41,13 +35,23 @@ func (u *URLTest) Now() string {
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...) proxy := u.fast(true)
c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else { } else {
u.onDialFailed() u.onDialFailed(proxy.Type(), err)
}
c = &callback.FirstWriteCallBackConn{
Conn: c,
Callback: func(err error) {
if err == nil {
u.onDialSuccess()
} else {
u.onDialFailed(proxy.Type(), err)
}
},
} }
return c, err return c, err
} }
@ -57,30 +61,19 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...) pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
u.onDialFailed()
} }
return pc, err return pc, err
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(*C.Metadata) C.Proxy { func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return u.fast(true) return u.fast(touch)
}
func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (any, error) {
return getProvidersProxies(u.providers, touch, u.filter), nil
})
return elm.([]C.Proxy)
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (any, error) { elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.proxies(touch) proxies := u.GetProxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
fastNotExist := true fastNotExist := true
@ -108,8 +101,11 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return u.fastNode, nil return u.fastNode, nil
}) })
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
u.Touch()
}
return elm.(C.Proxy) return elm
} }
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
@ -124,7 +120,7 @@ func (u *URLTest) SupportUDP() bool {
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
func (u *URLTest) MarshalJSON() ([]byte, error) { func (u *URLTest) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range u.proxies(false) { for _, proxy := range u.GetProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
@ -134,32 +130,6 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
}) })
} }
func (u *URLTest) onDialFailed() {
if u.failedTime.Load() == -1 {
log.Warnln("%s first failed", u.Name())
now := time.Now().UnixMilli()
u.failedTime.Store(now)
u.failedTimes.Store(1)
} else {
if u.failedTime.Load()-time.Now().UnixMilli() > 5*1000 {
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
} else {
failedCount := u.failedTimes.Inc()
log.Warnln("%s failed count: %d", u.Name(), failedCount)
if failedCount >= 5 {
log.Warnln("because %s failed multiple times, active health check", u.Name())
for _, proxyProvider := range u.providers {
go proxyProvider.HealthCheck()
}
u.failedTimes.Store(-1)
u.failedTime.Store(-1)
}
}
}
}
func parseURLTestOption(config map[string]any) []urlTestOption { func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{} opts := []urlTestOption{}
@ -175,19 +145,21 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{ urlTest := &URLTest{
Base: outbound.NewBase(outbound.BaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
outbound.BaseOption{
Name: option.Name, Name: option.Name,
Type: C.URLTest, Type: C.URLTest,
Interface: option.Interface, Interface: option.Interface,
RoutingMark: option.RoutingMark, RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}), }),
single: singledo.NewSingle(defaultGetProxiesDuration), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
fastSingle: singledo.NewSingle(time.Second * 10),
providers: providers,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
filter: option.Filter,
failedTimes: atomic.NewInt32(-1),
failedTime: atomic.NewInt64(-1),
} }
for _, option := range options { for _, option := range options {

View File

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

View File

@ -3,13 +3,15 @@ package adapter
import ( import (
"fmt" "fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
) )
func ParseProxy(mapping map[string]any) (C.Proxy, error) { func ParseProxy(mapping map[string]any) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer})
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {
return nil, fmt.Errorf("missing type") return nil, fmt.Errorf("missing type")
@ -40,14 +42,14 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
proxy = outbound.NewSocks5(*socksOption) proxy, err = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &outbound.HttpOption{} httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption) err = decoder.Decode(mapping, httpOption)
if err != nil { if err != nil {
break break
} }
proxy = outbound.NewHttp(*httpOption) proxy, err = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &outbound.VmessOption{ vmessOption := &outbound.VmessOption{
HTTPOpts: outbound.HTTPOptions{ HTTPOpts: outbound.HTTPOptions{
@ -55,6 +57,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
Path: []string{"/"}, Path: []string{"/"},
}, },
} }
if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 {
vmessOption.ClientFingerprint = GlobalUtlsClient
}
err = decoder.Decode(mapping, vmessOption) err = decoder.Decode(mapping, vmessOption)
if err != nil { if err != nil {
break break
@ -62,6 +69,11 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
proxy, err = outbound.NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "vless": case "vless":
vlessOption := &outbound.VlessOption{} vlessOption := &outbound.VlessOption{}
if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 {
vlessOption.ClientFingerprint = GlobalUtlsClient
}
err = decoder.Decode(mapping, vlessOption) err = decoder.Decode(mapping, vlessOption)
if err != nil { if err != nil {
break break
@ -76,11 +88,37 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
proxy, err = outbound.NewSnell(*snellOption) proxy, err = outbound.NewSnell(*snellOption)
case "trojan": case "trojan":
trojanOption := &outbound.TrojanOption{} trojanOption := &outbound.TrojanOption{}
if GlobalUtlsClient := tlsC.GetGlobalFingerprint(); len(GlobalUtlsClient) != 0 {
trojanOption.ClientFingerprint = GlobalUtlsClient
}
err = decoder.Decode(mapping, trojanOption) err = decoder.Decode(mapping, trojanOption)
if err != nil { if err != nil {
break break
} }
proxy, err = outbound.NewTrojan(*trojanOption) proxy, err = outbound.NewTrojan(*trojanOption)
case "hysteria":
hyOption := &outbound.HysteriaOption{}
err = decoder.Decode(mapping, hyOption)
if err != nil {
break
}
proxy, err = outbound.NewHysteria(*hyOption)
case "wireguard":
wgOption := &outbound.WireGuardOption{}
err = decoder.Decode(mapping, wgOption)
if err != nil {
break
}
proxy, err = outbound.NewWireGuard(*wgOption)
case "tuic":
tuicOption := &outbound.TuicOption{}
err = decoder.Decode(mapping, tuicOption)
if err != nil {
break
}
proxy, err = outbound.NewTuic(*tuicOption)
default: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }

View File

@ -1,185 +0,0 @@
package provider
import (
"bytes"
"crypto/md5"
"os"
"path/filepath"
"time"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
)
var (
fileMode os.FileMode = 0o666
dirMode os.FileMode = 0o755
)
type parser = func([]byte) (any, error)
type fetcher struct {
name string
vehicle types.Vehicle
updatedAt *time.Time
ticker *time.Ticker
done chan struct{}
hash [16]byte
parser parser
onUpdate func(any)
}
func (f *fetcher) Name() string {
return f.name
}
func (f *fetcher) VehicleType() types.VehicleType {
return f.vehicle.Type()
}
func (f *fetcher) Initial() (any, error) {
var (
buf []byte
err error
isLocal bool
)
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
buf, err = os.ReadFile(f.vehicle.Path())
modTime := stat.ModTime()
f.updatedAt = &modTime
isLocal = true
} else {
buf, err = f.vehicle.Read()
}
if err != nil {
return nil, err
}
proxies, err := f.parser(buf)
if err != nil {
if !isLocal {
return nil, err
}
// parse local file error, fallback to remote
buf, err = f.vehicle.Read()
if err != nil {
return nil, err
}
proxies, err = f.parser(buf)
if err != nil {
return nil, err
}
isLocal = false
}
if f.vehicle.Type() != types.File && !isLocal {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, err
}
}
f.hash = md5.Sum(buf)
// pull proxies automatically
if f.ticker != nil {
go f.pullLoop()
}
return proxies, nil
}
func (f *fetcher) Update() (any, bool, error) {
buf, err := f.vehicle.Read()
if err != nil {
return nil, false, err
}
now := time.Now()
hash := md5.Sum(buf)
if bytes.Equal(f.hash[:], hash[:]) {
f.updatedAt = &now
os.Chtimes(f.vehicle.Path(), now, now)
return nil, true, nil
}
proxies, err := f.parser(buf)
if err != nil {
return nil, false, err
}
if f.vehicle.Type() != types.File {
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
return nil, false, err
}
}
f.updatedAt = &now
f.hash = hash
return proxies, false, nil
}
func (f *fetcher) Destroy() error {
if f.ticker != nil {
f.done <- struct{}{}
}
return nil
}
func (f *fetcher) pullLoop() {
for {
select {
case <-f.ticker.C:
elm, same, err := f.Update()
if err != nil {
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
continue
}
if same {
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
continue
}
log.Infoln("[Provider] %s's proxies update", f.Name())
if f.onUpdate != nil {
f.onUpdate(elm)
}
case <-f.done:
f.ticker.Stop()
return
}
}
}
func safeWrite(path string, buf []byte) error {
dir := filepath.Dir(path)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, dirMode); err != nil {
return err
}
}
return os.WriteFile(path, buf, fileMode)
}
func newFetcher(name string, interval time.Duration, vehicle types.Vehicle, parser parser, onUpdate func(any)) *fetcher {
var ticker *time.Ticker
if interval != 0 {
ticker = time.NewTicker(interval)
}
return &fetcher{
name: name,
ticker: ticker,
vehicle: vehicle,
parser: parser,
done: make(chan struct{}, 1),
onUpdate: onUpdate,
}
}

View File

@ -5,8 +5,11 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/batch" "github.com/Dreamacro/clash/common/batch"
"github.com/Dreamacro/clash/common/singledo"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/gofrs/uuid"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@ -26,25 +29,21 @@ type HealthCheck struct {
lazy bool lazy bool
lastTouch *atomic.Int64 lastTouch *atomic.Int64
done chan struct{} done chan struct{}
singleDo *singledo.Single[struct{}]
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second) ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
go func() { go func() {
t := time.NewTicker(30 * time.Second) time.Sleep(30 * time.Second)
<-t.C hc.lazyCheck()
t.Stop()
hc.check()
}() }()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
now := time.Now().Unix() hc.lazyCheck()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
return return
@ -52,6 +51,17 @@ func (hc *HealthCheck) process() {
} }
} }
func (hc *HealthCheck) lazyCheck() bool {
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
return true
} else {
log.Debugln("Skip once health check because we are lazy")
return false
}
}
func (hc *HealthCheck) setProxy(proxies []C.Proxy) { func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies hc.proxies = proxies
} }
@ -65,17 +75,29 @@ func (hc *HealthCheck) touch() {
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10)) _, _, _ = hc.singleDo.Do(func() (struct{}, error) {
id := ""
if uid, err := uuid.NewV4(); err == nil {
id = uid.String()
}
log.Debugln("Start New Health Checking {%s}", id)
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range hc.proxies { for _, proxy := range hc.proxies {
p := proxy p := proxy
b.Go(p.Name(), func() (any, error) { b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel() defer cancel()
p.URLTest(ctx, hc.url) log.Debugln("Health Checking %s {%s}", p.Name(), id)
return nil, nil _, _ = p.URLTest(ctx, hc.url)
log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id)
return false, nil
}) })
} }
b.Wait() b.Wait()
log.Debugln("Finish A Health Checking {%s}", id)
return struct{}{}, nil
})
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {
@ -90,5 +112,6 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
lazy: lazy, lazy: lazy,
lastTouch: atomic.NewInt64(0), lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1), done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
} }
} }

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/Dreamacro/clash/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
) )
@ -25,6 +26,8 @@ type proxyProviderSchema struct {
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"` Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"` Filter string `provider:"filter,omitempty"`
ExcludeFilter string `provider:"exclude-filter,omitempty"`
ExcludeType string `provider:"exclude-type,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
} }
@ -51,14 +54,17 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
var vehicle types.Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
vehicle = NewFileVehicle(path) vehicle = resource.NewFileVehicle(path)
case "http": case "http":
vehicle = NewHTTPVehicle(schema.URL, path) vehicle = resource.NewHTTPVehicle(schema.URL, path)
default: default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter filter := schema.Filter
return NewProxySetProvider(name, interval, filter, vehicle, hc) excludeFilter := schema.ExcludeFilter
excludeType := schema.ExcludeType
return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, vehicle, hc)
} }

View File

@ -1,18 +1,25 @@
package provider package provider
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/dlclark/regexp2" "net/http"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/Dreamacro/clash/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
clashHttp "github.com/Dreamacro/clash/component/http"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider" types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
"gopkg.in/yaml.v2" "github.com/dlclark/regexp2"
"gopkg.in/yaml.v3"
) )
const ( const (
@ -23,15 +30,17 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
// for auto gc // ProxySetProvider for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
*fetcher *resource.Fetcher[[]C.Proxy]
proxies []C.Proxy proxies []C.Proxy
healthCheck *HealthCheck healthCheck *HealthCheck
version uint32
subscriptionInfo *SubscriptionInfo
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
@ -40,13 +49,17 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
"type": pp.Type().String(), "type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(), "proxies": pp.Proxies(),
//TODO maybe error because year value overflow "updatedAt": pp.UpdatedAt,
"updatedAt": pp.updatedAt, "subscriptionInfo": pp.subscriptionInfo,
}) })
} }
func (pp *proxySetProvider) Version() uint32 {
return pp.version
}
func (pp *proxySetProvider) Name() string { func (pp *proxySetProvider) Name() string {
return pp.name return pp.Fetcher.Name()
} }
func (pp *proxySetProvider) HealthCheck() { func (pp *proxySetProvider) HealthCheck() {
@ -54,24 +67,19 @@ func (pp *proxySetProvider) HealthCheck() {
} }
func (pp *proxySetProvider) Update() error { func (pp *proxySetProvider) Update() error {
elm, same, err := pp.fetcher.Update() elm, same, err := pp.Fetcher.Update()
if err == nil && !same { if err == nil && !same {
pp.onUpdate(elm) pp.OnUpdate(elm)
} }
return err return err
} }
func (pp *proxySetProvider) Initial() error { func (pp *proxySetProvider) Initial() error {
elm, err := pp.fetcher.Initial() elm, err := pp.Fetcher.Initial()
if err != nil { if err != nil {
return err return err
} }
pp.OnUpdate(elm)
pp.onUpdate(elm)
if pp.healthCheck.auto() {
go pp.healthCheck.process()
}
return nil return nil
} }
@ -83,85 +91,95 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy { func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch() pp.healthCheck.touch()
return pp.Proxies()
} }
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) { func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies pp.proxies = proxies
pp.healthCheck.setProxy(proxies) pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() { if pp.healthCheck.auto() {
go pp.healthCheck.check() defer func() { go pp.healthCheck.lazyCheck() }()
} }
} }
func (pp *proxySetProvider) getSubscriptionInfo() {
if pp.VehicleType() != types.HTTP {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo"))
if userInfoStr == "" {
resp2, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil)
if err != nil {
return
}
defer resp2.Body.Close()
userInfoStr = strings.TrimSpace(resp2.Header.Get("subscription-userinfo"))
if userInfoStr == "" {
return
}
}
pp.subscriptionInfo, err = NewSubscriptionInfo(userInfoStr)
if err != nil {
log.Warnln("[Provider] get subscription-userinfo: %e", err)
}
}()
}
func stopProxyProvider(pd *ProxySetProvider) { func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close() pd.healthCheck.close()
pd.fetcher.Destroy() _ = pd.Fetcher.Destroy()
} }
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
//filterReg, err := regexp.Compile(filter) excludeFilterReg, err := regexp2.Compile(excludeFilter, 0)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
}
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, 0) filterReg, err := regexp2.Compile(filter, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err) return nil, fmt.Errorf("invalid filter regex: %w", err)
} }
filterRegs = append(filterRegs, filterReg)
}
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{ pd := &proxySetProvider{
proxies: []C.Proxy{}, proxies: []C.Proxy{},
healthCheck: hc, healthCheck: hc,
} }
onUpdate := func(elm any) { fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg), proxiesOnUpdate(pd))
ret := elm.([]C.Proxy) pd.Fetcher = fetcher
pd.setProxies(ret)
}
proxiesParseAndFilter := func(buf []byte) (any, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
return nil, err
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
for idx, mapping := range schema.Proxies {
name, ok := mapping["name"]
mat, _ := filterReg.FindStringMatch(name.(string))
if ok && len(filter) > 0 && mat == nil {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
fetcher := newFetcher(name, interval, vehicle, proxiesParseAndFilter, onUpdate)
pd.fetcher = fetcher
pd.getSubscriptionInfo()
wrapper := &ProxySetProvider{pd} wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider) runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil return wrapper, nil
} }
// for auto gc // CompatibleProvider for auto gc
type CompatibleProvider struct { type CompatibleProvider struct {
*compatibleProvider *compatibleProvider
} }
@ -170,6 +188,7 @@ type compatibleProvider struct {
name string name string
healthCheck *HealthCheck healthCheck *HealthCheck
proxies []C.Proxy proxies []C.Proxy
version uint32
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
@ -181,6 +200,10 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
}) })
} }
func (cp *compatibleProvider) Version() uint32 {
return cp.version
}
func (cp *compatibleProvider) Name() string { func (cp *compatibleProvider) Name() string {
return cp.name return cp.name
} }
@ -194,10 +217,6 @@ func (cp *compatibleProvider) Update() error {
} }
func (cp *compatibleProvider) Initial() error { func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.auto() {
go cp.healthCheck.process()
}
return nil return nil
} }
@ -213,9 +232,8 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies return cp.proxies
} }
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy { func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch() cp.healthCheck.touch()
return cp.Proxies()
} }
func stopCompatibleProvider(pd *CompatibleProvider) { func stopCompatibleProvider(pd *CompatibleProvider) {
@ -227,6 +245,10 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
return nil, errors.New("provider need one proxy at least") return nil, errors.New("provider need one proxy at least")
} }
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{ pd := &compatibleProvider{
name: name, name: name,
proxies: proxies, proxies: proxies,
@ -237,3 +259,94 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
runtime.SetFinalizer(wrapper, stopCompatibleProvider) runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil return wrapper, nil
} }
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
pd.version += 1
pd.getSubscriptionInfo()
}
}
func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp) resource.Parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%s, %w", err.Error(), err1)
}
schema.Proxies = proxies
}
if schema.Proxies == nil {
return nil, errors.New("file must have a `proxies` field")
}
proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs {
for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 {
mType, ok := mapping["type"]
if !ok {
continue
}
pType, ok := mType.(string)
if !ok {
continue
}
flag := false
for i := range excludeTypeArray {
if strings.EqualFold(pType, excludeTypeArray[i]) {
flag = true
break
}
}
if flag {
continue
}
}
mName, ok := mapping["name"]
if !ok {
continue
}
name, ok := mName.(string)
if !ok {
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
}
if len(filter) > 0 {
if mat, _ := filterReg.FindStringMatch(name); mat == nil {
continue
}
}
if _, ok := proxiesSet[name]; ok {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxiesSet[name] = struct{}{}
proxies = append(proxies, proxy)
}
}
if len(proxies) == 0 {
if len(filter) > 0 {
return nil, errors.New("doesn't match any proxy, please check your filter")
}
return nil, errors.New("file doesn't have any proxy")
}
return proxies, nil
}
}

View File

@ -0,0 +1,57 @@
package provider
import (
"github.com/dlclark/regexp2"
"strconv"
"strings"
)
type SubscriptionInfo struct {
Upload int64
Download int64
Total int64
Expire int64
}
func NewSubscriptionInfo(str string) (si *SubscriptionInfo, err error) {
si = &SubscriptionInfo{}
str = strings.ToLower(str)
reTraffic := regexp2.MustCompile("upload=(\\d+); download=(\\d+); total=(\\d+)", 0)
reExpire := regexp2.MustCompile("expire=(\\d+)", 0)
match, err := reTraffic.FindStringMatch(str)
if err != nil || match == nil {
return nil, err
}
group := match.Groups()
si.Upload, err = str2uint64(group[1].String())
if err != nil {
return nil, err
}
si.Download, err = str2uint64(group[2].String())
if err != nil {
return nil, err
}
si.Total, err = str2uint64(group[3].String())
if err != nil {
return nil, err
}
match, _ = reExpire.FindStringMatch(str)
if match != nil {
group = match.Groups()
si.Expire, err = str2uint64(group[1].String())
if err != nil {
return nil, err
}
}
return
}
func str2uint64(str string) (int64, error) {
i, err := strconv.ParseInt(str, 10, 64)
return i, err
}

View File

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

28
check_amd64.sh Normal file
View File

@ -0,0 +1,28 @@
#!/bin/sh
flags=$(grep '^flags\b' </proc/cpuinfo | head -n 1)
flags=" ${flags#*:} "
has_flags () {
for flag; do
case "$flags" in
*" $flag "*) :;;
*) return 1;;
esac
done
}
determine_level () {
level=0
has_flags lm cmov cx8 fpu fxsr mmx syscall sse2 || return 0
level=1
has_flags cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 || return 0
level=2
has_flags avx avx2 bmi1 bmi2 f16c fma abm movbe xsave || return 0
level=3
has_flags avx512f avx512bw avx512cd avx512dq avx512vl || return 0
level=4
}
determine_level
echo "Your CPU supports amd64-v$level"
return $level

View File

@ -5,10 +5,10 @@ import (
"sync" "sync"
) )
type Option = func(b *Batch) type Option[T any] func(b *Batch[T])
type Result struct { type Result[T any] struct {
Value any Value T
Err error Err error
} }
@ -17,8 +17,8 @@ type Error struct {
Err error Err error
} }
func WithConcurrencyNum(n int) Option { func WithConcurrencyNum[T any](n int) Option[T] {
return func(b *Batch) { return func(b *Batch[T]) {
q := make(chan struct{}, n) q := make(chan struct{}, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
q <- struct{}{} q <- struct{}{}
@ -28,8 +28,8 @@ func WithConcurrencyNum(n int) Option {
} }
// Batch similar to errgroup, but can control the maximum number of concurrent // Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct { type Batch[T any] struct {
result map[string]Result result map[string]Result[T]
queue chan struct{} queue chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
mux sync.Mutex mux sync.Mutex
@ -38,7 +38,7 @@ type Batch struct {
cancel func() cancel func()
} }
func (b *Batch) Go(key string, fn func() (any, error)) { func (b *Batch[T]) Go(key string, fn func() (T, error)) {
b.wg.Add(1) b.wg.Add(1)
go func() { go func() {
defer b.wg.Done() defer b.wg.Done()
@ -59,14 +59,14 @@ func (b *Batch) Go(key string, fn func() (any, error)) {
}) })
} }
ret := Result{value, err} ret := Result[T]{value, err}
b.mux.Lock() b.mux.Lock()
defer b.mux.Unlock() defer b.mux.Unlock()
b.result[key] = ret b.result[key] = ret
}() }()
} }
func (b *Batch) Wait() *Error { func (b *Batch[T]) Wait() *Error {
b.wg.Wait() b.wg.Wait()
if b.cancel != nil { if b.cancel != nil {
b.cancel() b.cancel()
@ -74,26 +74,26 @@ func (b *Batch) Wait() *Error {
return b.err return b.err
} }
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) { func (b *Batch[T]) WaitAndGetResult() (map[string]Result[T], *Error) {
err := b.Wait() err := b.Wait()
return b.Result(), err return b.Result(), err
} }
func (b *Batch) Result() map[string]Result { func (b *Batch[T]) Result() map[string]Result[T] {
b.mux.Lock() b.mux.Lock()
defer b.mux.Unlock() defer b.mux.Unlock()
copy := map[string]Result{} copyM := map[string]Result[T]{}
for k, v := range b.result { for k, v := range b.result {
copy[k] = v copyM[k] = v
} }
return copy return copyM
} }
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) { func New[T any](ctx context.Context, opts ...Option[T]) (*Batch[T], context.Context) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
b := &Batch{ b := &Batch[T]{
result: map[string]Result{}, result: map[string]Result[T]{},
} }
for _, o := range opts { for _, o := range opts {

View File

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

20
common/buf/sing.go Normal file
View File

@ -0,0 +1,20 @@
package buf
import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
)
type Buffer = buf.Buffer
var StackNew = buf.StackNew
var StackNewSize = buf.StackNewSize
var KeepAlive = common.KeepAlive
//go:norace
func Dup[T any](obj T) T {
return common.Dup(obj)
}
var Must = common.Must
var Error = common.Error

106
common/cache/cache.go vendored
View File

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

View File

@ -1,72 +0,0 @@
package cache
import (
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCache_Basic(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
c := New[string, int](interval)
c.Put("int", 1, ttl)
d := New[string, string](interval)
d.Put("string", "a", ttl)
i := c.Get("int")
assert.Equal(t, i, 1, "should recv 1")
s := d.Get("string")
assert.Equal(t, s, "a", "should recv 'a'")
}
func TestCache_TTL(t *testing.T) {
interval := 200 * time.Millisecond
ttl := 20 * time.Millisecond
now := time.Now()
c := New[string, int](interval)
c.Put("int", 1, ttl)
c.Put("int2", 2, ttl)
i := c.Get("int")
_, expired := c.GetWithExpire("int2")
assert.Equal(t, i, 1, "should recv 1")
assert.True(t, now.Before(expired))
time.Sleep(ttl * 2)
i = c.Get("int")
j, _ := c.GetWithExpire("int2")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoCleanup(t *testing.T) {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New[string, int](interval)
c.Put("int", 1, ttl)
time.Sleep(ttl * 2)
i := c.Get("int")
j, _ := c.GetWithExpire("int")
assert.True(t, i == 0, "should recv 0")
assert.True(t, j == 0, "should recv 0")
}
func TestCache_AutoGC(t *testing.T) {
sign := make(chan struct{})
go func() {
interval := 10 * time.Millisecond
ttl := 15 * time.Millisecond
c := New[string, int](interval)
c.Put("int", 1, ttl)
sign <- struct{}{}
}()
<-sign
runtime.GC()
}

View File

@ -3,19 +3,20 @@ package cache
// Modified by https://github.com/die-net/lrucache // Modified by https://github.com/die-net/lrucache
import ( import (
"container/list"
"sync" "sync"
"time" "time"
"github.com/Dreamacro/clash/common/generics/list"
) )
// Option is part of Functional Options Pattern // Option is part of Functional Options Pattern
type Option[K comparable, V any] func(*LruCache[K, V]) type Option[K comparable, V any] func(*LruCache[K, V])
// EvictCallback is used to get a callback when a cache entry is evicted // EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback = func(key any, value any) type EvictCallback[K comparable, V any] func(key K, value V)
// WithEvict set the evict callback // WithEvict set the evict callback
func WithEvict[K comparable, V any](cb EvictCallback) Option[K, V] { func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
return func(l *LruCache[K, V]) { return func(l *LruCache[K, V]) {
l.onEvict = cb l.onEvict = cb
} }
@ -57,18 +58,18 @@ type LruCache[K comparable, V any] struct {
maxAge int64 maxAge int64
maxSize int maxSize int
mu sync.Mutex mu sync.Mutex
cache map[any]*list.Element cache map[K]*list.Element[*entry[K, V]]
lru *list.List // Front is least-recent lru *list.List[*entry[K, V]] // Front is least-recent
updateAgeOnGet bool updateAgeOnGet bool
staleReturn bool staleReturn bool
onEvict EvictCallback onEvict EvictCallback[K, V]
} }
// NewLRUCache creates an LruCache // New creates an LruCache
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{ lc := &LruCache[K, V]{
lru: list.New(), lru: list.New[*entry[K, V]](),
cache: make(map[any]*list.Element), cache: make(map[K]*list.Element[*entry[K, V]]),
} }
for _, option := range options { for _, option := range options {
@ -129,7 +130,7 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value.(*entry[K, V]) e := le.Value
e.value = value e.value = value
e.expires = expires.Unix() e.expires = expires.Unix()
} else { } else {
@ -154,11 +155,11 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
n.mu.Lock() n.mu.Lock()
defer n.mu.Unlock() defer n.mu.Unlock()
n.lru = list.New() n.lru = list.New[*entry[K, V]]()
n.cache = make(map[any]*list.Element) n.cache = make(map[K]*list.Element[*entry[K, V]])
for e := c.lru.Front(); e != nil; e = e.Next() { for e := c.lru.Front(); e != nil; e = e.Next() {
elm := e.Value.(*entry[K, V]) elm := e.Value
n.cache[elm.key] = n.lru.PushBack(elm) n.cache[elm.key] = n.lru.PushBack(elm)
} }
} }
@ -172,7 +173,7 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
return nil return nil
} }
if !c.staleReturn && c.maxAge > 0 && le.Value.(*entry[K, V]).expires <= time.Now().Unix() { if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() {
c.deleteElement(le) c.deleteElement(le)
c.maybeDeleteOldest() c.maybeDeleteOldest()
@ -180,7 +181,7 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
} }
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
el := le.Value.(*entry[K, V]) el := le.Value
if c.maxAge > 0 && c.updateAgeOnGet { if c.maxAge > 0 && c.updateAgeOnGet {
el.expires = time.Now().Unix() + c.maxAge el.expires = time.Now().Unix() + c.maxAge
} }
@ -201,15 +202,15 @@ func (c *LruCache[K, V]) Delete(key K) {
func (c *LruCache[K, V]) maybeDeleteOldest() { func (c *LruCache[K, V]) maybeDeleteOldest() {
if !c.staleReturn && c.maxAge > 0 { if !c.staleReturn && c.maxAge > 0 {
now := time.Now().Unix() now := time.Now().Unix()
for le := c.lru.Front(); le != nil && le.Value.(*entry[K, V]).expires <= now; le = c.lru.Front() { for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() {
c.deleteElement(le) c.deleteElement(le)
} }
} }
} }
func (c *LruCache[K, V]) deleteElement(le *list.Element) { func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
c.lru.Remove(le) c.lru.Remove(le)
e := le.Value.(*entry[K, V]) e := le.Value
delete(c.cache, e.key) delete(c.cache, e.key)
if c.onEvict != nil { if c.onEvict != nil {
c.onEvict(e.key, e.value) c.onEvict(e.key, e.value)
@ -219,7 +220,7 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element) {
func (c *LruCache[K, V]) Clear() error { func (c *LruCache[K, V]) Clear() error {
c.mu.Lock() c.mu.Lock()
c.cache = make(map[any]*list.Element) c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock() c.mu.Unlock()
return nil return nil

View File

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

View File

@ -0,0 +1,25 @@
package callback
import (
C "github.com/Dreamacro/clash/constant"
)
type FirstWriteCallBackConn struct {
C.Conn
Callback func(error)
written bool
}
func (c *FirstWriteCallBackConn) Write(b []byte) (n int, err error) {
defer func() {
if !c.written {
c.written = true
c.Callback(err)
}
}()
return c.Conn.Write(b)
}
func (c *FirstWriteCallBackConn) Upstream() any {
return c.Conn
}

45
common/convert/base64.go Normal file
View File

@ -0,0 +1,45 @@
package convert
import (
"encoding/base64"
"strings"
)
var (
encRaw = base64.RawStdEncoding
enc = base64.StdEncoding
)
// DecodeBase64 try to decode content from the given bytes,
// which can be in base64.RawStdEncoding, base64.StdEncoding or just plaintext.
func DecodeBase64(buf []byte) []byte {
result, err := tryDecodeBase64(buf)
if err != nil {
return buf
}
return result
}
func tryDecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, encRaw.DecodedLen(len(buf)))
n, err := encRaw.Decode(dBuf, buf)
if err != nil {
n, err = enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
}
return dBuf[:n], nil
}
func urlSafe(data string) string {
return strings.NewReplacer("+", "-", "/", "_").Replace(data)
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.RawURLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}

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

@ -0,0 +1,408 @@
package convert
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/Dreamacro/clash/log"
"net/url"
"strconv"
"strings"
)
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data := DecodeBase64(buf)
arr := strings.Split(string(data), "\n")
proxies := make([]map[string]any, 0, len(arr))
names := make(map[string]int, 200)
for _, line := range arr {
line = strings.TrimRight(line, " \r")
if line == "" {
continue
}
scheme, body, found := strings.Cut(line, "://")
if !found {
continue
}
scheme = strings.ToLower(scheme)
switch scheme {
case "hysteria":
urlHysteria, err := url.Parse(line)
if err != nil {
continue
}
query := urlHysteria.Query()
name := uniqueName(names, urlHysteria.Fragment)
hysteria := make(map[string]any, 20)
hysteria["name"] = name
hysteria["type"] = scheme
hysteria["server"] = urlHysteria.Hostname()
hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs")
hysteria["alpn"] = []string{query.Get("alpn")}
hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol")
up := query.Get("up")
down := query.Get("down")
if up == "" {
up = query.Get("upmbps")
}
if down == "" {
down = query.Get("downmbps")
}
hysteria["down"] = down
hysteria["up"] = up
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria)
case "trojan":
urlTrojan, err := url.Parse(line)
if err != nil {
continue
}
query := urlTrojan.Query()
name := uniqueName(names, urlTrojan.Fragment)
trojan := make(map[string]any, 20)
trojan["name"] = name
trojan["type"] = scheme
trojan["server"] = urlTrojan.Hostname()
trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username()
trojan["udp"] = true
trojan["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni
}
network := strings.ToLower(query.Get("type"))
if network != "" {
trojan["network"] = network
}
switch network {
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
trojan["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
trojan["grpc-opts"] = grpcOpts
}
if fingerprint := query.Get("fp"); fingerprint == "" {
trojan["client-fingerprint"] = "chrome"
} else {
trojan["client-fingerprint"] = fingerprint
}
proxies = append(proxies, trojan)
case "vless":
urlVLess, err := url.Parse(line)
if err != nil {
continue
}
query := urlVLess.Query()
vless := make(map[string]any, 20)
err = handleVShareLink(names, urlVLess, scheme, vless)
if err != nil {
log.Warnln("error:%s line:%s", err.Error(), line)
continue
}
if flow := query.Get("flow"); flow != "" {
vless["flow"] = strings.ToLower(flow)
}
proxies = append(proxies, vless)
case "vmess":
// V2RayN-styled share link
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
dcBuf, err := tryDecodeBase64([]byte(body))
if err != nil {
// Xray VMessAEAD share link
urlVMess, err := url.Parse(line)
if err != nil {
continue
}
query := urlVMess.Query()
vmess := make(map[string]any, 20)
err = handleVShareLink(names, urlVMess, scheme, vmess)
if err != nil {
log.Warnln("error:%s line:%s", err.Error(), line)
continue
}
vmess["alterId"] = 0
vmess["cipher"] = "auto"
if encryption := query.Get("encryption"); encryption != "" {
vmess["cipher"] = encryption
}
proxies = append(proxies, vmess)
continue
}
jsonDc := json.NewDecoder(bytes.NewReader(dcBuf))
values := make(map[string]any, 20)
if jsonDc.Decode(&values) != nil {
continue
}
tempName, ok := values["ps"].(string)
if !ok {
continue
}
name := uniqueName(names, tempName)
vmess := make(map[string]any, 20)
vmess["name"] = name
vmess["type"] = scheme
vmess["server"] = values["add"]
vmess["port"] = values["port"]
vmess["uuid"] = values["id"]
if alterId, ok := values["aid"]; ok {
vmess["alterId"] = alterId
} else {
vmess["alterId"] = 0
}
vmess["udp"] = true
vmess["xudp"] = true
vmess["tls"] = false
vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto"
if cipher, ok := values["scy"]; ok && cipher != "" {
vmess["cipher"] = cipher
}
if sni, ok := values["sni"]; ok && sni != "" {
vmess["servername"] = sni
}
network := strings.ToLower(values["net"].(string))
if values["type"] == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
vmess["network"] = network
tls := strings.ToLower(values["tls"].(string))
if strings.HasSuffix(tls, "tls") {
vmess["tls"] = true
}
switch network {
case "http":
headers := make(map[string]any)
httpOpts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)}
}
httpOpts["path"] = []string{"/"}
if path, ok := values["path"]; ok && path != "" {
httpOpts["path"] = []string{path.(string)}
}
httpOpts["headers"] = headers
vmess["http-opts"] = httpOpts
case "h2":
headers := make(map[string]any)
h2Opts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)}
}
h2Opts["path"] = values["path"]
h2Opts["headers"] = headers
vmess["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
wsOpts["path"] = []string{"/"}
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = host.(string)
}
if path, ok := values["path"]; ok && path != "" {
wsOpts["path"] = path.(string)
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = values["path"]
vmess["grpc-opts"] = grpcOpts
}
proxies = append(proxies, vmess)
case "ss":
urlSS, err := url.Parse(line)
if err != nil {
continue
}
name := uniqueName(names, urlSS.Fragment)
port := urlSS.Port()
if port == "" {
dcBuf, err := encRaw.DecodeString(urlSS.Host)
if err != nil {
continue
}
urlSS, err = url.Parse("ss://" + string(dcBuf))
if err != nil {
continue
}
}
var (
cipherRaw = urlSS.User.Username()
cipher string
password string
)
cipher = cipherRaw
if password, found = urlSS.User.Password(); !found {
dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw)
if err != nil {
dcBuf, _ = enc.DecodeString(cipherRaw)
}
cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found {
continue
}
err = VerifyMethod(cipher, password)
if err != nil {
dcBuf, _ = encRaw.DecodeString(cipherRaw)
cipher, password, found = strings.Cut(string(dcBuf), ":")
}
}
ss := make(map[string]any, 10)
ss["name"] = name
ss["type"] = scheme
ss["server"] = urlSS.Hostname()
ss["port"] = urlSS.Port()
ss["cipher"] = cipher
ss["password"] = password
query := urlSS.Query()
ss["udp"] = true
if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" {
ss["udp-over-tcp"] = true
}
if strings.Contains(query.Get("plugin"), "obfs") {
obfsParams := strings.Split(query.Get("plugin"), ";")
ss["plugin"] = "obfs"
ss["plugin-opts"] = map[string]any{
"host": obfsParams[2][10:],
"mode": obfsParams[1][5:],
}
}
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := encRaw.DecodeString(body)
if err != nil {
continue
}
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok {
continue
}
beforeArr := strings.Split(before, ":")
if len(beforeArr) != 6 {
continue
}
host := beforeArr[0]
port := beforeArr[1]
protocol := beforeArr[2]
method := beforeArr[3]
obfs := beforeArr[4]
password := decodeUrlSafe(urlSafe(beforeArr[5]))
query, err := url.ParseQuery(urlSafe(after))
if err != nil {
continue
}
remarks := decodeUrlSafe(query.Get("remarks"))
name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam")
ssr := make(map[string]any, 20)
ssr["name"] = name
ssr["type"] = scheme
ssr["server"] = host
ssr["port"] = port
ssr["cipher"] = method
ssr["password"] = password
ssr["obfs"] = obfs
ssr["protocol"] = protocol
ssr["udp"] = true
if obfsParam != "" {
ssr["obfs-param"] = obfsParam
}
if protocolParam != "" {
ssr["protocol-param"] = protocolParam
}
proxies = append(proxies, ssr)
}
}
if len(proxies) == 0 {
return nil, fmt.Errorf("convert v2ray subscribe error: format invalid")
}
return proxies, nil
}
func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok {
index++
names[name] = index
name = fmt.Sprintf("%s-%02d", name, index)
} else {
index = 0
names[name] = index
}
return name
}

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

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

123
common/convert/v.go Normal file
View File

@ -0,0 +1,123 @@
package convert
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
)
func handleVShareLink(names map[string]int, url *url.URL, scheme string, proxy map[string]any) error {
// Xray VMessAEAD / VLESS share link standard
// https://github.com/XTLS/Xray-core/discussions/716
query := url.Query()
proxy["name"] = uniqueName(names, url.Fragment)
if url.Hostname() == "" {
return errors.New("url.Hostname() is empty")
}
if url.Port() == "" {
return errors.New("url.Port() is empty")
}
proxy["type"] = scheme
proxy["server"] = url.Hostname()
proxy["port"] = url.Port()
proxy["uuid"] = url.User.Username()
proxy["udp"] = true
proxy["skip-cert-verify"] = false
proxy["tls"] = false
tls := strings.ToLower(query.Get("security"))
if strings.HasSuffix(tls, "tls") {
proxy["tls"] = true
if fingerprint := query.Get("fp"); fingerprint == "" {
proxy["client-fingerprint"] = "chrome"
} else {
proxy["client-fingerprint"] = fingerprint
}
}
if sni := query.Get("sni"); sni != "" {
proxy["servername"] = sni
}
switch query.Get("packetEncoding") {
case "none":
case "packet":
proxy["packet-addr"] = true
default:
proxy["xudp"] = true
}
network := strings.ToLower(query.Get("type"))
if network == "" {
network = "tcp"
}
fakeType := strings.ToLower(query.Get("headerType"))
if fakeType == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
proxy["network"] = network
switch network {
case "tcp":
if fakeType != "none" {
headers := make(map[string]any)
httpOpts := make(map[string]any)
httpOpts["path"] = []string{"/"}
if host := query.Get("host"); host != "" {
headers["Host"] = []string{host}
}
if method := query.Get("method"); method != "" {
httpOpts["method"] = method
}
if path := query.Get("path"); path != "" {
httpOpts["path"] = []string{path}
}
httpOpts["headers"] = headers
proxy["http-opts"] = httpOpts
}
case "http":
headers := make(map[string]any)
h2Opts := make(map[string]any)
h2Opts["path"] = []string{"/"}
if path := query.Get("path"); path != "" {
h2Opts["path"] = []string{path}
}
if host := query.Get("host"); host != "" {
h2Opts["host"] = []string{host}
}
h2Opts["headers"] = headers
proxy["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
headers["Host"] = query.Get("host")
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
if earlyData := query.Get("ed"); earlyData != "" {
med, err := strconv.Atoi(earlyData)
if err != nil {
return fmt.Errorf("bad WebSocket max early data size: %v", err)
}
wsOpts["max-early-data"] = med
}
if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" {
wsOpts["early-data-header-name"] = earlyDataHeader
}
proxy["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
proxy["grpc-opts"] = grpcOpts
}
return nil
}

View File

@ -0,0 +1,235 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package list implements a doubly linked list.
//
// To iterate over a list (where l is a *List):
//
// for e := l.Front(); e != nil; e = e.Next() {
// // do something with e.Value
// }
package list
// Element is an element of a linked list.
type Element[T any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Element[T]
// The list to which this element belongs.
list *List[T]
// The value stored with this element.
Value T
}
// Next returns the next list element or nil.
func (e *Element[T]) Next() *Element[T] {
if p := e.next; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// Prev returns the previous list element or nil.
func (e *Element[T]) Prev() *Element[T] {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List[T any] struct {
root Element[T] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *List[T]) Init() *List[T] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// New returns an initialized list.
func New[T any]() *List[T] { return new(List[T]).Init() }
// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List[T]) Len() int { return l.len }
// Front returns the first element of list l or nil if the list is empty.
func (l *List[T]) Front() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.next
}
// Back returns the last element of list l or nil if the list is empty.
func (l *List[T]) Back() *Element[T] {
if l.len == 0 {
return nil
}
return l.root.prev
}
// lazyInit lazily initializes a zero List value.
func (l *List[T]) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
// insert inserts e after at, increments l.len, and returns e.
func (l *List[T]) insert(e, at *Element[T]) *Element[T] {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] {
return l.insert(&Element[T]{Value: v}, at)
}
// remove removes e from its list, decrements l.len
func (l *List[T]) remove(e *Element[T]) {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
}
// move moves e to next to at.
func (l *List[T]) move(e, at *Element[T]) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List[T]) Remove(e *Element[T]) T {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List[T]) PushFront(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, &l.root)
}
// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List[T]) PushBack(v T) *Element[T] {
l.lazyInit()
return l.insertValue(v, l.root.prev)
}
// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark.prev)
}
// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] {
if mark.list != l {
return nil
}
// see comment in List.Remove about initialization of l
return l.insertValue(v, mark)
}
// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToFront(e *Element[T]) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List[T]) MoveToBack(e *Element[T]) {
if e.list != l || l.root.prev == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, l.root.prev)
}
// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveBefore(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark.prev)
}
// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List[T]) MoveAfter(e, mark *Element[T]) {
if e.list != l || e == mark || mark.list != l {
return
}
l.move(e, mark)
}
// PushBackList inserts a copy of another list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushBackList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
l.insertValue(e.Value, l.root.prev)
}
}
// PushFrontList inserts a copy of another list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List[T]) PushFrontList(other *List[T]) {
l.lazyInit()
for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
l.insertValue(e.Value, &l.root)
}
}

36
common/net/bind.go Normal file
View File

@ -0,0 +1,36 @@
package net
import "net"
type bindPacketConn struct {
net.PacketConn
rAddr net.Addr
}
func (wpc *bindPacketConn) Read(b []byte) (n int, err error) {
n, _, err = wpc.PacketConn.ReadFrom(b)
return n, err
}
func (wpc *bindPacketConn) Write(b []byte) (n int, err error) {
return wpc.PacketConn.WriteTo(b, wpc.rAddr)
}
func (wpc *bindPacketConn) RemoteAddr() net.Addr {
return wpc.rAddr
}
func (wpc *bindPacketConn) LocalAddr() net.Addr {
if wpc.PacketConn.LocalAddr() == nil {
return &net.UDPAddr{IP: net.IPv4zero, Port: 0}
} else {
return wpc.PacketConn.LocalAddr()
}
}
func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn {
return &bindPacketConn{
PacketConn: pc,
rAddr: rAddr,
}
}

View File

@ -3,18 +3,23 @@ package net
import ( import (
"bufio" "bufio"
"net" "net"
"github.com/Dreamacro/clash/common/buf"
) )
var _ ExtendedConn = (*BufferedConn)(nil)
type BufferedConn struct { type BufferedConn struct {
r *bufio.Reader r *bufio.Reader
net.Conn ExtendedConn
peeked bool
} }
func NewBufferedConn(c net.Conn) *BufferedConn { func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok { if bc, ok := c.(*BufferedConn); ok {
return bc return bc
} }
return &BufferedConn{bufio.NewReader(c), c} return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false}
} }
// Reader returns the internal bufio.Reader. // Reader returns the internal bufio.Reader.
@ -22,11 +27,20 @@ func (c *BufferedConn) Reader() *bufio.Reader {
return c.r return c.r
} }
func (c *BufferedConn) Peeked() bool {
return c.peeked
}
// Peek returns the next n bytes without advancing the reader. // Peek returns the next n bytes without advancing the reader.
func (c *BufferedConn) Peek(n int) ([]byte, error) { func (c *BufferedConn) Peek(n int) ([]byte, error) {
c.peeked = true
return c.r.Peek(n) return c.r.Peek(n)
} }
func (c *BufferedConn) Discard(n int) (discarded int, err error) {
return c.r.Discard(n)
}
func (c *BufferedConn) Read(p []byte) (int, error) { func (c *BufferedConn) Read(p []byte) (int, error) {
return c.r.Read(p) return c.r.Read(p)
} }
@ -42,3 +56,22 @@ func (c *BufferedConn) UnreadByte() error {
func (c *BufferedConn) Buffered() int { func (c *BufferedConn) Buffered() int {
return c.r.Buffered() return c.r.Buffered()
} }
func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) {
if c.r.Buffered() > 0 {
_, err = buffer.ReadOnceFrom(c.r)
return
}
return c.ExtendedConn.ReadBuffer(buffer)
}
func (c *BufferedConn) Upstream() any {
return c.ExtendedConn
}
func (c *BufferedConn) ReaderReplaceable() bool {
if c.r.Buffered() > 0 {
return false
}
return true
}

View File

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

104
common/net/refconn.go Normal file
View File

@ -0,0 +1,104 @@
package net
import (
"net"
"runtime"
"time"
)
type refConn struct {
conn net.Conn
ref any
}
func (c *refConn) Read(b []byte) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.conn.Read(b)
}
func (c *refConn) Write(b []byte) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.conn.Write(b)
}
func (c *refConn) Close() error {
defer runtime.KeepAlive(c.ref)
return c.conn.Close()
}
func (c *refConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.conn.LocalAddr()
}
func (c *refConn) RemoteAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.conn.RemoteAddr()
}
func (c *refConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetDeadline(t)
}
func (c *refConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetReadDeadline(t)
}
func (c *refConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetWriteDeadline(t)
}
func (c *refConn) Upstream() any {
return c.conn
}
func NewRefConn(conn net.Conn, ref any) net.Conn {
return &refConn{conn: conn, ref: ref}
}
type refPacketConn struct {
pc net.PacketConn
ref any
}
func (pc *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
defer runtime.KeepAlive(pc.ref)
return pc.pc.ReadFrom(p)
}
func (pc *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
defer runtime.KeepAlive(pc.ref)
return pc.pc.WriteTo(p, addr)
}
func (pc *refPacketConn) Close() error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.Close()
}
func (pc *refPacketConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(pc.ref)
return pc.pc.LocalAddr()
}
func (pc *refPacketConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetDeadline(t)
}
func (pc *refPacketConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetReadDeadline(t)
}
func (pc *refPacketConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(pc.ref)
return pc.pc.SetWriteDeadline(t)
}
func NewRefPacketConn(pc net.PacketConn, ref any) net.PacketConn {
return &refPacketConn{pc: pc, ref: ref}
}

24
common/net/relay.go Normal file
View File

@ -0,0 +1,24 @@
package net
//import (
// "io"
// "net"
// "time"
//)
//
//// Relay copies between left and right bidirectionally.
//func Relay(leftConn, rightConn net.Conn) {
// ch := make(chan error)
//
// go func() {
// // Wrapping to avoid using *net.TCPConn.(ReadFrom)
// // See also https://github.com/Dreamacro/clash/pull/1209
// _, err := io.Copy(WriteOnlyWriter{Writer: leftConn}, ReadOnlyReader{Reader: rightConn})
// leftConn.SetReadDeadline(time.Now())
// ch <- err
// }()
//
// _, _ = io.Copy(WriteOnlyWriter{Writer: rightConn}, ReadOnlyReader{Reader: leftConn})
// rightConn.SetReadDeadline(time.Now())
// <-ch
//}

22
common/net/sing.go Normal file
View File

@ -0,0 +1,22 @@
package net
import (
"context"
"net"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/network"
)
var NewExtendedConn = bufio.NewExtendedConn
var NewExtendedWriter = bufio.NewExtendedWriter
var NewExtendedReader = bufio.NewExtendedReader
type ExtendedConn = network.ExtendedConn
type ExtendedWriter = network.ExtendedWriter
type ExtendedReader = network.ExtendedReader
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
_ = bufio.CopyConn(context.TODO(), leftConn, rightConn)
}

52
common/net/tls.go Normal file
View File

@ -0,0 +1,52 @@
package net
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
)
func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
if certificate == "" || privateKey == "" {
return newRandomTLSKeyPair()
}
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
if painTextErr == nil {
return cert, nil
}
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())
}
return cert, nil
}
func newRandomTLSKeyPair() (tls.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return tls.Certificate{}, err
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(
rand.Reader,
&template,
&template,
&key.PublicKey,
key)
if err != nil {
return tls.Certificate{}, err
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return tls.Certificate{}, err
}
return tlsCert, nil
}

131
common/net/websocket.go Normal file
View File

@ -0,0 +1,131 @@
package net
import (
"encoding/binary"
"math/bits"
)
// kanged from https://github.com/nhooyr/websocket/blob/master/frame.go
// License: MIT
// MaskWebSocket applies the WebSocket masking algorithm to p
// with the given key.
// See https://tools.ietf.org/html/rfc6455#section-5.3
//
// The returned value is the correctly rotated key to
// to continue to mask/unmask the message.
//
// It is optimized for LittleEndian and expects the key
// to be in little endian.
//
// See https://github.com/golang/go/issues/31586
func MaskWebSocket(key uint32, b []byte) uint32 {
if len(b) >= 8 {
key64 := uint64(key)<<32 | uint64(key)
// At some point in the future we can clean these unrolled loops up.
// See https://github.com/golang/go/issues/31586#issuecomment-487436401
// Then we xor until b is less than 128 bytes.
for len(b) >= 128 {
v := binary.LittleEndian.Uint64(b)
binary.LittleEndian.PutUint64(b, v^key64)
v = binary.LittleEndian.Uint64(b[8:16])
binary.LittleEndian.PutUint64(b[8:16], v^key64)
v = binary.LittleEndian.Uint64(b[16:24])
binary.LittleEndian.PutUint64(b[16:24], v^key64)
v = binary.LittleEndian.Uint64(b[24:32])
binary.LittleEndian.PutUint64(b[24:32], v^key64)
v = binary.LittleEndian.Uint64(b[32:40])
binary.LittleEndian.PutUint64(b[32:40], v^key64)
v = binary.LittleEndian.Uint64(b[40:48])
binary.LittleEndian.PutUint64(b[40:48], v^key64)
v = binary.LittleEndian.Uint64(b[48:56])
binary.LittleEndian.PutUint64(b[48:56], v^key64)
v = binary.LittleEndian.Uint64(b[56:64])
binary.LittleEndian.PutUint64(b[56:64], v^key64)
v = binary.LittleEndian.Uint64(b[64:72])
binary.LittleEndian.PutUint64(b[64:72], v^key64)
v = binary.LittleEndian.Uint64(b[72:80])
binary.LittleEndian.PutUint64(b[72:80], v^key64)
v = binary.LittleEndian.Uint64(b[80:88])
binary.LittleEndian.PutUint64(b[80:88], v^key64)
v = binary.LittleEndian.Uint64(b[88:96])
binary.LittleEndian.PutUint64(b[88:96], v^key64)
v = binary.LittleEndian.Uint64(b[96:104])
binary.LittleEndian.PutUint64(b[96:104], v^key64)
v = binary.LittleEndian.Uint64(b[104:112])
binary.LittleEndian.PutUint64(b[104:112], v^key64)
v = binary.LittleEndian.Uint64(b[112:120])
binary.LittleEndian.PutUint64(b[112:120], v^key64)
v = binary.LittleEndian.Uint64(b[120:128])
binary.LittleEndian.PutUint64(b[120:128], v^key64)
b = b[128:]
}
// Then we xor until b is less than 64 bytes.
for len(b) >= 64 {
v := binary.LittleEndian.Uint64(b)
binary.LittleEndian.PutUint64(b, v^key64)
v = binary.LittleEndian.Uint64(b[8:16])
binary.LittleEndian.PutUint64(b[8:16], v^key64)
v = binary.LittleEndian.Uint64(b[16:24])
binary.LittleEndian.PutUint64(b[16:24], v^key64)
v = binary.LittleEndian.Uint64(b[24:32])
binary.LittleEndian.PutUint64(b[24:32], v^key64)
v = binary.LittleEndian.Uint64(b[32:40])
binary.LittleEndian.PutUint64(b[32:40], v^key64)
v = binary.LittleEndian.Uint64(b[40:48])
binary.LittleEndian.PutUint64(b[40:48], v^key64)
v = binary.LittleEndian.Uint64(b[48:56])
binary.LittleEndian.PutUint64(b[48:56], v^key64)
v = binary.LittleEndian.Uint64(b[56:64])
binary.LittleEndian.PutUint64(b[56:64], v^key64)
b = b[64:]
}
// Then we xor until b is less than 32 bytes.
for len(b) >= 32 {
v := binary.LittleEndian.Uint64(b)
binary.LittleEndian.PutUint64(b, v^key64)
v = binary.LittleEndian.Uint64(b[8:16])
binary.LittleEndian.PutUint64(b[8:16], v^key64)
v = binary.LittleEndian.Uint64(b[16:24])
binary.LittleEndian.PutUint64(b[16:24], v^key64)
v = binary.LittleEndian.Uint64(b[24:32])
binary.LittleEndian.PutUint64(b[24:32], v^key64)
b = b[32:]
}
// Then we xor until b is less than 16 bytes.
for len(b) >= 16 {
v := binary.LittleEndian.Uint64(b)
binary.LittleEndian.PutUint64(b, v^key64)
v = binary.LittleEndian.Uint64(b[8:16])
binary.LittleEndian.PutUint64(b[8:16], v^key64)
b = b[16:]
}
// Then we xor until b is less than 8 bytes.
for len(b) >= 8 {
v := binary.LittleEndian.Uint64(b)
binary.LittleEndian.PutUint64(b, v^key64)
b = b[8:]
}
}
// Then we xor until b is less than 4 bytes.
for len(b) >= 4 {
v := binary.LittleEndian.Uint32(b)
binary.LittleEndian.PutUint32(b, v^key)
b = b[4:]
}
// xor remaining bytes.
for i := range b {
b[i] ^= byte(key)
key = bits.RotateLeft32(key, -8)
}
return key
}

53
common/nnip/netip.go Normal file
View File

@ -0,0 +1,53 @@
package nnip
import (
"encoding/binary"
"net"
"net/netip"
)
// IpToAddr converts the net.IP to netip.Addr.
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
func IpToAddr(slice net.IP) netip.Addr {
ip := slice
if len(ip) != 4 {
if ip = slice.To4(); ip == nil {
ip = slice
}
}
if addr, ok := netip.AddrFromSlice(ip); ok {
return addr
}
return netip.Addr{}
}
// UnMasked returns p's last IP address.
// If p is invalid, UnMasked returns netip.Addr{}
func UnMasked(p netip.Prefix) netip.Addr {
if !p.IsValid() {
return netip.Addr{}
}
buf := p.Addr().As16()
hi := binary.BigEndian.Uint64(buf[:8])
lo := binary.BigEndian.Uint64(buf[8:])
bits := p.Bits()
if bits <= 32 {
bits += 96
}
hi = hi | ^uint64(0)>>bits
lo = lo | ^(^uint64(0) << (128 - bits))
binary.BigEndian.PutUint64(buf[:8], hi)
binary.BigEndian.PutUint64(buf[8:], lo)
addr := netip.AddrFrom16(buf)
if p.Addr().Is4() {
return addr.Unmap()
}
return addr
}

View File

@ -1,3 +1,3 @@
package observable package observable
type Iterable <-chan any type Iterable[T any] <-chan T

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import (
// Picker provides synchronization, and Context cancelation // Picker provides synchronization, and Context cancelation
// for groups of goroutines working on subtasks of a common task. // for groups of goroutines working on subtasks of a common task.
// Inspired by errGroup // Inspired by errGroup
type Picker struct { type Picker[T any] struct {
ctx context.Context ctx context.Context
cancel func() cancel func()
@ -17,12 +17,12 @@ type Picker struct {
once sync.Once once sync.Once
errOnce sync.Once errOnce sync.Once
result any result T
err error err error
} }
func newPicker(ctx context.Context, cancel func()) *Picker { func newPicker[T any](ctx context.Context, cancel func()) *Picker[T] {
return &Picker{ return &Picker[T]{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
} }
@ -30,20 +30,20 @@ func newPicker(ctx context.Context, cancel func()) *Picker {
// WithContext returns a new Picker and an associated Context derived from ctx. // WithContext returns a new Picker and an associated Context derived from ctx.
// and cancel when first element return. // and cancel when first element return.
func WithContext(ctx context.Context) (*Picker, context.Context) { func WithContext[T any](ctx context.Context) (*Picker[T], context.Context) {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
return newPicker(ctx, cancel), ctx return newPicker[T](ctx, cancel), ctx
} }
// WithTimeout returns a new Picker and an associated Context derived from ctx with timeout. // WithTimeout returns a new Picker and an associated Context derived from ctx with timeout.
func WithTimeout(ctx context.Context, timeout time.Duration) (*Picker, context.Context) { func WithTimeout[T any](ctx context.Context, timeout time.Duration) (*Picker[T], context.Context) {
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
return newPicker(ctx, cancel), ctx return newPicker[T](ctx, cancel), ctx
} }
// Wait blocks until all function calls from the Go method have returned, // Wait blocks until all function calls from the Go method have returned,
// then returns the first nil error result (if any) from them. // then returns the first nil error result (if any) from them.
func (p *Picker) Wait() any { func (p *Picker[T]) Wait() T {
p.wg.Wait() p.wg.Wait()
if p.cancel != nil { if p.cancel != nil {
p.cancel() p.cancel()
@ -52,13 +52,13 @@ func (p *Picker) Wait() any {
} }
// Error return the first error (if all success return nil) // Error return the first error (if all success return nil)
func (p *Picker) Error() error { func (p *Picker[T]) Error() error {
return p.err return p.err
} }
// Go calls the given function in a new goroutine. // Go calls the given function in a new goroutine.
// The first call to return a nil error cancels the group; its result will be returned by Wait. // The first call to return a nil error cancels the group; its result will be returned by Wait.
func (p *Picker) Go(f func() (any, error)) { func (p *Picker[T]) Go(f func() (T, error)) {
p.wg.Add(1) p.wg.Add(1)
go func() { go func() {

View File

@ -8,33 +8,38 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func sleepAndSend(ctx context.Context, delay int, input any) func() (any, error) { func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, error) {
return func() (any, error) { return func() (T, error) {
timer := time.NewTimer(time.Millisecond * time.Duration(delay)) timer := time.NewTimer(time.Millisecond * time.Duration(delay))
select { select {
case <-timer.C: case <-timer.C:
return input, nil return input, nil
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return getZero[T](), ctx.Err()
} }
} }
} }
func TestPicker_Basic(t *testing.T) { func TestPicker_Basic(t *testing.T) {
picker, ctx := WithContext(context.Background()) picker, ctx := WithContext[int](context.Background())
picker.Go(sleepAndSend(ctx, 30, 2)) picker.Go(sleepAndSend(ctx, 30, 2))
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.NotNil(t, number) assert.NotNil(t, number)
assert.Equal(t, number.(int), 1) assert.Equal(t, number, 1)
} }
func TestPicker_Timeout(t *testing.T) { func TestPicker_Timeout(t *testing.T) {
picker, ctx := WithTimeout(context.Background(), time.Millisecond*5) picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 20, 1))
number := picker.Wait() number := picker.Wait()
assert.Nil(t, number) assert.Equal(t, number, getZero[int]())
assert.NotNil(t, picker.Error()) assert.NotNil(t, picker.Error())
} }
func getZero[T any]() T {
var result T
return result
}

View File

@ -52,8 +52,8 @@ func (alloc *Allocator) Put(buf []byte) error {
return errors.New("allocator Put() incorrect buffer size") return errors.New("allocator Put() incorrect buffer size")
} }
//lint:ignore SA6002 ignore temporarily
//nolint //nolint
//lint:ignore SA6002 ignore temporarily
alloc.buffers[bits].Put(buf) alloc.buffers[bits].Put(buf)
return nil return nil
} }

7
common/pool/sing.go Normal file
View File

@ -0,0 +1,7 @@
package pool
import "github.com/sagernet/sing/common/buf"
func init() {
buf.DefaultAllocator = defaultAllocator
}

View File

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

View File

@ -10,13 +10,13 @@ import (
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
shardCount := atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (any, error) { call := func() (int, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 5)
return nil, nil return 0, nil
} }
var wg sync.WaitGroup var wg sync.WaitGroup
@ -38,32 +38,32 @@ func TestBasic(t *testing.T) {
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
call := func() (any, error) { callM := func() (int, error) {
foo++ foo++
return nil, nil return 0, nil
} }
single.Do(call) _, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
_, _, shard := single.Do(call) _, _, shard := single.Do(callM)
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
assert.True(t, shard) assert.True(t, shard)
} }
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle(time.Millisecond * 30) single := NewSingle[int](time.Millisecond * 30)
foo := 0 foo := 0
call := func() (any, error) { callM := func() (int, error) {
foo++ foo++
return nil, nil return 0, nil
} }
single.Do(call) _, _, _ = single.Do(callM)
single.Reset() single.Reset()
single.Do(call) _, _, _ = single.Do(callM)
assert.Equal(t, 2, foo) assert.Equal(t, 2, foo)
} }

View File

@ -3,6 +3,7 @@ package structure
// references: https://github.com/mitchellh/mapstructure // references: https://github.com/mitchellh/mapstructure
import ( import (
"encoding/base64"
"fmt" "fmt"
"reflect" "reflect"
"strconv" "strconv"
@ -13,8 +14,11 @@ import (
type Option struct { type Option struct {
TagName string TagName string
WeaklyTypedInput bool WeaklyTypedInput bool
KeyReplacer *strings.Replacer
} }
var DefaultKeyReplacer = strings.NewReplacer("_", "-")
// Decoder is the core of structure // Decoder is the core of structure
type Decoder struct { type Decoder struct {
option *Option option *Option
@ -31,7 +35,7 @@ func NewDecoder(option Option) *Decoder {
// Decode transform a map[string]any to a struct // Decode transform a map[string]any to a struct
func (d *Decoder) Decode(src map[string]any, dst any) error { func (d *Decoder) Decode(src map[string]any, dst any) error {
if reflect.TypeOf(dst).Kind() != reflect.Ptr { if reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("Decode must recive a ptr struct") return fmt.Errorf("decode must recive a ptr struct")
} }
t := reflect.TypeOf(dst).Elem() t := reflect.TypeOf(dst).Elem()
v := reflect.ValueOf(dst).Elem() v := reflect.ValueOf(dst).Elem()
@ -49,6 +53,23 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
omitempty := found && omitKey == "omitempty" omitempty := found && omitKey == "omitempty"
value, ok := src[key] value, ok := src[key]
if !ok {
if d.option.KeyReplacer != nil {
key = d.option.KeyReplacer.Replace(key)
}
for _strKey := range src {
strKey := _strKey
if d.option.KeyReplacer != nil {
strKey = d.option.KeyReplacer.Replace(strKey)
}
if strings.EqualFold(key, strKey) {
value = src[_strKey]
ok = true
break
}
}
}
if !ok || value == nil { if !ok || value == nil {
if omitempty { if omitempty {
continue continue
@ -65,9 +86,16 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
} }
func (d *Decoder) decode(name string, data any, val reflect.Value) error { func (d *Decoder) decode(name string, data any, val reflect.Value) error {
switch val.Kind() { kind := val.Kind()
case reflect.Int: switch {
case isInt(kind):
return d.decodeInt(name, data, val) return d.decodeInt(name, data, val)
case isUint(kind):
return d.decodeUint(name, data, val)
case isFloat(kind):
return d.decodeFloat(name, data, val)
}
switch kind {
case reflect.String: case reflect.String:
return d.decodeString(name, data, val) return d.decodeString(name, data, val)
case reflect.Bool: case reflect.Bool:
@ -85,13 +113,42 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error {
} }
} }
func isInt(kind reflect.Kind) bool {
switch kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return true
default:
return false
}
}
func isUint(kind reflect.Kind) bool {
switch kind {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return true
default:
return false
}
}
func isFloat(kind reflect.Kind) bool {
switch kind {
case reflect.Float32, reflect.Float64:
return true
default:
return false
}
}
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) { func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.Int: case isInt(kind):
val.SetInt(dataVal.Int()) val.SetInt(dataVal.Int())
case kind == reflect.Float64 && d.option.WeaklyTypedInput: case isUint(kind) && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Uint()))
case isFloat(kind) && d.option.WeaklyTypedInput:
val.SetInt(int64(dataVal.Float())) val.SetInt(int64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput: case kind == reflect.String && d.option.WeaklyTypedInput:
var i int64 var i int64
@ -110,14 +167,72 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error
return err return err
} }
func (d *Decoder) decodeUint(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case isUint(kind):
val.SetUint(dataVal.Uint())
case isInt(kind) && d.option.WeaklyTypedInput:
val.SetUint(uint64(dataVal.Int()))
case isFloat(kind) && d.option.WeaklyTypedInput:
val.SetUint(uint64(dataVal.Float()))
case kind == reflect.String && d.option.WeaklyTypedInput:
var i uint64
i, err = strconv.ParseUint(dataVal.String(), 0, val.Type().Bits())
if err == nil {
val.SetUint(i)
} else {
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
}
default:
err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
return err
}
func (d *Decoder) decodeFloat(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data)
kind := dataVal.Kind()
switch {
case isFloat(kind):
val.SetFloat(dataVal.Float())
case isUint(kind):
val.SetFloat(float64(dataVal.Uint()))
case isInt(kind) && d.option.WeaklyTypedInput:
val.SetFloat(float64(dataVal.Int()))
case kind == reflect.String && d.option.WeaklyTypedInput:
var i float64
i, err = strconv.ParseFloat(dataVal.String(), val.Type().Bits())
if err == nil {
val.SetFloat(i)
} else {
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
}
default:
err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'",
name, val.Type(), dataVal.Type(),
)
}
return err
}
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) { func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
dataVal := reflect.ValueOf(data) dataVal := reflect.ValueOf(data)
kind := dataVal.Kind() kind := dataVal.Kind()
switch { switch {
case kind == reflect.String: case kind == reflect.String:
val.SetString(dataVal.String()) val.SetString(dataVal.String())
case kind == reflect.Int && d.option.WeaklyTypedInput: case isInt(kind) && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatInt(dataVal.Int(), 10)) val.SetString(strconv.FormatInt(dataVal.Int(), 10))
case isUint(kind) && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
case isFloat(kind) && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatFloat(dataVal.Float(), 'E', -1, dataVal.Type().Bits()))
default: default:
err = fmt.Errorf( err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'", "'%s' expected type '%s', got unconvertible type '%s'",
@ -133,8 +248,10 @@ func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err erro
switch { switch {
case kind == reflect.Bool: case kind == reflect.Bool:
val.SetBool(dataVal.Bool()) val.SetBool(dataVal.Bool())
case kind == reflect.Int && d.option.WeaklyTypedInput: case isInt(kind) && d.option.WeaklyTypedInput:
val.SetBool(dataVal.Int() != 0) val.SetBool(dataVal.Int() != 0)
case isUint(kind) && d.option.WeaklyTypedInput:
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
default: default:
err = fmt.Errorf( err = fmt.Errorf(
"'%s' expected type '%s', got unconvertible type '%s'", "'%s' expected type '%s', got unconvertible type '%s'",
@ -149,6 +266,17 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
valType := val.Type() valType := val.Type()
valElemType := valType.Elem() valElemType := valType.Elem()
if dataVal.Kind() == reflect.String && valElemType.Kind() == reflect.Uint8 { // from encoding/json
s := []byte(dataVal.String())
b := make([]byte, base64.StdEncoding.DecodedLen(len(s)))
n, err := base64.StdEncoding.Decode(b, s)
if err != nil {
return fmt.Errorf("try decode '%s' by base64 error: %w", name, err)
}
val.SetBytes(b[:n])
return nil
}
if dataVal.Kind() != reflect.Slice { if dataVal.Kind() != reflect.Slice {
return fmt.Errorf("'%s' is not a slice", name) return fmt.Errorf("'%s' is not a slice", name)
} }
@ -159,9 +287,19 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
for valSlice.Len() <= i { for valSlice.Len() <= i {
valSlice = reflect.Append(valSlice, reflect.Zero(valElemType)) valSlice = reflect.Append(valSlice, reflect.Zero(valElemType))
} }
currentField := valSlice.Index(i)
fieldName := fmt.Sprintf("%s[%d]", name, i) fieldName := fmt.Sprintf("%s[%d]", name, i)
if currentData == nil {
// in weakly type mode, null will convert to zero value
if d.option.WeaklyTypedInput {
continue
}
// in non-weakly type mode, null will convert to nil if element's zero value is nil, otherwise return an error
if elemKind := valElemType.Kind(); elemKind == reflect.Map || elemKind == reflect.Slice {
continue
}
return fmt.Errorf("'%s' can not be null", fieldName)
}
currentField := valSlice.Index(i)
if err := d.decode(fieldName, currentData, currentField); err != nil { if err := d.decode(fieldName, currentData, currentField); err != nil {
return err return err
} }
@ -291,7 +429,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
field reflect.StructField field reflect.StructField
val reflect.Value val reflect.Value
} }
fields := []field{} var fields []field
for len(structs) > 0 { for len(structs) > 0 {
structVal := structs[0] structVal := structs[0]
structs = structs[1:] structs = structs[1:]
@ -343,12 +481,18 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
if !rawMapVal.IsValid() { if !rawMapVal.IsValid() {
// Do a slower search by iterating over each key and // Do a slower search by iterating over each key and
// doing case-insensitive search. // doing case-insensitive search.
if d.option.KeyReplacer != nil {
fieldName = d.option.KeyReplacer.Replace(fieldName)
}
for dataValKey := range dataValKeys { for dataValKey := range dataValKeys {
mK, ok := dataValKey.Interface().(string) mK, ok := dataValKey.Interface().(string)
if !ok { if !ok {
// Not a string key // Not a string key
continue continue
} }
if d.option.KeyReplacer != nil {
mK = d.option.KeyReplacer.Replace(mK)
}
if strings.EqualFold(mK, fieldName) { if strings.EqualFold(mK, fieldName) {
rawMapKey = dataValKey rawMapKey = dataValKey

View File

@ -137,3 +137,45 @@ func TestStructure_Nest(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, s.BazOptional, goal) assert.Equal(t, s.BazOptional, goal)
} }
func TestStructure_SliceNilValue(t *testing.T) {
rawMap := map[string]any{
"foo": 1,
"bar": []any{"bar", nil},
}
goal := &BazSlice{
Foo: 1,
Bar: []string{"bar", ""},
}
s := &BazSlice{}
err := weakTypeDecoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Equal(t, goal.Bar, s.Bar)
s = &BazSlice{}
err = decoder.Decode(rawMap, s)
assert.NotNil(t, err)
}
func TestStructure_SliceNilValueComplex(t *testing.T) {
rawMap := map[string]any{
"bar": []any{map[string]any{"bar": "foo"}, nil},
}
s := &struct {
Bar []map[string]any `test:"bar"`
}{}
err := decoder.Decode(rawMap, s)
assert.Nil(t, err)
assert.Nil(t, s.Bar[1])
ss := &struct {
Bar []Baz `test:"bar"`
}{}
err = decoder.Decode(rawMap, ss)
assert.NotNil(t, err)
}

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

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

16
common/utils/uuid.go Normal file
View File

@ -0,0 +1,16 @@
package utils
import (
"github.com/gofrs/uuid"
)
var uuidNamespace, _ = uuid.FromString("00000000-0000-0000-0000-000000000000")
// UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090
func UUIDMap(str string) (uuid.UUID, error) {
u, err := uuid.FromString(str)
if err != nil {
return uuid.NewV5(uuidNamespace, str), nil
}
return u, nil
}

74
common/utils/uuid_test.go Normal file
View File

@ -0,0 +1,74 @@
package utils
import (
"github.com/gofrs/uuid"
"reflect"
"testing"
)
func TestUUIDMap(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want uuid.UUID
wantErr bool
}{
{
name: "uuid-test-1",
args: args{
str: "82410302-039e-41b6-98b0-d964084b4170",
},
want: uuid.FromStringOrNil("82410302-039e-41b6-98b0-d964084b4170"),
wantErr: false,
},
{
name: "uuid-test-2",
args: args{
str: "88c502e6-d7eb-4c8e-8259-94cb13d83c77",
},
want: uuid.FromStringOrNil("88c502e6-d7eb-4c8e-8259-94cb13d83c77"),
wantErr: false,
},
{
name: "uuid-map-1",
args: args{
str: "123456",
},
want: uuid.FromStringOrNil("f8598425-92f2-5508-a071-4fc67f9040ac"),
wantErr: false,
},
// GENERATED BY 'xray uuid -i'
{
name: "uuid-map-2",
args: args{
str: "a9dk23bz0",
},
want: uuid.FromStringOrNil("c91481b6-fc0f-5d9e-b166-5ddf07b9c3c5"),
wantErr: false,
},
{
name: "uuid-map-2",
args: args{
str: "中文123",
},
want: uuid.FromStringOrNil("145c544c-2229-59e5-8dbb-3f33b7610d26"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := UUIDMap(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("UUIDMap() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UUIDMap() got = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -4,7 +4,9 @@ import (
"context" "context"
"errors" "errors"
"net" "net"
"net/netip"
"github.com/Dreamacro/clash/common/nnip"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -15,14 +17,16 @@ var (
ErrNotFound = errors.New("DNS option not found") ErrNotFound = errors.New("DNS option not found")
) )
func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, error) { func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]netip.Addr, error) {
conn, err := ListenDHCPClient(context, ifaceName) conn, err := ListenDHCPClient(context, ifaceName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() defer func() {
_ = conn.Close()
}()
result := make(chan []net.IP, 1) result := make(chan []netip.Addr, 1)
ifaceObj, err := iface.ResolveInterface(ifaceName) ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil { if err != nil {
@ -52,7 +56,7 @@ func ResolveDNSFromDHCP(context context.Context, ifaceName string) ([]net.IP, er
} }
} }
func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []net.IP) { func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []netip.Addr) {
defer close(result) defer close(result)
buf := make([]byte, dhcpv4.MaxMessageSize) buf := make([]byte, dhcpv4.MaxMessageSize)
@ -77,11 +81,17 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
} }
dns := pkt.DNS() dns := pkt.DNS()
if len(dns) == 0 { l := len(dns)
if l == 0 {
return return
} }
result <- dns dnsAddr := make([]netip.Addr, l)
for i := 0; i < l; i++ {
dnsAddr[i] = nnip.IpToAddr(dns[i])
}
result <- dnsAddr
return return
} }

View File

@ -1,7 +1,9 @@
package dialer package dialer
import ( import (
"context"
"net" "net"
"net/netip"
"syscall" "syscall"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
@ -9,23 +11,12 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
type controlFn = func(network, address string, c syscall.RawConn) error func bindControl(ifaceIdx int) controlFn {
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
func bindControl(ifaceIdx int, chain controlFn) controlFn { addrPort, err := netip.ParseAddrPort(address)
return func(network, address string, c syscall.RawConn) (err error) { if err == nil && !addrPort.Addr().IsGlobalUnicast() {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return return
} }
}
var innerErr error var innerErr error
err = c.Control(func(fd uintptr) { err = c.Control(func(fd uintptr) {
@ -45,13 +36,13 @@ func bindControl(ifaceIdx int, chain controlFn) controlFn {
} }
} }
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
ifaceObj, err := iface.ResolveInterface(ifaceName) ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil { if err != nil {
return err return err
} }
dialer.Control = bindControl(ifaceObj.Index, dialer.Control) addControlToDialer(dialer, bindControl(ifaceObj.Index))
return nil return nil
} }
@ -61,6 +52,10 @@ func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address
return "", err return "", err
} }
lc.Control = bindControl(ifaceObj.Index, lc.Control) addControlToListenConfig(lc, bindControl(ifaceObj.Index))
return address, nil return address, nil
} }
func ParseNetwork(network string, addr netip.Addr) string {
return network
}

View File

@ -1,29 +1,20 @@
package dialer package dialer
import ( import (
"context"
"net" "net"
"net/netip"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
type controlFn = func(network, address string, c syscall.RawConn) error func bindControl(ifaceName string) controlFn {
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
func bindControl(ifaceName string, chain controlFn) controlFn { addrPort, err := netip.ParseAddrPort(address)
return func(network, address string, c syscall.RawConn) (err error) { if err == nil && !addrPort.Addr().IsGlobalUnicast() {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address)
if err == nil {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return return
} }
}
var innerErr error var innerErr error
err = c.Control(func(fd uintptr) { err = c.Control(func(fd uintptr) {
@ -38,14 +29,18 @@ func bindControl(ifaceName string, chain controlFn) controlFn {
} }
} }
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ net.IP) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
dialer.Control = bindControl(ifaceName, dialer.Control) addControlToDialer(dialer, bindControl(ifaceName))
return nil return nil
} }
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) { func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
lc.Control = bindControl(ifaceName, lc.Control) addControlToListenConfig(lc, bindControl(ifaceName))
return address, nil return address, nil
} }
func ParseNetwork(network string, addr netip.Addr) string {
return network
}

View File

@ -1,30 +1,31 @@
//go:build !linux && !darwin //go:build !linux && !darwin && !windows
package dialer package dialer
import ( import (
"net" "net"
"net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/Dreamacro/clash/component/iface" "github.com/Dreamacro/clash/component/iface"
) )
func lookupLocalAddr(ifaceName string, network string, destination net.IP, port int) (net.Addr, error) { func lookupLocalAddr(ifaceName string, network string, destination netip.Addr, port int) (net.Addr, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName) ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var addr *net.IPNet var addr *netip.Prefix
switch network { switch network {
case "udp4", "tcp4": case "udp4", "tcp4":
addr, err = ifaceObj.PickIPv4Addr(destination) addr, err = ifaceObj.PickIPv4Addr(destination)
case "tcp6", "udp6": case "tcp6", "udp6":
addr, err = ifaceObj.PickIPv6Addr(destination) addr, err = ifaceObj.PickIPv6Addr(destination)
default: default:
if destination != nil { if destination.IsValid() {
if destination.To4() != nil { if destination.Is4() {
addr, err = ifaceObj.PickIPv4Addr(destination) addr, err = ifaceObj.PickIPv4Addr(destination)
} else { } else {
addr, err = ifaceObj.PickIPv6Addr(destination) addr, err = ifaceObj.PickIPv6Addr(destination)
@ -39,12 +40,12 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
if strings.HasPrefix(network, "tcp") { if strings.HasPrefix(network, "tcp") {
return &net.TCPAddr{ return &net.TCPAddr{
IP: addr.IP, IP: addr.Addr().AsSlice(),
Port: port, Port: port,
}, nil }, nil
} else if strings.HasPrefix(network, "udp") { } else if strings.HasPrefix(network, "udp") {
return &net.UDPAddr{ return &net.UDPAddr{
IP: addr.IP, IP: addr.Addr().AsSlice(),
Port: port, Port: port,
}, nil }, nil
} }
@ -52,7 +53,7 @@ func lookupLocalAddr(ifaceName string, network string, destination net.IP, port
return nil, iface.ErrAddrNotFound return nil, iface.ErrAddrNotFound
} }
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination net.IP) error { func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, network string, destination netip.Addr) error {
if !destination.IsGlobalUnicast() { if !destination.IsGlobalUnicast() {
return nil return nil
} }
@ -83,10 +84,20 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
local, _ := strconv.ParseUint(port, 10, 16) local, _ := strconv.ParseUint(port, 10, 16)
addr, err := lookupLocalAddr(ifaceName, network, nil, int(local)) addr, err := lookupLocalAddr(ifaceName, network, netip.Addr{}, int(local))
if err != nil { if err != nil {
return "", err return "", err
} }
return addr.String(), nil return addr.String(), nil
} }
func ParseNetwork(network string, addr netip.Addr) string {
// fix bindIfaceToListenConfig() force bind to an ipv4 address
if !strings.HasSuffix(network, "4") &&
!strings.HasSuffix(network, "6") &&
addr.Unmap().Is6() {
network += "6"
}
return network
}

View File

@ -0,0 +1,92 @@
package dialer
import (
"context"
"encoding/binary"
"net"
"net/netip"
"syscall"
"unsafe"
"github.com/Dreamacro/clash/component/iface"
)
const (
IP_UNICAST_IF = 31
IPV6_UNICAST_IF = 31
)
func bind4(handle syscall.Handle, ifaceIdx int) error {
var bytes [4]byte
binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx))
idx := *(*uint32)(unsafe.Pointer(&bytes[0]))
return syscall.SetsockoptInt(handle, syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx))
}
func bind6(handle syscall.Handle, ifaceIdx int) error {
return syscall.SetsockoptInt(handle, syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx)
}
func bindControl(ifaceIdx int) controlFn {
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
addrPort, err := netip.ParseAddrPort(address)
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
return
}
var innerErr error
err = c.Control(func(fd uintptr) {
handle := syscall.Handle(fd)
bind6err := bind6(handle, ifaceIdx)
bind4err := bind4(handle, ifaceIdx)
switch network {
case "ip6", "tcp6":
innerErr = bind6err
case "ip4", "tcp4", "udp4":
innerErr = bind4err
case "udp6":
// golang will set network to udp6 when listenUDP on wildcard ip (eg: ":0", "")
if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil {
// try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6
if bind4err != nil {
innerErr = bind6err
} else {
innerErr = bind4err
}
} else {
innerErr = bind6err
}
}
})
if innerErr != nil {
err = innerErr
}
return
}
}
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return err
}
addControlToDialer(dialer, bindControl(ifaceObj.Index))
return nil
}
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
ifaceObj, err := iface.ResolveInterface(ifaceName)
if err != nil {
return "", err
}
addControlToListenConfig(lc, bindControl(ifaceObj.Index))
return address, nil
}
func ParseNetwork(network string, addr netip.Addr) string {
return network
}

View File

@ -0,0 +1,22 @@
package dialer
import (
"context"
"net"
"syscall"
)
type controlFn = func(ctx context.Context, network, address string, c syscall.RawConn) error
func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) {
llc := *lc
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
switch {
case llc.Control != nil:
if err = llc.Control(network, address, c); err != nil {
return
}
}
return fn(context.Background(), network, address, c)
}
}

View File

@ -0,0 +1,22 @@
//go:build !go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.Control = func(network, address string, c syscall.RawConn) (err error) {
switch {
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(context.Background(), network, address, c)
}
}

View File

@ -0,0 +1,26 @@
//go:build go1.20
package dialer
import (
"context"
"net"
"syscall"
)
func addControlToDialer(d *net.Dialer, fn controlFn) {
ld := *d
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
switch {
case ld.ControlContext != nil:
if err = ld.ControlContext(ctx, network, address, c); err != nil {
return
}
case ld.Control != nil:
if err = ld.Control(network, address, c); err != nil {
return
}
}
return fn(ctx, network, address, c)
}
}

View File

@ -3,12 +3,28 @@ package dialer
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"net/netip"
"strings"
"sync"
"github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/resolver"
"go.uber.org/atomic"
) )
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { var (
dialMux sync.Mutex
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
tcpConcurrent = false
DisableIPv6 = false
ErrorInvalidedNetworkStack = errors.New("invalided network stack")
ErrorDisableIPv6 = errors.New("IPv6 is disabled, dialer cancel")
)
func applyOptions(options ...Option) *option {
opt := &option{ opt := &option{
interfaceName: DefaultInterface.Load(), interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()), routingMark: int(DefaultRoutingMark.Load()),
@ -22,53 +38,34 @@ func DialContext(ctx context.Context, network, address string, options ...Option
o(opt) o(opt)
} }
return opt
}
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := applyOptions(options...)
if opt.network == 4 || opt.network == 6 {
if strings.Contains(network, "tcp") {
network = "tcp"
} else {
network = "udp"
}
network = fmt.Sprintf("%s%d", network, opt.network)
}
switch network { switch network {
case "tcp4", "tcp6", "udp4", "udp6": case "tcp4", "tcp6", "udp4", "udp6":
host, port, err := net.SplitHostPort(address) return actualSingleDialContext(ctx, network, address, opt)
if err != nil {
return nil, err
}
var ip net.IP
switch network {
case "tcp4", "udp4":
if !opt.direct {
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv4(host)
}
default:
if !opt.direct {
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
} else {
ip, err = resolver.ResolveIPv6(host)
}
}
if err != nil {
return nil, err
}
return dialContext(ctx, network, ip, port, opt)
case "tcp", "udp": case "tcp", "udp":
return dualStackDialContext(ctx, network, address, opt) return actualDualStackDialContext(ctx, network, address, opt)
default: default:
return nil, errors.New("network invalid") return nil, ErrorInvalidedNetworkStack
} }
} }
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
cfg := &option{ cfg := applyOptions(options...)
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(cfg)
}
for _, o := range options {
o(cfg)
}
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.interfaceName != "" { if cfg.interfaceName != "" {
@ -88,7 +85,25 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
return lc.ListenPacket(ctx, network, address) return lc.ListenPacket(ctx, network, address)
} }
func dialContext(ctx context.Context, network string, destination net.IP, port string, opt *option) (net.Conn, error) { func SetDial(concurrent bool) {
dialMux.Lock()
tcpConcurrent = concurrent
if concurrent {
actualSingleDialContext = concurrentSingleDialContext
actualDualStackDialContext = concurrentDualStackDialContext
} else {
actualSingleDialContext = singleDialContext
actualDualStackDialContext = dualStackDialContext
}
dialMux.Unlock()
}
func GetDial() bool {
return tcpConcurrent
}
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
dialer := &net.Dialer{} dialer := &net.Dialer{}
if opt.interfaceName != "" { if opt.interfaceName != "" {
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil { if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
@ -99,7 +114,44 @@ func dialContext(ctx context.Context, network string, destination net.IP, port s
bindMarkToDialer(opt.routingMark, dialer, network, destination) bindMarkToDialer(opt.routingMark, dialer, network, destination)
} }
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port)) if DisableIPv6 && destination.Is6() {
return nil, ErrorDisableIPv6
}
address := net.JoinHostPort(destination.String(), port)
if opt.tfo {
return dialTFO(ctx, *dialer, network, address)
}
return dialer.DialContext(ctx, network, address)
}
func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip netip.Addr
switch network {
case "tcp4", "udp4":
if opt.resolver == nil {
ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host)
} else {
ip, err = resolver.ResolveIPv4WithResolver(ctx, host, opt.resolver)
}
default:
if opt.resolver == nil {
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host)
} else {
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, opt.resolver)
}
}
if err != nil {
err = fmt.Errorf("dns resolve failed:%w", err)
return nil, err
}
return dialContext(ctx, network, ip, port, opt)
} }
func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) { func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
@ -121,33 +173,34 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
results := make(chan dialResult) results := make(chan dialResult)
var primary, fallback dialResult var primary, fallback dialResult
startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) { startRacer := func(ctx context.Context, network, host string, r resolver.Resolver, ipv6 bool) {
result := dialResult{ipv6: ipv6, done: true} result := dialResult{ipv6: ipv6, done: true}
defer func() { defer func() {
select { select {
case results <- result: case results <- result:
case <-returned: case <-returned:
if result.Conn != nil { if result.Conn != nil {
result.Conn.Close() _ = result.Conn.Close()
} }
} }
}() }()
var ip net.IP var ip netip.Addr
if ipv6 { if ipv6 {
if !direct { if r == nil {
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host) ip, result.error = resolver.ResolveIPv6ProxyServerHost(ctx, host)
} else { } else {
ip, result.error = resolver.ResolveIPv6(host) ip, result.error = resolver.ResolveIPv6WithResolver(ctx, host, r)
} }
} else { } else {
if !direct { if r == nil {
ip, result.error = resolver.ResolveIPv4ProxyServerHost(host) ip, result.error = resolver.ResolveIPv4ProxyServerHost(ctx, host)
} else { } else {
ip, result.error = resolver.ResolveIPv4(host) ip, result.error = resolver.ResolveIPv4WithResolver(ctx, host, r)
} }
} }
if result.error != nil { if result.error != nil {
result.error = fmt.Errorf("dns resolve failed:%w", result.error)
return return
} }
result.resolved = true result.resolved = true
@ -155,10 +208,13 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
result.Conn, result.error = dialContext(ctx, network, ip, port, opt) result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
} }
go startRacer(ctx, network+"4", host, opt.direct, false) go startRacer(ctx, network+"4", host, opt.resolver, false)
go startRacer(ctx, network+"6", host, opt.direct, true) go startRacer(ctx, network+"6", host, opt.resolver, true)
for res := range results { count := 2
for i := 0; i < count; i++ {
select {
case res := <-results:
if res.error == nil { if res.error == nil {
return res.Conn, nil return res.Conn, nil
} }
@ -178,7 +234,195 @@ func dualStackDialContext(ctx context.Context, network, address string, opt *opt
return nil, primary.error return nil, primary.error
} }
} }
case <-ctx.Done():
err = ctx.Err()
break
}
} }
return nil, errors.New("never touched") if err == nil {
err = fmt.Errorf("dual stack dial failed")
} else {
err = fmt.Errorf("dual stack dial failed:%w", err)
}
return nil, err
}
func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
returned := make(chan struct{})
defer close(returned)
type dialResult struct {
ip netip.Addr
net.Conn
error
isPrimary bool
done bool
}
preferCount := atomic.NewInt32(0)
results := make(chan dialResult)
tcpRacer := func(ctx context.Context, ip netip.Addr) {
result := dialResult{ip: ip, done: true}
defer func() {
select {
case results <- result:
case <-returned:
if result.Conn != nil {
_ = result.Conn.Close()
}
}
}()
if strings.Contains(network, "tcp") {
network = "tcp"
} else {
network = "udp"
}
if ip.Is6() {
network += "6"
if opt.prefer != 4 {
result.isPrimary = true
}
}
if ip.Is4() {
network += "4"
if opt.prefer != 6 {
result.isPrimary = true
}
}
if result.isPrimary {
preferCount.Add(1)
}
result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
}
for _, ip := range ips {
go tcpRacer(ctx, ip)
}
connCount := len(ips)
var fallback dialResult
var primaryError error
var finalError error
for i := 0; i < connCount; i++ {
select {
case res := <-results:
if res.error == nil {
if res.isPrimary {
return res.Conn, nil
} else {
if !fallback.done || fallback.error != nil {
fallback = res
}
}
} else {
if res.isPrimary {
primaryError = res.error
preferCount.Add(-1)
if preferCount.Load() == 0 && fallback.done && fallback.error == nil {
return fallback.Conn, nil
}
}
}
case <-ctx.Done():
if fallback.done && fallback.error == nil {
return fallback.Conn, nil
}
finalError = ctx.Err()
break
}
}
if fallback.done && fallback.error == nil {
return fallback.Conn, nil
}
if primaryError != nil {
return nil, primaryError
}
if fallback.error != nil {
return nil, fallback.error
}
if finalError == nil {
finalError = fmt.Errorf("all ips %v tcp shake hands failed", ips)
} else {
finalError = fmt.Errorf("concurrent dial failed:%w", finalError)
}
return nil, finalError
}
func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
switch network {
case "tcp4", "udp4":
if opt.resolver == nil {
ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv4WithResolver(ctx, host, opt.resolver)
}
default:
if opt.resolver == nil {
ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host)
} else {
ips, err = resolver.LookupIPv6WithResolver(ctx, host, opt.resolver)
}
}
if err != nil {
err = fmt.Errorf("dns resolve failed:%w", err)
return nil, err
}
return concurrentDialContext(ctx, network, ips, port, opt)
}
func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ips []netip.Addr
if opt.resolver != nil {
ips, err = resolver.LookupIPWithResolver(ctx, host, opt.resolver)
} else {
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
}
if err != nil {
err = fmt.Errorf("dns resolve failed:%w", err)
return nil, err
}
return concurrentDialContext(ctx, network, ips, port, opt)
}
type Dialer struct {
Opt option
}
func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return DialContext(ctx, network, address, WithOption(d.Opt))
}
func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, WithOption(d.Opt))
}
func NewDialer(options ...Option) Dialer {
opt := applyOptions(options...)
return Dialer{Opt: *opt}
} }

View File

@ -3,41 +3,35 @@
package dialer package dialer
import ( import (
"context"
"net" "net"
"net/netip"
"syscall" "syscall"
) )
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) { func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
dialer.Control = bindMarkToControl(mark, dialer.Control) addControlToDialer(dialer, bindMarkToControl(mark))
} }
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) { func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
lc.Control = bindMarkToControl(mark, lc.Control) addControlToListenConfig(lc, bindMarkToControl(mark))
} }
func bindMarkToControl(mark int, chain controlFn) controlFn { func bindMarkToControl(mark int) controlFn {
return func(network, address string, c syscall.RawConn) (err error) { return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
defer func() {
if err == nil && chain != nil {
err = chain(network, address, c)
}
}()
ipStr, _, err := net.SplitHostPort(address) addrPort, err := netip.ParseAddrPort(address)
if err == nil { if err == nil && !addrPort.Addr().IsGlobalUnicast() {
ip := net.ParseIP(ipStr)
if ip != nil && !ip.IsGlobalUnicast() {
return return
} }
}
return c.Control(func(fd uintptr) { var innerErr error
switch network { err = c.Control(func(fd uintptr) {
case "tcp4", "udp4": innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
case "tcp6", "udp6":
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
}
}) })
if innerErr != nil {
err = innerErr
}
return
} }
} }

View File

@ -4,6 +4,7 @@ package dialer
import ( import (
"net" "net"
"net/netip"
"sync" "sync"
"github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/log"
@ -17,10 +18,10 @@ func printMarkWarn() {
}) })
} }
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ net.IP) { func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
printMarkWarn() printMarkWarn()
} }
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, address string) { func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
printMarkWarn() printMarkWarn()
} }

View File

@ -1,6 +1,10 @@
package dialer package dialer
import "go.uber.org/atomic" import (
"github.com/Dreamacro/clash/component/resolver"
"go.uber.org/atomic"
)
var ( var (
DefaultOptions []Option DefaultOptions []Option
@ -12,7 +16,10 @@ type option struct {
interfaceName string interfaceName string
addrReuse bool addrReuse bool
routingMark int routingMark int
direct bool network int
prefer int
tfo bool
resolver resolver.Resolver
} }
type Option func(opt *option) type Option func(opt *option)
@ -35,8 +42,42 @@ func WithRoutingMark(mark int) Option {
} }
} }
func WithDirect() Option { func WithResolver(r resolver.Resolver) Option {
return func(opt *option) { return func(opt *option) {
opt.direct = true opt.resolver = r
}
}
func WithPreferIPv4() Option {
return func(opt *option) {
opt.prefer = 4
}
}
func WithPreferIPv6() Option {
return func(opt *option) {
opt.prefer = 6
}
}
func WithOnlySingleStack(isIPv4 bool) Option {
return func(opt *option) {
if isIPv4 {
opt.network = 4
} else {
opt.network = 6
}
}
}
func WithTFO(tfo bool) Option {
return func(opt *option) {
opt.tfo = tfo
}
}
func WithOption(o option) Option {
return func(opt *option) {
*opt = o
} }
} }

View File

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

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