alroyso 7 months ago
parent
commit
b75f151c65
100 changed files with 14098 additions and 10 deletions
  1. 123 10
      build-clash-lib.py
  2. 83 0
      core/Clash.Meta/.github/ISSUE_TEMPLATE/bug_report.yml
  3. 5 0
      core/Clash.Meta/.github/ISSUE_TEMPLATE/config.yml
  4. 37 0
      core/Clash.Meta/.github/ISSUE_TEMPLATE/feature_request.yml
  5. 32 0
      core/Clash.Meta/.github/genReleaseNote.sh
  6. 17 0
      core/Clash.Meta/.github/mihomo.service
  7. 54 0
      core/Clash.Meta/.github/patch_go122/48042aa09c2f878c4faa576948b07fe625c4707a.diff
  8. 158 0
      core/Clash.Meta/.github/patch_go122/693def151adff1af707d82d28f55dba81ceb08e1.diff
  9. 162 0
      core/Clash.Meta/.github/patch_go122/7c1157f9544922e96945196b47b95664b1e39108.diff
  10. 26 0
      core/Clash.Meta/.github/release.sh
  11. 35 0
      core/Clash.Meta/.github/rename-cgo.sh
  12. 12 0
      core/Clash.Meta/.github/rename-go120.sh
  13. 16 0
      core/Clash.Meta/.github/workflows/Delete.yml
  14. 488 0
      core/Clash.Meta/.github/workflows/build.yml
  15. 33 0
      core/Clash.Meta/.github/workflows/trigger-cmfa-update.yml
  16. 28 0
      core/Clash.Meta/.gitignore
  17. 17 0
      core/Clash.Meta/.golangci.yaml
  18. 27 0
      core/Clash.Meta/Dockerfile
  19. 674 0
      core/Clash.Meta/LICENSE
  20. 169 0
      core/Clash.Meta/Makefile
  21. BIN
      core/Clash.Meta/Meta.png
  22. 100 0
      core/Clash.Meta/README.md
  23. 306 0
      core/Clash.Meta/adapter/adapter.go
  24. 73 0
      core/Clash.Meta/adapter/inbound/addition.go
  25. 45 0
      core/Clash.Meta/adapter/inbound/auth.go
  26. 20 0
      core/Clash.Meta/adapter/inbound/http.go
  27. 17 0
      core/Clash.Meta/adapter/inbound/https.go
  28. 57 0
      core/Clash.Meta/adapter/inbound/ipfilter.go
  29. 18 0
      core/Clash.Meta/adapter/inbound/listen.go
  30. 23 0
      core/Clash.Meta/adapter/inbound/listen_unix.go
  31. 15 0
      core/Clash.Meta/adapter/inbound/listen_windows.go
  32. 10 0
      core/Clash.Meta/adapter/inbound/mptcp_go120.go
  33. 11 0
      core/Clash.Meta/adapter/inbound/mptcp_go121.go
  34. 22 0
      core/Clash.Meta/adapter/inbound/packet.go
  35. 18 0
      core/Clash.Meta/adapter/inbound/socket.go
  36. 63 0
      core/Clash.Meta/adapter/inbound/util.go
  37. 287 0
      core/Clash.Meta/adapter/outbound/base.go
  38. 109 0
      core/Clash.Meta/adapter/outbound/direct.go
  39. 159 0
      core/Clash.Meta/adapter/outbound/dns.go
  40. 186 0
      core/Clash.Meta/adapter/outbound/http.go
  41. 290 0
      core/Clash.Meta/adapter/outbound/hysteria.go
  42. 205 0
      core/Clash.Meta/adapter/outbound/hysteria2.go
  43. 41 0
      core/Clash.Meta/adapter/outbound/reality.go
  44. 128 0
      core/Clash.Meta/adapter/outbound/reject.go
  45. 337 0
      core/Clash.Meta/adapter/outbound/shadowsocks.go
  46. 253 0
      core/Clash.Meta/adapter/outbound/shadowsocksr.go
  47. 135 0
      core/Clash.Meta/adapter/outbound/singmux.go
  48. 216 0
      core/Clash.Meta/adapter/outbound/snell.go
  49. 250 0
      core/Clash.Meta/adapter/outbound/socks5.go
  50. 208 0
      core/Clash.Meta/adapter/outbound/ssh.go
  51. 344 0
      core/Clash.Meta/adapter/outbound/trojan.go
  52. 316 0
      core/Clash.Meta/adapter/outbound/tuic.go
  53. 164 0
      core/Clash.Meta/adapter/outbound/util.go
  54. 611 0
      core/Clash.Meta/adapter/outbound/vless.go
  55. 536 0
      core/Clash.Meta/adapter/outbound/vmess.go
  56. 663 0
      core/Clash.Meta/adapter/outbound/wireguard.go
  57. 44 0
      core/Clash.Meta/adapter/outbound/wireguard_test.go
  58. 177 0
      core/Clash.Meta/adapter/outboundgroup/fallback.go
  59. 295 0
      core/Clash.Meta/adapter/outboundgroup/groupbase.go
  60. 278 0
      core/Clash.Meta/adapter/outboundgroup/loadbalance.go
  61. 220 0
      core/Clash.Meta/adapter/outboundgroup/parser.go
  62. 64 0
      core/Clash.Meta/adapter/outboundgroup/patch_android.go
  63. 172 0
      core/Clash.Meta/adapter/outboundgroup/relay.go
  64. 126 0
      core/Clash.Meta/adapter/outboundgroup/selector.go
  65. 255 0
      core/Clash.Meta/adapter/outboundgroup/urltest.go
  66. 6 0
      core/Clash.Meta/adapter/outboundgroup/util.go
  67. 167 0
      core/Clash.Meta/adapter/parser.go
  68. 236 0
      core/Clash.Meta/adapter/provider/healthcheck.go
  69. 113 0
      core/Clash.Meta/adapter/provider/parser.go
  70. 9 0
      core/Clash.Meta/adapter/provider/patch.go
  71. 415 0
      core/Clash.Meta/adapter/provider/provider.go
  72. 39 0
      core/Clash.Meta/adapter/provider/subscription_info.go
  73. 21 0
      core/Clash.Meta/android_tz.go
  74. 28 0
      core/Clash.Meta/check_amd64.sh
  75. 235 0
      core/Clash.Meta/common/arc/arc.go
  76. 105 0
      core/Clash.Meta/common/arc/arc_test.go
  77. 32 0
      core/Clash.Meta/common/arc/entry.go
  78. 198 0
      core/Clash.Meta/common/atomic/type.go
  79. 75 0
      core/Clash.Meta/common/atomic/value.go
  80. 105 0
      core/Clash.Meta/common/batch/batch.go
  81. 83 0
      core/Clash.Meta/common/batch/batch_test.go
  82. 21 0
      core/Clash.Meta/common/buf/sing.go
  83. 55 0
      core/Clash.Meta/common/callback/callback.go
  84. 61 0
      core/Clash.Meta/common/callback/close_callback.go
  85. 36 0
      core/Clash.Meta/common/cmd/cmd.go
  86. 11 0
      core/Clash.Meta/common/cmd/cmd_other.go
  87. 40 0
      core/Clash.Meta/common/cmd/cmd_test.go
  88. 12 0
      core/Clash.Meta/common/cmd/cmd_windows.go
  89. 45 0
      core/Clash.Meta/common/convert/base64.go
  90. 534 0
      core/Clash.Meta/common/convert/converter.go
  91. 35 0
      core/Clash.Meta/common/convert/converter_test.go
  92. 323 0
      core/Clash.Meta/common/convert/util.go
  93. 136 0
      core/Clash.Meta/common/convert/v.go
  94. 294 0
      core/Clash.Meta/common/lru/lrucache.go
  95. 184 0
      core/Clash.Meta/common/lru/lrucache_test.go
  96. 50 0
      core/Clash.Meta/common/murmur3/murmur.go
  97. 144 0
      core/Clash.Meta/common/murmur3/murmur32.go
  98. 36 0
      core/Clash.Meta/common/net/addr.go
  99. 45 0
      core/Clash.Meta/common/net/bind.go
  100. 106 0
      core/Clash.Meta/common/net/bufconn.go

+ 123 - 10
build-clash-lib.py

@@ -2,21 +2,134 @@
 import os
 import os
 import sys
 import sys
 import platform
 import platform
+import argparse
 
 
-if __name__ == "__main__":
+def build_architecture(arch, output, output_dir):
+    os.environ["GOARCH"] = arch
+    os.system(f"go build -buildmode=c-shared -o {output}")
+
+    # 检查生成的头文件
+    headers = [f for f in os.listdir(output_dir) if f.endswith(".h") and "libclash" in f]
+    if not headers:
+        raise FileNotFoundError(f"No header file generated for architecture: {arch}")
+
+    # 重命名第一个找到的头文件为 libclash.h
+    for header in headers:
+        old_path = os.path.join(output_dir, header)
+        new_path = os.path.join(output_dir, "libclash.h")
+        if old_path != new_path:
+            os.rename(old_path, new_path)
+            print(f"[info] Renamed {header} to libclash.h")
+            break  # 只需要处理一次
+
+def clean_up_dynamic_libs(output_dir, output_x86_64, output_arm64):
+    # 删除多余的动态库,确保只保留 libclash.dylib
+    files_to_delete = [output_x86_64, output_arm64]
+    for lib_file in files_to_delete:
+        if os.path.exists(lib_file):
+            os.remove(lib_file)
+            print(f"[info] Deleted extra dynamic library: {lib_file}")
+
+def clean_up_headers(output_dir):
+    # 删除多余的头文件,确保只有 libclash.h 存在
+    headers = [f for f in os.listdir(output_dir) if f.endswith(".h") and f != "libclash.h"]
+    for header in headers:
+        header_path = os.path.join(output_dir, header)
+        os.remove(header_path)
+        print(f"[info] Deleted extra header file: {header_path}")
+
+def package_universal_binary(output_x86_64, output_arm64, final_output, output_dir):
+    # 确保生成的二进制文件存在
+    print(f"[info] Checking {output_x86_64} and {output_arm64} for packaging...")
+    if not os.path.exists(output_x86_64):
+        raise FileNotFoundError(f"File not found: {output_x86_64}")
+    if not os.path.exists(output_arm64):
+        raise FileNotFoundError(f"File not found: {output_arm64}")
+
+    # 使用 lipo 将 x86_64 和 arm64 合并为通用二进制文件
+    print("[info] Creating universal binary for macOS...")
+    os.system(f"lipo -create -output {final_output} {output_x86_64} {output_arm64}")
+    if not os.path.exists(final_output):
+        raise FileNotFoundError(f"Failed to create universal binary: {final_output}")
+    print(f"[info] Universal binary created: {final_output}")
+
+    # 清理多余的动态库
+    clean_up_dynamic_libs(output_dir, output_x86_64, output_arm64)
+
+    # 清理并重命名头文件为 libclash.h
+    clean_up_headers(output_dir)
+
+def build(target):
     os.chdir("core")
     os.chdir("core")
     os.environ["CGO_ENABLED"] = "1"
     os.environ["CGO_ENABLED"] = "1"
-    output = "libclash"
+    output_base = "../libclash/libclash"
+    output_dir = "../libclash"
+
+    # 根据平台设置动态库文件扩展名
     if sys.platform == 'win32':
     if sys.platform == 'win32':
-        output += ".dll"
+        output_x86_64 = output_base + ".dll"
+        final_output = output_x86_64
     elif sys.platform == "darwin":
     elif sys.platform == "darwin":
-        output += ".dylib"
+        output_x86_64 = output_base + "_x86_64.dylib"
+        output_arm64 = output_base + "_arm64.dylib"
+        final_output = output_base + ".dylib"
     else:
     else:
-        output += ".so"
-    processor = platform.processor()
-    if "arm" in processor or "Apple" in processor:
-        print("[warn] arm/Apple also compiles out amd64 target")
-        os.environ["GOARCH"] = "arm64"
-    os.system(f"go build -buildmode=c-shared -o {output}")
+        output_x86_64 = output_base + "_x86_64.so"
+        output_arm64 = output_base + "_arm64.so"
+        final_output = output_base + ".so"
+
+    if sys.platform == 'win32':
+        # Windows 平台仅编译 x86_64 版本
+        print("[info] Compiling x86_64 target for Windows...")
+        build_architecture("amd64", output_x86_64, output_dir)
+    elif sys.platform == "darwin":
+        # macOS 平台编译 x86_64 和 arm64,并合并为通用二进制
+        print("[info] Compiling x86_64 target for macOS...")
+        build_architecture("amd64", output_x86_64, output_dir)
+
+        print("[info] Compiling arm64 target for macOS...")
+        build_architecture("arm64", output_arm64, output_dir)
+
+        # 在 build 阶段也执行 lipo 合并并清理
+        package_universal_binary(output_x86_64, output_arm64, final_output, output_dir)
+    else:
+        # Linux 平台分别编译 x86_64 和 arm64,不合并为通用二进制
+        print("[info] Compiling x86_64 target for Linux...")
+        build_architecture("amd64", output_x86_64, output_dir)
+
+        print("[info] Compiling arm64 target for Linux...")
+        build_architecture("arm64", output_arm64, output_dir)
 
 
     os.chdir("..")
     os.chdir("..")
+
+def run_flutter_distributor():
+    print("[info] Running flutter_distributor for packaging...")
+    # 使用 flutter_distributor 打包 Flutter 项目
+    os.system("flutter_distributor package --skip-clean --platform all")
+
+def package():
+    if sys.platform == "darwin":
+        # 打包 macOS 的通用二进制(通过 lipo 合并)
+        output_base = "../libclash/libclash"
+        output_x86_64 = output_base + "_x86_64.dylib"
+        output_arm64 = output_base + "_arm64.dylib"
+        final_output = output_base + ".dylib"
+        output_dir = "../libclash"
+        package_universal_binary(output_x86_64, output_arm64, final_output, output_dir)
+
+    # 执行 flutter_distributor 进行打包
+    run_flutter_distributor()
+
+def main():
+    parser = argparse.ArgumentParser(description="Build and package libclash dynamic libraries.")
+    parser.add_argument("action", choices=["build", "package"], help="Specify whether to build or package.")
+    args = parser.parse_args()
+
+    if args.action == "build":
+        build("build")
+    elif args.action == "package":
+        build("package")  # 在打包时也先进行构建
+        package()
+
+if __name__ == "__main__":
+    main()

+ 83 - 0
core/Clash.Meta/.github/ISSUE_TEMPLATE/bug_report.yml

@@ -0,0 +1,83 @@
+name: Bug report
+description: Create a report to help us improve
+title: "[Bug] "
+labels: ["bug"]
+body:
+  - type: checkboxes
+    id: ensure
+    attributes:
+      label: Verify steps
+      description: "
+在提交之前,请确认
+Please verify that you've followed these steps
+"
+      options:
+        - label: "
+确保你使用的是**本仓库**最新的的 mihomo 或 mihomo Alpha 版本
+Ensure you are using the latest version of Mihomo or Mihomo Alpha from **this repository**.
+"
+          required: true
+        - label: "
+如果你可以自己 debug 并解决的话,提交 PR 吧
+Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
+"
+          required: false
+        - label: "
+我已经在 [Issue Tracker](……/) 中找过我要提出的问题
+I have searched on the [issue tracker](……/) for a related issue.
+"
+          required: true
+        - label: "
+我已经使用 Alpha 分支版本测试过,问题依旧存在
+I have tested using the dev branch, and the issue still exists.
+"
+          required: true
+        - label: "
+我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法自行解决问题
+I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue.
+"
+          required: true
+        - label: "
+这是 Mihomo 核心的问题,并非我所使用的 Mihomo 衍生版本(如 OpenMihomo、KoolMihomo 等)的特定问题
+This is an issue of the Mihomo core *per se*, not to the derivatives of Mihomo, like OpenMihomo or KoolMihomo.
+"
+          required: true
+  - type: input
+    attributes:
+      label: Mihomo version
+      description: "use `mihomo -v`"
+    validations:
+      required: true
+  - type: dropdown
+    id: os
+    attributes:
+      label: What OS are you seeing the problem on?
+      multiple: true
+      options:
+        - macOS
+        - Windows
+        - Linux
+        - OpenBSD/FreeBSD
+  - type: textarea
+    attributes:
+      render: yaml
+      label: "Mihomo config"
+      description: "
+在下方附上 Mihomo core 配置文件,请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等)
+Paste the Mihomo core configuration file below, please make sure that there is no sensitive information in the configuration file (e.g., server address/url, password, port)
+"
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      render: shell
+      label: Mihomo log
+      description: "
+在下方附上 Mihomo Core 的日志,log level 使用 DEBUG
+Paste the Mihomo core log below with the log level set to `DEBUG`.
+"
+  - type: textarea
+    attributes:
+      label: Description
+    validations:
+      required: true

+ 5 - 0
core/Clash.Meta/.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: mihomo Community Support
+    url: https://github.com/MetaCubeX/mihomo/discussions
+    about: Please ask and answer questions about mihomo here.

+ 37 - 0
core/Clash.Meta/.github/ISSUE_TEMPLATE/feature_request.yml

@@ -0,0 +1,37 @@
+name: Feature request
+description: Suggest an idea for this project
+title: "[Feature] "
+labels: ["enhancement"]
+body:
+  - type: checkboxes
+    id: ensure
+    attributes:
+      label: Verify steps
+      description: "
+在提交之前,请确认
+Please verify that you've followed these steps
+"
+      options:
+        - label: "
+我已经在 [Issue Tracker](……/) 中找过我要提出的请求
+I have searched on the [issue tracker](……/) for a related feature request.
+"
+          required: true
+        - label: "
+我已经仔细看过 [Documentation](https://wiki.metacubex.one/) 并无法找到这个功能
+I have read the [documentation](https://wiki.metacubex.one/) and was unable to solve the issue.
+"
+          required: true
+  - type: textarea
+    attributes:
+      label: Description
+      description: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Mihomo Core 的行为是什麽?
+    validations:
+      required: true
+  - type: textarea
+    attributes:
+      label: Possible Solution
+      description: "
+此项非必须,但是如果你有想法的话欢迎提出。
+Not obligatory, but suggest a fix/reason for the bug, or ideas how to implement the addition or change
+"

+ 32 - 0
core/Clash.Meta/.github/genReleaseNote.sh

@@ -0,0 +1,32 @@
+#!/bin/bash
+
+while getopts "v:" opt; do
+  case $opt in
+    v)
+      version_range=$OPTARG
+      ;;
+    \?)
+      echo "Invalid option: -$OPTARG" >&2
+      exit 1
+      ;;
+  esac
+done
+
+if [ -z "$version_range" ]; then
+  echo "Please provide the version range using -v option. Example: ./genReleashNote.sh -v v1.14.1...v1.14.2"
+  exit 1
+fi
+
+echo "## What's Changed" > release.md
+git log --pretty=format:"* %h %s by @%an" --grep="^feat" -i $version_range | sort -f | uniq >> release.md
+echo "" >> release.md
+
+echo "## BUG & Fix" >> release.md
+git log --pretty=format:"* %h %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md
+echo "" >> release.md
+
+echo "## Maintenance" >> release.md
+git log --pretty=format:"* %h %s by @%an" --grep="^chore\|^docs\|^refactor" -i $version_range | sort -f | uniq >> release.md
+echo "" >> release.md
+
+echo "**Full Changelog**: https://github.com/MetaCubeX/mihomo/compare/$version_range" >> release.md

+ 17 - 0
core/Clash.Meta/.github/mihomo.service

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

+ 54 - 0
core/Clash.Meta/.github/patch_go122/48042aa09c2f878c4faa576948b07fe625c4707a.diff

@@ -0,0 +1,54 @@
+diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go
+index 06e684c7116b4..b311a5c74684b 100644
+--- a/src/syscall/exec_windows.go
++++ b/src/syscall/exec_windows.go
+@@ -319,17 +319,6 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle
+ 		}
+ 	}
+
+-	var maj, min, build uint32
+-	rtlGetNtVersionNumbers(&maj, &min, &build)
+-	isWin7 := maj < 6 || (maj == 6 && min <= 1)
+-	// NT kernel handles are divisible by 4, with the bottom 3 bits left as
+-	// a tag. The fully set tag correlates with the types of handles we're
+-	// concerned about here.  Except, the kernel will interpret some
+-	// special handle values, like -1, -2, and so forth, so kernelbase.dll
+-	// checks to see that those bottom three bits are checked, but that top
+-	// bit is not checked.
+-	isLegacyWin7ConsoleHandle := func(handle Handle) bool { return isWin7 && handle&0x10000003 == 3 }
+-
+ 	p, _ := GetCurrentProcess()
+ 	parentProcess := p
+ 	if sys.ParentProcess != 0 {
+@@ -338,15 +327,7 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle
+ 	fd := make([]Handle, len(attr.Files))
+ 	for i := range attr.Files {
+ 		if attr.Files[i] > 0 {
+-			destinationProcessHandle := parentProcess
+-
+-			// On Windows 7, console handles aren't real handles, and can only be duplicated
+-			// into the current process, not a parent one, which amounts to the same thing.
+-			if parentProcess != p && isLegacyWin7ConsoleHandle(Handle(attr.Files[i])) {
+-				destinationProcessHandle = p
+-			}
+-
+-			err := DuplicateHandle(p, Handle(attr.Files[i]), destinationProcessHandle, &fd[i], 0, true, DUPLICATE_SAME_ACCESS)
++			err := DuplicateHandle(p, Handle(attr.Files[i]), parentProcess, &fd[i], 0, true, DUPLICATE_SAME_ACCESS)
+ 			if err != nil {
+ 				return 0, 0, err
+ 			}
+@@ -377,14 +358,6 @@ func StartProcess(argv0 string, argv []string, attr *ProcAttr) (pid int, handle
+
+ 	fd = append(fd, sys.AdditionalInheritedHandles...)
+
+-	// On Windows 7, console handles aren't real handles, so don't pass them
+-	// through to PROC_THREAD_ATTRIBUTE_HANDLE_LIST.
+-	for i := range fd {
+-		if isLegacyWin7ConsoleHandle(fd[i]) {
+-			fd[i] = 0
+-		}
+-	}
+-
+ 	// The presence of a NULL handle in the list is enough to cause PROC_THREAD_ATTRIBUTE_HANDLE_LIST
+ 	// to treat the entire list as empty, so remove NULL handles.
+ 	j := 0

+ 158 - 0
core/Clash.Meta/.github/patch_go122/693def151adff1af707d82d28f55dba81ceb08e1.diff

@@ -0,0 +1,158 @@
+diff --git a/src/crypto/rand/rand.go b/src/crypto/rand/rand.go
+index 62738e2cb1a7d..d0dcc7cc71fc0 100644
+--- a/src/crypto/rand/rand.go
++++ b/src/crypto/rand/rand.go
+@@ -15,7 +15,7 @@ import "io"
+ // available, /dev/urandom otherwise.
+ // On OpenBSD and macOS, Reader uses getentropy(2).
+ // On other Unix-like systems, Reader reads from /dev/urandom.
+-// On Windows systems, Reader uses the RtlGenRandom API.
++// On Windows systems, Reader uses the ProcessPrng API.
+ // On JS/Wasm, Reader uses the Web Crypto API.
+ // On WASIP1/Wasm, Reader uses random_get from wasi_snapshot_preview1.
+ var Reader io.Reader
+diff --git a/src/crypto/rand/rand_windows.go b/src/crypto/rand/rand_windows.go
+index 6c0655c72b692..7380f1f0f1e6e 100644
+--- a/src/crypto/rand/rand_windows.go
++++ b/src/crypto/rand/rand_windows.go
+@@ -15,11 +15,8 @@ func init() { Reader = &rngReader{} }
+ 
+ type rngReader struct{}
+ 
+-func (r *rngReader) Read(b []byte) (n int, err error) {
+-	// RtlGenRandom only returns 1<<32-1 bytes at a time. We only read at
+-	// most 1<<31-1 bytes at a time so that  this works the same on 32-bit
+-	// and 64-bit systems.
+-	if err := batched(windows.RtlGenRandom, 1<<31-1)(b); err != nil {
++func (r *rngReader) Read(b []byte) (int, error) {
++	if err := windows.ProcessPrng(b); err != nil {
+ 		return 0, err
+ 	}
+ 	return len(b), nil
+diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go
+index ab4ad2ec64108..5854ca60b5cef 100644
+--- a/src/internal/syscall/windows/syscall_windows.go
++++ b/src/internal/syscall/windows/syscall_windows.go
+@@ -373,7 +373,7 @@ func ErrorLoadingGetTempPath2() error {
+ //sys	DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock
+ //sys	CreateEvent(eventAttrs *SecurityAttributes, manualReset uint32, initialState uint32, name *uint16) (handle syscall.Handle, err error) = kernel32.CreateEventW
+ 
+-//sys	RtlGenRandom(buf []byte) (err error) = advapi32.SystemFunction036
++//sys	ProcessPrng(buf []byte) (err error) = bcryptprimitives.ProcessPrng
+ 
+ type FILE_ID_BOTH_DIR_INFO struct {
+ 	NextEntryOffset uint32
+diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go
+index e3f6d8d2a2208..5a587ad4f146c 100644
+--- a/src/internal/syscall/windows/zsyscall_windows.go
++++ b/src/internal/syscall/windows/zsyscall_windows.go
+@@ -37,13 +37,14 @@ func errnoErr(e syscall.Errno) error {
+ }
+ 
+ var (
+-	modadvapi32 = syscall.NewLazyDLL(sysdll.Add("advapi32.dll"))
+-	modiphlpapi = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll"))
+-	modkernel32 = syscall.NewLazyDLL(sysdll.Add("kernel32.dll"))
+-	modnetapi32 = syscall.NewLazyDLL(sysdll.Add("netapi32.dll"))
+-	modpsapi    = syscall.NewLazyDLL(sysdll.Add("psapi.dll"))
+-	moduserenv  = syscall.NewLazyDLL(sysdll.Add("userenv.dll"))
+-	modws2_32   = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll"))
++	modadvapi32         = syscall.NewLazyDLL(sysdll.Add("advapi32.dll"))
++	modbcryptprimitives = syscall.NewLazyDLL(sysdll.Add("bcryptprimitives.dll"))
++	modiphlpapi         = syscall.NewLazyDLL(sysdll.Add("iphlpapi.dll"))
++	modkernel32         = syscall.NewLazyDLL(sysdll.Add("kernel32.dll"))
++	modnetapi32         = syscall.NewLazyDLL(sysdll.Add("netapi32.dll"))
++	modpsapi            = syscall.NewLazyDLL(sysdll.Add("psapi.dll"))
++	moduserenv          = syscall.NewLazyDLL(sysdll.Add("userenv.dll"))
++	modws2_32           = syscall.NewLazyDLL(sysdll.Add("ws2_32.dll"))
+ 
+ 	procAdjustTokenPrivileges             = modadvapi32.NewProc("AdjustTokenPrivileges")
+ 	procDuplicateTokenEx                  = modadvapi32.NewProc("DuplicateTokenEx")
+@@ -55,7 +56,7 @@ var (
+ 	procQueryServiceStatus                = modadvapi32.NewProc("QueryServiceStatus")
+ 	procRevertToSelf                      = modadvapi32.NewProc("RevertToSelf")
+ 	procSetTokenInformation               = modadvapi32.NewProc("SetTokenInformation")
+-	procSystemFunction036                 = modadvapi32.NewProc("SystemFunction036")
++	procProcessPrng                       = modbcryptprimitives.NewProc("ProcessPrng")
+ 	procGetAdaptersAddresses              = modiphlpapi.NewProc("GetAdaptersAddresses")
+ 	procCreateEventW                      = modkernel32.NewProc("CreateEventW")
+ 	procGetACP                            = modkernel32.NewProc("GetACP")
+@@ -179,12 +180,12 @@ func SetTokenInformation(tokenHandle syscall.Token, tokenInformationClass uint32
+ 	return
+ }
+ 
+-func RtlGenRandom(buf []byte) (err error) {
++func ProcessPrng(buf []byte) (err error) {
+ 	var _p0 *byte
+ 	if len(buf) > 0 {
+ 		_p0 = &buf[0]
+ 	}
+-	r1, _, e1 := syscall.Syscall(procSystemFunction036.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0)
++	r1, _, e1 := syscall.Syscall(procProcessPrng.Addr(), 2, uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), 0)
+ 	if r1 == 0 {
+ 		err = errnoErr(e1)
+ 	}
+diff --git a/src/runtime/os_windows.go b/src/runtime/os_windows.go
+index 8ca8d7790909e..3772a864b2ff4 100644
+--- a/src/runtime/os_windows.go
++++ b/src/runtime/os_windows.go
+@@ -127,15 +127,8 @@ var (
+ 	_WriteFile,
+ 	_ stdFunction
+ 
+-	// Use RtlGenRandom to generate cryptographically random data.
+-	// This approach has been recommended by Microsoft (see issue
+-	// 15589 for details).
+-	// The RtlGenRandom is not listed in advapi32.dll, instead
+-	// RtlGenRandom function can be found by searching for SystemFunction036.
+-	// Also some versions of Mingw cannot link to SystemFunction036
+-	// when building executable as Cgo. So load SystemFunction036
+-	// manually during runtime startup.
+-	_RtlGenRandom stdFunction
++	// Use ProcessPrng to generate cryptographically random data.
++	_ProcessPrng stdFunction
+ 
+ 	// Load ntdll.dll manually during startup, otherwise Mingw
+ 	// links wrong printf function to cgo executable (see issue
+@@ -151,11 +144,11 @@ var (
+ )
+ 
+ var (
+-	advapi32dll = [...]uint16{'a', 'd', 'v', 'a', 'p', 'i', '3', '2', '.', 'd', 'l', 'l', 0}
+-	ntdlldll    = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0}
+-	powrprofdll = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0}
+-	winmmdll    = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0}
+-	ws2_32dll   = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0}
++	bcryptprimitivesdll = [...]uint16{'b', 'c', 'r', 'y', 'p', 't', 'p', 'r', 'i', 'm', 'i', 't', 'i', 'v', 'e', 's', '.', 'd', 'l', 'l', 0}
++	ntdlldll            = [...]uint16{'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0}
++	powrprofdll         = [...]uint16{'p', 'o', 'w', 'r', 'p', 'r', 'o', 'f', '.', 'd', 'l', 'l', 0}
++	winmmdll            = [...]uint16{'w', 'i', 'n', 'm', 'm', '.', 'd', 'l', 'l', 0}
++	ws2_32dll           = [...]uint16{'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0}
+ )
+ 
+ // Function to be called by windows CreateThread
+@@ -251,11 +244,11 @@ func windowsLoadSystemLib(name []uint16) uintptr {
+ }
+ 
+ func loadOptionalSyscalls() {
+-	a32 := windowsLoadSystemLib(advapi32dll[:])
+-	if a32 == 0 {
+-		throw("advapi32.dll not found")
++	bcryptPrimitives := windowsLoadSystemLib(bcryptprimitivesdll[:])
++	if bcryptPrimitives == 0 {
++		throw("bcryptprimitives.dll not found")
+ 	}
+-	_RtlGenRandom = windowsFindfunc(a32, []byte("SystemFunction036\000"))
++	_ProcessPrng = windowsFindfunc(bcryptPrimitives, []byte("ProcessPrng\000"))
+ 
+ 	n32 := windowsLoadSystemLib(ntdlldll[:])
+ 	if n32 == 0 {
+@@ -531,7 +524,7 @@ func osinit() {
+ //go:nosplit
+ func readRandom(r []byte) int {
+ 	n := 0
+-	if stdcall2(_RtlGenRandom, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 {
++	if stdcall2(_ProcessPrng, uintptr(unsafe.Pointer(&r[0])), uintptr(len(r)))&0xff != 0 {
+ 		n = len(r)
+ 	}
+ 	return n

+ 162 - 0
core/Clash.Meta/.github/patch_go122/7c1157f9544922e96945196b47b95664b1e39108.diff

@@ -0,0 +1,162 @@
+diff --git a/src/net/hook_windows.go b/src/net/hook_windows.go
+index ab8656cbbf343..28c49cc6de7e7 100644
+--- a/src/net/hook_windows.go
++++ b/src/net/hook_windows.go
+@@ -14,7 +14,6 @@ var (
+ 	testHookDialChannel = func() { time.Sleep(time.Millisecond) } // see golang.org/issue/5349
+ 
+ 	// Placeholders for socket system calls.
+-	socketFunc    func(int, int, int) (syscall.Handle, error)                                                 = syscall.Socket
+ 	wsaSocketFunc func(int32, int32, int32, *syscall.WSAProtocolInfo, uint32, uint32) (syscall.Handle, error) = windows.WSASocket
+ 	connectFunc   func(syscall.Handle, syscall.Sockaddr) error                                                = syscall.Connect
+ 	listenFunc    func(syscall.Handle, int) error                                                             = syscall.Listen
+diff --git a/src/net/internal/socktest/main_test.go b/src/net/internal/socktest/main_test.go
+index 0197feb3f199a..967ce6795aedb 100644
+--- a/src/net/internal/socktest/main_test.go
++++ b/src/net/internal/socktest/main_test.go
+@@ -2,7 +2,7 @@
+ // Use of this source code is governed by a BSD-style
+ // license that can be found in the LICENSE file.
+ 
+-//go:build !js && !plan9 && !wasip1
++//go:build !js && !plan9 && !wasip1 && !windows
+ 
+ package socktest_test
+ 
+diff --git a/src/net/internal/socktest/main_windows_test.go b/src/net/internal/socktest/main_windows_test.go
+deleted file mode 100644
+index df1cb97784b51..0000000000000
+--- a/src/net/internal/socktest/main_windows_test.go
++++ /dev/null
+@@ -1,22 +0,0 @@
+-// Copyright 2015 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 socktest_test
+-
+-import "syscall"
+-
+-var (
+-	socketFunc func(int, int, int) (syscall.Handle, error)
+-	closeFunc  func(syscall.Handle) error
+-)
+-
+-func installTestHooks() {
+-	socketFunc = sw.Socket
+-	closeFunc = sw.Closesocket
+-}
+-
+-func uninstallTestHooks() {
+-	socketFunc = syscall.Socket
+-	closeFunc = syscall.Closesocket
+-}
+diff --git a/src/net/internal/socktest/sys_windows.go b/src/net/internal/socktest/sys_windows.go
+index 8c1c862f33c9b..1c42e5c7f34b7 100644
+--- a/src/net/internal/socktest/sys_windows.go
++++ b/src/net/internal/socktest/sys_windows.go
+@@ -9,38 +9,6 @@ import (
+ 	"syscall"
+ )
+ 
+-// Socket wraps syscall.Socket.
+-func (sw *Switch) Socket(family, sotype, proto int) (s syscall.Handle, err error) {
+-	sw.once.Do(sw.init)
+-
+-	so := &Status{Cookie: cookie(family, sotype, proto)}
+-	sw.fmu.RLock()
+-	f, _ := sw.fltab[FilterSocket]
+-	sw.fmu.RUnlock()
+-
+-	af, err := f.apply(so)
+-	if err != nil {
+-		return syscall.InvalidHandle, err
+-	}
+-	s, so.Err = syscall.Socket(family, sotype, proto)
+-	if err = af.apply(so); err != nil {
+-		if so.Err == nil {
+-			syscall.Closesocket(s)
+-		}
+-		return syscall.InvalidHandle, err
+-	}
+-
+-	sw.smu.Lock()
+-	defer sw.smu.Unlock()
+-	if so.Err != nil {
+-		sw.stats.getLocked(so.Cookie).OpenFailed++
+-		return syscall.InvalidHandle, so.Err
+-	}
+-	nso := sw.addLocked(s, family, sotype, proto)
+-	sw.stats.getLocked(nso.Cookie).Opened++
+-	return s, nil
+-}
+-
+ // WSASocket wraps [syscall.WSASocket].
+ func (sw *Switch) WSASocket(family, sotype, proto int32, protinfo *syscall.WSAProtocolInfo, group uint32, flags uint32) (s syscall.Handle, err error) {
+ 	sw.once.Do(sw.init)
+diff --git a/src/net/main_windows_test.go b/src/net/main_windows_test.go
+index 07f21b72eb1fc..bc024c0bbd82d 100644
+--- a/src/net/main_windows_test.go
++++ b/src/net/main_windows_test.go
+@@ -8,7 +8,6 @@ import "internal/poll"
+ 
+ var (
+ 	// Placeholders for saving original socket system calls.
+-	origSocket      = socketFunc
+ 	origWSASocket   = wsaSocketFunc
+ 	origClosesocket = poll.CloseFunc
+ 	origConnect     = connectFunc
+@@ -18,7 +17,6 @@ var (
+ )
+ 
+ func installTestHooks() {
+-	socketFunc = sw.Socket
+ 	wsaSocketFunc = sw.WSASocket
+ 	poll.CloseFunc = sw.Closesocket
+ 	connectFunc = sw.Connect
+@@ -28,7 +26,6 @@ func installTestHooks() {
+ }
+ 
+ func uninstallTestHooks() {
+-	socketFunc = origSocket
+ 	wsaSocketFunc = origWSASocket
+ 	poll.CloseFunc = origClosesocket
+ 	connectFunc = origConnect
+diff --git a/src/net/sock_windows.go b/src/net/sock_windows.go
+index fa11c7af2e727..5540135a2c43e 100644
+--- a/src/net/sock_windows.go
++++ b/src/net/sock_windows.go
+@@ -19,21 +19,6 @@ func maxListenerBacklog() int {
+ func sysSocket(family, sotype, proto int) (syscall.Handle, error) {
+ 	s, err := wsaSocketFunc(int32(family), int32(sotype), int32(proto),
+ 		nil, 0, windows.WSA_FLAG_OVERLAPPED|windows.WSA_FLAG_NO_HANDLE_INHERIT)
+-	if err == nil {
+-		return s, nil
+-	}
+-	// WSA_FLAG_NO_HANDLE_INHERIT flag is not supported on some
+-	// old versions of Windows, see
+-	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms742212(v=vs.85).aspx
+-	// for details. Just use syscall.Socket, if windows.WSASocket failed.
+-
+-	// See ../syscall/exec_unix.go for description of ForkLock.
+-	syscall.ForkLock.RLock()
+-	s, err = socketFunc(family, sotype, proto)
+-	if err == nil {
+-		syscall.CloseOnExec(s)
+-	}
+-	syscall.ForkLock.RUnlock()
+ 	if err != nil {
+ 		return syscall.InvalidHandle, os.NewSyscallError("socket", err)
+ 	}
+diff --git a/src/syscall/exec_windows.go b/src/syscall/exec_windows.go
+index 0a93bc0a80d4e..06e684c7116b4 100644
+--- a/src/syscall/exec_windows.go
++++ b/src/syscall/exec_windows.go
+@@ -14,6 +14,7 @@ import (
+ 	"unsafe"
+ )
+ 
++// ForkLock is not used on Windows.
+ var ForkLock sync.RWMutex
+ 
+ // EscapeArg rewrites command line argument s as prescribed

+ 26 - 0
core/Clash.Meta/.github/release.sh

@@ -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

+ 35 - 0
core/Clash.Meta/.github/rename-cgo.sh

@@ -0,0 +1,35 @@
+#!/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 mihomo-darwin-arm64-cgo
+    elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then
+        echo "rename darwin-10.16-amd64 $FILENAME"
+        mv $FILENAME mihomo-darwin-amd64-cgo
+    elif [[ $FILENAME =~ "windows-4.0-386" ]];then
+        echo "rename windows 386 $FILENAME"
+        mv $FILENAME mihomo-windows-386-cgo.exe
+    elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then
+        echo "rename windows amd64 $FILENAME"
+        mv $FILENAME mihomo-windows-amd64-cgo.exe
+    elif [[ $FILENAME =~ "mihomo-linux-arm-5" ]];then
+        echo "rename mihomo-linux-arm-5 $FILENAME"
+        mv $FILENAME mihomo-linux-armv5-cgo
+    elif [[ $FILENAME =~ "mihomo-linux-arm-6" ]];then
+        echo "rename mihomo-linux-arm-6 $FILENAME"
+        mv $FILENAME mihomo-linux-armv6-cgo
+    elif [[ $FILENAME =~ "mihomo-linux-arm-7" ]];then
+        echo "rename mihomo-linux-arm-7 $FILENAME"
+        mv $FILENAME mihomo-linux-armv7-cgo
+    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

+ 12 - 0
core/Clash.Meta/.github/rename-go120.sh

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

+ 16 - 0
core/Clash.Meta/.github/workflows/Delete.yml

@@ -0,0 +1,16 @@
+name: Delete old workflow runs
+on:
+  schedule:
+    - cron: '0 0 1 * *'
+# Run monthly, at 00:00 on the 1st day of month.
+
+jobs:
+  del_runs:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Delete workflow runs
+        uses: GitRML/delete-workflow-runs@main
+        with:
+          token: ${{ secrets.AUTH_PAT }}
+          repository: ${{ github.repository }}
+          retain_days: 30

+ 488 - 0
core/Clash.Meta/.github/workflows/build.yml

@@ -0,0 +1,488 @@
+name: Build
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "Tag version to release"
+        required: true
+  push:
+    paths-ignore:
+      - "docs/**"
+      - "README.md"
+      - ".github/ISSUE_TEMPLATE/**"
+    branches:
+      - Alpha
+    tags:
+      - "v*"
+  pull_request_target:
+    branches:
+      - Alpha
+concurrency:
+  group: "${{ github.workflow }}-${{ github.ref }}"
+  cancel-in-progress: true
+  
+env:
+  REGISTRY: docker.io
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        jobs:
+          - { goos: darwin, goarch: arm64, output: arm64 }
+          - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible }
+          - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
+
+          - { goos: linux, goarch: '386', output: '386' }
+          - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test }
+          - { goos: linux, goarch: amd64, goamd64: v3, output: amd64 }
+          - { goos: linux, goarch: arm64, output: arm64 }
+          - { goos: linux, goarch: arm, goarm: '5', output: armv5 }
+          - { goos: linux, goarch: arm, goarm: '6', output: armv6 }
+          - { goos: linux, goarch: arm, goarm: '7', output: armv7 }
+          - { goos: linux, goarch: mips, mips: hardfloat, output: mips-hardfloat }
+          - { goos: linux, goarch: mips, mips: softfloat, output: mips-softfloat }
+          - { goos: linux, goarch: mipsle, mips: hardfloat, output: mipsle-hardfloat }
+          - { goos: linux, goarch: mipsle, mips: softfloat, output: mipsle-softfloat }
+          - { goos: linux, goarch: mips64, output: mips64 }
+          - { goos: linux, goarch: mips64le, output: mips64le }
+          - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' }
+          - { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2' }
+          - { goos: linux, goarch: riscv64, output: riscv64 }
+          - { goos: linux, goarch: s390x, output: s390x }
+
+          - { goos: windows, goarch: '386', output: '386' }
+          - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible }
+          - { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
+          - { goos: windows, goarch: arm, goarm: '7', output: armv7 }
+          - { goos: windows, goarch: arm64, output: arm64 }
+
+          - { goos: freebsd, goarch: '386', output: '386' }
+          - { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible }
+          - { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 }
+          - { goos: freebsd, goarch: arm64, output: arm64 }
+
+          - { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' }
+          - { goos: android, goarch: amd64, ndk: x86_64-linux-android34, output: amd64 }
+          - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
+          - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
+
+          # Go 1.21 can revert commit `9e4385` to work on Windows 7
+          # https://github.com/golang/go/issues/64622#issuecomment-1847475161
+          # (OR we can just use golang1.21.4 which unneeded any patch)
+          - { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' }
+          - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go121, goversion: '1.21' }
+          - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go121, goversion: '1.21' }
+
+          # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
+          - { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' }
+          - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
+          - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
+
+          # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
+          - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' }
+          - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
+          - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
+
+          # only for test
+          - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' }
+          - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test }
+          - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - name: Set up Go
+      if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }}
+      uses: actions/setup-go@v5
+      with:
+        go-version: '1.22'
+
+    - name: Set up Go
+      if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }}
+      uses: actions/setup-go@v5
+      with:
+        go-version: ${{ matrix.jobs.goversion }}
+
+    - name: Set up Go1.22 loongarch abi1
+      if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
+      run: |
+        wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.22.0/go1.22.0.linux-amd64-abi1.tar.gz
+        sudo tar zxf go1.22.0.linux-amd64-abi1.tar.gz -C /usr/local
+        echo "/usr/local/go/bin" >> $GITHUB_PATH
+
+    - name: Set up Go1.22 loongarch abi2
+      if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }}
+      run: |
+        wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.22.0/go1.22.0.linux-amd64-abi2.tar.gz
+        sudo tar zxf go1.22.0.linux-amd64-abi2.tar.gz -C /usr/local
+        echo "/usr/local/go/bin" >> $GITHUB_PATH
+
+      # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
+      # this patch file only works on golang1.22.x
+      # that means after golang1.23 release it must be changed
+      # revert:
+      # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
+      # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
+      # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
+    - name: Revert Golang1.22 commit for Windows7/8
+      if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
+      run: |
+        cd $(go env GOROOT)
+        patch --verbose -R -p 1 < $GITHUB_WORKSPACE/.github/patch_go122/693def151adff1af707d82d28f55dba81ceb08e1.diff
+        patch --verbose -R -p 1 < $GITHUB_WORKSPACE/.github/patch_go122/7c1157f9544922e96945196b47b95664b1e39108.diff
+        patch --verbose -R -p 1 < $GITHUB_WORKSPACE/.github/patch_go122/48042aa09c2f878c4faa576948b07fe625c4707a.diff
+
+      # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
+    - name: Revert Golang1.21 commit for Windows7/8
+      if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.21' }}
+      run: |
+        cd $(go env GOROOT)
+        curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
+
+    - name: Set variables
+      if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
+      run: echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
+      shell: bash
+
+    - name: Set variables
+      if: ${{ github.event_name != 'workflow_dispatch' && github.ref_name == 'Alpha' }}
+      run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
+      shell: bash
+
+    - name: Set Time Variable
+      run: |
+        echo "BUILDTIME=$(date)" >> $GITHUB_ENV
+        echo "CGO_ENABLED=0" >> $GITHUB_ENV
+        echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV
+
+    - name: Setup NDK
+      if: ${{ matrix.jobs.goos == 'android' }}
+      uses: nttld/setup-ndk@v1
+      id: setup-ndk
+      with:
+        ndk-version: r26c
+
+    - name: Set NDK path
+      if: ${{ matrix.jobs.goos == 'android' }}
+      run: |
+        echo "CC=${{steps.setup-ndk.outputs.ndk-path}}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{matrix.jobs.ndk}}-clang" >> $GITHUB_ENV
+        echo "CGO_ENABLED=1" >> $GITHUB_ENV
+        echo "BUILDTAG=" >> $GITHUB_ENV
+
+    - name: Test
+      if: ${{ matrix.jobs.test == 'test' }}
+      run: |
+        go test ./...
+
+    - name: Update CA
+      run: |
+        sudo apt-get install ca-certificates
+        sudo update-ca-certificates
+        cp -f /etc/ssl/certs/ca-certificates.crt component/ca/ca-certificates.crt
+
+    - name: Build core
+      env:
+        GOOS: ${{matrix.jobs.goos}}
+        GOARCH: ${{matrix.jobs.goarch}}
+        GOAMD64: ${{matrix.jobs.goamd64}}
+        GOARM: ${{matrix.jobs.arm}}
+        GOMIPS: ${{matrix.jobs.mips}}
+      run: |
+        echo $CGO_ENABLED
+        go build -v -tags "with_gvisor" -trimpath -ldflags "${BUILDTAG} -X 'github.com/metacubex/mihomo/constant.Version=${VERSION}' -X 'github.com/metacubex/mihomo/constant.BuildTime=${BUILDTIME}' -w -s -buildid="
+        if [ "${{matrix.jobs.goos}}" = "windows" ]; then
+          cp mihomo.exe mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe
+          zip -r mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.zip mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}.exe
+        else
+          cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}
+          gzip -c mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}} > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.gz
+          rm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}
+        fi
+
+    - name: Create DEB package
+      if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }}
+      run: |
+        sudo apt-get install dpkg
+        if [ "${{matrix.jobs.abi}}" = "1" ]; then
+          ARCH=loongarch64
+        elif [ "${{matrix.jobs.goarm}}" = "7" ]; then
+          ARCH=armhf
+        elif [ "${{matrix.jobs.goarch}}" = "arm" ]; then
+          ARCH=armel
+        else
+          ARCH=${{matrix.jobs.goarch}}
+        fi
+        PackageVersion=$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$' | sed 's/v//g' )
+        if [ $(git branch | awk -F ' ' '{print $2}') = "Alpha" ]; then
+          PackageVersion="$(echo "${PackageVersion}" | awk -F '.' '{$NF = $NF + 1; print}' OFS='.')-${VERSION}"
+        fi
+
+        mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN
+        mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin
+        mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo
+        mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/
+        mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo
+
+        cp mihomo mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin/mihomo
+        cp LICENSE mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/share/licenses/mihomo/
+        cp .github/mihomo.service mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/systemd/system/
+
+        cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo/config.yaml <<EOF
+        mixed-port: 7890
+        external-controller: 127.0.0.1:9090
+        EOF
+
+        cat > mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN/control <<EOF
+        Package: mihomo
+        Version: ${PackageVersion}
+        Section:
+        Priority: extra
+        Architecture: ${ARCH}
+        Maintainer: MetaCubeX <none@example.com>
+        Homepage: https://wiki.metacubex.one/
+        Description: The universal proxy platform.
+        EOF
+
+        dpkg-deb -Z gzip --build mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}
+
+    - name: Convert DEB to RPM
+      if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }}
+      run: |
+        sudo apt-get install -y alien
+        alien --to-rpm --scripts mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb
+        mv mihomo*.rpm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm
+
+    # - name: Convert DEB to PKG
+    #   if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') && !contains(matrix.jobs.goarch, 'loong64') }}
+    #   run: |
+    #     docker pull archlinux
+    #     docker run --rm -v ./:/mnt archlinux bash -c "
+    #       pacman -Syu pkgfile base-devel --noconfirm
+    #       curl -L https://github.com/helixarch/debtap/raw/master/debtap > /usr/bin/debtap
+    #       chmod 755 /usr/bin/debtap
+    #       debtap -u 
+    #       debtap -Q /mnt/mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb
+    #     "
+    #     mv mihomo*.pkg.tar.zst mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst
+
+    - name: Save version
+      run: |
+        echo ${VERSION} > version.txt
+      shell: bash
+
+    - name: Archive production artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: "${{ matrix.jobs.goos }}-${{ matrix.jobs.output }}"
+        path: |
+          mihomo*.gz
+          mihomo*.deb
+          mihomo*.rpm
+          mihomo*.zip
+          version.txt
+
+  Upload-Prerelease:
+    permissions: write-all
+    if: ${{ github.event_name != 'workflow_dispatch' && github.ref_type == 'branch' && !startsWith(github.event_name, 'pull_request') }}
+    needs: [build]
+    runs-on: ubuntu-latest
+    steps:
+    - name: Download all workflow run artifacts
+      uses: actions/download-artifact@v4
+      with:
+        path: bin/
+        merge-multiple: true
+
+    - name: Delete current release assets
+      uses: 8Mi-Tech/delete-release-assets-action@main
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        tag: Prerelease-${{ github.ref_name }}
+        deleteOnlyFromDrafts: false
+    - name: Set Env
+      run: |
+        echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
+      shell: bash
+
+    - name: Tag Repo
+      uses: richardsimko/update-tag@v1
+      with:
+        tag_name: Prerelease-${{ github.ref_name }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      
+    - run: |
+        cat > release.txt << 'EOF'
+        Release created at  ${{ env.BUILDTIME }}
+        Synchronize ${{ github.ref_name }} branch code updates, keeping only the latest version
+        <br>
+        [我应该下载哪个文件? / Which file should I download?](https://github.com/MetaCubeX/mihomo/wiki/FAQ)
+        [二进制文件筛选 / Binary file selector](https://metacubex.github.io/Meta-Docs/startup/#_1)
+        [查看文档 / Docs](https://metacubex.github.io/Meta-Docs/)
+        EOF
+
+    - name: Upload Prerelease
+      uses: softprops/action-gh-release@v1
+      if: ${{  success() }}
+      with:
+        tag_name: Prerelease-${{ github.ref_name }}
+        files: |
+          bin/*
+        prerelease: true
+        generate_release_notes: true
+        body_path: release.txt
+
+  Upload-Release:
+    permissions: write-all
+    if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
+    needs: [build]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          ref: Meta
+          fetch-depth: '0'
+          fetch-tags: 'true'
+
+      - name: Get tags
+        run: |
+          echo "CURRENTVERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
+          git fetch --tags
+          echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD)" >> $GITHUB_ENV
+
+      - name: Merge Alpha branch into Meta
+        run: |
+          git config --global user.email "github-actions[bot]@users.noreply.github.com"
+          git config --global user.name "github-actions[bot]"
+          git fetch origin Alpha:Alpha
+          git merge Alpha
+          git push origin Meta
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Tag the commit
+        run: |
+          git tag ${{ github.event.inputs.version }}
+          git push origin ${{ github.event.inputs.version }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Generate release notes
+        run: |
+            cp ./.github/genReleaseNote.sh ./
+            bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION}
+            rm ./genReleaseNote.sh
+  
+      - uses: actions/download-artifact@v4
+        with:
+          path: bin/
+          merge-multiple: true
+  
+      - name: Display structure of downloaded files
+        run: ls -R
+        working-directory: bin
+  
+      - name: Upload Release
+        uses: softprops/action-gh-release@v2
+        if: ${{ success() }}
+        with:
+          tag_name: ${{ github.event.inputs.version }}
+          files: bin/*
+          body_path: release.md
+
+  Docker:
+    if: ${{ !startsWith(github.event_name, 'pull_request') }}
+    permissions: write-all
+    needs: [build]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - uses: actions/download-artifact@v4
+        with:
+          path: bin/
+          merge-multiple: true
+
+      - name: Display structure of downloaded files
+        run: ls -R
+        working-directory: bin
+
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Setup Docker buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          version: latest
+      
+      # Extract metadata (tags, labels) for Docker
+      # https://github.com/docker/metadata-action
+      - name: Extract Docker metadata
+        if: ${{ github.event_name != 'workflow_dispatch' }}
+        id: meta_alpha
+        uses: docker/metadata-action@v5
+        with:
+          images: '${{ env.REGISTRY }}/${{ github.repository }}'
+      
+      # Extract metadata (tags, labels) for Docker
+      # https://github.com/docker/metadata-action
+      - name: Extract Docker metadata
+        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
+        id: meta_release
+        uses: docker/metadata-action@v5
+        with:
+          images: '${{ env.REGISTRY }}/${{ github.repository }}'
+          tags: |
+            ${{ github.event.inputs.version }}
+          flavor: |
+            latest=true
+          labels: org.opencontainers.image.version=${{ github.event.inputs.version }}
+      
+      - name: Show files
+        run: |
+          ls .
+          ls bin/
+      
+      - name: login to docker REGISTRY
+        uses: docker/login-action@v3
+        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
+        if: ${{ github.event_name != 'workflow_dispatch' }}
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          file: ./Dockerfile
+          push: ${{ github.event_name != 'pull_request' }}
+          platforms: |
+            linux/386
+            linux/amd64
+            linux/arm64
+            linux/arm/v7
+          tags: ${{ steps.meta_alpha.outputs.tags }}
+          labels: ${{ steps.meta_alpha.outputs.labels }}
+      
+      - name: Build and push Docker image
+        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }}
+        uses: docker/build-push-action@v5
+        with:
+          context: .
+          file: ./Dockerfile
+          push: ${{ github.event_name != 'pull_request' }}
+          platforms: |
+            linux/386
+            linux/amd64
+            linux/arm64
+            linux/arm/v7
+          tags: ${{ steps.meta_release.outputs.tags }}
+          labels: ${{ steps.meta_release.outputs.labels }}

+ 33 - 0
core/Clash.Meta/.github/workflows/trigger-cmfa-update.yml

@@ -0,0 +1,33 @@
+name: Trigger CMFA Update
+on:
+  workflow_dispatch:
+  push:
+    paths-ignore:
+      - "docs/**"
+      - "README.md"
+      - ".github/ISSUE_TEMPLATE/**"
+    branches:
+      - Alpha
+    tags:
+      - "v*"
+  pull_request_target:
+    branches:
+      - Alpha
+      
+jobs:
+  # Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies
+  trigger-CMFA-update:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: tibdex/github-app-token@v1
+        id: generate-token
+        with:
+          app_id: ${{ secrets.MAINTAINER_APPID }}
+          private_key: ${{ secrets.MAINTAINER_APP_PRIVATE_KEY }}
+      
+      - name: Trigger update-dependencies
+        run: |
+          curl -X POST https://api.github.com/repos/MetaCubeX/ClashMetaForAndroid/dispatches \
+            -H "Accept: application/vnd.github.everest-preview+json" \
+            -H "Authorization: token ${{ steps.generate-token.outputs.token }}" \
+            -d '{"event_type": "core-updated"}'

+ 28 - 0
core/Clash.Meta/.gitignore

@@ -0,0 +1,28 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin/*
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# go mod vendor
+vendor
+
+# GoLand
+.idea/*
+
+# macOS file
+.DS_Store
+
+# test suite
+test/config/cache*
+/output
+.vscode/
+.fleet/

+ 17 - 0
core/Clash.Meta/.golangci.yaml

@@ -0,0 +1,17 @@
+linters:
+  disable-all: true
+  enable:
+    - gofumpt
+    - staticcheck
+    - govet
+    - gci
+
+linters-settings:
+  gci:
+    custom-order: true
+    sections:
+      - standard
+      - prefix(github.com/metacubex/mihomo)
+      - default
+  staticcheck:
+    go: '1.19'

+ 27 - 0
core/Clash.Meta/Dockerfile

@@ -0,0 +1,27 @@
+FROM alpine:latest as builder
+ARG TARGETPLATFORM
+RUN echo "I'm building for $TARGETPLATFORM"
+
+RUN apk add --no-cache gzip && \
+    mkdir /mihomo-config && \
+    wget -O /mihomo-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \
+    wget -O /mihomo-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \
+    wget -O /mihomo-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat
+
+COPY docker/file-name.sh /mihomo/file-name.sh
+WORKDIR /mihomo
+COPY bin/ bin/
+RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \
+    FILE_NAME=`ls bin/ | egrep "$FILE_NAME.gz"|awk NR==1` && echo $FILE_NAME && \
+    mv bin/$FILE_NAME mihomo.gz && gzip -d mihomo.gz && echo "$FILE_NAME" > /mihomo-config/test
+FROM alpine:latest
+LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo"
+
+RUN apk add --no-cache ca-certificates tzdata iptables
+
+VOLUME ["/root/.config/mihomo/"]
+
+COPY --from=builder /mihomo-config/ /root/.config/mihomo/
+COPY --from=builder /mihomo/mihomo /mihomo
+RUN chmod +x /mihomo
+ENTRYPOINT [ "/mihomo" ]

+ 674 - 0
core/Clash.Meta/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 169 - 0
core/Clash.Meta/Makefile

@@ -0,0 +1,169 @@
+NAME=mihomo
+BINDIR=bin
+BRANCH=$(shell git branch --show-current)
+ifeq ($(BRANCH),Alpha)
+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)
+GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/metacubex/mihomo/constant.Version=$(VERSION)" \
+		-X "github.com/metacubex/mihomo/constant.BuildTime=$(BUILDTIME)" \
+		-w -s -buildid='
+
+PLATFORM_LIST = \
+	darwin-amd64-compatible \
+	darwin-amd64 \
+	darwin-arm64 \
+	linux-amd64-compatible \
+	linux-amd64 \
+	linux-armv5 \
+	linux-armv6 \
+	linux-armv7 \
+	linux-arm64 \
+	linux-mips64 \
+	linux-mips64le \
+	linux-mips-softfloat \
+	linux-mips-hardfloat \
+	linux-mipsle-softfloat \
+	linux-mipsle-hardfloat \
+	linux-riscv64 \
+	linux-loong64 \
+	android-arm64 \
+	freebsd-386 \
+	freebsd-amd64 \
+	freebsd-arm64
+
+WINDOWS_ARCH_LIST = \
+	windows-386 \
+	windows-amd64-compatible \
+	windows-amd64 \
+	windows-arm64 \
+    windows-arm32v7
+
+all:linux-amd64 linux-arm64\
+	darwin-amd64 darwin-arm64\
+ 	windows-amd64 windows-arm64\
+
+
+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)-$@
+
+darwin-amd64-compatible:
+	GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+darwin-arm64:
+	GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-386:
+	GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-amd64:
+	GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-amd64-compatible:
+	GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-arm64:
+	GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-armv5:
+	GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-armv6:
+	GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-armv7:
+	GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mips-softfloat:
+	GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mips-hardfloat:
+	GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mipsle-softfloat:
+	GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mipsle-hardfloat:
+	GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mips64:
+	GOARCH=mips64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-mips64le:
+	GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+linux-riscv64:
+	GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+	
+linux-loong64:
+	GOARCH=loong64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+android-arm64:
+	GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+freebsd-386:
+	GOARCH=386 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+freebsd-amd64:
+	GOARCH=amd64 GOOS=freebsd GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+freebsd-arm64:
+	GOARCH=arm64 GOOS=freebsd $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
+
+windows-386:
+	GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+windows-amd64:
+	GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+windows-amd64-compatible:
+	GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+windows-arm64:
+	GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+windows-arm32v7:
+	GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
+
+gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
+zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
+
+$(gz_releases): %.gz : %
+	chmod +x $(BINDIR)/$(NAME)-$(basename $@)
+	gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
+
+$(zip_releases): %.zip : %
+	zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
+
+all-arch: $(PLATFORM_LIST) $(WINDOWS_ARCH_LIST)
+
+releases: $(gz_releases) $(zip_releases)
+
+vet:
+	go test ./...
+
+lint:
+	golangci-lint run ./...
+
+clean:
+	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 ./...

BIN
core/Clash.Meta/Meta.png


+ 100 - 0
core/Clash.Meta/README.md

@@ -0,0 +1,100 @@
+<h1 align="center">
+  <img src="Meta.png" alt="Meta Kennel" width="200">
+  <br>Meta Kernel<br>
+</h1>
+
+<h3 align="center">Another Mihomo Kernel.</h3>
+
+<p align="center">
+  <a href="https://goreportcard.com/report/github.com/MetaCubeX/mihomo">
+    <img src="https://goreportcard.com/badge/github.com/MetaCubeX/mihomo?style=flat-square">
+  </a>
+  <img src="https://img.shields.io/github/go-mod/go-version/MetaCubeX/mihomo/Alpha?style=flat-square">
+  <a href="https://github.com/MetaCubeX/mihomo/releases">
+    <img src="https://img.shields.io/github/release/MetaCubeX/mihomo/all.svg?style=flat-square">
+  </a>
+  <a href="https://github.com/MetaCubeX/mihomo">
+    <img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square">
+  </a>
+</p>
+
+## Features
+
+- Local HTTP/HTTPS/SOCKS server with authentication support
+- VMess, VLESS, Shadowsocks, Trojan, Snell, TUIC, Hysteria protocol support
+- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP.
+- Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
+- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node
+  based off latency
+- Remote providers, allowing users to get node lists remotely instead of hard-coding in config
+- Netfilter TCP redirecting. Deploy Mihomo on your Internet gateway with `iptables`.
+- Comprehensive HTTP RESTful API controller
+
+## Dashboard
+
+A web dashboard with first-class support for this project has been created; it can be checked out at [metacubexd](https://github.com/MetaCubeX/metacubexd).
+
+## Configration example
+
+Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml).
+
+## Docs
+
+Documentation can be found in [mihomo Docs](https://wiki.metacubex.one/).
+
+## For development
+
+Requirements:
+[Go 1.20 or newer](https://go.dev/dl/)
+
+Build mihomo:
+
+```shell
+git clone https://github.com/MetaCubeX/mihomo.git
+cd mihomo && go mod download
+go build
+```
+
+Set go proxy if a connection to GitHub is not possible:
+
+```shell
+go env -w GOPROXY=https://goproxy.io,direct
+```
+
+Build with gvisor tun stack:
+
+```shell
+go build -tags with_gvisor
+```
+
+### IPTABLES configuration
+
+Work on Linux OS which supported `iptables`
+
+```yaml
+# Enable the TPROXY listener
+tproxy-port: 9898
+
+iptables:
+  enable: true # default is false
+  inbound-interface: eth0 # detect the inbound interface, default is 'lo'
+```
+
+## Debugging
+
+Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug
+API.
+
+## Credits
+
+- [Dreamacro/clash](https://github.com/Dreamacro/clash)
+- [SagerNet/sing-box](https://github.com/SagerNet/sing-box)
+- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
+- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
+- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
+- [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
+
+## License
+
+This software is released under the GPL-3.0 license.
+

+ 306 - 0
core/Clash.Meta/adapter/adapter.go

@@ -0,0 +1,306 @@
+package adapter
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"net/netip"
+	"net/url"
+	"strconv"
+	"time"
+
+	"github.com/metacubex/mihomo/common/atomic"
+	"github.com/metacubex/mihomo/common/queue"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/puzpuzpuz/xsync/v3"
+)
+
+var UnifiedDelay = atomic.NewBool(false)
+
+const (
+	defaultHistoriesNum = 10
+)
+
+type internalProxyState struct {
+	alive   atomic.Bool
+	history *queue.Queue[C.DelayHistory]
+}
+
+type Proxy struct {
+	C.ProxyAdapter
+	alive   atomic.Bool
+	history *queue.Queue[C.DelayHistory]
+	extra   *xsync.MapOf[string, *internalProxyState]
+}
+
+// AliveForTestUrl implements C.Proxy
+func (p *Proxy) AliveForTestUrl(url string) bool {
+	if state, ok := p.extra.Load(url); ok {
+		return state.alive.Load()
+	}
+
+	return p.alive.Load()
+}
+
+// Dial implements C.Proxy
+func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
+	defer cancel()
+	return p.DialContext(ctx, metadata)
+}
+
+// DialContext implements C.ProxyAdapter
+func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
+	return conn, err
+}
+
+// DialUDP implements C.ProxyAdapter
+func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), C.DefaultUDPTimeout)
+	defer cancel()
+	return p.ListenPacketContext(ctx, metadata)
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
+	return pc, err
+}
+
+// DelayHistory implements C.Proxy
+func (p *Proxy) DelayHistory() []C.DelayHistory {
+	queueM := p.history.Copy()
+	histories := []C.DelayHistory{}
+	for _, item := range queueM {
+		histories = append(histories, item)
+	}
+	return histories
+}
+
+// DelayHistoryForTestUrl implements C.Proxy
+func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
+	var queueM []C.DelayHistory
+
+	if state, ok := p.extra.Load(url); ok {
+		queueM = state.history.Copy()
+	}
+	histories := []C.DelayHistory{}
+	for _, item := range queueM {
+		histories = append(histories, item)
+	}
+	return histories
+}
+
+// ExtraDelayHistories return all delay histories for each test URL
+// implements C.Proxy
+func (p *Proxy) ExtraDelayHistories() map[string]C.ProxyState {
+	histories := map[string]C.ProxyState{}
+
+	p.extra.Range(func(k string, v *internalProxyState) bool {
+		testUrl := k
+		state := v
+
+		queueM := state.history.Copy()
+		var history []C.DelayHistory
+
+		for _, item := range queueM {
+			history = append(history, item)
+		}
+
+		histories[testUrl] = C.ProxyState{
+			Alive:   state.alive.Load(),
+			History: history,
+		}
+		return true
+	})
+	return histories
+}
+
+// LastDelayForTestUrl return last history record of the specified URL. if proxy is not alive, return the max value of uint16.
+// implements C.Proxy
+func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
+	var maxDelay uint16 = 0xffff
+
+	alive := false
+	var history C.DelayHistory
+
+	if state, ok := p.extra.Load(url); ok {
+		alive = state.alive.Load()
+		history = state.history.Last()
+	}
+
+	if !alive || history.Delay == 0 {
+		return maxDelay
+	}
+	return history.Delay
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (p *Proxy) MarshalJSON() ([]byte, error) {
+	inner, err := p.ProxyAdapter.MarshalJSON()
+	if err != nil {
+		return inner, err
+	}
+
+	mapping := map[string]any{}
+	_ = json.Unmarshal(inner, &mapping)
+	mapping["history"] = p.DelayHistory()
+	mapping["extra"] = p.ExtraDelayHistories()
+	mapping["alive"] = p.alive.Load()
+	mapping["name"] = p.Name()
+	mapping["udp"] = p.SupportUDP()
+	mapping["xudp"] = p.SupportXUDP()
+	mapping["tfo"] = p.SupportTFO()
+	return json.Marshal(mapping)
+}
+
+// URLTest get the delay for the specified URL
+// implements C.Proxy
+func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (t uint16, err error) {
+	var satisfied bool
+
+	defer func() {
+		alive := err == nil
+		record := C.DelayHistory{Time: time.Now()}
+		if alive {
+			record.Delay = t
+		}
+
+		p.alive.Store(alive)
+		p.history.Put(record)
+		if p.history.Len() > defaultHistoriesNum {
+			p.history.Pop()
+		}
+
+		state, ok := p.extra.Load(url)
+		if !ok {
+			state = &internalProxyState{
+				history: queue.New[C.DelayHistory](defaultHistoriesNum),
+				alive:   atomic.NewBool(true),
+			}
+			p.extra.Store(url, state)
+		}
+
+		if !satisfied {
+			record.Delay = 0
+			alive = false
+		}
+
+		state.alive.Store(alive)
+		state.history.Put(record)
+		if state.history.Len() > defaultHistoriesNum {
+			state.history.Pop()
+		}
+
+	}()
+
+	unifiedDelay := UnifiedDelay.Load()
+
+	addr, err := urlToMetadata(url)
+	if err != nil {
+		return
+	}
+
+	start := time.Now()
+	instance, err := p.DialContext(ctx, &addr)
+	if err != nil {
+		return
+	}
+	defer func() {
+		_ = instance.Close()
+	}()
+
+	req, err := http.NewRequest(http.MethodHead, url, nil)
+	if err != nil {
+		return
+	}
+	req = req.WithContext(ctx)
+
+	transport := &http.Transport{
+		DialContext: func(context.Context, string, string) (net.Conn, error) {
+			return instance, nil
+		},
+		// from http.DefaultTransport
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+		TLSClientConfig:       ca.GetGlobalTLSConfig(&tls.Config{}),
+	}
+
+	client := http.Client{
+		Timeout:   30 * time.Second,
+		Transport: transport,
+		CheckRedirect: func(req *http.Request, via []*http.Request) error {
+			return http.ErrUseLastResponse
+		},
+	}
+
+	defer client.CloseIdleConnections()
+
+	resp, err := client.Do(req)
+
+	if err != nil {
+		return
+	}
+
+	_ = resp.Body.Close()
+
+	if unifiedDelay {
+		second := time.Now()
+		resp, err = client.Do(req)
+		if err == nil {
+			_ = resp.Body.Close()
+			start = second
+		}
+	}
+
+	satisfied = resp != nil && (expectedStatus == nil || expectedStatus.Check(uint16(resp.StatusCode)))
+	t = uint16(time.Since(start) / time.Millisecond)
+	return
+}
+func NewProxy(adapter C.ProxyAdapter) *Proxy {
+	return &Proxy{
+		ProxyAdapter: adapter,
+		history:      queue.New[C.DelayHistory](defaultHistoriesNum),
+		alive:        atomic.NewBool(true),
+		extra:        xsync.NewMapOf[string, *internalProxyState]()}
+}
+
+func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
+	u, err := url.Parse(rawURL)
+	if err != nil {
+		return
+	}
+
+	port := u.Port()
+	if port == "" {
+		switch u.Scheme {
+		case "https":
+			port = "443"
+		case "http":
+			port = "80"
+		default:
+			err = fmt.Errorf("%s scheme not Support", rawURL)
+			return
+		}
+	}
+	uintPort, err := strconv.ParseUint(port, 10, 16)
+	if err != nil {
+		return
+	}
+
+	addr = C.Metadata{
+		Host:    u.Hostname(),
+		DstIP:   netip.Addr{},
+		DstPort: uint16(uintPort),
+	}
+	return
+}

+ 73 - 0
core/Clash.Meta/adapter/inbound/addition.go

@@ -0,0 +1,73 @@
+package inbound
+
+import (
+	"net"
+
+	C "github.com/metacubex/mihomo/constant"
+)
+
+type Addition func(metadata *C.Metadata)
+
+func ApplyAdditions(metadata *C.Metadata, additions ...Addition) {
+	for _, addition := range additions {
+		addition(metadata)
+	}
+}
+
+func WithInName(name string) Addition {
+	return func(metadata *C.Metadata) {
+		metadata.InName = name
+	}
+}
+
+func WithInUser(user string) Addition {
+	return func(metadata *C.Metadata) {
+		metadata.InUser = user
+	}
+}
+
+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
+	}
+}
+
+func WithDstAddr(addr net.Addr) Addition {
+	return func(metadata *C.Metadata) {
+		_ = metadata.SetRemoteAddr(addr)
+	}
+}
+
+func WithSrcAddr(addr net.Addr) Addition {
+	return func(metadata *C.Metadata) {
+		m := C.Metadata{}
+		if err := m.SetRemoteAddr(addr); err == nil {
+			metadata.SrcIP = m.DstIP
+			metadata.SrcPort = m.DstPort
+		}
+	}
+}
+
+func WithInAddr(addr net.Addr) Addition {
+	return func(metadata *C.Metadata) {
+		m := C.Metadata{}
+		if err := m.SetRemoteAddr(addr); err == nil {
+			metadata.InIP = m.DstIP
+			metadata.InPort = m.DstPort
+		}
+	}
+}
+
+func WithDSCP(dscp uint8) Addition {
+	return func(metadata *C.Metadata) {
+		metadata.DSCP = dscp
+	}
+}
+
+func Placeholder(metadata *C.Metadata) {}

+ 45 - 0
core/Clash.Meta/adapter/inbound/auth.go

@@ -0,0 +1,45 @@
+package inbound
+
+import (
+	"net"
+	"net/netip"
+
+	C "github.com/metacubex/mihomo/constant"
+)
+
+var skipAuthPrefixes []netip.Prefix
+
+func SetSkipAuthPrefixes(prefixes []netip.Prefix) {
+	skipAuthPrefixes = prefixes
+}
+
+func SkipAuthPrefixes() []netip.Prefix {
+	return skipAuthPrefixes
+}
+
+func SkipAuthRemoteAddr(addr net.Addr) bool {
+	m := C.Metadata{}
+	if err := m.SetRemoteAddr(addr); err != nil {
+		return false
+	}
+	return skipAuth(m.AddrPort().Addr())
+}
+
+func SkipAuthRemoteAddress(addr string) bool {
+	m := C.Metadata{}
+	if err := m.SetRemoteAddress(addr); err != nil {
+		return false
+	}
+	return skipAuth(m.AddrPort().Addr())
+}
+
+func skipAuth(addr netip.Addr) bool {
+	if addr.IsValid() {
+		for _, prefix := range skipAuthPrefixes {
+			if prefix.Contains(addr.Unmap()) {
+				return true
+			}
+		}
+	}
+	return false
+}

+ 20 - 0
core/Clash.Meta/adapter/inbound/http.go

@@ -0,0 +1,20 @@
+package inbound
+
+import (
+	"net"
+
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+// NewHTTP receive normal http request and return HTTPContext
+func NewHTTP(target socks5.Addr, srcConn net.Conn, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
+	metadata := parseSocksAddr(target)
+	metadata.NetWork = C.TCP
+	metadata.Type = C.HTTP
+	metadata.RawSrcAddr = srcConn.RemoteAddr()
+	metadata.RawDstAddr = srcConn.LocalAddr()
+	ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(srcConn.LocalAddr()))
+	ApplyAdditions(metadata, additions...)
+	return conn, metadata
+}

+ 17 - 0
core/Clash.Meta/adapter/inbound/https.go

@@ -0,0 +1,17 @@
+package inbound
+
+import (
+	"net"
+	"net/http"
+
+	C "github.com/metacubex/mihomo/constant"
+)
+
+// NewHTTPS receive CONNECT request and return ConnContext
+func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) {
+	metadata := parseHTTPAddr(request)
+	metadata.Type = C.HTTPS
+	ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
+	ApplyAdditions(metadata, additions...)
+	return conn, metadata
+}

+ 57 - 0
core/Clash.Meta/adapter/inbound/ipfilter.go

@@ -0,0 +1,57 @@
+package inbound
+
+import (
+	"net"
+	"net/netip"
+
+	C "github.com/metacubex/mihomo/constant"
+)
+
+var lanAllowedIPs []netip.Prefix
+var lanDisAllowedIPs []netip.Prefix
+
+func SetAllowedIPs(prefixes []netip.Prefix) {
+	lanAllowedIPs = prefixes
+}
+
+func SetDisAllowedIPs(prefixes []netip.Prefix) {
+	lanDisAllowedIPs = prefixes
+}
+
+func AllowedIPs() []netip.Prefix {
+	return lanAllowedIPs
+}
+
+func DisAllowedIPs() []netip.Prefix {
+	return lanDisAllowedIPs
+}
+
+func IsRemoteAddrDisAllowed(addr net.Addr) bool {
+	m := C.Metadata{}
+	if err := m.SetRemoteAddr(addr); err != nil {
+		return false
+	}
+	return isAllowed(m.AddrPort().Addr().Unmap()) && !isDisAllowed(m.AddrPort().Addr().Unmap())
+}
+
+func isAllowed(addr netip.Addr) bool {
+	if addr.IsValid() {
+		for _, prefix := range lanAllowedIPs {
+			if prefix.Contains(addr) {
+				return true
+			}
+		}
+	}
+	return false
+}
+
+func isDisAllowed(addr netip.Addr) bool {
+	if addr.IsValid() {
+		for _, prefix := range lanDisAllowedIPs {
+			if prefix.Contains(addr) {
+				return true
+			}
+		}
+	}
+	return false
+}

+ 18 - 0
core/Clash.Meta/adapter/inbound/listen.go

@@ -0,0 +1,18 @@
+package inbound
+
+import (
+	"context"
+	"net"
+)
+
+func SetMPTCP(open bool) {
+	setMultiPathTCP(getListenConfig(), 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)
+}

+ 23 - 0
core/Clash.Meta/adapter/inbound/listen_unix.go

@@ -0,0 +1,23 @@
+//go:build unix
+
+package inbound
+
+import (
+	"net"
+
+	"github.com/metacubex/tfo-go"
+)
+
+var (
+	lc = tfo.ListenConfig{
+		DisableTFO: true,
+	}
+)
+
+func SetTfo(open bool) {
+	lc.DisableTFO = !open
+}
+
+func getListenConfig() *net.ListenConfig {
+	return &lc.ListenConfig
+}

+ 15 - 0
core/Clash.Meta/adapter/inbound/listen_windows.go

@@ -0,0 +1,15 @@
+package inbound
+
+import (
+	"net"
+)
+
+var (
+	lc = net.ListenConfig{}
+)
+
+func SetTfo(open bool) {}
+
+func getListenConfig() *net.ListenConfig {
+	return &lc
+}

+ 10 - 0
core/Clash.Meta/adapter/inbound/mptcp_go120.go

@@ -0,0 +1,10 @@
+//go:build !go1.21
+
+package inbound
+
+import "net"
+
+const multipathTCPAvailable = false
+
+func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
+}

+ 11 - 0
core/Clash.Meta/adapter/inbound/mptcp_go121.go

@@ -0,0 +1,11 @@
+//go:build go1.21
+
+package inbound
+
+import "net"
+
+const multipathTCPAvailable = true
+
+func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
+	listenConfig.SetMultipathTCP(open)
+}

+ 22 - 0
core/Clash.Meta/adapter/inbound/packet.go

@@ -0,0 +1,22 @@
+package inbound
+
+import (
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+// NewPacket is PacketAdapter generator
+func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) {
+	metadata := parseSocksAddr(target)
+	metadata.NetWork = C.UDP
+	metadata.Type = source
+	metadata.RawSrcAddr = packet.LocalAddr()
+	metadata.RawDstAddr = metadata.UDPAddr()
+	ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr()))
+	if p, ok := packet.(C.UDPPacketInAddr); ok {
+		ApplyAdditions(metadata, WithInAddr(p.InAddr()))
+	}
+	ApplyAdditions(metadata, additions...)
+
+	return packet, metadata
+}

+ 18 - 0
core/Clash.Meta/adapter/inbound/socket.go

@@ -0,0 +1,18 @@
+package inbound
+
+import (
+	"net"
+
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+// NewSocket receive TCP inbound and return ConnContext
+func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) {
+	metadata := parseSocksAddr(target)
+	metadata.NetWork = C.TCP
+	metadata.Type = source
+	ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
+	ApplyAdditions(metadata, additions...)
+	return conn, metadata
+}

+ 63 - 0
core/Clash.Meta/adapter/inbound/util.go

@@ -0,0 +1,63 @@
+package inbound
+
+import (
+	"net"
+	"net/http"
+	"net/netip"
+	"strconv"
+	"strings"
+
+	"github.com/metacubex/mihomo/common/nnip"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+func parseSocksAddr(target socks5.Addr) *C.Metadata {
+	metadata := &C.Metadata{}
+
+	switch target[0] {
+	case socks5.AtypDomainName:
+		// trim for FQDN
+		metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
+		metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
+	case socks5.AtypIPv4:
+		metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
+		metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
+	case socks5.AtypIPv6:
+		ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
+		metadata.DstIP = ip6.Unmap()
+		metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
+	}
+
+	return metadata
+}
+
+func parseHTTPAddr(request *http.Request) *C.Metadata {
+	host := request.URL.Hostname()
+	port := request.URL.Port()
+	if port == "" {
+		port = "80"
+	}
+
+	// trim FQDN (#737)
+	host = strings.TrimRight(host, ".")
+
+	var uint16Port uint16
+	if port, err := strconv.ParseUint(port, 10, 16); err == nil {
+		uint16Port = uint16(port)
+	}
+
+	metadata := &C.Metadata{
+		NetWork: C.TCP,
+		Host:    host,
+		DstIP:   netip.Addr{},
+		DstPort: uint16Port,
+	}
+
+	ip, err := netip.ParseAddr(host)
+	if err == nil {
+		metadata.DstIP = ip
+	}
+
+	return metadata
+}

+ 287 - 0
core/Clash.Meta/adapter/outbound/base.go

@@ -0,0 +1,287 @@
+package outbound
+
+import (
+	"context"
+	"encoding/json"
+	"net"
+	"strings"
+	"syscall"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+)
+
+type Base struct {
+	name   string
+	addr   string
+	iface  string
+	tp     C.AdapterType
+	udp    bool
+	xudp   bool
+	tfo    bool
+	mpTcp  bool
+	rmark  int
+	id     string
+	prefer C.DNSPrefer
+}
+
+// Name implements C.ProxyAdapter
+func (b *Base) Name() string {
+	return b.name
+}
+
+// Id implements C.ProxyAdapter
+func (b *Base) Id() string {
+	if b.id == "" {
+		b.id = utils.NewUUIDV6().String()
+	}
+
+	return b.id
+}
+
+// Type implements C.ProxyAdapter
+func (b *Base) Type() C.AdapterType {
+	return b.tp
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	return c, C.ErrNotSupport
+}
+
+func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	return nil, C.ErrNotSupport
+}
+
+// DialContextWithDialer implements C.ProxyAdapter
+func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
+	return nil, C.ErrNotSupport
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	return nil, C.ErrNotSupport
+}
+
+// ListenPacketWithDialer implements C.ProxyAdapter
+func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
+	return nil, C.ErrNotSupport
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (b *Base) SupportWithDialer() C.NetWork {
+	return C.InvalidNet
+}
+
+// SupportUOT implements C.ProxyAdapter
+func (b *Base) SupportUOT() bool {
+	return false
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (b *Base) SupportUDP() bool {
+	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
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (b *Base) IsL3Protocol(metadata *C.Metadata) bool {
+	return false
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (b *Base) MarshalJSON() ([]byte, error) {
+	return json.Marshal(map[string]string{
+		"type": b.Type().String(),
+		"id":   b.Id(),
+	})
+}
+
+// Addr implements C.ProxyAdapter
+func (b *Base) Addr() string {
+	return b.addr
+}
+
+// Unwrap implements C.ProxyAdapter
+func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	return nil
+}
+
+// DialOptions return []dialer.Option from struct
+func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
+	if b.iface != "" {
+		opts = append(opts, dialer.WithInterface(b.iface))
+	}
+
+	if b.rmark != 0 {
+		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))
+	}
+
+	if b.mpTcp {
+		opts = append(opts, dialer.WithMPTCP(true))
+	}
+
+	return opts
+}
+
+type BasicOption struct {
+	TFO         bool   `proxy:"tfo,omitempty" group:"tfo,omitempty"`
+	MPTCP       bool   `proxy:"mptcp,omitempty" group:"mptcp,omitempty"`
+	Interface   string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
+	RoutingMark int    `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
+	IPVersion   string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
+	DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
+}
+
+type BaseOption struct {
+	Name        string
+	Addr        string
+	Type        C.AdapterType
+	UDP         bool
+	XUDP        bool
+	TFO         bool
+	MPTCP       bool
+	Interface   string
+	RoutingMark int
+	Prefer      C.DNSPrefer
+}
+
+func NewBase(opt BaseOption) *Base {
+	return &Base{
+		name:   opt.Name,
+		addr:   opt.Addr,
+		tp:     opt.Type,
+		udp:    opt.UDP,
+		xudp:   opt.XUDP,
+		tfo:    opt.TFO,
+		mpTcp:  opt.MPTCP,
+		iface:  opt.Interface,
+		rmark:  opt.RoutingMark,
+		prefer: opt.Prefer,
+	}
+}
+
+type conn struct {
+	N.ExtendedConn
+	chain                   C.Chain
+	actualRemoteDestination string
+}
+
+func (c *conn) RemoteDestination() string {
+	return c.actualRemoteDestination
+}
+
+// Chains implements C.Connection
+func (c *conn) Chains() C.Chain {
+	return c.chain
+}
+
+// AppendToChains implements C.Connection
+func (c *conn) AppendToChains(a C.ProxyAdapter) {
+	c.chain = append(c.chain, a.Name())
+}
+
+func (c *conn) Upstream() any {
+	return c.ExtendedConn
+}
+
+func (c *conn) WriterReplaceable() bool {
+	return true
+}
+
+func (c *conn) ReaderReplaceable() bool {
+	return true
+}
+
+func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
+	if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
+		c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
+	}
+	return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
+}
+
+type packetConn struct {
+	N.EnhancePacketConn
+	chain                   C.Chain
+	adapterName             string
+	connID                  string
+	actualRemoteDestination string
+}
+
+func (c *packetConn) RemoteDestination() string {
+	return c.actualRemoteDestination
+}
+
+// Chains implements C.Connection
+func (c *packetConn) Chains() C.Chain {
+	return c.chain
+}
+
+// AppendToChains implements C.Connection
+func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
+	c.chain = append(c.chain, a.Name())
+}
+
+func (c *packetConn) LocalAddr() net.Addr {
+	lAddr := c.EnhancePacketConn.LocalAddr()
+	return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy
+}
+
+func (c *packetConn) Upstream() any {
+	return c.EnhancePacketConn
+}
+
+func (c *packetConn) WriterReplaceable() bool {
+	return true
+}
+
+func (c *packetConn) ReaderReplaceable() bool {
+	return true
+}
+
+func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
+	epc := N.NewEnhancePacketConn(pc)
+	if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
+		epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
+	}
+	return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
+}
+
+func parseRemoteDestination(addr string) string {
+	if dst, _, err := net.SplitHostPort(addr); err == nil {
+		return dst
+	} else {
+		if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
+			return dst
+		} else {
+			return ""
+		}
+	}
+}

+ 109 - 0
core/Clash.Meta/adapter/outbound/direct.go

@@ -0,0 +1,109 @@
+package outbound
+
+import (
+	"context"
+	"errors"
+	"os"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/loopback"
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/features"
+)
+
+var DisableLoopBackDetector, _ = strconv.ParseBool(os.Getenv("DISABLE_LOOPBACK_DETECTOR"))
+
+type Direct struct {
+	*Base
+	loopBack *loopback.Detector
+}
+
+type DirectOption struct {
+	BasicOption
+	Name string `proxy:"name"`
+}
+
+// DialContext implements C.ProxyAdapter
+func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	if !features.Android && !DisableLoopBackDetector {
+		if err := d.loopBack.CheckConn(metadata); err != nil {
+			return nil, err
+		}
+	}
+	opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
+	c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
+	if err != nil {
+		return nil, err
+	}
+	N.TCPKeepAlive(c)
+	return d.loopBack.NewConn(NewConn(c, d)), nil
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	if !features.Android && !DisableLoopBackDetector {
+		if err := d.loopBack.CheckPacketConn(metadata); err != nil {
+			return nil, err
+		}
+	}
+	// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
+	if !metadata.Resolved() {
+		ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DefaultResolver)
+		if err != nil {
+			return nil, errors.New("can't resolve ip")
+		}
+		metadata.DstIP = ip
+	}
+	pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
+	if err != nil {
+		return nil, err
+	}
+	return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
+}
+
+func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
+	return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
+}
+
+func NewDirectWithOption(option DirectOption) *Direct {
+	return &Direct{
+		Base: &Base{
+			name:   option.Name,
+			tp:     C.Direct,
+			udp:    true,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		loopBack: loopback.NewDetector(),
+	}
+}
+
+func NewDirect() *Direct {
+	return &Direct{
+		Base: &Base{
+			name:   "DIRECT",
+			tp:     C.Direct,
+			udp:    true,
+			prefer: C.DualStack,
+		},
+		loopBack: loopback.NewDetector(),
+	}
+}
+
+func NewCompatible() *Direct {
+	return &Direct{
+		Base: &Base{
+			name:   "COMPATIBLE",
+			tp:     C.Compatible,
+			udp:    true,
+			prefer: C.DualStack,
+		},
+		loopBack: loopback.NewDetector(),
+	}
+}

+ 159 - 0
core/Clash.Meta/adapter/outbound/dns.go

@@ -0,0 +1,159 @@
+package outbound
+
+import (
+	"context"
+	"net"
+	"time"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/pool"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+)
+
+type Dns struct {
+	*Base
+}
+
+type DnsOption struct {
+	BasicOption
+	Name string `proxy:"name"`
+}
+
+// DialContext implements C.ProxyAdapter
+func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	left, right := N.Pipe()
+	go resolver.RelayDnsConn(context.Background(), right, 0)
+	return NewConn(left, d), nil
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	return newPacketConn(&dnsPacketConn{
+		response: make(chan dnsPacket, 1),
+		ctx:      ctx,
+		cancel:   cancel,
+	}, d), nil
+}
+
+type dnsPacket struct {
+	data []byte
+	put  func()
+	addr net.Addr
+}
+
+// dnsPacketConn implements net.PacketConn
+type dnsPacketConn struct {
+	response chan dnsPacket
+	ctx      context.Context
+	cancel   context.CancelFunc
+}
+
+func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
+	select {
+	case packet := <-d.response:
+		return packet.data, packet.put, packet.addr, nil
+	case <-d.ctx.Done():
+		return nil, nil, nil, net.ErrClosed
+	}
+}
+
+func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	select {
+	case packet := <-d.response:
+		n = copy(p, packet.data)
+		if packet.put != nil {
+			packet.put()
+		}
+		return n, packet.addr, nil
+	case <-d.ctx.Done():
+		return 0, nil, net.ErrClosed
+	}
+}
+
+func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	select {
+	case <-d.ctx.Done():
+		return 0, net.ErrClosed
+	default:
+	}
+
+	if len(p) > resolver.SafeDnsPacketSize {
+		// wtf???
+		return len(p), nil
+	}
+
+	buf := pool.Get(resolver.SafeDnsPacketSize)
+	put := func() { _ = pool.Put(buf) }
+	copy(buf, p) // avoid p be changed after WriteTo returned
+
+	go func() { // don't block the WriteTo function
+		ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout)
+		defer cancel()
+
+		buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf)
+		if err != nil {
+			put()
+			return
+		}
+
+		packet := dnsPacket{
+			data: buf,
+			put:  put,
+			addr: addr,
+		}
+		select {
+		case d.response <- packet:
+			break
+		case <-d.ctx.Done():
+			put()
+		}
+	}()
+	return len(p), nil
+}
+
+func (d *dnsPacketConn) Close() error {
+	d.cancel()
+	return nil
+}
+
+func (*dnsPacketConn) LocalAddr() net.Addr {
+	return &net.UDPAddr{
+		IP:   net.IPv4(127, 0, 0, 1),
+		Port: 53,
+		Zone: "",
+	}
+}
+
+func (*dnsPacketConn) SetDeadline(t time.Time) error {
+	return nil
+}
+
+func (*dnsPacketConn) SetReadDeadline(t time.Time) error {
+	return nil
+}
+
+func (*dnsPacketConn) SetWriteDeadline(t time.Time) error {
+	return nil
+}
+
+func NewDnsWithOption(option DnsOption) *Dns {
+	return &Dns{
+		Base: &Base{
+			name:   option.Name,
+			tp:     C.Dns,
+			udp:    true,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+	}
+}

+ 186 - 0
core/Clash.Meta/adapter/outbound/http.go

@@ -0,0 +1,186 @@
+package outbound
+
+import (
+	"bufio"
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"errors"
+	"fmt"
+
+	"io"
+	"net"
+	"net/http"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+)
+
+type Http struct {
+	*Base
+	user      string
+	pass      string
+	tlsConfig *tls.Config
+	option    *HttpOption
+}
+
+type HttpOption struct {
+	BasicOption
+	Name           string            `proxy:"name"`
+	Server         string            `proxy:"server"`
+	Port           int               `proxy:"port"`
+	UserName       string            `proxy:"username,omitempty"`
+	Password       string            `proxy:"password,omitempty"`
+	TLS            bool              `proxy:"tls,omitempty"`
+	SNI            string            `proxy:"sni,omitempty"`
+	SkipCertVerify bool              `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint    string            `proxy:"fingerprint,omitempty"`
+	Headers        map[string]string `proxy:"headers,omitempty"`
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	if h.tlsConfig != nil {
+		cc := tls.Client(c, h.tlsConfig)
+		err := cc.HandshakeContext(ctx)
+		c = cc
+		if err != nil {
+			return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
+		}
+	}
+
+	if err := h.shakeHand(metadata, c); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// DialContext implements C.ProxyAdapter
+func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	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) {
+	if len(h.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(h.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", h.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = h.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewConn(c, h), nil
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (h *Http) SupportWithDialer() C.NetWork {
+	return C.TCP
+}
+
+func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
+	addr := metadata.RemoteAddress()
+	HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n"
+	tempHeaders := map[string]string{
+		"Host":             addr,
+		"User-Agent":       "Go-http-client/1.1",
+		"Proxy-Connection": "Keep-Alive",
+	}
+
+	for key, value := range h.option.Headers {
+		tempHeaders[key] = value
+	}
+
+	if h.user != "" && h.pass != "" {
+		auth := h.user + ":" + h.pass
+		tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
+	}
+
+	for key, value := range tempHeaders {
+		HeaderString += key + ": " + value + "\r\n"
+	}
+
+	HeaderString += "\r\n"
+
+	_, err := rw.Write([]byte(HeaderString))
+
+	if err != nil {
+		return err
+	}
+
+	resp, err := http.ReadResponse(bufio.NewReader(rw), nil)
+
+	if err != nil {
+		return err
+	}
+
+	if resp.StatusCode == http.StatusOK {
+		return nil
+	}
+
+	if resp.StatusCode == http.StatusProxyAuthRequired {
+		return errors.New("HTTP need auth")
+	}
+
+	if resp.StatusCode == http.StatusMethodNotAllowed {
+		return errors.New("CONNECT method not allowed by proxy")
+	}
+
+	if resp.StatusCode >= http.StatusInternalServerError {
+		return errors.New(resp.Status)
+	}
+
+	return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
+}
+
+func NewHttp(option HttpOption) (*Http, error) {
+	var tlsConfig *tls.Config
+	if option.TLS {
+		sni := option.Server
+		if option.SNI != "" {
+			sni = option.SNI
+		}
+		var err error
+		tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
+			InsecureSkipVerify: option.SkipCertVerify,
+			ServerName:         sni,
+		}, option.Fingerprint)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &Http{
+		Base: &Base{
+			name:   option.Name,
+			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
+			tp:     C.Http,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		user:      option.UserName,
+		pass:      option.Password,
+		tlsConfig: tlsConfig,
+		option:    &option,
+	}, nil
+}

+ 290 - 0
core/Clash.Meta/adapter/outbound/hysteria.go

@@ -0,0 +1,290 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/base64"
+	"fmt"
+	"net"
+	"net/netip"
+	"strconv"
+	"time"
+
+	"github.com/metacubex/quic-go"
+	"github.com/metacubex/quic-go/congestion"
+	M "github.com/sagernet/sing/common/metadata"
+
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+	hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion"
+	"github.com/metacubex/mihomo/transport/hysteria/core"
+	"github.com/metacubex/mihomo/transport/hysteria/obfs"
+	"github.com/metacubex/mihomo/transport/hysteria/pmtud_fix"
+	"github.com/metacubex/mihomo/transport/hysteria/transport"
+	"github.com/metacubex/mihomo/transport/hysteria/utils"
+)
+
+const (
+	mbpsToBps = 125000
+
+	DefaultStreamReceiveWindow     = 15728640 // 15 MB/s
+	DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
+
+	DefaultALPN        = "hysteria"
+	DefaultProtocol    = "udp"
+	DefaultHopInterval = 10
+)
+
+type Hysteria struct {
+	*Base
+
+	option *HysteriaOption
+	client *core.Client
+}
+
+func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx, opts...))
+	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) {
+	udpConn, err := h.client.DialUDP(h.genHdc(ctx, opts...))
+	if err != nil {
+		return nil, err
+	}
+	return newPacketConn(&hyPacketConn{udpConn}, h), nil
+}
+
+func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
+	return &hyDialerWithContext{
+		ctx: context.Background(),
+		hyDialer: func(network string) (net.PacketConn, error) {
+			var err error
+			var cDialer C.Dialer = dialer.NewDialer(h.Base.DialOptions(opts...)...)
+			if len(h.option.DialerProxy) > 0 {
+				cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer)
+				if err != nil {
+					return nil, err
+				}
+			}
+			rAddrPort, _ := netip.ParseAddrPort(h.Addr())
+			return cDialer.ListenPacket(ctx, network, "", rAddrPort)
+		},
+		remoteAddr: func(addr string) (net.Addr, error) {
+			return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
+		},
+	}
+}
+
+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 err error
+	tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
+	if err != nil {
+		return nil, err
+	}
+
+	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),
+		},
+		option: &option,
+		client: client,
+	}, nil
+}
+
+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) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
+	b, addrStr, err := c.UDPConn.ReadFrom()
+	if err != nil {
+		return
+	}
+	data = 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)
+}

+ 205 - 0
core/Clash.Meta/adapter/outbound/hysteria2.go

@@ -0,0 +1,205 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net"
+	"runtime"
+	"strconv"
+	"time"
+
+	CN "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+	tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
+
+	"github.com/metacubex/sing-quic/hysteria2"
+
+	"github.com/metacubex/randv2"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+func init() {
+	hysteria2.SetCongestionController = tuicCommon.SetCongestionController
+}
+
+const minHopInterval = 5
+const defaultHopInterval = 30
+
+type Hysteria2 struct {
+	*Base
+
+	option *Hysteria2Option
+	client *hysteria2.Client
+	dialer proxydialer.SingDialer
+}
+
+type Hysteria2Option struct {
+	BasicOption
+	Name           string   `proxy:"name"`
+	Server         string   `proxy:"server"`
+	Port           int      `proxy:"port,omitempty"`
+	Ports          string   `proxy:"ports,omitempty"`
+	HopInterval    int      `proxy:"hop-interval,omitempty"`
+	Up             string   `proxy:"up,omitempty"`
+	Down           string   `proxy:"down,omitempty"`
+	Password       string   `proxy:"password,omitempty"`
+	Obfs           string   `proxy:"obfs,omitempty"`
+	ObfsPassword   string   `proxy:"obfs-password,omitempty"`
+	SNI            string   `proxy:"sni,omitempty"`
+	SkipCertVerify bool     `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint    string   `proxy:"fingerprint,omitempty"`
+	ALPN           []string `proxy:"alpn,omitempty"`
+	CustomCA       string   `proxy:"ca,omitempty"`
+	CustomCAString string   `proxy:"ca-str,omitempty"`
+	CWND           int      `proxy:"cwnd,omitempty"`
+	UdpMTU         int      `proxy:"udp-mtu,omitempty"`
+}
+
+func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	options := h.Base.DialOptions(opts...)
+	h.dialer.SetDialer(dialer.NewDialer(options...))
+	c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+	if err != nil {
+		return nil, err
+	}
+	return NewConn(CN.NewRefConn(c, h), h), nil
+}
+
+func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
+	options := h.Base.DialOptions(opts...)
+	h.dialer.SetDialer(dialer.NewDialer(options...))
+	pc, err := h.client.ListenPacket(ctx)
+	if err != nil {
+		return nil, err
+	}
+	if pc == nil {
+		return nil, errors.New("packetConn is nil")
+	}
+	return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil
+}
+
+func closeHysteria2(h *Hysteria2) {
+	if h.client != nil {
+		_ = h.client.CloseWithError(errors.New("proxy removed"))
+	}
+}
+
+func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+	var salamanderPassword string
+	if len(option.Obfs) > 0 {
+		if option.ObfsPassword == "" {
+			return nil, errors.New("missing obfs password")
+		}
+		switch option.Obfs {
+		case hysteria2.ObfsTypeSalamander:
+			salamanderPassword = option.ObfsPassword
+		default:
+			return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs)
+		}
+	}
+
+	serverName := option.Server
+	if option.SNI != "" {
+		serverName = option.SNI
+	}
+
+	tlsConfig := &tls.Config{
+		ServerName:         serverName,
+		InsecureSkipVerify: option.SkipCertVerify,
+		MinVersion:         tls.VersionTLS13,
+	}
+
+	var err error
+	tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(option.ALPN) > 0 {
+		tlsConfig.NextProtos = option.ALPN
+	}
+
+	if option.UdpMTU == 0 {
+		// "1200" from quic-go's MaxDatagramSize
+		// "-3" from quic-go's DatagramFrame.MaxDataLen
+		option.UdpMTU = 1200 - 3
+	}
+
+	singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
+
+	clientOptions := hysteria2.ClientOptions{
+		Context:            context.TODO(),
+		Dialer:             singDialer,
+		Logger:             log.SingLogger,
+		SendBPS:            StringToBps(option.Up),
+		ReceiveBPS:         StringToBps(option.Down),
+		SalamanderPassword: salamanderPassword,
+		Password:           option.Password,
+		TLSConfig:          tlsConfig,
+		UDPDisabled:        false,
+		CWND:               option.CWND,
+		UdpMTU:             option.UdpMTU,
+		ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
+			return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
+		},
+	}
+
+	var ranges utils.IntRanges[uint16]
+	var serverAddress []string
+	if option.Ports != "" {
+		ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
+		if err != nil {
+			return nil, err
+		}
+		ranges.Range(func(port uint16) bool {
+			serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port))))
+			return true
+		})
+		if len(serverAddress) > 0 {
+			clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
+				return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
+			}
+
+			if option.HopInterval == 0 {
+				option.HopInterval = defaultHopInterval
+			} else if option.HopInterval < minHopInterval {
+				option.HopInterval = minHopInterval
+			}
+			clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
+		}
+	}
+	if option.Port == 0 && len(serverAddress) == 0 {
+		return nil, errors.New("invalid port")
+	}
+
+	client, err := hysteria2.NewClient(clientOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	outbound := &Hysteria2{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.Hysteria2,
+			udp:    true,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option: &option,
+		client: client,
+		dialer: singDialer,
+	}
+	runtime.SetFinalizer(outbound, closeHysteria2)
+
+	return outbound, nil
+}

+ 41 - 0
core/Clash.Meta/adapter/outbound/reality.go

@@ -0,0 +1,41 @@
+package outbound
+
+import (
+	"crypto/ecdh"
+	"encoding/base64"
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	tlsC "github.com/metacubex/mihomo/component/tls"
+)
+
+type RealityOptions struct {
+	PublicKey string `proxy:"public-key"`
+	ShortID   string `proxy:"short-id"`
+}
+
+func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
+	if o.PublicKey != "" {
+		config := new(tlsC.RealityConfig)
+
+		const x25519ScalarSize = 32
+		var publicKey [x25519ScalarSize]byte
+		n, err := base64.RawURLEncoding.Decode(publicKey[:], []byte(o.PublicKey))
+		if err != nil || n != x25519ScalarSize {
+			return nil, errors.New("invalid REALITY public key")
+		}
+		config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey[:])
+		if err != nil {
+			return nil, fmt.Errorf("fail to create REALITY public key: %w", err)
+		}
+
+		n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID))
+		if err != nil || n > tlsC.RealityMaxShortIDLen {
+			return nil, errors.New("invalid REALITY short ID")
+		}
+
+		return config, nil
+	}
+	return nil, nil
+}

+ 128 - 0
core/Clash.Meta/adapter/outbound/reject.go

@@ -0,0 +1,128 @@
+package outbound
+
+import (
+	"context"
+	"io"
+	"net"
+	"time"
+
+	"github.com/metacubex/mihomo/common/buf"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+)
+
+type Reject struct {
+	*Base
+	drop bool
+}
+
+type RejectOption struct {
+	Name string `proxy:"name"`
+}
+
+// DialContext implements C.ProxyAdapter
+func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	if r.drop {
+		return NewConn(dropConn{}, r), nil
+	}
+	return NewConn(nopConn{}, r), nil
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	return newPacketConn(&nopPacketConn{}, r), nil
+}
+
+func NewRejectWithOption(option RejectOption) *Reject {
+	return &Reject{
+		Base: &Base{
+			name: option.Name,
+			tp:   C.Direct,
+			udp:  true,
+		},
+	}
+}
+
+func NewReject() *Reject {
+	return &Reject{
+		Base: &Base{
+			name:   "REJECT",
+			tp:     C.Reject,
+			udp:    true,
+			prefer: C.DualStack,
+		},
+	}
+}
+
+func NewRejectDrop() *Reject {
+	return &Reject{
+		Base: &Base{
+			name:   "REJECT-DROP",
+			tp:     C.RejectDrop,
+			udp:    true,
+			prefer: C.DualStack,
+		},
+		drop: true,
+	}
+}
+
+func NewPass() *Reject {
+	return &Reject{
+		Base: &Base{
+			name:   "PASS",
+			tp:     C.Pass,
+			udp:    true,
+			prefer: C.DualStack,
+		},
+	}
+}
+
+type nopConn struct{}
+
+func (rw nopConn) Read(b []byte) (int, error) { return 0, io.EOF }
+
+func (rw nopConn) ReadBuffer(buffer *buf.Buffer) error { return io.EOF }
+
+func (rw nopConn) Write(b []byte) (int, error)          { return 0, io.EOF }
+func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF }
+func (rw nopConn) Close() error                         { return nil }
+func (rw nopConn) LocalAddr() net.Addr                  { return nil }
+func (rw nopConn) RemoteAddr() net.Addr                 { return nil }
+func (rw nopConn) SetDeadline(time.Time) error          { return nil }
+func (rw nopConn) SetReadDeadline(time.Time) error      { return nil }
+func (rw nopConn) SetWriteDeadline(time.Time) error     { return nil }
+
+var udpAddrIPv4Unspecified = &net.UDPAddr{IP: net.IPv4zero, Port: 0}
+
+type nopPacketConn struct{}
+
+func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
+	return len(b), nil
+}
+func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	return 0, nil, io.EOF
+}
+func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
+	return nil, nil, nil, io.EOF
+}
+func (npc nopPacketConn) Close() error                     { return nil }
+func (npc nopPacketConn) LocalAddr() net.Addr              { return udpAddrIPv4Unspecified }
+func (npc nopPacketConn) SetDeadline(time.Time) error      { return nil }
+func (npc nopPacketConn) SetReadDeadline(time.Time) error  { return nil }
+func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
+
+type dropConn struct{}
+
+func (rw dropConn) Read(b []byte) (int, error) { return 0, io.EOF }
+func (rw dropConn) ReadBuffer(buffer *buf.Buffer) error {
+	time.Sleep(C.DefaultDropTime)
+	return io.EOF
+}
+func (rw dropConn) Write(b []byte) (int, error)          { return 0, io.EOF }
+func (rw dropConn) WriteBuffer(buffer *buf.Buffer) error { return io.EOF }
+func (rw dropConn) Close() error                         { return nil }
+func (rw dropConn) LocalAddr() net.Addr                  { return nil }
+func (rw dropConn) RemoteAddr() net.Addr                 { return nil }
+func (rw dropConn) SetDeadline(time.Time) error          { return nil }
+func (rw dropConn) SetReadDeadline(time.Time) error      { return nil }
+func (rw dropConn) SetWriteDeadline(time.Time) error     { return nil }

+ 337 - 0
core/Clash.Meta/adapter/outbound/shadowsocks.go

@@ -0,0 +1,337 @@
+package outbound
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/structure"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/restls"
+	obfs "github.com/metacubex/mihomo/transport/simple-obfs"
+	shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
+	v2rayObfs "github.com/metacubex/mihomo/transport/v2ray-plugin"
+
+	restlsC "github.com/3andne/restls-client-go"
+	shadowsocks "github.com/metacubex/sing-shadowsocks2"
+	"github.com/sagernet/sing/common/bufio"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/common/uot"
+)
+
+type ShadowSocks struct {
+	*Base
+	method shadowsocks.Method
+
+	option *ShadowSocksOption
+	// obfs
+	obfsMode        string
+	obfsOption      *simpleObfsOption
+	v2rayOption     *v2rayObfs.Option
+	shadowTLSOption *shadowtls.ShadowTLSOption
+	restlsConfig    *restlsC.Config
+}
+
+type ShadowSocksOption struct {
+	BasicOption
+	Name              string         `proxy:"name"`
+	Server            string         `proxy:"server"`
+	Port              int            `proxy:"port"`
+	Password          string         `proxy:"password"`
+	Cipher            string         `proxy:"cipher"`
+	UDP               bool           `proxy:"udp,omitempty"`
+	Plugin            string         `proxy:"plugin,omitempty"`
+	PluginOpts        map[string]any `proxy:"plugin-opts,omitempty"`
+	UDPOverTCP        bool           `proxy:"udp-over-tcp,omitempty"`
+	UDPOverTCPVersion int            `proxy:"udp-over-tcp-version,omitempty"`
+	ClientFingerprint string         `proxy:"client-fingerprint,omitempty"`
+}
+
+type simpleObfsOption struct {
+	Mode string `obfs:"mode,omitempty"`
+	Host string `obfs:"host,omitempty"`
+}
+
+type v2rayObfsOption struct {
+	Mode                     string            `obfs:"mode"`
+	Host                     string            `obfs:"host,omitempty"`
+	Path                     string            `obfs:"path,omitempty"`
+	TLS                      bool              `obfs:"tls,omitempty"`
+	Fingerprint              string            `obfs:"fingerprint,omitempty"`
+	Headers                  map[string]string `obfs:"headers,omitempty"`
+	SkipCertVerify           bool              `obfs:"skip-cert-verify,omitempty"`
+	Mux                      bool              `obfs:"mux,omitempty"`
+	V2rayHttpUpgrade         bool              `obfs:"v2ray-http-upgrade,omitempty"`
+	V2rayHttpUpgradeFastOpen bool              `obfs:"v2ray-http-upgrade-fast-open,omitempty"`
+}
+
+type shadowTLSOption struct {
+	Password       string `obfs:"password"`
+	Host           string `obfs:"host"`
+	Fingerprint    string `obfs:"fingerprint,omitempty"`
+	SkipCertVerify bool   `obfs:"skip-cert-verify,omitempty"`
+	Version        int    `obfs:"version,omitempty"`
+}
+
+type restlsOption struct {
+	Password     string `obfs:"password"`
+	Host         string `obfs:"host"`
+	VersionHint  string `obfs:"version-hint"`
+	RestlsScript string `obfs:"restls-script,omitempty"`
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	useEarly := false
+	switch ss.obfsMode {
+	case "tls":
+		c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
+	case "http":
+		_, port, _ := net.SplitHostPort(ss.addr)
+		c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
+	case "websocket":
+		var err error
+		c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
+		if err != nil {
+			return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
+		}
+	case shadowtls.Mode:
+		var err error
+		c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
+		if err != nil {
+			return nil, err
+		}
+		useEarly = true
+	case restls.Mode:
+		var err error
+		c, err = restls.NewRestls(ctx, c, ss.restlsConfig)
+		if err != nil {
+			return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err)
+		}
+		useEarly = true
+	}
+	useEarly = useEarly || N.NeedHandshake(c)
+	if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
+		uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion))
+		if useEarly {
+			return ss.method.DialEarlyConn(c, uotDestination), nil
+		} else {
+			return ss.method.DialConn(c, uotDestination)
+		}
+	}
+	if useEarly {
+		return ss.method.DialEarlyConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)), nil
+	} else {
+		return ss.method.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+	}
+}
+
+// DialContext implements C.ProxyAdapter
+func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	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) {
+	if len(ss.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", ss.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = ss.StreamConnContext(ctx, c, metadata)
+	return NewConn(c, ss), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	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 ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata)
+	}
+	if len(ss.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer)
+	if err != nil {
+		return nil, err
+	}
+
+	pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
+	if err != nil {
+		return nil, err
+	}
+	pc = ss.method.DialPacketConn(bufio.NewBindPacketConn(pc, addr))
+	return newPacketConn(pc, ss), nil
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (ss *ShadowSocks) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+// ListenPacketOnStreamConn implements C.ProxyAdapter
+func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
+	if ss.option.UDPOverTCP {
+		// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
+		if !metadata.Resolved() {
+			ip, err := resolver.ResolveIP(ctx, metadata.Host)
+			if err != nil {
+				return nil, errors.New("can't resolve ip")
+			}
+			metadata.DstIP = ip
+		}
+
+		destination := M.SocksaddrFromNet(metadata.UDPAddr())
+		if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
+			return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
+		} else {
+			return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil
+		}
+	}
+	return nil, C.ErrNotSupport
+}
+
+// SupportUOT implements C.ProxyAdapter
+func (ss *ShadowSocks) SupportUOT() bool {
+	return ss.option.UDPOverTCP
+}
+
+func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+	method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{
+		Password: option.Password,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
+	}
+
+	var v2rayOption *v2rayObfs.Option
+	var obfsOption *simpleObfsOption
+	var shadowTLSOpt *shadowtls.ShadowTLSOption
+	var restlsConfig *restlsC.Config
+	obfsMode := ""
+
+	decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
+	if option.Plugin == "obfs" {
+		opts := simpleObfsOption{Host: "bing.com"}
+		if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
+			return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err)
+		}
+
+		if opts.Mode != "tls" && opts.Mode != "http" {
+			return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
+		}
+		obfsMode = opts.Mode
+		obfsOption = &opts
+	} else if option.Plugin == "v2ray-plugin" {
+		opts := v2rayObfsOption{Host: "bing.com", Mux: true}
+		if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
+			return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
+		}
+
+		if opts.Mode != "websocket" {
+			return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
+		}
+		obfsMode = opts.Mode
+		v2rayOption = &v2rayObfs.Option{
+			Host:                     opts.Host,
+			Path:                     opts.Path,
+			Headers:                  opts.Headers,
+			Mux:                      opts.Mux,
+			V2rayHttpUpgrade:         opts.V2rayHttpUpgrade,
+			V2rayHttpUpgradeFastOpen: opts.V2rayHttpUpgradeFastOpen,
+		}
+
+		if opts.TLS {
+			v2rayOption.TLS = true
+			v2rayOption.SkipCertVerify = opts.SkipCertVerify
+			v2rayOption.Fingerprint = opts.Fingerprint
+		}
+	} 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: option.ClientFingerprint,
+			SkipCertVerify:    opt.SkipCertVerify,
+			Version:           opt.Version,
+		}
+	} else if option.Plugin == restls.Mode {
+		obfsMode = restls.Mode
+		restlsOpt := &restlsOption{}
+		if err := decoder.Decode(option.PluginOpts, restlsOpt); err != nil {
+			return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
+		}
+
+		restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
+		if err != nil {
+			return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
+		}
+
+	}
+	switch option.UDPOverTCPVersion {
+	case uot.Version, uot.LegacyVersion:
+	case 0:
+		option.UDPOverTCPVersion = uot.LegacyVersion
+	default:
+		return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion)
+	}
+
+	return &ShadowSocks{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.Shadowsocks,
+			udp:    option.UDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		method: method,
+
+		option:          &option,
+		obfsMode:        obfsMode,
+		v2rayOption:     v2rayOption,
+		obfsOption:      obfsOption,
+		shadowTLSOption: shadowTLSOpt,
+		restlsConfig:    restlsConfig,
+	}, nil
+}

+ 253 - 0
core/Clash.Meta/adapter/outbound/shadowsocksr.go

@@ -0,0 +1,253 @@
+package outbound
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/shadowsocks/core"
+	"github.com/metacubex/mihomo/transport/shadowsocks/shadowaead"
+	"github.com/metacubex/mihomo/transport/shadowsocks/shadowstream"
+	"github.com/metacubex/mihomo/transport/socks5"
+	"github.com/metacubex/mihomo/transport/ssr/obfs"
+	"github.com/metacubex/mihomo/transport/ssr/protocol"
+)
+
+type ShadowSocksR struct {
+	*Base
+	option   *ShadowSocksROption
+	cipher   core.Cipher
+	obfs     obfs.Obfs
+	protocol protocol.Protocol
+}
+
+type ShadowSocksROption struct {
+	BasicOption
+	Name          string `proxy:"name"`
+	Server        string `proxy:"server"`
+	Port          int    `proxy:"port"`
+	Password      string `proxy:"password"`
+	Cipher        string `proxy:"cipher"`
+	Obfs          string `proxy:"obfs"`
+	ObfsParam     string `proxy:"obfs-param,omitempty"`
+	Protocol      string `proxy:"protocol"`
+	ProtocolParam string `proxy:"protocol-param,omitempty"`
+	UDP           bool   `proxy:"udp,omitempty"`
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	c = ssr.obfs.StreamConn(c)
+	c = ssr.cipher.StreamConn(c)
+	var (
+		iv  []byte
+		err error
+	)
+	switch conn := c.(type) {
+	case *shadowstream.Conn:
+		iv, err = conn.ObtainWriteIV()
+		if err != nil {
+			return nil, err
+		}
+	case *shadowaead.Conn:
+		return nil, fmt.Errorf("invalid connection type")
+	}
+	c = ssr.protocol.StreamConn(c, iv)
+	_, err = c.Write(serializesSocksAddr(metadata))
+	return c, err
+}
+
+// DialContext implements C.ProxyAdapter
+func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	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) {
+	if len(ssr.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = ssr.StreamConnContext(ctx, c, metadata)
+	return NewConn(c, ssr), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	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) {
+	if len(ssr.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer)
+	if err != nil {
+		return nil, err
+	}
+
+	pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
+	if err != nil {
+		return nil, err
+	}
+
+	epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc))
+	epc = ssr.protocol.PacketConn(epc)
+	return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
+	// SSR protocol compatibility
+	// https://github.com/metacubex/mihomo/pull/2056
+	if option.Cipher == "none" {
+		option.Cipher = "dummy"
+	}
+
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+	cipher := option.Cipher
+	password := option.Password
+	coreCiph, err := core.PickCipher(cipher, nil, password)
+	if err != nil {
+		return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
+	}
+	var (
+		ivSize int
+		key    []byte
+	)
+
+	if option.Cipher == "dummy" {
+		ivSize = 0
+		key = core.Kdf(option.Password, 16)
+	} else {
+		ciph, ok := coreCiph.(*core.StreamCipher)
+		if !ok {
+			return nil, fmt.Errorf("%s is not none or a supported stream cipher in ssr", cipher)
+		}
+		ivSize = ciph.IVSize()
+		key = ciph.Key
+	}
+
+	obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
+		Host:   option.Server,
+		Port:   option.Port,
+		Key:    key,
+		IVSize: ivSize,
+		Param:  option.ObfsParam,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
+	}
+
+	protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
+		Key:      key,
+		Overhead: obfsOverhead,
+		Param:    option.ProtocolParam,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
+	}
+
+	return &ShadowSocksR{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.ShadowsocksR,
+			udp:    option.UDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option:   &option,
+		cipher:   coreCiph,
+		obfs:     obfs,
+		protocol: protocol,
+	}, nil
+}
+
+type ssrPacketConn struct {
+	N.EnhancePacketConn
+	rAddr net.Addr
+}
+
+func (spc *ssrPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
+	packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
+	if err != nil {
+		return
+	}
+	return spc.EnhancePacketConn.WriteTo(packet[3:], spc.rAddr)
+}
+
+func (spc *ssrPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	n, _, e := spc.EnhancePacketConn.ReadFrom(b)
+	if e != nil {
+		return 0, nil, e
+	}
+
+	addr := socks5.SplitAddr(b[:n])
+	if addr == nil {
+		return 0, nil, errors.New("parse addr error")
+	}
+
+	udpAddr := addr.UDPAddr()
+	if udpAddr == nil {
+		return 0, nil, errors.New("parse addr error")
+	}
+
+	copy(b, b[len(addr):])
+	return n - len(addr), udpAddr, e
+}
+
+func (spc *ssrPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
+	data, put, _, err = spc.EnhancePacketConn.WaitReadFrom()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	_addr := socks5.SplitAddr(data)
+	if _addr == nil {
+		if put != nil {
+			put()
+		}
+		return nil, nil, nil, errors.New("parse addr error")
+	}
+
+	addr = _addr.UDPAddr()
+	if addr == nil {
+		if put != nil {
+			put()
+		}
+		return nil, nil, nil, errors.New("parse addr error")
+	}
+
+	data = data[len(_addr):]
+	return
+}

+ 135 - 0
core/Clash.Meta/adapter/outbound/singmux.go

@@ -0,0 +1,135 @@
+package outbound
+
+import (
+	"context"
+	"errors"
+	"runtime"
+
+	CN "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+
+	mux "github.com/sagernet/sing-mux"
+	E "github.com/sagernet/sing/common/exceptions"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+type SingMux struct {
+	C.ProxyAdapter
+	base    ProxyBase
+	client  *mux.Client
+	dialer  proxydialer.SingDialer
+	onlyTcp bool
+}
+
+type SingMuxOption struct {
+	Enabled        bool         `proxy:"enabled,omitempty"`
+	Protocol       string       `proxy:"protocol,omitempty"`
+	MaxConnections int          `proxy:"max-connections,omitempty"`
+	MinStreams     int          `proxy:"min-streams,omitempty"`
+	MaxStreams     int          `proxy:"max-streams,omitempty"`
+	Padding        bool         `proxy:"padding,omitempty"`
+	Statistic      bool         `proxy:"statistic,omitempty"`
+	OnlyTcp        bool         `proxy:"only-tcp,omitempty"`
+	BrutalOpts     BrutalOption `proxy:"brutal-opts,omitempty"`
+}
+
+type BrutalOption struct {
+	Enabled bool   `proxy:"enabled,omitempty"`
+	Up      string `proxy:"up,omitempty"`
+	Down    string `proxy:"down,omitempty"`
+}
+
+type ProxyBase interface {
+	DialOptions(opts ...dialer.Option) []dialer.Option
+}
+
+func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	options := s.base.DialOptions(opts...)
+	s.dialer.SetDialer(dialer.NewDialer(options...))
+	c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+	if err != nil {
+		return nil, err
+	}
+	return NewConn(CN.NewRefConn(c, s), s.ProxyAdapter), err
+}
+
+func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
+	if s.onlyTcp {
+		return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
+	}
+	options := s.base.DialOptions(opts...)
+	s.dialer.SetDialer(dialer.NewDialer(options...))
+
+	// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
+	if !metadata.Resolved() {
+		ip, err := resolver.ResolveIP(ctx, metadata.Host)
+		if err != nil {
+			return nil, errors.New("can't resolve ip")
+		}
+		metadata.DstIP = ip
+	}
+
+	pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
+	if err != nil {
+		return nil, err
+	}
+	if pc == nil {
+		return nil, E.New("packetConn is nil")
+	}
+	return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), s), s.ProxyAdapter), nil
+}
+
+func (s *SingMux) SupportUDP() bool {
+	if s.onlyTcp {
+		return s.ProxyAdapter.SupportUDP()
+	}
+	return true
+}
+
+func (s *SingMux) SupportUOT() bool {
+	if s.onlyTcp {
+		return s.ProxyAdapter.SupportUOT()
+	}
+	return true
+}
+
+func closeSingMux(s *SingMux) {
+	_ = s.client.Close()
+}
+
+func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) {
+	// TODO
+	// "TCP Brutal is only supported on Linux-based systems"
+
+	singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(), option.Statistic)
+	client, err := mux.NewClient(mux.Options{
+		Dialer:         singDialer,
+		Logger:         log.SingLogger,
+		Protocol:       option.Protocol,
+		MaxConnections: option.MaxConnections,
+		MinStreams:     option.MinStreams,
+		MaxStreams:     option.MaxStreams,
+		Padding:        option.Padding,
+		Brutal: mux.BrutalOptions{
+			Enabled:    option.BrutalOpts.Enabled,
+			SendBPS:    StringToBps(option.BrutalOpts.Up),
+			ReceiveBPS: StringToBps(option.BrutalOpts.Down),
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	outbound := &SingMux{
+		ProxyAdapter: proxy,
+		base:         base,
+		client:       client,
+		dialer:       singDialer,
+		onlyTcp:      option.OnlyTcp,
+	}
+	runtime.SetFinalizer(outbound, closeSingMux)
+	return outbound, nil
+}

+ 216 - 0
core/Clash.Meta/adapter/outbound/snell.go

@@ -0,0 +1,216 @@
+package outbound
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/structure"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	obfs "github.com/metacubex/mihomo/transport/simple-obfs"
+	"github.com/metacubex/mihomo/transport/snell"
+)
+
+type Snell struct {
+	*Base
+	option     *SnellOption
+	psk        []byte
+	pool       *snell.Pool
+	obfsOption *simpleObfsOption
+	version    int
+}
+
+type SnellOption struct {
+	BasicOption
+	Name     string         `proxy:"name"`
+	Server   string         `proxy:"server"`
+	Port     int            `proxy:"port"`
+	Psk      string         `proxy:"psk"`
+	UDP      bool           `proxy:"udp,omitempty"`
+	Version  int            `proxy:"version,omitempty"`
+	ObfsOpts map[string]any `proxy:"obfs-opts,omitempty"`
+}
+
+type streamOption struct {
+	psk        []byte
+	version    int
+	addr       string
+	obfsOption *simpleObfsOption
+}
+
+func streamConn(c net.Conn, option streamOption) *snell.Snell {
+	switch option.obfsOption.Mode {
+	case "tls":
+		c = obfs.NewTLSObfs(c, option.obfsOption.Host)
+	case "http":
+		_, port, _ := net.SplitHostPort(option.addr)
+		c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
+	}
+	return snell.StreamConn(c, option.psk, option.version)
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
+	if metadata.NetWork == C.UDP {
+		err := snell.WriteUDPHeader(c, s.version)
+		return c, err
+	}
+	err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
+	return c, err
+}
+
+// DialContext implements C.ProxyAdapter
+func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	if s.version == snell.Version2 && len(opts) == 0 {
+		c, err := s.pool.Get()
+		if err != nil {
+			return nil, err
+		}
+
+		if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil {
+			c.Close()
+			return nil, err
+		}
+		return NewConn(c, s), err
+	}
+
+	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) {
+	if len(s.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", s.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = s.StreamConnContext(ctx, c, metadata)
+	return NewConn(c, s), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	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) {
+	var err error
+	if len(s.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", s.addr)
+	if err != nil {
+		return nil, err
+	}
+	N.TCPKeepAlive(c)
+	c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
+
+	err = snell.WriteUDPHeader(c, s.version)
+	if err != nil {
+		return nil, err
+	}
+
+	pc := snell.PacketConn(c)
+	return newPacketConn(pc, s), nil
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (s *Snell) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+// SupportUOT implements C.ProxyAdapter
+func (s *Snell) SupportUOT() bool {
+	return true
+}
+
+func NewSnell(option SnellOption) (*Snell, error) {
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+	psk := []byte(option.Psk)
+
+	decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
+	obfsOption := &simpleObfsOption{Host: "bing.com"}
+	if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
+		return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
+	}
+
+	switch obfsOption.Mode {
+	case "tls", "http", "":
+		break
+	default:
+		return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
+	}
+
+	// backward compatible
+	if option.Version == 0 {
+		option.Version = snell.DefaultSnellVersion
+	}
+	switch option.Version {
+	case snell.Version1, snell.Version2:
+		if option.UDP {
+			return nil, fmt.Errorf("snell version %d not support UDP", option.Version)
+		}
+	case snell.Version3:
+	default:
+		return nil, fmt.Errorf("snell version error: %d", option.Version)
+	}
+
+	s := &Snell{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.Snell,
+			udp:    option.UDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option:     &option,
+		psk:        psk,
+		obfsOption: obfsOption,
+		version:    option.Version,
+	}
+
+	if option.Version == snell.Version2 {
+		s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
+			var err error
+			var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions()...)
+			if len(s.option.DialerProxy) > 0 {
+				cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
+				if err != nil {
+					return nil, err
+				}
+			}
+			c, err := cDialer.DialContext(ctx, "tcp", addr)
+			if err != nil {
+				return nil, err
+			}
+
+			N.TCPKeepAlive(c)
+			return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
+		})
+	}
+	return s, nil
+}

+ 250 - 0
core/Clash.Meta/adapter/outbound/socks5.go

@@ -0,0 +1,250 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/netip"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+type Socks5 struct {
+	*Base
+	option         *Socks5Option
+	user           string
+	pass           string
+	tls            bool
+	skipCertVerify bool
+	tlsConfig      *tls.Config
+}
+
+type Socks5Option struct {
+	BasicOption
+	Name           string `proxy:"name"`
+	Server         string `proxy:"server"`
+	Port           int    `proxy:"port"`
+	UserName       string `proxy:"username,omitempty"`
+	Password       string `proxy:"password,omitempty"`
+	TLS            bool   `proxy:"tls,omitempty"`
+	UDP            bool   `proxy:"udp,omitempty"`
+	SkipCertVerify bool   `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint    string `proxy:"fingerprint,omitempty"`
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	if ss.tls {
+		cc := tls.Client(c, ss.tlsConfig)
+		err := cc.HandshakeContext(ctx)
+		c = cc
+		if err != nil {
+			return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
+		}
+	}
+
+	var user *socks5.User
+	if ss.user != "" {
+		user = &socks5.User{
+			Username: ss.user,
+			Password: ss.pass,
+		}
+	}
+	if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// DialContext implements C.ProxyAdapter
+func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	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) {
+	if len(ss.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", ss.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = ss.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewConn(c, ss), nil
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (ss *Socks5) SupportWithDialer() C.NetWork {
+	return C.TCP
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
+	var cDialer C.Dialer = dialer.NewDialer(ss.Base.DialOptions(opts...)...)
+	if len(ss.option.DialerProxy) > 0 {
+		cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
+	if err != nil {
+		err = fmt.Errorf("%s connect error: %w", ss.addr, err)
+		return
+	}
+
+	if ss.tls {
+		cc := tls.Client(c, ss.tlsConfig)
+		ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
+		defer cancel()
+		err = cc.HandshakeContext(ctx)
+		c = cc
+	}
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	N.TCPKeepAlive(c)
+	var user *socks5.User
+	if ss.user != "" {
+		user = &socks5.User{
+			Username: ss.user,
+			Password: ss.pass,
+		}
+	}
+
+	udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
+	bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
+	if err != nil {
+		err = fmt.Errorf("client hanshake error: %w", err)
+		return
+	}
+
+	// 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 := cDialer.ListenPacket(ctx, "udp", "", bindUDPAddr.AddrPort())
+	if err != nil {
+		return
+	}
+
+	go func() {
+		io.Copy(io.Discard, c)
+		c.Close()
+		// A UDP association terminates when the TCP connection that the UDP
+		// ASSOCIATE request arrived on terminates. RFC1928
+		pc.Close()
+	}()
+
+	return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
+}
+
+func NewSocks5(option Socks5Option) (*Socks5, error) {
+	var tlsConfig *tls.Config
+	if option.TLS {
+		tlsConfig = &tls.Config{
+			InsecureSkipVerify: option.SkipCertVerify,
+			ServerName:         option.Server,
+		}
+
+		var err error
+		tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &Socks5{
+		Base: &Base{
+			name:   option.Name,
+			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
+			tp:     C.Socks5,
+			udp:    option.UDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option:         &option,
+		user:           option.UserName,
+		pass:           option.Password,
+		tls:            option.TLS,
+		skipCertVerify: option.SkipCertVerify,
+		tlsConfig:      tlsConfig,
+	}, nil
+}
+
+type socksPacketConn struct {
+	net.PacketConn
+	rAddr   net.Addr
+	tcpConn net.Conn
+}
+
+func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
+	packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
+	if err != nil {
+		return
+	}
+	return uc.PacketConn.WriteTo(packet, uc.rAddr)
+}
+
+func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	n, _, e := uc.PacketConn.ReadFrom(b)
+	if e != nil {
+		return 0, nil, e
+	}
+	addr, payload, err := socks5.DecodeUDPPacket(b)
+	if err != nil {
+		return 0, nil, err
+	}
+
+	udpAddr := addr.UDPAddr()
+	if udpAddr == nil {
+		return 0, nil, errors.New("parse udp addr error")
+	}
+
+	// due to DecodeUDPPacket is mutable, record addr length
+	copy(b, payload)
+	return n - len(addr) - 3, udpAddr, nil
+}
+
+func (uc *socksPacketConn) Close() error {
+	uc.tcpConn.Close()
+	return uc.PacketConn.Close()
+}

+ 208 - 0
core/Clash.Meta/adapter/outbound/ssh.go

@@ -0,0 +1,208 @@
+package outbound
+
+import (
+	"bytes"
+	"context"
+	"encoding/base64"
+	"fmt"
+	"net"
+	"os"
+	"runtime"
+	"strconv"
+	"strings"
+	"sync"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+
+	"github.com/metacubex/randv2"
+	"golang.org/x/crypto/ssh"
+)
+
+type Ssh struct {
+	*Base
+
+	option *SshOption
+	client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer
+}
+
+type SshOption struct {
+	BasicOption
+	Name                 string   `proxy:"name"`
+	Server               string   `proxy:"server"`
+	Port                 int      `proxy:"port"`
+	UserName             string   `proxy:"username"`
+	Password             string   `proxy:"password,omitempty"`
+	PrivateKey           string   `proxy:"private-key,omitempty"`
+	PrivateKeyPassphrase string   `proxy:"private-key-passphrase,omitempty"`
+	HostKey              []string `proxy:"host-key,omitempty"`
+	HostKeyAlgorithms    []string `proxy:"host-key-algorithms,omitempty"`
+}
+
+func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions(opts...)...)
+	if len(s.option.DialerProxy) > 0 {
+		cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	client, err := s.client.connect(ctx, cDialer, s.addr)
+	if err != nil {
+		return nil, err
+	}
+	c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress())
+	if err != nil {
+		return nil, err
+	}
+
+	return NewConn(N.NewRefConn(c, s), s), nil
+}
+
+type sshClient struct {
+	config *ssh.ClientConfig
+	client *ssh.Client
+	cMutex sync.Mutex
+}
+
+func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) {
+	s.cMutex.Lock()
+	defer s.cMutex.Unlock()
+	if s.client != nil {
+		return s.client, nil
+	}
+	c, err := cDialer.DialContext(ctx, "tcp", addr)
+	if err != nil {
+		return nil, err
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	if ctx.Done() != nil {
+		done := N.SetupContextForConn(ctx, c)
+		defer done(&err)
+	}
+
+	clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config)
+	if err != nil {
+		return nil, err
+	}
+	client = ssh.NewClient(clientConn, chans, reqs)
+
+	s.client = client
+
+	go func() {
+		_ = client.Wait() // wait shutdown
+		_ = client.Close()
+		s.cMutex.Lock()
+		defer s.cMutex.Unlock()
+		if s.client == client {
+			s.client = nil
+		}
+	}()
+
+	return client, nil
+}
+
+func (s *sshClient) Close() error {
+	s.cMutex.Lock()
+	defer s.cMutex.Unlock()
+	if s.client != nil {
+		return s.client.Close()
+	}
+	return nil
+}
+
+func closeSsh(s *Ssh) {
+	_ = s.client.Close()
+}
+
+func NewSsh(option SshOption) (*Ssh, error) {
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+
+	config := ssh.ClientConfig{
+		User:              option.UserName,
+		HostKeyCallback:   ssh.InsecureIgnoreHostKey(),
+		HostKeyAlgorithms: option.HostKeyAlgorithms,
+	}
+
+	if option.PrivateKey != "" {
+		var b []byte
+		var err error
+		if strings.Contains(option.PrivateKey, "PRIVATE KEY") {
+			b = []byte(option.PrivateKey)
+		} else {
+			b, err = os.ReadFile(C.Path.Resolve(option.PrivateKey))
+			if err != nil {
+				return nil, err
+			}
+		}
+		var pKey ssh.Signer
+		if option.PrivateKeyPassphrase != "" {
+			pKey, err = ssh.ParsePrivateKeyWithPassphrase(b, []byte(option.PrivateKeyPassphrase))
+		} else {
+			pKey, err = ssh.ParsePrivateKey(b)
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		config.Auth = append(config.Auth, ssh.PublicKeys(pKey))
+	}
+
+	if option.Password != "" {
+		config.Auth = append(config.Auth, ssh.Password(option.Password))
+	}
+
+	if len(option.HostKey) != 0 {
+		keys := make([]ssh.PublicKey, len(option.HostKey))
+		for i, hostKey := range option.HostKey {
+			key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(hostKey))
+			if err != nil {
+				return nil, fmt.Errorf("parse host key :%s", key)
+			}
+			keys[i] = key
+		}
+		config.HostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+			serverKey := key.Marshal()
+			for _, hostKey := range keys {
+				if bytes.Equal(serverKey, hostKey.Marshal()) {
+					return nil
+				}
+			}
+			return fmt.Errorf("host key mismatch, server send :%s %s", key.Type(), base64.StdEncoding.EncodeToString(serverKey))
+		}
+	}
+
+	version := "SSH-2.0-OpenSSH_"
+	if randv2.IntN(2) == 0 {
+		version += "7." + strconv.Itoa(randv2.IntN(10))
+	} else {
+		version += "8." + strconv.Itoa(randv2.IntN(9))
+	}
+	config.ClientVersion = version
+
+	outbound := &Ssh{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.Ssh,
+			udp:    false,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option: &option,
+		client: &sshClient{
+			config: &config,
+		},
+	}
+	runtime.SetFinalizer(outbound, closeSsh)
+
+	return outbound, nil
+}

+ 344 - 0
core/Clash.Meta/adapter/outbound/trojan.go

@@ -0,0 +1,344 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"strconv"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	tlsC "github.com/metacubex/mihomo/component/tls"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/gun"
+	"github.com/metacubex/mihomo/transport/shadowsocks/core"
+	"github.com/metacubex/mihomo/transport/trojan"
+)
+
+type Trojan struct {
+	*Base
+	instance *trojan.Trojan
+	option   *TrojanOption
+
+	// for gun mux
+	gunTLSConfig *tls.Config
+	gunConfig    *gun.Config
+	transport    *gun.TransportWrap
+
+	realityConfig *tlsC.RealityConfig
+
+	ssCipher core.Cipher
+}
+
+type TrojanOption struct {
+	BasicOption
+	Name              string         `proxy:"name"`
+	Server            string         `proxy:"server"`
+	Port              int            `proxy:"port"`
+	Password          string         `proxy:"password"`
+	ALPN              []string       `proxy:"alpn,omitempty"`
+	SNI               string         `proxy:"sni,omitempty"`
+	SkipCertVerify    bool           `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint       string         `proxy:"fingerprint,omitempty"`
+	UDP               bool           `proxy:"udp,omitempty"`
+	Network           string         `proxy:"network,omitempty"`
+	RealityOpts       RealityOptions `proxy:"reality-opts,omitempty"`
+	GrpcOpts          GrpcOptions    `proxy:"grpc-opts,omitempty"`
+	WSOpts            WSOptions      `proxy:"ws-opts,omitempty"`
+	SSOpts            TrojanSSOption `proxy:"ss-opts,omitempty"`
+	ClientFingerprint string         `proxy:"client-fingerprint,omitempty"`
+}
+
+// TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5
+type TrojanSSOption struct {
+	Enabled  bool   `proxy:"enabled,omitempty"`
+	Method   string `proxy:"method,omitempty"`
+	Password string `proxy:"password,omitempty"`
+}
+
+func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) {
+	if t.option.Network == "ws" {
+		host, port, _ := net.SplitHostPort(t.addr)
+		wsOpts := &trojan.WebsocketOption{
+			Host:                     host,
+			Port:                     port,
+			Path:                     t.option.WSOpts.Path,
+			V2rayHttpUpgrade:         t.option.WSOpts.V2rayHttpUpgrade,
+			V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
+			Headers:                  http.Header{},
+		}
+
+		if t.option.SNI != "" {
+			wsOpts.Host = t.option.SNI
+		}
+
+		if len(t.option.WSOpts.Headers) != 0 {
+			for key, value := range t.option.WSOpts.Headers {
+				wsOpts.Headers.Add(key, value)
+			}
+		}
+
+		return t.instance.StreamWebsocketConn(ctx, c, wsOpts)
+	}
+
+	return t.instance.StreamConn(ctx, c)
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	var err error
+
+	if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
+		t.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
+	}
+
+	if t.transport != nil {
+		c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
+	} else {
+		c, err = t.plainStream(ctx, c)
+	}
+
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
+	}
+
+	if t.ssCipher != nil {
+		c = t.ssCipher.StreamConn(c)
+	}
+
+	if metadata.NetWork == C.UDP {
+		err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
+		return c, err
+	}
+	err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
+	return c, err
+}
+
+// DialContext implements C.ProxyAdapter
+func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	// gun transport
+	if t.transport != nil && len(opts) == 0 {
+		c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
+		if err != nil {
+			return nil, err
+		}
+
+		if t.ssCipher != nil {
+			c = t.ssCipher.StreamConn(c)
+		}
+
+		if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
+			c.Close()
+			return nil, err
+		}
+
+		return NewConn(c, t), nil
+	}
+	return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
+}
+
+// DialContextWithDialer implements C.ProxyAdapter
+func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
+	if len(t.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", t.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
+	}
+	N.TCPKeepAlive(c)
+
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = t.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewConn(c, t), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
+	var c net.Conn
+
+	// grpc transport
+	if t.transport != nil && len(opts) == 0 {
+		c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
+		if err != nil {
+			return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
+		}
+		defer func(c net.Conn) {
+			safeConnClose(c, err)
+		}(c)
+
+		if t.ssCipher != nil {
+			c = t.ssCipher.StreamConn(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) {
+	if len(t.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", t.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
+	}
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+	N.TCPKeepAlive(c)
+	c, err = t.plainStream(ctx, c)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
+	}
+
+	if t.ssCipher != nil {
+		c = t.ssCipher.StreamConn(c)
+	}
+
+	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
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (t *Trojan) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+// ListenPacketOnStreamConn implements C.ProxyAdapter
+func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
+	pc := 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) {
+	addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+
+	tOption := &trojan.Option{
+		Password:          option.Password,
+		ALPN:              option.ALPN,
+		ServerName:        option.Server,
+		SkipCertVerify:    option.SkipCertVerify,
+		Fingerprint:       option.Fingerprint,
+		ClientFingerprint: option.ClientFingerprint,
+	}
+
+	if option.SNI != "" {
+		tOption.ServerName = option.SNI
+	}
+
+	t := &Trojan{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			tp:     C.Trojan,
+			udp:    option.UDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		instance: trojan.New(tOption),
+		option:   &option,
+	}
+
+	var err error
+	t.realityConfig, err = option.RealityOpts.Parse()
+	if err != nil {
+		return nil, err
+	}
+	tOption.Reality = t.realityConfig
+
+	if option.SSOpts.Enabled {
+		if option.SSOpts.Password == "" {
+			return nil, errors.New("empty password")
+		}
+		if option.SSOpts.Method == "" {
+			option.SSOpts.Method = "AES-128-GCM"
+		}
+		ciph, err := core.PickCipher(option.SSOpts.Method, nil, option.SSOpts.Password)
+		if err != nil {
+			return nil, err
+		}
+		t.ssCipher = ciph
+	}
+
+	if option.Network == "grpc" {
+		dialFn := func(network, addr string) (net.Conn, error) {
+			var err error
+			var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...)
+			if len(t.option.DialerProxy) > 0 {
+				cDialer, err = proxydialer.NewByName(t.option.DialerProxy, cDialer)
+				if err != nil {
+					return nil, err
+				}
+			}
+			c, err := cDialer.DialContext(context.Background(), "tcp", t.addr)
+			if err != nil {
+				return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
+			}
+			N.TCPKeepAlive(c)
+			return c, nil
+		}
+
+		tlsConfig := &tls.Config{
+			NextProtos:         option.ALPN,
+			MinVersion:         tls.VersionTLS12,
+			InsecureSkipVerify: tOption.SkipCertVerify,
+			ServerName:         tOption.ServerName,
+		}
+
+		var err error
+		tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
+		if err != nil {
+			return nil, err
+		}
+
+		t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)
+
+		t.gunTLSConfig = tlsConfig
+		t.gunConfig = &gun.Config{
+			ServiceName: option.GrpcOpts.GrpcServiceName,
+			Host:        tOption.ServerName,
+		}
+	}
+
+	return t, nil
+}

+ 316 - 0
core/Clash.Meta/adapter/outbound/tuic.go

@@ -0,0 +1,316 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"math"
+	"net"
+	"strconv"
+	"time"
+
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/tuic"
+
+	"github.com/gofrs/uuid/v5"
+	"github.com/metacubex/quic-go"
+	M "github.com/sagernet/sing/common/metadata"
+	"github.com/sagernet/sing/common/uot"
+)
+
+type Tuic struct {
+	*Base
+	option *TuicOption
+	client *tuic.PoolClient
+}
+
+type TuicOption struct {
+	BasicOption
+	Name                  string   `proxy:"name"`
+	Server                string   `proxy:"server"`
+	Port                  int      `proxy:"port"`
+	Token                 string   `proxy:"token,omitempty"`
+	UUID                  string   `proxy:"uuid,omitempty"`
+	Password              string   `proxy:"password,omitempty"`
+	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"`
+	CWND                 int    `proxy:"cwnd,omitempty"`
+	SkipCertVerify       bool   `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint          string `proxy:"fingerprint,omitempty"`
+	CustomCA             string `proxy:"ca,omitempty"`
+	CustomCAString       string `proxy:"ca-str,omitempty"`
+	ReceiveWindowConn    int    `proxy:"recv-window-conn,omitempty"`
+	ReceiveWindow        int    `proxy:"recv-window,omitempty"`
+	DisableMTUDiscovery  bool   `proxy:"disable-mtu-discovery,omitempty"`
+	MaxDatagramFrameSize int    `proxy:"max-datagram-frame-size,omitempty"`
+	SNI                  string `proxy:"sni,omitempty"`
+
+	UDPOverStream        bool `proxy:"udp-over-stream,omitempty"`
+	UDPOverStreamVersion int  `proxy:"udp-over-stream-version,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) {
+	if t.option.UDPOverStream {
+		uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
+		uotMetadata := *metadata
+		uotMetadata.Host = uotDestination.Fqdn
+		uotMetadata.DstPort = uotDestination.Port
+		c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata)
+		if err != nil {
+			return nil, err
+		}
+
+		// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
+		if !metadata.Resolved() {
+			ip, err := resolver.ResolveIP(ctx, metadata.Host)
+			if err != nil {
+				return nil, errors.New("can't resolve ip")
+			}
+			metadata.DstIP = ip
+		}
+
+		destination := M.SocksaddrFromNet(metadata.UDPAddr())
+		if t.option.UDPOverStreamVersion == uot.LegacyVersion {
+			return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil
+		} else {
+			return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil
+		}
+	}
+	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() C.NetWork {
+	return C.ALLNet
+}
+
+func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) {
+	if len(t.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+	udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer)
+	if err != nil {
+		return nil, nil, err
+	}
+	addr = udpAddr
+	var pc net.PacketConn
+	pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
+	if err != nil {
+		return nil, nil, err
+	}
+	transport = &quic.Transport{Conn: pc}
+	transport.SetCreatedConn(true) // auto close conn
+	transport.SetSingleUse(true)   // auto close transport
+	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 err error
+	tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
+	if err != nil {
+		return nil, err
+	}
+
+	if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
+		tlsConfig.NextProtos = option.ALPN
+	} else {
+		tlsConfig.NextProtos = []string{"h3"}
+	}
+
+	if option.RequestTimeout == 0 {
+		option.RequestTimeout = 8000
+	}
+
+	if option.HeartbeatInterval <= 0 {
+		option.HeartbeatInterval = 10000
+	}
+
+	udpRelayMode := tuic.QUIC
+	if option.UdpRelayMode != "quic" {
+		udpRelayMode = tuic.NATIVE
+	}
+
+	if option.MaxUdpRelayPacketSize == 0 {
+		option.MaxUdpRelayPacketSize = 1252
+	}
+
+	if option.MaxOpenStreams == 0 {
+		option.MaxOpenStreams = 100
+	}
+
+	if option.CWND == 0 {
+		option.CWND = 32
+	}
+
+	packetOverHead := tuic.PacketOverHeadV4
+	if len(option.Token) == 0 {
+		packetOverHead = tuic.PacketOverHeadV5
+	}
+
+	if option.MaxDatagramFrameSize == 0 {
+		option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead
+	}
+
+	if option.MaxDatagramFrameSize > 1400 {
+		option.MaxDatagramFrameSize = 1400
+	}
+	option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead
+
+	// 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,
+		MaxDatagramFrameSize:           int64(option.MaxDatagramFrameSize),
+		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))
+	}
+	if option.DisableSni {
+		tlsConfig.ServerName = ""
+		tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
+	}
+
+	switch option.UDPOverStreamVersion {
+	case uot.Version, uot.LegacyVersion:
+	case 0:
+		option.UDPOverStreamVersion = uot.LegacyVersion
+	default:
+		return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion)
+	}
+
+	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),
+		},
+		option: &option,
+	}
+
+	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
+	}
+
+	if len(option.Token) > 0 {
+		tkn := tuic.GenTKN(option.Token)
+		clientOption := &tuic.ClientOptionV4{
+			TlsConfig:             tlsConfig,
+			QuicConfig:            quicConfig,
+			Token:                 tkn,
+			UdpRelayMode:          udpRelayMode,
+			CongestionController:  option.CongestionController,
+			ReduceRtt:             option.ReduceRtt,
+			RequestTimeout:        time.Duration(option.RequestTimeout) * time.Millisecond,
+			MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
+			FastOpen:              option.FastOpen,
+			MaxOpenStreams:        clientMaxOpenStreams,
+			CWND:                  option.CWND,
+		}
+
+		t.client = tuic.NewPoolClientV4(clientOption)
+	} else {
+		maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
+		if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
+			maxUdpRelayPacketSize = tuic.MaxFragSizeV5
+		}
+		clientOption := &tuic.ClientOptionV5{
+			TlsConfig:             tlsConfig,
+			QuicConfig:            quicConfig,
+			Uuid:                  uuid.FromStringOrNil(option.UUID),
+			Password:              option.Password,
+			UdpRelayMode:          udpRelayMode,
+			CongestionController:  option.CongestionController,
+			ReduceRtt:             option.ReduceRtt,
+			MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
+			MaxOpenStreams:        clientMaxOpenStreams,
+			CWND:                  option.CWND,
+		}
+
+		t.client = tuic.NewPoolClientV5(clientOption)
+	}
+
+	return t, nil
+}

+ 164 - 0
core/Clash.Meta/adapter/outbound/util.go

@@ -0,0 +1,164 @@
+package outbound
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"fmt"
+	"net"
+	"net/netip"
+	"regexp"
+	"strconv"
+	"sync"
+
+	"github.com/metacubex/mihomo/component/resolver"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/transport/socks5"
+)
+
+var (
+	globalClientSessionCache tls.ClientSessionCache
+	once                     sync.Once
+)
+
+func getClientSessionCache() tls.ClientSessionCache {
+	once.Do(func() {
+		globalClientSessionCache = tls.NewLRUClientSessionCache(128)
+	})
+	return globalClientSessionCache
+}
+
+func serializesSocksAddr(metadata *C.Metadata) []byte {
+	var buf [][]byte
+	addrType := metadata.AddrType()
+	aType := uint8(addrType)
+	p := uint(metadata.DstPort)
+	port := []byte{uint8(p >> 8), uint8(p & 0xff)}
+	switch addrType {
+	case socks5.AtypDomainName:
+		lenM := uint8(len(metadata.Host))
+		host := []byte(metadata.Host)
+		buf = [][]byte{{aType, lenM}, host, port}
+	case socks5.AtypIPv4:
+		host := metadata.DstIP.AsSlice()
+		buf = [][]byte{{aType}, host, port}
+	case socks5.AtypIPv6:
+		host := metadata.DstIP.AsSlice()
+		buf = [][]byte{{aType}, host, port}
+	}
+	return bytes.Join(buf, nil)
+}
+
+func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) {
+	host, port, err := net.SplitHostPort(address)
+	if err != nil {
+		return nil, err
+	}
+
+	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 {
+		return nil, err
+	}
+	return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
+}
+
+func safeConnClose(c net.Conn, err error) {
+	if err != nil && c != nil {
+		_ = c.Close()
+	}
+}
+
+var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
+
+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 = 1
+	switch m[2] {
+	case "T":
+		n *= 1000
+		fallthrough
+	case "G":
+		n *= 1000
+		fallthrough
+	case "M":
+		n *= 1000
+		fallthrough
+	case "K":
+		n *= 1000
+	}
+	v, _ := strconv.ParseUint(m[1], 10, 64)
+	n *= v
+	if m[3] == "b" {
+		// Bits, need to convert to bytes
+		n /= 8
+	}
+	return n
+}

+ 611 - 0
core/Clash.Meta/adapter/outbound/vless.go

@@ -0,0 +1,611 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strconv"
+	"sync"
+
+	"github.com/metacubex/mihomo/common/convert"
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	tlsC "github.com/metacubex/mihomo/component/tls"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+	"github.com/metacubex/mihomo/transport/gun"
+	"github.com/metacubex/mihomo/transport/socks5"
+	"github.com/metacubex/mihomo/transport/vless"
+	"github.com/metacubex/mihomo/transport/vmess"
+
+	vmessSing "github.com/metacubex/sing-vmess"
+	"github.com/metacubex/sing-vmess/packetaddr"
+	M "github.com/sagernet/sing/common/metadata"
+)
+
+const (
+	// max packet length
+	maxLength = 1024 << 3
+)
+
+type Vless struct {
+	*Base
+	client *vless.Client
+	option *VlessOption
+
+	// for gun mux
+	gunTLSConfig *tls.Config
+	gunConfig    *gun.Config
+	transport    *gun.TransportWrap
+
+	realityConfig *tlsC.RealityConfig
+}
+
+type VlessOption struct {
+	BasicOption
+	Name              string            `proxy:"name"`
+	Server            string            `proxy:"server"`
+	Port              int               `proxy:"port"`
+	UUID              string            `proxy:"uuid"`
+	Flow              string            `proxy:"flow,omitempty"`
+	TLS               bool              `proxy:"tls,omitempty"`
+	ALPN              []string          `proxy:"alpn,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"`
+	RealityOpts       RealityOptions    `proxy:"reality-opts,omitempty"`
+	HTTPOpts          HTTPOptions       `proxy:"http-opts,omitempty"`
+	HTTP2Opts         HTTP2Options      `proxy:"h2-opts,omitempty"`
+	GrpcOpts          GrpcOptions       `proxy:"grpc-opts,omitempty"`
+	WSOpts            WSOptions         `proxy:"ws-opts,omitempty"`
+	WSPath            string            `proxy:"ws-path,omitempty"`
+	WSHeaders         map[string]string `proxy:"ws-headers,omitempty"`
+	SkipCertVerify    bool              `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint       string            `proxy:"fingerprint,omitempty"`
+	ServerName        string            `proxy:"servername,omitempty"`
+	ClientFingerprint string            `proxy:"client-fingerprint,omitempty"`
+}
+
+func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	var err error
+
+	if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
+		v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
+	}
+
+	switch v.option.Network {
+	case "ws":
+		host, port, _ := net.SplitHostPort(v.addr)
+		wsOpts := &vmess.WebsocketConfig{
+			Host:                     host,
+			Port:                     port,
+			Path:                     v.option.WSOpts.Path,
+			MaxEarlyData:             v.option.WSOpts.MaxEarlyData,
+			EarlyDataHeaderName:      v.option.WSOpts.EarlyDataHeaderName,
+			V2rayHttpUpgrade:         v.option.WSOpts.V2rayHttpUpgrade,
+			V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
+			ClientFingerprint:        v.option.ClientFingerprint,
+			Headers:                  http.Header{},
+		}
+
+		if len(v.option.WSOpts.Headers) != 0 {
+			for key, value := range v.option.WSOpts.Headers {
+				wsOpts.Headers.Add(key, value)
+			}
+		}
+		if v.option.TLS {
+			wsOpts.TLS = true
+			tlsConfig := &tls.Config{
+				MinVersion:         tls.VersionTLS12,
+				ServerName:         host,
+				InsecureSkipVerify: v.option.SkipCertVerify,
+				NextProtos:         []string{"http/1.1"},
+			}
+
+			wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
+			if err != nil {
+				return nil, err
+			}
+
+			if v.option.ServerName != "" {
+				wsOpts.TLSConfig.ServerName = v.option.ServerName
+			} else if host := wsOpts.Headers.Get("Host"); 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(ctx, c, wsOpts)
+	case "http":
+		// readability first, so just copy default TLS logic
+		c, err = v.streamTLSConn(ctx, c, false)
+		if err != nil {
+			return nil, err
+		}
+
+		host, _, _ := net.SplitHostPort(v.addr)
+		httpOpts := &vmess.HTTPConfig{
+			Host:    host,
+			Method:  v.option.HTTPOpts.Method,
+			Path:    v.option.HTTPOpts.Path,
+			Headers: v.option.HTTPOpts.Headers,
+		}
+
+		c = vmess.StreamHTTPConn(c, httpOpts)
+	case "h2":
+		c, err = v.streamTLSConn(ctx, c, true)
+		if err != nil {
+			return nil, err
+		}
+
+		h2Opts := &vmess.H2Config{
+			Hosts: v.option.HTTP2Opts.Host,
+			Path:  v.option.HTTP2Opts.Path,
+		}
+
+		c, err = vmess.StreamH2Conn(c, h2Opts)
+	case "grpc":
+		c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
+	default:
+		// default tcp network
+		// handle TLS
+		c, err = v.streamTLSConn(ctx, c, false)
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return v.streamConn(c, metadata)
+}
+
+func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
+	if metadata.NetWork == C.UDP {
+		if v.option.PacketAddr {
+			metadata = &C.Metadata{
+				NetWork: C.UDP,
+				Host:    packetaddr.SeqPacketMagicAddress,
+				DstPort: 443,
+			}
+		} else {
+			metadata = &C.Metadata{ // a clear metadata only contains ip
+				NetWork: C.UDP,
+				DstIP:   metadata.DstIP,
+				DstPort: metadata.DstPort,
+			}
+		}
+		conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
+		if v.option.PacketAddr {
+			conn = packetaddr.NewBindConn(conn)
+		}
+	} else {
+		conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false))
+	}
+	if err != nil {
+		conn = nil
+	}
+	return
+}
+
+func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
+	if v.option.TLS {
+		host, _, _ := net.SplitHostPort(v.addr)
+
+		tlsOpts := vmess.TLSConfig{
+			Host:              host,
+			SkipCertVerify:    v.option.SkipCertVerify,
+			FingerPrint:       v.option.Fingerprint,
+			ClientFingerprint: v.option.ClientFingerprint,
+			Reality:           v.realityConfig,
+			NextProtos:        v.option.ALPN,
+		}
+
+		if isH2 {
+			tlsOpts.NextProtos = []string{"h2"}
+		}
+
+		if v.option.ServerName != "" {
+			tlsOpts.Host = v.option.ServerName
+		}
+
+		return vmess.StreamTLSConn(ctx, conn, &tlsOpts)
+	}
+
+	return conn, nil
+}
+
+// DialContext implements C.ProxyAdapter
+func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	// gun transport
+	if v.transport != nil && len(opts) == 0 {
+		c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
+		if err != nil {
+			return nil, err
+		}
+		defer func(c net.Conn) {
+			safeConnClose(c, err)
+		}(c)
+
+		c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
+		if err != nil {
+			return nil, err
+		}
+
+		return NewConn(c, v), nil
+	}
+	return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
+}
+
+// DialContextWithDialer implements C.ProxyAdapter
+func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
+	if len(v.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", v.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+	}
+	N.TCPKeepAlive(c)
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = v.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+	}
+	return NewConn(c, v), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+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 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
+	}
+	var c net.Conn
+	// gun transport
+	if v.transport != nil && len(opts) == 0 {
+		c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
+		if err != nil {
+			return nil, err
+		}
+		defer func(c net.Conn) {
+			safeConnClose(c, err)
+		}(c)
+
+		c, err = v.streamConn(c, metadata)
+		if err != nil {
+			return nil, fmt.Errorf("new vless client error: %v", err)
+		}
+
+		return v.ListenPacketOnStreamConn(ctx, 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) {
+	if len(v.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// 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 {
+		return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+	}
+	N.TCPKeepAlive(c)
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = v.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, fmt.Errorf("new vless client error: %v", err)
+	}
+
+	return v.ListenPacketOnStreamConn(ctx, c, metadata)
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (v *Vless) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+// ListenPacketOnStreamConn implements C.ProxyAdapter
+func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
+	// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
+	if !metadata.Resolved() {
+		ip, err := resolver.ResolveIP(ctx, metadata.Host)
+		if err != nil {
+			return nil, errors.New("can't resolve ip")
+		}
+		metadata.DstIP = ip
+	}
+
+	if v.option.XUDP {
+		var globalID [8]byte
+		if metadata.SourceValid() {
+			globalID = utils.GlobalID(metadata.SourceAddress())
+		}
+		return newPacketConn(N.NewThreadSafePacketConn(
+			vmessSing.NewXUDPConn(c,
+				globalID,
+				M.SocksaddrFromNet(metadata.UDPAddr())),
+		), v), nil
+	} else if v.option.PacketAddr {
+		return newPacketConn(N.NewThreadSafePacketConn(
+			packetaddr.NewConn(&vlessPacketConn{
+				Conn: c, rAddr: metadata.UDPAddr(),
+			}, M.SocksaddrFromNet(metadata.UDPAddr())),
+		), v), nil
+	}
+	return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil
+}
+
+// SupportUOT implements C.ProxyAdapter
+func (v *Vless) SupportUOT() bool {
+	return true
+}
+
+func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
+	var addrType byte
+	var addr []byte
+	switch metadata.AddrType() {
+	case socks5.AtypIPv4:
+		addrType = vless.AtypIPv4
+		addr = make([]byte, net.IPv4len)
+		copy(addr[:], metadata.DstIP.AsSlice())
+	case socks5.AtypIPv6:
+		addrType = vless.AtypIPv6
+		addr = make([]byte, net.IPv6len)
+		copy(addr[:], metadata.DstIP.AsSlice())
+	case socks5.AtypDomainName:
+		addrType = vless.AtypDomainName
+		addr = make([]byte, len(metadata.Host)+1)
+		addr[0] = byte(len(metadata.Host))
+		copy(addr[1:], metadata.Host)
+	}
+
+	return &vless.DstAddr{
+		UDP:      metadata.NetWork == C.UDP,
+		AddrType: addrType,
+		Addr:     addr,
+		Port:     metadata.DstPort,
+		Mux:      metadata.NetWork == C.UDP && xudp,
+	}
+}
+
+type vlessPacketConn struct {
+	net.Conn
+	rAddr  net.Addr
+	remain int
+	mux    sync.Mutex
+	cache  [2]byte
+}
+
+func (c *vlessPacketConn) writePacket(payload []byte) (int, error) {
+	binary.BigEndian.PutUint16(c.cache[:], uint16(len(payload)))
+
+	if _, err := c.Conn.Write(c.cache[:]); err != nil {
+		return 0, err
+	}
+
+	return c.Conn.Write(payload)
+}
+
+func (c *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
+	total := len(b)
+	if total == 0 {
+		return 0, nil
+	}
+
+	if total <= maxLength {
+		return c.writePacket(b)
+	}
+
+	offset := 0
+
+	for offset < total {
+		cursor := offset + maxLength
+		if cursor > total {
+			cursor = total
+		}
+
+		n, err := c.writePacket(b[offset:cursor])
+		if err != nil {
+			return offset + n, err
+		}
+
+		offset = cursor
+		if offset == total {
+			break
+		}
+	}
+
+	return total, nil
+}
+
+func (c *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	c.mux.Lock()
+	defer c.mux.Unlock()
+
+	if c.remain > 0 {
+		length := len(b)
+		if c.remain < length {
+			length = c.remain
+		}
+
+		n, err := c.Conn.Read(b[:length])
+		if err != nil {
+			return 0, c.rAddr, err
+		}
+
+		c.remain -= n
+		return n, c.rAddr, nil
+	}
+
+	if _, err := c.Conn.Read(b[:2]); err != nil {
+		return 0, c.rAddr, err
+	}
+
+	total := int(binary.BigEndian.Uint16(b[:2]))
+	if total == 0 {
+		return 0, c.rAddr, nil
+	}
+
+	length := len(b)
+	if length > total {
+		length = total
+	}
+
+	if _, err := io.ReadFull(c.Conn, b[:length]); err != nil {
+		return 0, c.rAddr, errors.New("read packet error")
+	}
+
+	c.remain = total - length
+
+	return length, c.rAddr, nil
+}
+
+func NewVless(option VlessOption) (*Vless, error) {
+	var addons *vless.Addons
+	if option.Network != "ws" && len(option.Flow) >= 16 {
+		option.Flow = option.Flow[:16]
+		switch option.Flow {
+		case vless.XRV:
+			log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
+			addons = &vless.Addons{
+				Flow: option.Flow,
+			}
+		case vless.XRO, vless.XRD, vless.XRS:
+			log.Fatalln("Legacy XTLS protocol %s is deprecated and no longer supported", option.Flow)
+		default:
+			return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
+		}
+	}
+
+	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
+		}
+	}
+	if option.XUDP {
+		option.PacketAddr = false
+	}
+
+	client, err := vless.NewClient(option.UUID, addons)
+	if err != nil {
+		return nil, err
+	}
+
+	v := &Vless{
+		Base: &Base{
+			name:   option.Name,
+			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
+			tp:     C.Vless,
+			udp:    option.UDP,
+			xudp:   option.XUDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		client: client,
+		option: &option,
+	}
+
+	v.realityConfig, err = v.option.RealityOpts.Parse()
+	if err != nil {
+		return nil, err
+	}
+
+	switch option.Network {
+	case "h2":
+		if len(option.HTTP2Opts.Host) == 0 {
+			option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
+		}
+	case "grpc":
+		dialFn := func(network, addr string) (net.Conn, error) {
+			var err error
+			var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
+			if len(v.option.DialerProxy) > 0 {
+				cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
+				if err != nil {
+					return nil, err
+				}
+			}
+			c, err := cDialer.DialContext(context.Background(), "tcp", v.addr)
+			if err != nil {
+				return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+			}
+			N.TCPKeepAlive(c)
+			return c, nil
+		}
+
+		gunConfig := &gun.Config{
+			ServiceName:       v.option.GrpcOpts.GrpcServiceName,
+			Host:              v.option.ServerName,
+			ClientFingerprint: v.option.ClientFingerprint,
+		}
+		if option.ServerName == "" {
+			gunConfig.Host = v.addr
+		}
+		var tlsConfig *tls.Config
+		if option.TLS {
+			tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
+				InsecureSkipVerify: v.option.SkipCertVerify,
+				ServerName:         v.option.ServerName,
+			})
+			if option.ServerName == "" {
+				host, _, _ := net.SplitHostPort(v.addr)
+				tlsConfig.ServerName = host
+			}
+		}
+
+		v.gunTLSConfig = tlsConfig
+		v.gunConfig = gunConfig
+
+		v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
+	}
+
+	return v, nil
+}

+ 536 - 0
core/Clash.Meta/adapter/outbound/vmess.go

@@ -0,0 +1,536 @@
+package outbound
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"sync"
+
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/ca"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	tlsC "github.com/metacubex/mihomo/component/tls"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/ntp"
+	"github.com/metacubex/mihomo/transport/gun"
+	mihomoVMess "github.com/metacubex/mihomo/transport/vmess"
+
+	vmess "github.com/metacubex/sing-vmess"
+	"github.com/metacubex/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 {
+	*Base
+	client *vmess.Client
+	option *VmessOption
+
+	// for gun mux
+	gunTLSConfig *tls.Config
+	gunConfig    *gun.Config
+	transport    *gun.TransportWrap
+
+	realityConfig *tlsC.RealityConfig
+}
+
+type VmessOption struct {
+	BasicOption
+	Name                string         `proxy:"name"`
+	Server              string         `proxy:"server"`
+	Port                int            `proxy:"port"`
+	UUID                string         `proxy:"uuid"`
+	AlterID             int            `proxy:"alterId"`
+	Cipher              string         `proxy:"cipher"`
+	UDP                 bool           `proxy:"udp,omitempty"`
+	Network             string         `proxy:"network,omitempty"`
+	TLS                 bool           `proxy:"tls,omitempty"`
+	ALPN                []string       `proxy:"alpn,omitempty"`
+	SkipCertVerify      bool           `proxy:"skip-cert-verify,omitempty"`
+	Fingerprint         string         `proxy:"fingerprint,omitempty"`
+	ServerName          string         `proxy:"servername,omitempty"`
+	RealityOpts         RealityOptions `proxy:"reality-opts,omitempty"`
+	HTTPOpts            HTTPOptions    `proxy:"http-opts,omitempty"`
+	HTTP2Opts           HTTP2Options   `proxy:"h2-opts,omitempty"`
+	GrpcOpts            GrpcOptions    `proxy:"grpc-opts,omitempty"`
+	WSOpts              WSOptions      `proxy:"ws-opts,omitempty"`
+	PacketAddr          bool           `proxy:"packet-addr,omitempty"`
+	XUDP                bool           `proxy:"xudp,omitempty"`
+	PacketEncoding      string         `proxy:"packet-encoding,omitempty"`
+	GlobalPadding       bool           `proxy:"global-padding,omitempty"`
+	AuthenticatedLength bool           `proxy:"authenticated-length,omitempty"`
+	ClientFingerprint   string         `proxy:"client-fingerprint,omitempty"`
+}
+
+type HTTPOptions struct {
+	Method  string              `proxy:"method,omitempty"`
+	Path    []string            `proxy:"path,omitempty"`
+	Headers map[string][]string `proxy:"headers,omitempty"`
+}
+
+type HTTP2Options struct {
+	Host []string `proxy:"host,omitempty"`
+	Path string   `proxy:"path,omitempty"`
+}
+
+type GrpcOptions struct {
+	GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
+}
+
+type WSOptions struct {
+	Path                     string            `proxy:"path,omitempty"`
+	Headers                  map[string]string `proxy:"headers,omitempty"`
+	MaxEarlyData             int               `proxy:"max-early-data,omitempty"`
+	EarlyDataHeaderName      string            `proxy:"early-data-header-name,omitempty"`
+	V2rayHttpUpgrade         bool              `proxy:"v2ray-http-upgrade,omitempty"`
+	V2rayHttpUpgradeFastOpen bool              `proxy:"v2ray-http-upgrade-fast-open,omitempty"`
+}
+
+// StreamConnContext implements C.ProxyAdapter
+func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	var err error
+
+	if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
+		v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
+	}
+
+	switch v.option.Network {
+	case "ws":
+		host, port, _ := net.SplitHostPort(v.addr)
+		wsOpts := &mihomoVMess.WebsocketConfig{
+			Host:                     host,
+			Port:                     port,
+			Path:                     v.option.WSOpts.Path,
+			MaxEarlyData:             v.option.WSOpts.MaxEarlyData,
+			EarlyDataHeaderName:      v.option.WSOpts.EarlyDataHeaderName,
+			V2rayHttpUpgrade:         v.option.WSOpts.V2rayHttpUpgrade,
+			V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
+			ClientFingerprint:        v.option.ClientFingerprint,
+			Headers:                  http.Header{},
+		}
+
+		if len(v.option.WSOpts.Headers) != 0 {
+			for key, value := range v.option.WSOpts.Headers {
+				wsOpts.Headers.Add(key, value)
+			}
+		}
+
+		if v.option.TLS {
+			wsOpts.TLS = true
+			tlsConfig := &tls.Config{
+				ServerName:         host,
+				InsecureSkipVerify: v.option.SkipCertVerify,
+				NextProtos:         []string{"http/1.1"},
+			}
+
+			wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
+			if err != nil {
+				return nil, err
+			}
+
+			if v.option.ServerName != "" {
+				wsOpts.TLSConfig.ServerName = v.option.ServerName
+			} else if host := wsOpts.Headers.Get("Host"); host != "" {
+				wsOpts.TLSConfig.ServerName = host
+			}
+		}
+		c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts)
+	case "http":
+		// readability first, so just copy default TLS logic
+		if v.option.TLS {
+			host, _, _ := net.SplitHostPort(v.addr)
+			tlsOpts := &mihomoVMess.TLSConfig{
+				Host:              host,
+				SkipCertVerify:    v.option.SkipCertVerify,
+				ClientFingerprint: v.option.ClientFingerprint,
+				Reality:           v.realityConfig,
+				NextProtos:        v.option.ALPN,
+			}
+
+			if v.option.ServerName != "" {
+				tlsOpts.Host = v.option.ServerName
+			}
+			c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		host, _, _ := net.SplitHostPort(v.addr)
+		httpOpts := &mihomoVMess.HTTPConfig{
+			Host:    host,
+			Method:  v.option.HTTPOpts.Method,
+			Path:    v.option.HTTPOpts.Path,
+			Headers: v.option.HTTPOpts.Headers,
+		}
+
+		c = mihomoVMess.StreamHTTPConn(c, httpOpts)
+	case "h2":
+		host, _, _ := net.SplitHostPort(v.addr)
+		tlsOpts := mihomoVMess.TLSConfig{
+			Host:              host,
+			SkipCertVerify:    v.option.SkipCertVerify,
+			FingerPrint:       v.option.Fingerprint,
+			NextProtos:        []string{"h2"},
+			ClientFingerprint: v.option.ClientFingerprint,
+			Reality:           v.realityConfig,
+		}
+
+		if v.option.ServerName != "" {
+			tlsOpts.Host = v.option.ServerName
+		}
+
+		c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts)
+		if err != nil {
+			return nil, err
+		}
+
+		h2Opts := &mihomoVMess.H2Config{
+			Hosts: v.option.HTTP2Opts.Host,
+			Path:  v.option.HTTP2Opts.Path,
+		}
+
+		c, err = mihomoVMess.StreamH2Conn(c, h2Opts)
+	case "grpc":
+		c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
+	default:
+		// handle TLS
+		if v.option.TLS {
+			host, _, _ := net.SplitHostPort(v.addr)
+			tlsOpts := &mihomoVMess.TLSConfig{
+				Host:              host,
+				SkipCertVerify:    v.option.SkipCertVerify,
+				FingerPrint:       v.option.Fingerprint,
+				ClientFingerprint: v.option.ClientFingerprint,
+				Reality:           v.realityConfig,
+				NextProtos:        v.option.ALPN,
+			}
+
+			if v.option.ServerName != "" {
+				tlsOpts.Host = v.option.ServerName
+			}
+
+			c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
+		}
+	}
+
+	if err != nil {
+		return nil, err
+	}
+	return v.streamConn(c, metadata)
+}
+
+func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
+	if metadata.NetWork == C.UDP {
+		if v.option.XUDP {
+			var globalID [8]byte
+			if metadata.SourceValid() {
+				globalID = utils.GlobalID(metadata.SourceAddress())
+			}
+			if N.NeedHandshake(c) {
+				conn = v.client.DialEarlyXUDPPacketConn(c,
+					globalID,
+					M.SocksaddrFromNet(metadata.UDPAddr()))
+			} else {
+				conn, err = v.client.DialXUDPPacketConn(c,
+					globalID,
+					M.SocksaddrFromNet(metadata.UDPAddr()))
+			}
+		} else if v.option.PacketAddr {
+			if N.NeedHandshake(c) {
+				conn = v.client.DialEarlyPacketConn(c,
+					M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
+			} else {
+				conn, err = v.client.DialPacketConn(c,
+					M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
+			}
+			conn = packetaddr.NewBindConn(conn)
+		} else {
+			if N.NeedHandshake(c) {
+				conn = v.client.DialEarlyPacketConn(c,
+					M.SocksaddrFromNet(metadata.UDPAddr()))
+			} else {
+				conn, err = v.client.DialPacketConn(c,
+					M.SocksaddrFromNet(metadata.UDPAddr()))
+			}
+		}
+	} else {
+		if N.NeedHandshake(c) {
+			conn = v.client.DialEarlyConn(c,
+				M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+		} else {
+			conn, err = v.client.DialConn(c,
+				M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+		}
+	}
+	if err != nil {
+		conn = nil
+	}
+	return
+}
+
+// DialContext implements C.ProxyAdapter
+func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	// gun transport
+	if v.transport != nil && len(opts) == 0 {
+		c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
+		if err != nil {
+			return nil, err
+		}
+		defer func(c net.Conn) {
+			safeConnClose(c, err)
+		}(c)
+
+		c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
+		if err != nil {
+			return nil, err
+		}
+
+		return NewConn(c, v), nil
+	}
+	return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
+}
+
+// DialContextWithDialer implements C.ProxyAdapter
+func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
+	if len(v.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+	c, err := dialer.DialContext(ctx, "tcp", v.addr)
+	if err != nil {
+		return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+	}
+	N.TCPKeepAlive(c)
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = v.StreamConnContext(ctx, c, metadata)
+	return NewConn(c, v), err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+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 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
+	}
+	var c net.Conn
+	// gun transport
+	if v.transport != nil && len(opts) == 0 {
+		c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
+		if err != nil {
+			return nil, err
+		}
+		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(ctx, 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) {
+	if len(v.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// 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())
+	}
+	N.TCPKeepAlive(c)
+	defer func(c net.Conn) {
+		safeConnClose(c, err)
+	}(c)
+
+	c, err = v.StreamConnContext(ctx, c, metadata)
+	if err != nil {
+		return nil, fmt.Errorf("new vmess client error: %v", err)
+	}
+	return v.ListenPacketOnStreamConn(ctx, c, metadata)
+}
+
+// SupportWithDialer implements C.ProxyAdapter
+func (v *Vmess) SupportWithDialer() C.NetWork {
+	return C.ALLNet
+}
+
+// ListenPacketOnStreamConn implements C.ProxyAdapter
+func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
+	// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
+	if !metadata.Resolved() {
+		ip, err := resolver.ResolveIP(ctx, metadata.Host)
+		if err != nil {
+			return nil, errors.New("can't resolve ip")
+		}
+		metadata.DstIP = ip
+	}
+
+	if pc, ok := c.(net.PacketConn); ok {
+		return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
+	}
+	return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
+}
+
+// SupportUOT implements C.ProxyAdapter
+func (v *Vmess) SupportUOT() bool {
+	return true
+}
+
+func NewVmess(option VmessOption) (*Vmess, error) {
+	security := strings.ToLower(option.Cipher)
+	var options []vmess.ClientOption
+	if option.GlobalPadding {
+		options = append(options, vmess.ClientWithGlobalPadding())
+	}
+	if option.AuthenticatedLength {
+		options = append(options, vmess.ClientWithAuthenticatedLength())
+	}
+	options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
+	client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
+	if err != nil {
+		return nil, err
+	}
+
+	switch option.PacketEncoding {
+	case "packetaddr", "packet":
+		option.PacketAddr = true
+	case "xudp":
+		option.XUDP = true
+	}
+	if option.XUDP {
+		option.PacketAddr = false
+	}
+
+	v := &Vmess{
+		Base: &Base{
+			name:   option.Name,
+			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
+			tp:     C.Vmess,
+			udp:    option.UDP,
+			xudp:   option.XUDP,
+			tfo:    option.TFO,
+			mpTcp:  option.MPTCP,
+			iface:  option.Interface,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		client: client,
+		option: &option,
+	}
+
+	switch option.Network {
+	case "h2":
+		if len(option.HTTP2Opts.Host) == 0 {
+			option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
+		}
+	case "grpc":
+		dialFn := func(network, addr string) (net.Conn, error) {
+			var err error
+			var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
+			if len(v.option.DialerProxy) > 0 {
+				cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
+				if err != nil {
+					return nil, err
+				}
+			}
+			c, err := cDialer.DialContext(context.Background(), "tcp", v.addr)
+			if err != nil {
+				return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
+			}
+			N.TCPKeepAlive(c)
+			return c, nil
+		}
+
+		gunConfig := &gun.Config{
+			ServiceName:       v.option.GrpcOpts.GrpcServiceName,
+			Host:              v.option.ServerName,
+			ClientFingerprint: v.option.ClientFingerprint,
+		}
+		if option.ServerName == "" {
+			gunConfig.Host = v.addr
+		}
+		var tlsConfig *tls.Config
+		if option.TLS {
+			tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
+				InsecureSkipVerify: v.option.SkipCertVerify,
+				ServerName:         v.option.ServerName,
+			})
+			if option.ServerName == "" {
+				host, _, _ := net.SplitHostPort(v.addr)
+				tlsConfig.ServerName = host
+			}
+		}
+
+		v.gunTLSConfig = tlsConfig
+		v.gunConfig = gunConfig
+
+		v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
+	}
+
+	v.realityConfig, err = v.option.RealityOpts.Parse()
+	if err != nil {
+		return nil, err
+	}
+
+	return v, nil
+}
+
+type vmessPacketConn struct {
+	net.Conn
+	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) {
+	allowedAddr := uc.rAddr
+	destAddr := addr
+	if allowedAddr.String() != destAddr.String() {
+		return 0, ErrUDPRemoteAddrMismatch
+	}
+	uc.access.Lock()
+	defer uc.access.Unlock()
+	return uc.Conn.Write(b)
+}
+
+func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+	n, err := uc.Conn.Read(b)
+	return n, uc.rAddr, err
+}

+ 663 - 0
core/Clash.Meta/adapter/outbound/wireguard.go

@@ -0,0 +1,663 @@
+package outbound
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"net"
+	"net/netip"
+	"runtime"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/metacubex/mihomo/common/atomic"
+	CN "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	"github.com/metacubex/mihomo/component/resolver"
+	"github.com/metacubex/mihomo/component/slowdown"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/dns"
+	"github.com/metacubex/mihomo/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"
+	"github.com/sagernet/wireguard-go/device"
+)
+
+type WireGuard struct {
+	*Base
+	bind      *wireguard.ClientBind
+	device    *device.Device
+	tunDevice wireguard.Device
+	dialer    proxydialer.SingDialer
+	resolver  *dns.Resolver
+	refP      *refProxyAdapter
+
+	initOk        atomic.Bool
+	initMutex     sync.Mutex
+	initErr       error
+	option        WireGuardOption
+	connectAddr   M.Socksaddr
+	localPrefixes []netip.Prefix
+
+	serverAddrMap   map[M.Socksaddr]netip.AddrPort
+	serverAddrTime  atomic.TypedValue[time.Time]
+	serverAddrMutex sync.Mutex
+
+	closeCh chan struct{} // for test
+}
+
+type WireGuardOption struct {
+	BasicOption
+	WireGuardPeerOption
+	Name                string `proxy:"name"`
+	Ip                  string `proxy:"ip,omitempty"`
+	Ipv6                string `proxy:"ipv6,omitempty"`
+	PrivateKey          string `proxy:"private-key"`
+	Workers             int    `proxy:"workers,omitempty"`
+	MTU                 int    `proxy:"mtu,omitempty"`
+	UDP                 bool   `proxy:"udp,omitempty"`
+	PersistentKeepalive int    `proxy:"persistent-keepalive,omitempty"`
+
+	Peers []WireGuardPeerOption `proxy:"peers,omitempty"`
+
+	RemoteDnsResolve bool     `proxy:"remote-dns-resolve,omitempty"`
+	Dns              []string `proxy:"dns,omitempty"`
+
+	RefreshServerIPInterval int `proxy:"refresh-server-ip-interval,omitempty"`
+}
+
+type WireGuardPeerOption struct {
+	Server       string   `proxy:"server"`
+	Port         int      `proxy:"port"`
+	PublicKey    string   `proxy:"public-key,omitempty"`
+	PreSharedKey string   `proxy:"pre-shared-key,omitempty"`
+	Reserved     []uint8  `proxy:"reserved,omitempty"`
+	AllowedIPs   []string `proxy:"allowed-ips,omitempty"`
+}
+
+type wgSingErrorHandler struct {
+	name string
+}
+
+var _ E.Handler = (*wgSingErrorHandler)(nil)
+
+func (w wgSingErrorHandler) NewError(ctx context.Context, err error) {
+	if E.IsClosedOrCanceled(err) {
+		log.SingLogger.Debug(fmt.Sprintf("[WG](%s) connection closed: %s", w.name, err))
+		return
+	}
+	log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err))
+}
+
+type wgNetDialer struct {
+	tunDevice wireguard.Device
+}
+
+var _ dialer.NetDialer = (*wgNetDialer)(nil)
+
+func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
+	return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap())
+}
+
+func (option WireGuardPeerOption) Addr() M.Socksaddr {
+	return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port))
+}
+
+func (option WireGuardOption) Prefixes() ([]netip.Prefix, error) {
+	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")
+	}
+	return localPrefixes, nil
+}
+
+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: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()),
+	}
+	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))
+		}
+		copy(reserved[:], option.Reserved)
+	}
+	var isConnect bool
+	if len(option.Peers) < 2 {
+		isConnect = true
+		if len(option.Peers) == 1 {
+			outbound.connectAddr = option.Peers[0].Addr()
+		} else {
+			outbound.connectAddr = option.Addr()
+		}
+	}
+	outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, outbound.connectAddr.AddrPort(), reserved)
+
+	var err error
+	outbound.localPrefixes, err = option.Prefixes()
+	if err != nil {
+		return nil, err
+	}
+
+	{
+		bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey)
+		if err != nil {
+			return nil, E.Cause(err, "decode private key")
+		}
+		option.PrivateKey = hex.EncodeToString(bytes)
+	}
+
+	if len(option.Peers) > 0 {
+		for i := range option.Peers {
+			peer := &option.Peers[i] // we need modify option here
+			bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
+			if err != nil {
+				return nil, E.Cause(err, "decode public key for peer ", i)
+			}
+			peer.PublicKey = hex.EncodeToString(bytes)
+
+			if peer.PreSharedKey != "" {
+				bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey)
+				if err != nil {
+					return nil, E.Cause(err, "decode pre shared key for peer ", i)
+				}
+				peer.PreSharedKey = hex.EncodeToString(bytes)
+			}
+
+			if len(peer.AllowedIPs) == 0 {
+				return nil, E.New("missing allowed_ips for peer ", i)
+			}
+
+			if len(peer.Reserved) > 0 {
+				if len(peer.Reserved) != 3 {
+					return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved))
+				}
+			}
+		}
+	} else {
+		{
+			bytes, err := base64.StdEncoding.DecodeString(option.PublicKey)
+			if err != nil {
+				return nil, E.Cause(err, "decode peer public key")
+			}
+			option.PublicKey = 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")
+			}
+			option.PreSharedKey = hex.EncodeToString(bytes)
+		}
+	}
+	outbound.option = option
+
+	mtu := option.MTU
+	if mtu == 0 {
+		mtu = 1408
+	}
+	if len(outbound.localPrefixes) == 0 {
+		return nil, E.New("missing local address")
+	}
+	outbound.tunDevice, err = wireguard.NewStackDevice(outbound.localPrefixes, uint32(mtu))
+	if err != nil {
+		return nil, E.Cause(err, "create WireGuard device")
+	}
+	outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
+		Verbosef: func(format string, args ...interface{}) {
+			log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
+		},
+		Errorf: func(format string, args ...interface{}) {
+			log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
+		},
+	}, option.Workers)
+
+	var has6 bool
+	for _, address := range outbound.localPrefixes {
+		if !address.Addr().Unmap().Is4() {
+			has6 = true
+			break
+		}
+	}
+
+	refP := &refProxyAdapter{}
+	outbound.refP = refP
+	if option.RemoteDnsResolve && len(option.Dns) > 0 {
+		nss, err := dns.ParseNameServer(option.Dns)
+		if err != nil {
+			return nil, err
+		}
+		for i := range nss {
+			nss[i].ProxyAdapter = refP
+		}
+		outbound.resolver = dns.NewResolver(dns.Config{
+			Main: nss,
+			IPv6: has6,
+		})
+	}
+
+	return outbound, nil
+}
+
+func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.AddrPort, error) {
+	if address.Addr.IsValid() {
+		return address.AddrPort(), nil
+	}
+	udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", address.String(), w.prefer)
+	if err != nil {
+		return netip.AddrPort{}, err
+	}
+	// net.ResolveUDPAddr maybe return 4in6 address, so unmap at here
+	addrPort := udpAddr.AddrPort()
+	return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port()), nil
+}
+
+func (w *WireGuard) init(ctx context.Context) error {
+	err := w.init0(ctx)
+	if err != nil {
+		return err
+	}
+	w.updateServerAddr(ctx)
+	return nil
+}
+
+func (w *WireGuard) init0(ctx context.Context) error {
+	if w.initOk.Load() {
+		return nil
+	}
+	w.initMutex.Lock()
+	defer w.initMutex.Unlock()
+	// double check like sync.Once
+	if w.initOk.Load() {
+		return nil
+	}
+	if w.initErr != nil {
+		return w.initErr
+	}
+
+	w.bind.ResetReservedForEndpoint()
+	w.serverAddrMap = make(map[M.Socksaddr]netip.AddrPort)
+	ipcConf, err := w.genIpcConf(ctx, false)
+	if err != nil {
+		// !!! do not set initErr here !!!
+		// let us can retry domain resolve in next time
+		return err
+	}
+
+	if debug.Enabled {
+		log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", w.option.Name, ipcConf))
+	}
+	err = w.device.IpcSet(ipcConf)
+	if err != nil {
+		w.initErr = E.Cause(err, "setup wireguard")
+		return w.initErr
+	}
+	w.serverAddrTime.Store(time.Now())
+
+	err = w.tunDevice.Start()
+	if err != nil {
+		w.initErr = err
+		return w.initErr
+	}
+
+	w.initOk.Store(true)
+	return nil
+}
+
+func (w *WireGuard) updateServerAddr(ctx context.Context) {
+	if w.option.RefreshServerIPInterval != 0 && time.Since(w.serverAddrTime.Load()) > time.Second*time.Duration(w.option.RefreshServerIPInterval) {
+		if w.serverAddrMutex.TryLock() {
+			defer w.serverAddrMutex.Unlock()
+			ipcConf, err := w.genIpcConf(ctx, true)
+			if err != nil {
+				log.Warnln("[WG](%s)UpdateServerAddr failed to generate wireguard ipc conf: %s", w.option.Name, err)
+				return
+			}
+			err = w.device.IpcSet(ipcConf)
+			if err != nil {
+				log.Warnln("[WG](%s)UpdateServerAddr failed to update wireguard ipc conf: %s", w.option.Name, err)
+				return
+			}
+			w.serverAddrTime.Store(time.Now())
+		}
+	}
+}
+
+func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, error) {
+	ipcConf := ""
+	if !updateOnly {
+		ipcConf += "private_key=" + w.option.PrivateKey + "\n"
+	}
+	if len(w.option.Peers) > 0 {
+		for i, peer := range w.option.Peers {
+			peerAddr := peer.Addr()
+			destination, err := w.resolve(ctx, peerAddr)
+			if err != nil {
+				return "", E.Cause(err, "resolve endpoint domain for peer ", i)
+			}
+			if w.serverAddrMap[peerAddr] != destination {
+				w.serverAddrMap[peerAddr] = destination
+			} else if updateOnly {
+				continue
+			}
+
+			if len(w.option.Peers) == 1 { // must call SetConnectAddr if isConnect == true
+				w.bind.SetConnectAddr(destination)
+			}
+			ipcConf += "public_key=" + peer.PublicKey + "\n"
+			if updateOnly {
+				ipcConf += "update_only=true\n"
+			}
+			ipcConf += "endpoint=" + destination.String() + "\n"
+			if len(peer.Reserved) > 0 {
+				var reserved [3]uint8
+				copy(reserved[:], w.option.Reserved)
+				w.bind.SetReservedForEndpoint(destination, reserved)
+			}
+			if updateOnly {
+				continue
+			}
+			if peer.PreSharedKey != "" {
+				ipcConf += "preshared_key=" + peer.PreSharedKey + "\n"
+			}
+			for _, allowedIP := range peer.AllowedIPs {
+				ipcConf += "allowed_ip=" + allowedIP + "\n"
+			}
+			if w.option.PersistentKeepalive != 0 {
+				ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive)
+			}
+		}
+	} else {
+		destination, err := w.resolve(ctx, w.connectAddr)
+		if err != nil {
+			return "", E.Cause(err, "resolve endpoint domain")
+		}
+		if w.serverAddrMap[w.connectAddr] != destination {
+			w.serverAddrMap[w.connectAddr] = destination
+		} else if updateOnly {
+			return "", nil
+		}
+		w.bind.SetConnectAddr(destination) // must call SetConnectAddr if isConnect == true
+		ipcConf += "public_key=" + w.option.PublicKey + "\n"
+		if updateOnly {
+			ipcConf += "update_only=true\n"
+		}
+		ipcConf += "endpoint=" + destination.String() + "\n"
+		if updateOnly {
+			return ipcConf, nil
+		}
+		if w.option.PreSharedKey != "" {
+			ipcConf += "preshared_key=" + w.option.PreSharedKey + "\n"
+		}
+		var has4, has6 bool
+		for _, address := range w.localPrefixes {
+			if address.Addr().Is4() {
+				has4 = true
+			} else {
+				has6 = true
+			}
+		}
+		if has4 {
+			ipcConf += "allowed_ip=0.0.0.0/0\n"
+		}
+		if has6 {
+			ipcConf += "allowed_ip=::/0\n"
+		}
+
+		if w.option.PersistentKeepalive != 0 {
+			ipcConf += fmt.Sprintf("persistent_keepalive_interval=%d\n", w.option.PersistentKeepalive)
+		}
+	}
+	return ipcConf, nil
+}
+
+func closeWireGuard(w *WireGuard) {
+	if w.device != nil {
+		w.device.Close()
+	}
+	_ = common.Close(w.tunDevice)
+	if w.closeCh != nil {
+		close(w.closeCh)
+	}
+}
+
+func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
+	options := w.Base.DialOptions(opts...)
+	w.dialer.SetDialer(dialer.NewDialer(options...))
+	var conn net.Conn
+	if err = w.init(ctx); err != nil {
+		return nil, err
+	}
+	if !metadata.Resolved() || w.resolver != nil {
+		r := resolver.DefaultResolver
+		if w.resolver != nil {
+			w.refP.SetProxyAdapter(w)
+			defer w.refP.ClearProxyAdapter()
+			r = w.resolver
+		}
+		options = append(options, dialer.WithResolver(r))
+		options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
+		conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
+	} else {
+		conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
+	}
+	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) {
+	options := w.Base.DialOptions(opts...)
+	w.dialer.SetDialer(dialer.NewDialer(options...))
+	var pc net.PacketConn
+	if err = w.init(ctx); err != nil {
+		return nil, err
+	}
+	if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
+		r := resolver.DefaultResolver
+		if w.resolver != nil {
+			w.refP.SetProxyAdapter(w)
+			defer w.refP.ClearProxyAdapter()
+			r = w.resolver
+		}
+		ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
+		if err != nil {
+			return nil, errors.New("can't resolve ip")
+		}
+		metadata.DstIP = ip
+	}
+	pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
+	if err != nil {
+		return nil, err
+	}
+	if pc == nil {
+		return nil, E.New("packetConn is nil")
+	}
+	return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
+	return true
+}
+
+type refProxyAdapter struct {
+	proxyAdapter C.ProxyAdapter
+	count        int
+	mutex        sync.Mutex
+}
+
+func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	r.proxyAdapter = proxyAdapter
+	r.count++
+}
+
+func (r *refProxyAdapter) ClearProxyAdapter() {
+	r.mutex.Lock()
+	defer r.mutex.Unlock()
+	r.count--
+	if r.count == 0 {
+		r.proxyAdapter = nil
+	}
+}
+
+func (r *refProxyAdapter) Name() string {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.Name()
+	}
+	return ""
+}
+
+func (r *refProxyAdapter) Type() C.AdapterType {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.Type()
+	}
+	return C.AdapterType(0)
+}
+
+func (r *refProxyAdapter) Addr() string {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.Addr()
+	}
+	return ""
+}
+
+func (r *refProxyAdapter) SupportUDP() bool {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.SupportUDP()
+	}
+	return false
+}
+
+func (r *refProxyAdapter) SupportXUDP() bool {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.SupportXUDP()
+	}
+	return false
+}
+
+func (r *refProxyAdapter) SupportTFO() bool {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.SupportTFO()
+	}
+	return false
+}
+
+func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.MarshalJSON()
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.DialContext(ctx, metadata, opts...)
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) SupportUOT() bool {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.SupportUOT()
+	}
+	return false
+}
+
+func (r *refProxyAdapter) SupportWithDialer() C.NetWork {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.SupportWithDialer()
+	}
+	return C.InvalidNet
+}
+
+func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
+	}
+	return nil, C.ErrNotSupport
+}
+
+func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.IsL3Protocol(metadata)
+	}
+	return false
+}
+
+func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	if r.proxyAdapter != nil {
+		return r.proxyAdapter.Unwrap(metadata, touch)
+	}
+	return nil
+}
+
+var _ C.ProxyAdapter = (*refProxyAdapter)(nil)

+ 44 - 0
core/Clash.Meta/adapter/outbound/wireguard_test.go

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

+ 177 - 0
core/Clash.Meta/adapter/outboundgroup/fallback.go

@@ -0,0 +1,177 @@
+package outboundgroup
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"time"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/common/callback"
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+)
+
+type Fallback struct {
+	*GroupBase
+	disableUDP     bool
+	testUrl        string
+	selected       string
+	expectedStatus string
+	Hidden         bool
+	Icon           string
+}
+
+func (f *Fallback) Now() string {
+	proxy := f.findAliveProxy(false)
+	return proxy.Name()
+}
+
+// DialContext implements C.ProxyAdapter
+func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	proxy := f.findAliveProxy(true)
+	c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
+	if err == nil {
+		c.AppendToChains(f)
+	} else {
+		f.onDialFailed(proxy.Type(), err)
+	}
+
+	if N.NeedHandshake(c) {
+		c = callback.NewFirstWriteCallBackConn(c, func(err error) {
+			if err == nil {
+				f.onDialSuccess()
+			} else {
+				f.onDialFailed(proxy.Type(), err)
+			}
+		})
+	}
+
+	return c, err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	proxy := f.findAliveProxy(true)
+	pc, err := proxy.ListenPacketContext(ctx, metadata, f.Base.DialOptions(opts...)...)
+	if err == nil {
+		pc.AppendToChains(f)
+	}
+
+	return pc, err
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (f *Fallback) SupportUDP() bool {
+	if f.disableUDP {
+		return false
+	}
+
+	proxy := f.findAliveProxy(false)
+	return proxy.SupportUDP()
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (f *Fallback) IsL3Protocol(metadata *C.Metadata) bool {
+	return f.findAliveProxy(false).IsL3Protocol(metadata)
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (f *Fallback) MarshalJSON() ([]byte, error) {
+	all := []string{}
+	for _, proxy := range f.GetProxies(false) {
+		all = append(all, proxy.Name())
+	}
+	return json.Marshal(map[string]any{
+		"type":           f.Type().String(),
+		"now":            f.Now(),
+		"all":            all,
+		"testUrl":        f.testUrl,
+		"expectedStatus": f.expectedStatus,
+		"fixed":          f.selected,
+		"hidden":         f.Hidden,
+		"icon":           f.Icon,
+	})
+}
+
+// Unwrap implements C.ProxyAdapter
+func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	proxy := f.findAliveProxy(touch)
+	return proxy
+}
+
+func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
+	proxies := f.GetProxies(touch)
+	for _, proxy := range proxies {
+		if len(f.selected) == 0 {
+			if proxy.AliveForTestUrl(f.testUrl) {
+				return proxy
+			}
+		} else {
+			if proxy.Name() == f.selected {
+				if proxy.AliveForTestUrl(f.testUrl) {
+					return proxy
+				} else {
+					f.selected = ""
+				}
+			}
+		}
+	}
+
+	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.AliveForTestUrl(f.testUrl) {
+		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
+		defer cancel()
+		expectedStatus, _ := utils.NewUnsignedRanges[uint16](f.expectedStatus)
+		_, _ = p.URLTest(ctx, f.testUrl, expectedStatus)
+	}
+
+	return nil
+}
+
+func (f *Fallback) ForceSet(name string) {
+	f.selected = name
+}
+
+func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
+	return &Fallback{
+		GroupBase: NewGroupBase(GroupBaseOption{
+			outbound.BaseOption{
+				Name:        option.Name,
+				Type:        C.Fallback,
+				Interface:   option.Interface,
+				RoutingMark: option.RoutingMark,
+			},
+			option.Filter,
+			option.ExcludeFilter,
+			option.ExcludeType,
+			option.TestTimeout,
+			option.MaxFailedTimes,
+			providers,
+		}),
+		disableUDP:     option.DisableUDP,
+		testUrl:        option.URL,
+		expectedStatus: option.ExpectedStatus,
+		Hidden:         option.Hidden,
+		Icon:           option.Icon,
+	}
+}

+ 295 - 0
core/Clash.Meta/adapter/outboundgroup/groupbase.go

@@ -0,0 +1,295 @@
+package outboundgroup
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/common/atomic"
+	"github.com/metacubex/mihomo/common/utils"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+	types "github.com/metacubex/mihomo/constant/provider"
+	"github.com/metacubex/mihomo/log"
+	"github.com/metacubex/mihomo/tunnel"
+
+	"github.com/dlclark/regexp2"
+)
+
+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
+	TestTimeout      int
+	maxFailedTimes   int
+}
+
+type GroupBaseOption struct {
+	outbound.BaseOption
+	filter         string
+	excludeFilter  string
+	excludeType    string
+	TestTimeout    int
+	maxFailedTimes int
+	providers      []provider.ProxyProvider
+}
+
+func NewGroupBase(opt GroupBaseOption) *GroupBase {
+	var excludeFilterReg *regexp2.Regexp
+	if opt.excludeFilter != "" {
+		excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, regexp2.None)
+	}
+	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, regexp2.None)
+			filterRegs = append(filterRegs, filterReg)
+		}
+	}
+
+	gb := &GroupBase{
+		Base:             outbound.NewBase(opt.BaseOption),
+		filterRegs:       filterRegs,
+		excludeFilterReg: excludeFilterReg,
+		excludeTypeArray: excludeTypeArray,
+		providers:        opt.providers,
+		failedTesting:    atomic.NewBool(false),
+		TestTimeout:      opt.TestTimeout,
+		maxFailedTimes:   opt.maxFailedTimes,
+	}
+
+	if gb.TestTimeout == 0 {
+		gb.TestTimeout = 5000
+	}
+	if gb.maxFailedTimes == 0 {
+		gb.maxFailedTimes = 5
+	}
+
+	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.MatchString(name); mat {
+							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(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.MatchString(name); mat {
+					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.MatchString(name); mat {
+				continue
+			}
+			newProxies = append(newProxies, p)
+		}
+		proxies = newProxies
+	}
+
+	if len(proxies) == 0 {
+		return append(proxies, tunnel.Proxies()["COMPATIBLE"])
+	}
+
+	return proxies
+}
+
+func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (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, expectedStatus)
+			if err == nil {
+				lock.Lock()
+				mp[proxy.Name()] = delay
+				lock.Unlock()
+			}
+
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+
+	if len(mp) == 0 {
+		return mp, fmt.Errorf("get delay: all proxies timeout")
+	} else {
+		return mp, nil
+	}
+}
+
+func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) {
+	if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop {
+		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) > time.Duration(gb.TestTimeout)*time.Millisecond {
+				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) onDialSuccess() {
+	if !gb.failedTesting.Load() {
+		gb.failedTimes = 0
+	}
+}

+ 278 - 0
core/Clash.Meta/adapter/outboundgroup/loadbalance.go

@@ -0,0 +1,278 @@
+package outboundgroup
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net"
+	"sync"
+	"time"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/common/callback"
+	"github.com/metacubex/mihomo/common/lru"
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+
+	"golang.org/x/net/publicsuffix"
+)
+
+type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy
+
+type LoadBalance struct {
+	*GroupBase
+	disableUDP     bool
+	strategyFn     strategyFn
+	testUrl        string
+	expectedStatus string
+	Hidden         bool
+	Icon           string
+}
+
+var errStrategy = errors.New("unsupported strategy")
+
+func parseStrategy(config map[string]any) string {
+	if strategy, ok := config["strategy"].(string); ok {
+		return strategy
+	}
+	return "consistent-hashing"
+}
+
+func getKey(metadata *C.Metadata) string {
+	if metadata == nil {
+		return ""
+	}
+
+	if metadata.Host != "" {
+		// ip host
+		if ip := net.ParseIP(metadata.Host); ip != nil {
+			return metadata.Host
+		}
+
+		if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil {
+			return etld
+		}
+	}
+
+	if !metadata.DstIP.IsValid() {
+		return ""
+	}
+
+	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 {
+	var b, j int64
+
+	for j < int64(buckets) {
+		b = j
+		key = key*2862933555777941757 + 1
+		j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
+	}
+
+	return int32(b)
+}
+
+// DialContext implements C.ProxyAdapter
+func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
+	proxy := lb.Unwrap(metadata, true)
+	c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
+
+	if err == nil {
+		c.AppendToChains(lb)
+	} else {
+		lb.onDialFailed(proxy.Type(), err)
+	}
+
+	if N.NeedHandshake(c) {
+		c = callback.NewFirstWriteCallBackConn(c, func(err error) {
+			if err == nil {
+				lb.onDialSuccess()
+			} else {
+				lb.onDialFailed(proxy.Type(), err)
+			}
+		})
+	}
+
+	return
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
+	defer func() {
+		if err == nil {
+			pc.AppendToChains(lb)
+		}
+	}()
+
+	proxy := lb.Unwrap(metadata, true)
+	return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (lb *LoadBalance) SupportUDP() bool {
+	return !lb.disableUDP
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool {
+	return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
+}
+
+func strategyRoundRobin(url string) strategyFn {
+	idx := 0
+	idxMutex := sync.Mutex{}
+	return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
+		idxMutex.Lock()
+		defer idxMutex.Unlock()
+
+		i := 0
+		length := len(proxies)
+
+		if touch {
+			defer func() {
+				idx = (idx + i) % length
+			}()
+		}
+
+		for ; i < length; i++ {
+			id := (idx + i) % length
+			proxy := proxies[id]
+			if proxy.AliveForTestUrl(url) {
+				i++
+				return proxy
+			}
+		}
+
+		return proxies[0]
+	}
+}
+
+func strategyConsistentHashing(url string) strategyFn {
+	maxRetry := 5
+	return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
+		key := utils.MapHash(getKey(metadata))
+		buckets := int32(len(proxies))
+		for i := 0; i < maxRetry; i, key = i+1, key+1 {
+			idx := jumpHash(key, buckets)
+			proxy := proxies[idx]
+			if proxy.AliveForTestUrl(url) {
+				return proxy
+			}
+		}
+
+		// when availability is poor, traverse the entire list to get the available nodes
+		for _, proxy := range proxies {
+			if proxy.AliveForTestUrl(url) {
+				return proxy
+			}
+		}
+
+		return proxies[0]
+	}
+}
+
+func strategyStickySessions(url string) strategyFn {
+	ttl := time.Minute * 10
+	maxRetry := 5
+	lruCache := lru.New[uint64, int](
+		lru.WithAge[uint64, int](int64(ttl.Seconds())),
+		lru.WithSize[uint64, int](1000))
+	return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
+		key := utils.MapHash(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.AliveForTestUrl(url) {
+				if nowIdx != idx {
+					lruCache.Set(key, nowIdx)
+				}
+
+				return proxy
+			} else {
+				nowIdx = int(jumpHash(key+uint64(time.Now().UnixNano()), int32(length)))
+			}
+		}
+
+		lruCache.Set(key, 0)
+		return proxies[0]
+	}
+}
+
+// Unwrap implements C.ProxyAdapter
+func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	proxies := lb.GetProxies(touch)
+	return lb.strategyFn(proxies, metadata, touch)
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
+	var all []string
+	for _, proxy := range lb.GetProxies(false) {
+		all = append(all, proxy.Name())
+	}
+	return json.Marshal(map[string]any{
+		"type":           lb.Type().String(),
+		"all":            all,
+		"testUrl":        lb.testUrl,
+		"expectedStatus": lb.expectedStatus,
+		"hidden":         lb.Hidden,
+		"icon":           lb.Icon,
+	})
+}
+
+func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
+	var strategyFn strategyFn
+	switch strategy {
+	case "consistent-hashing":
+		strategyFn = strategyConsistentHashing(option.URL)
+	case "round-robin":
+		strategyFn = strategyRoundRobin(option.URL)
+	case "sticky-sessions":
+		strategyFn = strategyStickySessions(option.URL)
+	default:
+		return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
+	}
+	return &LoadBalance{
+		GroupBase: NewGroupBase(GroupBaseOption{
+			outbound.BaseOption{
+				Name:        option.Name,
+				Type:        C.LoadBalance,
+				Interface:   option.Interface,
+				RoutingMark: option.RoutingMark,
+			},
+			option.Filter,
+			option.ExcludeFilter,
+			option.ExcludeType,
+			option.TestTimeout,
+			option.MaxFailedTimes,
+			providers,
+		}),
+		strategyFn:     strategyFn,
+		disableUDP:     option.DisableUDP,
+		testUrl:        option.URL,
+		expectedStatus: option.ExpectedStatus,
+		Hidden:         option.Hidden,
+		Icon:           option.Icon,
+	}, nil
+}

+ 220 - 0
core/Clash.Meta/adapter/outboundgroup/parser.go

@@ -0,0 +1,220 @@
+package outboundgroup
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/dlclark/regexp2"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/adapter/provider"
+	"github.com/metacubex/mihomo/common/structure"
+	"github.com/metacubex/mihomo/common/utils"
+	C "github.com/metacubex/mihomo/constant"
+	types "github.com/metacubex/mihomo/constant/provider"
+)
+
+var (
+	errFormat            = errors.New("format error")
+	errType              = errors.New("unsupported type")
+	errMissProxy         = errors.New("`use` or `proxies` missing")
+	errDuplicateProvider = errors.New("duplicate provider name")
+)
+
+type GroupCommonOption struct {
+	outbound.BasicOption
+	Name                string   `group:"name"`
+	Type                string   `group:"type"`
+	Proxies             []string `group:"proxies,omitempty"`
+	Use                 []string `group:"use,omitempty"`
+	URL                 string   `group:"url,omitempty"`
+	Interval            int      `group:"interval,omitempty"`
+	TestTimeout         int      `group:"timeout,omitempty"`
+	MaxFailedTimes      int      `group:"max-failed-times,omitempty"`
+	Lazy                bool     `group:"lazy,omitempty"`
+	DisableUDP          bool     `group:"disable-udp,omitempty"`
+	Filter              string   `group:"filter,omitempty"`
+	ExcludeFilter       string   `group:"exclude-filter,omitempty"`
+	ExcludeType         string   `group:"exclude-type,omitempty"`
+	ExpectedStatus      string   `group:"expected-status,omitempty"`
+	IncludeAll          bool     `group:"include-all,omitempty"`
+	IncludeAllProxies   bool     `group:"include-all-proxies,omitempty"`
+	IncludeAllProviders bool     `group:"include-all-providers,omitempty"`
+	Hidden              bool     `group:"hidden,omitempty"`
+	Icon                string   `group:"icon,omitempty"`
+}
+
+func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
+	decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
+
+	groupOption := &GroupCommonOption{
+		Lazy: true,
+	}
+	if err := decoder.Decode(config, groupOption); err != nil {
+		return nil, errFormat
+	}
+
+	if groupOption.Type == "" || groupOption.Name == "" {
+		return nil, errFormat
+	}
+
+	groupName := groupOption.Name
+
+	providers := []types.ProxyProvider{}
+
+	if groupOption.IncludeAll {
+		groupOption.IncludeAllProviders = true
+		groupOption.IncludeAllProxies = true
+	}
+
+	if groupOption.IncludeAllProviders {
+		groupOption.Use = AllProviders
+	}
+	if groupOption.IncludeAllProxies {
+		if groupOption.Filter != "" {
+			var filterRegs []*regexp2.Regexp
+			for _, filter := range strings.Split(groupOption.Filter, "`") {
+				filterReg := regexp2.MustCompile(filter, regexp2.None)
+				filterRegs = append(filterRegs, filterReg)
+			}
+			for _, p := range AllProxies {
+				for _, filterReg := range filterRegs {
+					if mat, _ := filterReg.MatchString(p); mat {
+						groupOption.Proxies = append(groupOption.Proxies, p)
+					}
+				}
+			}
+		} else {
+			groupOption.Proxies = append(groupOption.Proxies, AllProxies...)
+		}
+	}
+
+	if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
+		return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
+	}
+
+	expectedStatus, err := utils.NewUnsignedRanges[uint16](groupOption.ExpectedStatus)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", groupName, err)
+	}
+
+	status := strings.TrimSpace(groupOption.ExpectedStatus)
+	if status == "" {
+		status = "*"
+	}
+	groupOption.ExpectedStatus = status
+
+	if len(groupOption.Use) != 0 {
+		PDs, err := getProviders(providersMap, groupOption.Use)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", groupName, err)
+		}
+
+		// if test URL is empty, use the first health check URL of providers
+		if groupOption.URL == "" {
+			for _, pd := range PDs {
+				if pd.HealthCheckURL() != "" {
+					groupOption.URL = pd.HealthCheckURL()
+					break
+				}
+			}
+			if groupOption.URL == "" {
+				groupOption.URL = C.DefaultTestURL
+			}
+		} else {
+			addTestUrlToProviders(PDs, groupOption.URL, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
+		}
+		providers = append(providers, PDs...)
+	}
+
+	if len(groupOption.Proxies) != 0 {
+		ps, err := getProxies(proxyMap, groupOption.Proxies)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", groupName, err)
+		}
+
+		if _, ok := providersMap[groupName]; ok {
+			return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
+		}
+
+		if groupOption.URL == "" {
+			groupOption.URL = C.DefaultTestURL
+		}
+
+		// select don't need auto health check
+		if groupOption.Type != "select" && groupOption.Type != "relay" {
+			if groupOption.Interval == 0 {
+				groupOption.Interval = 300
+			}
+		}
+
+		hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus)
+
+		pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", groupName, err)
+		}
+
+		providers = append([]types.ProxyProvider{pd}, providers...)
+		providersMap[groupName] = pd
+	}
+
+	var group C.ProxyAdapter
+	switch groupOption.Type {
+	case "url-test":
+		opts := parseURLTestOption(config)
+		group = NewURLTest(groupOption, providers, opts...)
+	case "select":
+		group = NewSelector(groupOption, providers)
+	case "fallback":
+		group = NewFallback(groupOption, providers)
+	case "load-balance":
+		strategy := parseStrategy(config)
+		return NewLoadBalance(groupOption, providers, strategy)
+	case "relay":
+		group = NewRelay(groupOption, providers)
+	default:
+		return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
+	}
+
+	return group, nil
+}
+
+func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
+	var ps []C.Proxy
+	for _, name := range list {
+		p, ok := mapping[name]
+		if !ok {
+			return nil, fmt.Errorf("'%s' not found", name)
+		}
+		ps = append(ps, p)
+	}
+	return ps, nil
+}
+
+func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]types.ProxyProvider, error) {
+	var ps []types.ProxyProvider
+	for _, name := range list {
+		p, ok := mapping[name]
+		if !ok {
+			return nil, fmt.Errorf("'%s' not found", name)
+		}
+
+		if p.VehicleType() == types.Compatible {
+			return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
+		}
+		ps = append(ps, p)
+	}
+	return ps, nil
+}
+
+func addTestUrlToProviders(providers []types.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
+	if len(providers) == 0 || len(url) == 0 {
+		return
+	}
+
+	for _, pd := range providers {
+		pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval)
+	}
+}

+ 64 - 0
core/Clash.Meta/adapter/outboundgroup/patch_android.go

@@ -0,0 +1,64 @@
+//go:build android
+
+package outboundgroup
+
+import (
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+)
+
+type ProxyGroup interface {
+	C.ProxyAdapter
+
+	Providers() []provider.ProxyProvider
+	Proxies() []C.Proxy
+	Now() string
+}
+
+func (f *Fallback) Providers() []provider.ProxyProvider {
+	return f.providers
+}
+
+func (lb *LoadBalance) Providers() []provider.ProxyProvider {
+	return lb.providers
+}
+
+func (f *Fallback) Proxies() []C.Proxy {
+	return f.GetProxies(false)
+}
+
+func (lb *LoadBalance) Proxies() []C.Proxy {
+	return lb.GetProxies(false)
+}
+
+func (lb *LoadBalance) Now() string {
+	return ""
+}
+
+func (r *Relay) Providers() []provider.ProxyProvider {
+	return r.providers
+}
+
+func (r *Relay) Proxies() []C.Proxy {
+	return r.GetProxies(false)
+}
+
+func (r *Relay) Now() string {
+	return ""
+}
+
+func (s *Selector) Providers() []provider.ProxyProvider {
+	return s.providers
+}
+
+func (s *Selector) Proxies() []C.Proxy {
+	return s.GetProxies(false)
+}
+
+func (u *URLTest) Providers() []provider.ProxyProvider {
+	return u.providers
+}
+
+func (u *URLTest) Proxies() []C.Proxy {
+	return u.GetProxies(false)
+}

+ 172 - 0
core/Clash.Meta/adapter/outboundgroup/relay.go

@@ -0,0 +1,172 @@
+package outboundgroup
+
+import (
+	"context"
+	"encoding/json"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+	"github.com/metacubex/mihomo/log"
+)
+
+type Relay struct {
+	*GroupBase
+	Hidden bool
+	Icon   string
+}
+
+// DialContext implements C.ProxyAdapter
+func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	proxies, chainProxies := r.proxies(metadata, true)
+
+	switch len(proxies) {
+	case 0:
+		return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
+	case 1:
+		return proxies[0].DialContext(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.New(proxy, d, false)
+	}
+	last := proxies[len(proxies)-1]
+	conn, err := last.DialContextWithDialer(ctx, d, metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	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.New(proxy, d, false)
+	}
+	last := proxies[len(proxies)-1]
+	pc, err := last.ListenPacketWithDialer(ctx, d, metadata)
+	if err != nil {
+		return nil, err
+	}
+
+	for i := len(chainProxies) - 2; i >= 0; i-- {
+		pc.AppendToChains(chainProxies[i])
+	}
+
+	pc.AppendToChains(r)
+
+	return pc, nil
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (r *Relay) SupportUDP() bool {
+	proxies, _ := r.proxies(nil, false)
+	if len(proxies) == 0 { // C.Direct
+		return true
+	}
+	for i := len(proxies) - 1; i >= 0; i-- {
+		proxy := proxies[i]
+		if !proxy.SupportUDP() {
+			return false
+		}
+		if proxy.SupportUOT() {
+			return true
+		}
+		switch proxy.SupportWithDialer() {
+		case C.ALLNet:
+		case C.UDP:
+		default: // C.TCP and C.InvalidNet
+			return false
+		}
+	}
+	return true
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (r *Relay) MarshalJSON() ([]byte, error) {
+	all := []string{}
+	for _, proxy := range r.GetProxies(false) {
+		all = append(all, proxy.Name())
+	}
+	return json.Marshal(map[string]any{
+		"type":   r.Type().String(),
+		"all":    all,
+		"hidden": r.Hidden,
+		"icon":   r.Icon,
+	})
+}
+
+func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy) {
+	rawProxies := r.GetProxies(touch)
+
+	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) Addr() string {
+	proxies, _ := r.proxies(nil, false)
+	return proxies[len(proxies)-1].Addr()
+}
+
+func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
+	log.Warnln("The group [%s] with relay type is deprecated, please using dialer-proxy instead", option.Name)
+	return &Relay{
+		GroupBase: NewGroupBase(GroupBaseOption{
+			outbound.BaseOption{
+				Name:        option.Name,
+				Type:        C.Relay,
+				Interface:   option.Interface,
+				RoutingMark: option.RoutingMark,
+			},
+			"",
+			"",
+			"",
+			5000,
+			5,
+			providers,
+		}),
+		Hidden: option.Hidden,
+		Icon:   option.Icon,
+	}
+}

+ 126 - 0
core/Clash.Meta/adapter/outboundgroup/selector.go

@@ -0,0 +1,126 @@
+package outboundgroup
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+)
+
+type Selector struct {
+	*GroupBase
+	disableUDP bool
+	selected   string
+	Hidden     bool
+	Icon       string
+}
+
+// DialContext implements C.ProxyAdapter
+func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
+	if err == nil {
+		c.AppendToChains(s)
+	}
+	return c, err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
+	if err == nil {
+		pc.AppendToChains(s)
+	}
+	return pc, err
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (s *Selector) SupportUDP() bool {
+	if s.disableUDP {
+		return false
+	}
+
+	return s.selectedProxy(false).SupportUDP()
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (s *Selector) IsL3Protocol(metadata *C.Metadata) bool {
+	return s.selectedProxy(false).IsL3Protocol(metadata)
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (s *Selector) MarshalJSON() ([]byte, error) {
+	all := []string{}
+	for _, proxy := range s.GetProxies(false) {
+		all = append(all, proxy.Name())
+	}
+
+	return json.Marshal(map[string]any{
+		"type":   s.Type().String(),
+		"now":    s.Now(),
+		"all":    all,
+		"hidden": s.Hidden,
+		"icon":   s.Icon,
+	})
+}
+
+func (s *Selector) Now() string {
+	return s.selectedProxy(false).Name()
+}
+
+func (s *Selector) Set(name string) error {
+	for _, proxy := range s.GetProxies(false) {
+		if proxy.Name() == name {
+			s.selected = name
+			return nil
+		}
+	}
+
+	return errors.New("proxy not exist")
+}
+
+func (s *Selector) ForceSet(name string) {
+	s.selected = name
+}
+
+// Unwrap implements C.ProxyAdapter
+func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	return s.selectedProxy(touch)
+}
+
+func (s *Selector) selectedProxy(touch bool) C.Proxy {
+	proxies := s.GetProxies(touch)
+	for _, proxy := range proxies {
+		if proxy.Name() == s.selected {
+			return proxy
+		}
+	}
+
+	return proxies[0]
+}
+
+func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
+	return &Selector{
+		GroupBase: NewGroupBase(GroupBaseOption{
+			outbound.BaseOption{
+				Name:        option.Name,
+				Type:        C.Selector,
+				Interface:   option.Interface,
+				RoutingMark: option.RoutingMark,
+			},
+			option.Filter,
+			option.ExcludeFilter,
+			option.ExcludeType,
+			option.TestTimeout,
+			option.MaxFailedTimes,
+			providers,
+		}),
+		selected:   "COMPATIBLE",
+		disableUDP: option.DisableUDP,
+		Hidden:     option.Hidden,
+		Icon:       option.Icon,
+	}
+}

+ 255 - 0
core/Clash.Meta/adapter/outboundgroup/urltest.go

@@ -0,0 +1,255 @@
+package outboundgroup
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/common/callback"
+	N "github.com/metacubex/mihomo/common/net"
+	"github.com/metacubex/mihomo/common/singledo"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/dialer"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/constant/provider"
+)
+
+type urlTestOption func(*URLTest)
+
+func urlTestWithTolerance(tolerance uint16) urlTestOption {
+	return func(u *URLTest) {
+		u.tolerance = tolerance
+	}
+}
+
+type URLTest struct {
+	*GroupBase
+	selected       string
+	testUrl        string
+	expectedStatus string
+	tolerance      uint16
+	disableUDP     bool
+	Hidden         bool
+	Icon           string
+	fastNode       C.Proxy
+	fastSingle     *singledo.Single[C.Proxy]
+}
+
+func (u *URLTest) Now() string {
+	return u.fast(false).Name()
+}
+
+func (u *URLTest) Set(name string) error {
+	var p C.Proxy
+	for _, proxy := range u.GetProxies(false) {
+		if proxy.Name() == name {
+			p = proxy
+			break
+		}
+	}
+	if p == nil {
+		return errors.New("proxy not exist")
+	}
+	u.selected = name
+	u.fast(false)
+	return nil
+}
+
+func (u *URLTest) ForceSet(name string) {
+	u.selected = name
+}
+
+// DialContext implements C.ProxyAdapter
+func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
+	proxy := u.fast(true)
+	c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
+	if err == nil {
+		c.AppendToChains(u)
+	} else {
+		u.onDialFailed(proxy.Type(), err)
+	}
+
+	if N.NeedHandshake(c) {
+		c = callback.NewFirstWriteCallBackConn(c, func(err error) {
+			if err == nil {
+				u.onDialSuccess()
+			} else {
+				u.onDialFailed(proxy.Type(), err)
+			}
+		})
+	}
+
+	return c, err
+}
+
+// ListenPacketContext implements C.ProxyAdapter
+func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
+	proxy := u.fast(true)
+	pc, err := proxy.ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
+	if err == nil {
+		pc.AppendToChains(u)
+	}
+
+	return pc, err
+}
+
+// Unwrap implements C.ProxyAdapter
+func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
+	return u.fast(touch)
+}
+
+func (u *URLTest) fast(touch bool) C.Proxy {
+	proxies := u.GetProxies(touch)
+	if u.selected != "" {
+		for _, proxy := range proxies {
+			if !proxy.AliveForTestUrl(u.testUrl) {
+				continue
+			}
+			if proxy.Name() == u.selected {
+				u.fastNode = proxy
+				return proxy
+			}
+		}
+	}
+
+	elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
+		fast := proxies[0]
+		minDelay := fast.LastDelayForTestUrl(u.testUrl)
+		fastNotExist := true
+
+		for _, proxy := range proxies[1:] {
+			if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
+				fastNotExist = false
+			}
+
+			if !proxy.AliveForTestUrl(u.testUrl) {
+				continue
+			}
+
+			delay := proxy.LastDelayForTestUrl(u.testUrl)
+			if delay < minDelay {
+				fast = proxy
+				minDelay = delay
+			}
+
+		}
+		// tolerance
+		if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance {
+			u.fastNode = fast
+		}
+		return u.fastNode, nil
+	})
+	if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
+		u.Touch()
+	}
+
+	return elm
+}
+
+// SupportUDP implements C.ProxyAdapter
+func (u *URLTest) SupportUDP() bool {
+	if u.disableUDP {
+		return false
+	}
+	return u.fast(false).SupportUDP()
+}
+
+// IsL3Protocol implements C.ProxyAdapter
+func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool {
+	return u.fast(false).IsL3Protocol(metadata)
+}
+
+// MarshalJSON implements C.ProxyAdapter
+func (u *URLTest) MarshalJSON() ([]byte, error) {
+	all := []string{}
+	for _, proxy := range u.GetProxies(false) {
+		all = append(all, proxy.Name())
+	}
+	return json.Marshal(map[string]any{
+		"type":           u.Type().String(),
+		"now":            u.Now(),
+		"all":            all,
+		"testUrl":        u.testUrl,
+		"expectedStatus": u.expectedStatus,
+		"fixed":          u.selected,
+		"hidden":         u.Hidden,
+		"icon":           u.Icon,
+	})
+}
+
+func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
+	var wg sync.WaitGroup
+	var lock sync.Mutex
+	mp := map[string]uint16{}
+	proxies := u.GetProxies(false)
+	for _, proxy := range proxies {
+		proxy := proxy
+		wg.Add(1)
+		go func() {
+			delay, err := proxy.URLTest(ctx, u.testUrl, expectedStatus)
+			if err == nil {
+				lock.Lock()
+				mp[proxy.Name()] = delay
+				lock.Unlock()
+			}
+
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+
+	if len(mp) == 0 {
+		return mp, fmt.Errorf("get delay: all proxies timeout")
+	} else {
+		return mp, nil
+	}
+}
+
+func parseURLTestOption(config map[string]any) []urlTestOption {
+	opts := []urlTestOption{}
+
+	// tolerance
+	if elm, ok := config["tolerance"]; ok {
+		if tolerance, ok := elm.(int); ok {
+			opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
+		}
+	}
+
+	return opts
+}
+
+func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
+	urlTest := &URLTest{
+		GroupBase: NewGroupBase(GroupBaseOption{
+			outbound.BaseOption{
+				Name:        option.Name,
+				Type:        C.URLTest,
+				Interface:   option.Interface,
+				RoutingMark: option.RoutingMark,
+			},
+
+			option.Filter,
+			option.ExcludeFilter,
+			option.ExcludeType,
+			option.TestTimeout,
+			option.MaxFailedTimes,
+			providers,
+		}),
+		fastSingle:     singledo.NewSingle[C.Proxy](time.Second * 10),
+		disableUDP:     option.DisableUDP,
+		testUrl:        option.URL,
+		expectedStatus: option.ExpectedStatus,
+		Hidden:         option.Hidden,
+		Icon:           option.Icon,
+	}
+
+	for _, option := range options {
+		option(urlTest)
+	}
+
+	return urlTest
+}

+ 6 - 0
core/Clash.Meta/adapter/outboundgroup/util.go

@@ -0,0 +1,6 @@
+package outboundgroup
+
+type SelectAble interface {
+	Set(string) error
+	ForceSet(name string)
+}

+ 167 - 0
core/Clash.Meta/adapter/parser.go

@@ -0,0 +1,167 @@
+package adapter
+
+import (
+	"fmt"
+
+	tlsC "github.com/metacubex/mihomo/component/tls"
+
+	"github.com/metacubex/mihomo/adapter/outbound"
+	"github.com/metacubex/mihomo/common/structure"
+	C "github.com/metacubex/mihomo/constant"
+)
+
+func ParseProxy(mapping map[string]any) (C.Proxy, error) {
+	decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer})
+	proxyType, existType := mapping["type"].(string)
+	if !existType {
+		return nil, fmt.Errorf("missing type")
+	}
+
+	var (
+		proxy C.ProxyAdapter
+		err   error
+	)
+	switch proxyType {
+	case "ss":
+		ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
+		err = decoder.Decode(mapping, ssOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewShadowSocks(*ssOption)
+	case "ssr":
+		ssrOption := &outbound.ShadowSocksROption{}
+		err = decoder.Decode(mapping, ssrOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewShadowSocksR(*ssrOption)
+	case "socks5":
+		socksOption := &outbound.Socks5Option{}
+		err = decoder.Decode(mapping, socksOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewSocks5(*socksOption)
+	case "http":
+		httpOption := &outbound.HttpOption{}
+		err = decoder.Decode(mapping, httpOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewHttp(*httpOption)
+	case "vmess":
+		vmessOption := &outbound.VmessOption{
+			HTTPOpts: outbound.HTTPOptions{
+				Method: "GET",
+				Path:   []string{"/"},
+			},
+			ClientFingerprint: tlsC.GetGlobalFingerprint(),
+		}
+
+		err = decoder.Decode(mapping, vmessOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewVmess(*vmessOption)
+	case "vless":
+		vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
+		err = decoder.Decode(mapping, vlessOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewVless(*vlessOption)
+	case "snell":
+		snellOption := &outbound.SnellOption{}
+		err = decoder.Decode(mapping, snellOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewSnell(*snellOption)
+	case "trojan":
+		trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
+		err = decoder.Decode(mapping, trojanOption)
+		if err != nil {
+			break
+		}
+		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 "hysteria2":
+		hyOption := &outbound.Hysteria2Option{}
+		err = decoder.Decode(mapping, hyOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewHysteria2(*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)
+	case "direct":
+		directOption := &outbound.DirectOption{}
+		err = decoder.Decode(mapping, directOption)
+		if err != nil {
+			break
+		}
+		proxy = outbound.NewDirectWithOption(*directOption)
+	case "dns":
+		dnsOptions := &outbound.DnsOption{}
+		err = decoder.Decode(mapping, dnsOptions)
+		if err != nil {
+			break
+		}
+		proxy = outbound.NewDnsWithOption(*dnsOptions)
+	case "reject":
+		rejectOption := &outbound.RejectOption{}
+		err = decoder.Decode(mapping, rejectOption)
+		if err != nil {
+			break
+		}
+		proxy = outbound.NewRejectWithOption(*rejectOption)
+	case "ssh":
+		sshOption := &outbound.SshOption{}
+		err = decoder.Decode(mapping, sshOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewSsh(*sshOption)
+	default:
+		return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	if muxMapping, muxExist := mapping["smux"].(map[string]any); muxExist {
+		muxOption := &outbound.SingMuxOption{}
+		err = decoder.Decode(muxMapping, muxOption)
+		if err != nil {
+			return nil, err
+		}
+		if muxOption.Enabled {
+			proxy, err = outbound.NewSingMux(*muxOption, proxy, proxy.(outbound.ProxyBase))
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return NewProxy(proxy), nil
+}

+ 236 - 0
core/Clash.Meta/adapter/provider/healthcheck.go

@@ -0,0 +1,236 @@
+package provider
+
+import (
+	"context"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/metacubex/mihomo/common/atomic"
+	"github.com/metacubex/mihomo/common/batch"
+	"github.com/metacubex/mihomo/common/singledo"
+	"github.com/metacubex/mihomo/common/utils"
+	C "github.com/metacubex/mihomo/constant"
+	"github.com/metacubex/mihomo/log"
+
+	"github.com/dlclark/regexp2"
+)
+
+type HealthCheckOption struct {
+	URL      string
+	Interval uint
+}
+
+type extraOption struct {
+	expectedStatus utils.IntRanges[uint16]
+	filters        map[string]struct{}
+}
+
+type HealthCheck struct {
+	url            string
+	extra          map[string]*extraOption
+	mu             sync.Mutex
+	started        atomic.Bool
+	proxies        []C.Proxy
+	interval       time.Duration
+	lazy           bool
+	expectedStatus utils.IntRanges[uint16]
+	lastTouch      atomic.TypedValue[time.Time]
+	done           chan struct{}
+	singleDo       *singledo.Single[struct{}]
+	timeout        time.Duration
+}
+
+func (hc *HealthCheck) process() {
+	if hc.started.Load() {
+		log.Warnln("Skip start health check timer due to it's started")
+		return
+	}
+
+	ticker := time.NewTicker(hc.interval)
+	hc.start()
+	for {
+		select {
+		case <-ticker.C:
+			lastTouch := hc.lastTouch.Load()
+			since := time.Since(lastTouch)
+			if !hc.lazy || since < hc.interval {
+				hc.check()
+			} else {
+				log.Debugln("Skip once health check because we are lazy")
+			}
+		case <-hc.done:
+			ticker.Stop()
+			hc.stop()
+			return
+		}
+	}
+}
+
+func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
+	hc.proxies = proxies
+}
+
+func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
+	url = strings.TrimSpace(url)
+	if len(url) == 0 || url == hc.url {
+		log.Debugln("ignore invalid health check url: %s", url)
+		return
+	}
+
+	hc.mu.Lock()
+	defer hc.mu.Unlock()
+
+	// if the provider has not set up health checks, then modify it to be the same as the group's interval
+	if hc.interval == 0 {
+		hc.interval = time.Duration(interval) * time.Second
+	}
+
+	if hc.extra == nil {
+		hc.extra = make(map[string]*extraOption)
+	}
+
+	// prioritize the use of previously registered configurations, especially those from provider
+	if _, ok := hc.extra[url]; ok {
+		// provider default health check does not set filter
+		if url != hc.url && len(filter) != 0 {
+			splitAndAddFiltersToExtra(filter, hc.extra[url])
+		}
+
+		log.Debugln("health check url: %s exists", url)
+		return
+	}
+
+	option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
+	splitAndAddFiltersToExtra(filter, option)
+	hc.extra[url] = option
+
+	if hc.auto() && !hc.started.Load() {
+		go hc.process()
+	}
+}
+
+func splitAndAddFiltersToExtra(filter string, option *extraOption) {
+	filter = strings.TrimSpace(filter)
+	if len(filter) != 0 {
+		for _, regex := range strings.Split(filter, "`") {
+			regex = strings.TrimSpace(regex)
+			if len(regex) != 0 {
+				option.filters[regex] = struct{}{}
+			}
+		}
+	}
+}
+
+func (hc *HealthCheck) auto() bool {
+	return hc.interval != 0
+}
+
+func (hc *HealthCheck) touch() {
+	hc.lastTouch.Store(time.Now())
+}
+
+func (hc *HealthCheck) start() {
+	hc.started.Store(true)
+}
+
+func (hc *HealthCheck) stop() {
+	hc.started.Store(false)
+}
+
+func (hc *HealthCheck) check() {
+	if len(hc.proxies) == 0 {
+		return
+	}
+
+	_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
+		id := utils.NewUUIDV4().String()
+		log.Debugln("Start New Health Checking {%s}", id)
+		b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
+
+		// execute default health check
+		option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
+		hc.execute(b, hc.url, id, option)
+
+		// execute extra health check
+		if len(hc.extra) != 0 {
+			for url, option := range hc.extra {
+				hc.execute(b, url, id, option)
+			}
+		}
+		b.Wait()
+		log.Debugln("Finish A Health Checking {%s}", id)
+		return struct{}{}, nil
+	})
+}
+
+func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
+	url = strings.TrimSpace(url)
+	if len(url) == 0 {
+		log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
+		return
+	}
+
+	var filterReg *regexp2.Regexp
+	var expectedStatus utils.IntRanges[uint16]
+	if option != nil {
+		expectedStatus = option.expectedStatus
+		if len(option.filters) != 0 {
+			filters := make([]string, 0, len(option.filters))
+			for filter := range option.filters {
+				filters = append(filters, filter)
+			}
+
+			filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None)
+		}
+	}
+
+	for _, proxy := range hc.proxies {
+		// skip proxies that do not require health check
+		if filterReg != nil {
+			if match, _ := filterReg.MatchString(proxy.Name()); !match {
+				continue
+			}
+		}
+
+		p := proxy
+		b.Go(p.Name(), func() (bool, error) {
+			ctx, cancel := context.WithTimeout(context.Background(), hc.timeout)
+			defer cancel()
+			log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
+			delay, _ := p.URLTest(ctx, url, expectedStatus)
+			name := p.Name()
+			if HealthcheckHook != nil {
+				HealthcheckHook(name, delay)
+			}
+			log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", name, url, p.AliveForTestUrl(url), delay, uid)
+			return false, nil
+		})
+	}
+}
+
+func (hc *HealthCheck) close() {
+	hc.done <- struct{}{}
+}
+
+func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck {
+	if url == "" {
+		expectedStatus = nil
+		interval = 0
+	}
+	if timeout == 0 {
+		timeout = 5000
+	}
+
+	return &HealthCheck{
+		proxies:        proxies,
+		url:            url,
+		timeout:        time.Duration(timeout) * time.Millisecond,
+		extra:          map[string]*extraOption{},
+		interval:       time.Duration(interval) * time.Second,
+		lazy:           lazy,
+		expectedStatus: expectedStatus,
+		done:           make(chan struct{}, 1),
+		singleDo:       singledo.NewSingle[struct{}](time.Second),
+	}
+}

+ 113 - 0
core/Clash.Meta/adapter/provider/parser.go

@@ -0,0 +1,113 @@
+package provider
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/metacubex/mihomo/common/structure"
+	"github.com/metacubex/mihomo/common/utils"
+	"github.com/metacubex/mihomo/component/resource"
+	C "github.com/metacubex/mihomo/constant"
+	types "github.com/metacubex/mihomo/constant/provider"
+)
+
+var (
+	errVehicleType = errors.New("unsupport vehicle type")
+	errSubPath     = errors.New("path is not subpath of home directory")
+)
+
+type healthCheckSchema struct {
+	Enable         bool   `provider:"enable"`
+	URL            string `provider:"url"`
+	Interval       int    `provider:"interval"`
+	TestTimeout    int    `provider:"timeout,omitempty"`
+	Lazy           bool   `provider:"lazy,omitempty"`
+	ExpectedStatus string `provider:"expected-status,omitempty"`
+}
+
+type OverrideSchema struct {
+	TFO              *bool   `provider:"tfo,omitempty"`
+	MPTcp            *bool   `provider:"mptcp,omitempty"`
+	UDP              *bool   `provider:"udp,omitempty"`
+	UDPOverTCP       *bool   `provider:"udp-over-tcp,omitempty"`
+	Up               *string `provider:"up,omitempty"`
+	Down             *string `provider:"down,omitempty"`
+	DialerProxy      *string `provider:"dialer-proxy,omitempty"`
+	SkipCertVerify   *bool   `provider:"skip-cert-verify,omitempty"`
+	Interface        *string `provider:"interface-name,omitempty"`
+	RoutingMark      *int    `provider:"routing-mark,omitempty"`
+	IPVersion        *string `provider:"ip-version,omitempty"`
+	AdditionalPrefix *string `provider:"additional-prefix,omitempty"`
+	AdditionalSuffix *string `provider:"additional-suffix,omitempty"`
+}
+
+type proxyProviderSchema struct {
+	Type          string `provider:"type"`
+	Path          string `provider:"path,omitempty"`
+	URL           string `provider:"url,omitempty"`
+	Proxy         string `provider:"proxy,omitempty"`
+	Interval      int    `provider:"interval,omitempty"`
+	Filter        string `provider:"filter,omitempty"`
+	ExcludeFilter string `provider:"exclude-filter,omitempty"`
+	ExcludeType   string `provider:"exclude-type,omitempty"`
+	DialerProxy   string `provider:"dialer-proxy,omitempty"`
+
+	HealthCheck healthCheckSchema   `provider:"health-check,omitempty"`
+	Override    OverrideSchema      `provider:"override,omitempty"`
+	Header      map[string][]string `provider:"header,omitempty"`
+}
+
+func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
+	decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
+
+	schema := &proxyProviderSchema{
+		HealthCheck: healthCheckSchema{
+			Lazy: true,
+		},
+	}
+	if err := decoder.Decode(mapping, schema); err != nil {
+		return nil, err
+	}
+
+	expectedStatus, err := utils.NewUnsignedRanges[uint16](schema.HealthCheck.ExpectedStatus)
+	if err != nil {
+		return nil, err
+	}
+
+	var hcInterval uint
+	if schema.HealthCheck.Enable {
+		if schema.HealthCheck.Interval == 0 {
+			schema.HealthCheck.Interval = 300
+		}
+		hcInterval = uint(schema.HealthCheck.Interval)
+	}
+	hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus)
+
+	var vehicle types.Vehicle
+	switch schema.Type {
+	case "file":
+		path := C.Path.Resolve(schema.Path)
+		vehicle = resource.NewFileVehicle(path)
+	case "http":
+		path := C.Path.GetPathByHash("proxies", schema.URL)
+		if schema.Path != "" {
+			path = C.Path.Resolve(schema.Path)
+			if !C.Path.IsSafePath(path) {
+				return nil, fmt.Errorf("%w: %s", errSubPath, path)
+			}
+		}
+		vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header)
+	default:
+		return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
+	}
+
+	interval := time.Duration(uint(schema.Interval)) * time.Second
+	filter := schema.Filter
+	excludeFilter := schema.ExcludeFilter
+	excludeType := schema.ExcludeType
+	dialerProxy := schema.DialerProxy
+	override := schema.Override
+
+	return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, dialerProxy, override, vehicle, hc)
+}

+ 9 - 0
core/Clash.Meta/adapter/provider/patch.go

@@ -0,0 +1,9 @@
+package provider
+
+type Healthcheck func(name string, delay uint16)
+
+var HealthcheckHook Healthcheck
+
+func (pp *proxySetProvider) Count() int {
+	return len(pp.proxies)
+}

+ 415 - 0
core/Clash.Meta/adapter/provider/provider.go

@@ -0,0 +1,415 @@
+package provider
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/metacubex/mihomo/adapter"
+	"github.com/metacubex/mihomo/common/convert"
+	"github.com/metacubex/mihomo/common/utils"
+	mihomoHttp "github.com/metacubex/mihomo/component/http"
+	"github.com/metacubex/mihomo/component/resource"
+	C "github.com/metacubex/mihomo/constant"
+	types "github.com/metacubex/mihomo/constant/provider"
+	"github.com/metacubex/mihomo/log"
+	"github.com/metacubex/mihomo/tunnel/statistic"
+
+	"github.com/dlclark/regexp2"
+	"gopkg.in/yaml.v3"
+)
+
+const (
+	ReservedName = "default"
+)
+
+type ProxySchema struct {
+	Proxies []map[string]any `yaml:"proxies"`
+}
+
+// ProxySetProvider for auto gc
+type ProxySetProvider struct {
+	*proxySetProvider
+}
+
+type proxySetProvider struct {
+	*resource.Fetcher[[]C.Proxy]
+	proxies          []C.Proxy
+	healthCheck      *HealthCheck
+	version          uint32
+	subscriptionInfo *SubscriptionInfo
+}
+
+func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
+	return json.Marshal(map[string]any{
+		"name":             pp.Name(),
+		"type":             pp.Type().String(),
+		"vehicleType":      pp.VehicleType().String(),
+		"proxies":          pp.Proxies(),
+		"testUrl":          pp.healthCheck.url,
+		"expectedStatus":   pp.healthCheck.expectedStatus.String(),
+		"updatedAt":        pp.UpdatedAt,
+		"subscriptionInfo": pp.subscriptionInfo,
+	})
+}
+
+func (pp *proxySetProvider) Version() uint32 {
+	return pp.version
+}
+
+func (pp *proxySetProvider) Name() string {
+	return pp.Fetcher.Name()
+}
+
+func (pp *proxySetProvider) HealthCheck() {
+	pp.healthCheck.check()
+}
+
+func (pp *proxySetProvider) Update() error {
+	elm, same, err := pp.Fetcher.Update()
+	if err == nil && !same {
+		pp.OnUpdate(elm)
+	}
+	return err
+}
+
+func (pp *proxySetProvider) Initial() error {
+	elm, err := pp.Fetcher.Initial()
+	if err != nil {
+		return err
+	}
+	pp.OnUpdate(elm)
+	pp.getSubscriptionInfo()
+	pp.closeAllConnections()
+	return nil
+}
+
+func (pp *proxySetProvider) Type() types.ProviderType {
+	return types.Proxy
+}
+
+func (pp *proxySetProvider) Proxies() []C.Proxy {
+	return pp.proxies
+}
+
+func (pp *proxySetProvider) Touch() {
+	pp.healthCheck.touch()
+}
+
+func (pp *proxySetProvider) HealthCheckURL() string {
+	return pp.healthCheck.url
+}
+
+func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
+	pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
+}
+
+func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
+	pp.proxies = proxies
+	pp.healthCheck.setProxy(proxies)
+	if pp.healthCheck.auto() {
+		go pp.healthCheck.check()
+	}
+}
+
+func (pp *proxySetProvider) getSubscriptionInfo() {
+	if pp.VehicleType() != types.HTTP {
+		return
+	}
+	go func() {
+		ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
+		defer cancel()
+		resp, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
+			http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil, pp.Vehicle().Proxy())
+		if err != nil {
+			return
+		}
+		defer resp.Body.Close()
+
+		userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo"))
+		if userInfoStr == "" {
+			resp2, err := mihomoHttp.HttpRequestWithProxy(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
+				http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil, pp.Vehicle().Proxy())
+			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 (pp *proxySetProvider) closeAllConnections() {
+	statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
+		for _, chain := range c.Chains() {
+			if chain == pp.Name() {
+				_ = c.Close()
+				break
+			}
+		}
+		return true
+	})
+}
+
+func stopProxyProvider(pd *ProxySetProvider) {
+	pd.healthCheck.close()
+	_ = pd.Fetcher.Destroy()
+}
+
+func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
+	excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
+	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, regexp2.None)
+		if err != nil {
+			return nil, fmt.Errorf("invalid filter regex: %w", err)
+		}
+		filterRegs = append(filterRegs, filterReg)
+	}
+
+	if hc.auto() {
+		go hc.process()
+	}
+
+	pd := &proxySetProvider{
+		proxies:     []C.Proxy{},
+		healthCheck: hc,
+	}
+
+	fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy, override), proxiesOnUpdate(pd))
+	pd.Fetcher = fetcher
+	wrapper := &ProxySetProvider{pd}
+	runtime.SetFinalizer(wrapper, stopProxyProvider)
+	return wrapper, nil
+}
+
+// CompatibleProvider for auto gc
+type CompatibleProvider struct {
+	*compatibleProvider
+}
+
+type compatibleProvider struct {
+	name        string
+	healthCheck *HealthCheck
+	proxies     []C.Proxy
+	version     uint32
+}
+
+func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
+	return json.Marshal(map[string]any{
+		"name":           cp.Name(),
+		"type":           cp.Type().String(),
+		"vehicleType":    cp.VehicleType().String(),
+		"proxies":        cp.Proxies(),
+		"testUrl":        cp.healthCheck.url,
+		"expectedStatus": cp.healthCheck.expectedStatus.String(),
+	})
+}
+
+func (cp *compatibleProvider) Version() uint32 {
+	return cp.version
+}
+
+func (cp *compatibleProvider) Name() string {
+	return cp.name
+}
+
+func (cp *compatibleProvider) HealthCheck() {
+	cp.healthCheck.check()
+}
+
+func (cp *compatibleProvider) Update() error {
+	return nil
+}
+
+func (cp *compatibleProvider) Initial() error {
+	if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
+		cp.HealthCheck()
+	}
+	return nil
+}
+
+func (cp *compatibleProvider) VehicleType() types.VehicleType {
+	return types.Compatible
+}
+
+func (cp *compatibleProvider) Type() types.ProviderType {
+	return types.Proxy
+}
+
+func (cp *compatibleProvider) Proxies() []C.Proxy {
+	return cp.proxies
+}
+
+func (cp *compatibleProvider) Touch() {
+	cp.healthCheck.touch()
+}
+
+func (cp *compatibleProvider) HealthCheckURL() string {
+	return cp.healthCheck.url
+}
+
+func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
+	cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
+}
+
+func stopCompatibleProvider(pd *CompatibleProvider) {
+	pd.healthCheck.close()
+}
+
+func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
+	if len(proxies) == 0 {
+		return nil, errors.New("provider need one proxy at least")
+	}
+
+	if hc.auto() {
+		go hc.process()
+	}
+
+	pd := &compatibleProvider{
+		name:        name,
+		proxies:     proxies,
+		healthCheck: hc,
+	}
+
+	wrapper := &CompatibleProvider{pd}
+	runtime.SetFinalizer(wrapper, stopCompatibleProvider)
+	return wrapper, nil
+}
+
+func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
+	return func(elm []C.Proxy) {
+		pd.setProxies(elm)
+		pd.version += 1
+		pd.getSubscriptionInfo()
+	}
+}
+
+func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp, dialerProxy string, override OverrideSchema) resource.Parser[[]C.Proxy] {
+	return func(buf []byte) ([]C.Proxy, error) {
+		schema := &ProxySchema{}
+
+		if err := yaml.Unmarshal(buf, schema); err != nil {
+			proxies, err1 := convert.ConvertsV2Ray(buf)
+			if err1 != nil {
+				return nil, fmt.Errorf("%w, %w", err, 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.MatchString(name); mat {
+						continue
+					}
+				}
+				if len(filter) > 0 {
+					if mat, _ := filterReg.MatchString(name); !mat {
+						continue
+					}
+				}
+				if _, ok := proxiesSet[name]; ok {
+					continue
+				}
+
+				if len(dialerProxy) > 0 {
+					mapping["dialer-proxy"] = dialerProxy
+				}
+
+				val := reflect.ValueOf(override)
+				for i := 0; i < val.NumField(); i++ {
+					field := val.Field(i)
+					if field.IsNil() {
+						continue
+					}
+					fieldName := strings.Split(val.Type().Field(i).Tag.Get("provider"), ",")[0]
+					switch fieldName {
+					case "additional-prefix":
+						name := mapping["name"].(string)
+						mapping["name"] = *field.Interface().(*string) + name
+					case "additional-suffix":
+						name := mapping["name"].(string)
+						mapping["name"] = name + *field.Interface().(*string)
+					default:
+						mapping[fieldName] = field.Elem().Interface()
+					}
+				}
+
+				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
+	}
+}

+ 39 - 0
core/Clash.Meta/adapter/provider/subscription_info.go

@@ -0,0 +1,39 @@
+package provider
+
+import (
+	"strconv"
+	"strings"
+)
+
+type SubscriptionInfo struct {
+	Upload   int64
+	Download int64
+	Total    int64
+	Expire   int64
+}
+
+func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo, err error) {
+	userinfo = strings.ToLower(userinfo)
+	userinfo = strings.ReplaceAll(userinfo, " ", "")
+	si = new(SubscriptionInfo)
+	for _, field := range strings.Split(userinfo, ";") {
+		switch name, value, _ := strings.Cut(field, "="); name {
+		case "upload":
+			si.Upload, err = strconv.ParseInt(value, 10, 64)
+		case "download":
+			si.Download, err = strconv.ParseInt(value, 10, 64)
+		case "total":
+			si.Total, err = strconv.ParseInt(value, 10, 64)
+		case "expire":
+			if value == "" {
+				si.Expire = 0
+			} else {
+				si.Expire, err = strconv.ParseInt(value, 10, 64)
+			}
+		}
+		if err != nil {
+			return
+		}
+	}
+	return
+}

+ 21 - 0
core/Clash.Meta/android_tz.go

@@ -0,0 +1,21 @@
+// Copyright 2014 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.
+
+// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89
+
+package main
+
+// #include <time.h>
+import "C"
+import "time"
+
+func init() {
+	var currentT C.time_t
+	var currentTM C.struct_tm
+	C.time(&currentT)
+	C.localtime_r(&currentT, &currentTM)
+	tzOffset := int(currentTM.tm_gmtoff)
+	tz := C.GoString(currentTM.tm_zone)
+	time.Local = time.FixedZone(tz, tzOffset)
+}

+ 28 - 0
core/Clash.Meta/check_amd64.sh

@@ -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

+ 235 - 0
core/Clash.Meta/common/arc/arc.go

@@ -0,0 +1,235 @@
+package arc
+
+import (
+	"sync"
+	"time"
+
+	list "github.com/bahlo/generic-list-go"
+	"github.com/samber/lo"
+)
+
+//modify from https://github.com/alexanderGugel/arc
+
+// Option is part of Functional Options Pattern
+type Option[K comparable, V any] func(*ARC[K, V])
+
+func WithSize[K comparable, V any](maxSize int) Option[K, V] {
+	return func(a *ARC[K, V]) {
+		a.c = maxSize
+	}
+}
+
+type ARC[K comparable, V any] struct {
+	p     int
+	c     int
+	t1    *list.List[*entry[K, V]]
+	b1    *list.List[*entry[K, V]]
+	t2    *list.List[*entry[K, V]]
+	b2    *list.List[*entry[K, V]]
+	mutex sync.Mutex
+	len   int
+	cache map[K]*entry[K, V]
+}
+
+// New returns a new Adaptive Replacement Cache (ARC).
+func New[K comparable, V any](options ...Option[K, V]) *ARC[K, V] {
+	arc := &ARC[K, V]{
+		p:     0,
+		t1:    list.New[*entry[K, V]](),
+		b1:    list.New[*entry[K, V]](),
+		t2:    list.New[*entry[K, V]](),
+		b2:    list.New[*entry[K, V]](),
+		len:   0,
+		cache: make(map[K]*entry[K, V]),
+	}
+
+	for _, option := range options {
+		option(arc)
+	}
+	return arc
+}
+
+// Set inserts a new key-value pair into the cache.
+// This optimizes future access to this entry (side effect).
+func (a *ARC[K, V]) Set(key K, value V) {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+
+	a.set(key, value)
+}
+
+func (a *ARC[K, V]) set(key K, value V) {
+	a.setWithExpire(key, value, time.Unix(0, 0))
+}
+
+// SetWithExpire stores any representation of a response for a given key and given expires.
+// The expires time will round to second.
+func (a *ARC[K, V]) SetWithExpire(key K, value V, expires time.Time) {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+
+	a.setWithExpire(key, value, expires)
+}
+
+func (a *ARC[K, V]) setWithExpire(key K, value V, expires time.Time) {
+	ent, ok := a.cache[key]
+	if !ok {
+		a.len++
+		ent := &entry[K, V]{key: key, value: value, ghost: false, expires: expires.Unix()}
+		a.req(ent)
+		a.cache[key] = ent
+		return
+	}
+
+	if ent.ghost {
+		a.len++
+	}
+
+	ent.value = value
+	ent.ghost = false
+	ent.expires = expires.Unix()
+	a.req(ent)
+}
+
+// Get retrieves a previously via Set inserted entry.
+// This optimizes future access to this entry (side effect).
+func (a *ARC[K, V]) Get(key K) (value V, ok bool) {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+
+	ent, ok := a.get(key)
+	if !ok {
+		return lo.Empty[V](), false
+	}
+	return ent.value, true
+}
+
+func (a *ARC[K, V]) get(key K) (e *entry[K, V], ok bool) {
+	ent, ok := a.cache[key]
+	if !ok {
+		return ent, false
+	}
+	a.req(ent)
+	return ent, !ent.ghost
+}
+
+// GetWithExpire returns any representation of a cached response,
+// a time.Time Give expected expires,
+// and a bool set to true if the key was found.
+// This method will NOT update the expires.
+func (a *ARC[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+
+	ent, ok := a.get(key)
+	if !ok {
+		return lo.Empty[V](), time.Time{}, false
+	}
+
+	return ent.value, time.Unix(ent.expires, 0), true
+}
+
+// Len determines the number of currently cached entries.
+// This method is side-effect free in the sense that it does not attempt to optimize random cache access.
+func (a *ARC[K, V]) Len() int {
+	a.mutex.Lock()
+	defer a.mutex.Unlock()
+
+	return a.len
+}
+
+func (a *ARC[K, V]) req(ent *entry[K, V]) {
+	switch {
+	case ent.ll == a.t1 || ent.ll == a.t2:
+		// Case I
+		ent.setMRU(a.t2)
+	case ent.ll == a.b1:
+		// Case II
+		// Cache Miss in t1 and t2
+
+		// Adaptation
+		var d int
+		if a.b1.Len() >= a.b2.Len() {
+			d = 1
+		} else {
+			d = a.b2.Len() / a.b1.Len()
+		}
+		a.p = min(a.p+d, a.c)
+
+		a.replace(ent)
+		ent.setMRU(a.t2)
+	case ent.ll == a.b2:
+		// Case III
+		// Cache Miss in t1 and t2
+
+		// Adaptation
+		var d int
+		if a.b2.Len() >= a.b1.Len() {
+			d = 1
+		} else {
+			d = a.b1.Len() / a.b2.Len()
+		}
+		a.p = max(a.p-d, 0)
+
+		a.replace(ent)
+		ent.setMRU(a.t2)
+	case ent.ll == nil && a.t1.Len()+a.b1.Len() == a.c:
+		// Case IV A
+		if a.t1.Len() < a.c {
+			a.delLRU(a.b1)
+			a.replace(ent)
+		} else {
+			a.delLRU(a.t1)
+		}
+		ent.setMRU(a.t1)
+	case ent.ll == nil && a.t1.Len()+a.b1.Len() < a.c:
+		// Case IV B
+		if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() >= a.c {
+			if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() == 2*a.c {
+				a.delLRU(a.b2)
+			}
+			a.replace(ent)
+		}
+		ent.setMRU(a.t1)
+	case ent.ll == nil:
+		// Case IV, not A nor B
+		ent.setMRU(a.t1)
+	}
+}
+
+func (a *ARC[K, V]) delLRU(list *list.List[*entry[K, V]]) {
+	lru := list.Back()
+	list.Remove(lru)
+	a.len--
+	delete(a.cache, lru.Value.key)
+}
+
+func (a *ARC[K, V]) replace(ent *entry[K, V]) {
+	if a.t1.Len() > 0 && ((a.t1.Len() > a.p) || (ent.ll == a.b2 && a.t1.Len() == a.p)) {
+		lru := a.t1.Back().Value
+		lru.value = lo.Empty[V]()
+		lru.ghost = true
+		a.len--
+		lru.setMRU(a.b1)
+	} else {
+		lru := a.t2.Back().Value
+		lru.value = lo.Empty[V]()
+		lru.ghost = true
+		a.len--
+		lru.setMRU(a.b2)
+	}
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+func max(a int, b int) int {
+	if a < b {
+		return b
+	}
+	return a
+}

+ 105 - 0
core/Clash.Meta/common/arc/arc_test.go

@@ -0,0 +1,105 @@
+package arc
+
+import (
+	"testing"
+)
+
+func TestInsertion(t *testing.T) {
+	cache := New[string, string](WithSize[string, string](3))
+	if got, want := cache.Len(), 0; got != want {
+		t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want)
+	}
+
+	const (
+		k1 = "Hello"
+		k2 = "Hallo"
+		k3 = "Ciao"
+		k4 = "Salut"
+
+		v1 = "World"
+		v2 = "Worlds"
+		v3 = "Welt"
+	)
+
+	// Insert the first value
+	cache.Set(k1, v1)
+	if got, want := cache.Len(), 1; got != want {
+		t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want)
+	}
+	if got, ok := cache.Get(k1); !ok || got != v1 {
+		t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v1)
+	}
+
+	// Replace existing value for a given key
+	cache.Set(k1, v2)
+	if got, want := cache.Len(), 1; got != want {
+		t.Errorf("re-insertion: cache.Len(): got %d want %d", cache.Len(), want)
+	}
+	if got, ok := cache.Get(k1); !ok || got != v2 {
+		t.Errorf("re-insertion: cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2)
+	}
+
+	// Add a second different key
+	cache.Set(k2, v3)
+	if got, want := cache.Len(), 2; got != want {
+		t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want)
+	}
+	if got, ok := cache.Get(k1); !ok || got != v2 {
+		t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2)
+	}
+	if got, ok := cache.Get(k2); !ok || got != v3 {
+		t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k2, got, ok, v3)
+	}
+
+	// Fill cache
+	cache.Set(k3, v1)
+	if got, want := cache.Len(), 3; got != want {
+		t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want)
+	}
+
+	// Exceed size, this should not exceed size:
+	cache.Set(k4, v1)
+	if got, want := cache.Len(), 3; got != want {
+		t.Errorf("insertion of key out of size: cache.Len(): got %d want %d", cache.Len(), want)
+	}
+}
+
+func TestEviction(t *testing.T) {
+	size := 3
+	cache := New[string, string](WithSize[string, string](size))
+	if got, want := cache.Len(), 0; got != want {
+		t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want)
+	}
+
+	tests := []struct {
+		k, v string
+	}{
+		{"k1", "v1"},
+		{"k2", "v2"},
+		{"k3", "v3"},
+		{"k4", "v4"},
+	}
+	for i, tt := range tests[:size] {
+		cache.Set(tt.k, tt.v)
+		if got, want := cache.Len(), i+1; got != want {
+			t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want)
+		}
+	}
+
+	// Exceed size and check we don't outgrow it:
+	cache.Set(tests[size].k, tests[size].v)
+	if got := cache.Len(); got != size {
+		t.Errorf("insertion of overflow key #%d: cache.Len(): got %d want %d", 4, cache.Len(), size)
+	}
+
+	// Check that LRU got evicted:
+	if got, ok := cache.Get(tests[0].k); ok || got != "" {
+		t.Errorf("cache.Get(%v): got (%v,%t) want (<nil>,true)", tests[0].k, got, ok)
+	}
+
+	for _, tt := range tests[1:] {
+		if got, ok := cache.Get(tt.k); !ok || got != tt.v {
+			t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", tt.k, got, ok, tt.v)
+		}
+	}
+}

+ 32 - 0
core/Clash.Meta/common/arc/entry.go

@@ -0,0 +1,32 @@
+package arc
+
+import (
+	list "github.com/bahlo/generic-list-go"
+)
+
+type entry[K comparable, V any] struct {
+	key     K
+	value   V
+	ll      *list.List[*entry[K, V]]
+	el      *list.Element[*entry[K, V]]
+	ghost   bool
+	expires int64
+}
+
+func (e *entry[K, V]) setLRU(list *list.List[*entry[K, V]]) {
+	e.detach()
+	e.ll = list
+	e.el = e.ll.PushBack(e)
+}
+
+func (e *entry[K, V]) setMRU(list *list.List[*entry[K, V]]) {
+	e.detach()
+	e.ll = list
+	e.el = e.ll.PushFront(e)
+}
+
+func (e *entry[K, V]) detach() {
+	if e.ll != nil {
+		e.ll.Remove(e.el)
+	}
+}

+ 198 - 0
core/Clash.Meta/common/atomic/type.go

@@ -0,0 +1,198 @@
+package atomic
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"sync/atomic"
+)
+
+type Bool struct {
+	atomic.Bool
+}
+
+func NewBool(val bool) (i Bool) {
+	i.Store(val)
+	return
+}
+
+func (i *Bool) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Bool) UnmarshalJSON(b []byte) error {
+	var v bool
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Bool) String() string {
+	v := i.Load()
+	return strconv.FormatBool(v)
+}
+
+type Pointer[T any] struct {
+	atomic.Pointer[T]
+}
+
+func NewPointer[T any](v *T) (p Pointer[T]) {
+	if v != nil {
+		p.Store(v)
+	}
+	return
+}
+
+func (p *Pointer[T]) MarshalJSON() ([]byte, error) {
+	return json.Marshal(p.Load())
+}
+
+func (p *Pointer[T]) UnmarshalJSON(b []byte) error {
+	var v *T
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	p.Store(v)
+	return nil
+}
+
+func (p *Pointer[T]) String() string {
+	return fmt.Sprint(p.Load())
+}
+
+type Int32 struct {
+	atomic.Int32
+}
+
+func NewInt32(val int32) (i Int32) {
+	i.Store(val)
+	return
+}
+
+func (i *Int32) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Int32) UnmarshalJSON(b []byte) error {
+	var v int32
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Int32) String() string {
+	v := i.Load()
+	return strconv.FormatInt(int64(v), 10)
+}
+
+type Int64 struct {
+	atomic.Int64
+}
+
+func NewInt64(val int64) (i Int64) {
+	i.Store(val)
+	return
+}
+
+func (i *Int64) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Int64) UnmarshalJSON(b []byte) error {
+	var v int64
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Int64) String() string {
+	v := i.Load()
+	return strconv.FormatInt(int64(v), 10)
+}
+
+type Uint32 struct {
+	atomic.Uint32
+}
+
+func NewUint32(val uint32) (i Uint32) {
+	i.Store(val)
+	return
+}
+
+func (i *Uint32) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Uint32) UnmarshalJSON(b []byte) error {
+	var v uint32
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Uint32) String() string {
+	v := i.Load()
+	return strconv.FormatUint(uint64(v), 10)
+}
+
+type Uint64 struct {
+	atomic.Uint64
+}
+
+func NewUint64(val uint64) (i Uint64) {
+	i.Store(val)
+	return
+}
+
+func (i *Uint64) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Uint64) UnmarshalJSON(b []byte) error {
+	var v uint64
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Uint64) String() string {
+	v := i.Load()
+	return strconv.FormatUint(uint64(v), 10)
+}
+
+type Uintptr struct {
+	atomic.Uintptr
+}
+
+func NewUintptr(val uintptr) (i Uintptr) {
+	i.Store(val)
+	return
+}
+
+func (i *Uintptr) MarshalJSON() ([]byte, error) {
+	return json.Marshal(i.Load())
+}
+
+func (i *Uintptr) UnmarshalJSON(b []byte) error {
+	var v uintptr
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	i.Store(v)
+	return nil
+}
+
+func (i *Uintptr) String() string {
+	v := i.Load()
+	return strconv.FormatUint(uint64(v), 10)
+}

+ 75 - 0
core/Clash.Meta/common/atomic/value.go

@@ -0,0 +1,75 @@
+package atomic
+
+import (
+	"encoding/json"
+	"sync/atomic"
+)
+
+func DefaultValue[T any]() T {
+	var defaultValue T
+	return defaultValue
+}
+
+type TypedValue[T any] struct {
+	_     noCopy
+	value atomic.Value
+}
+
+// tValue is a struct with determined type to resolve atomic.Value usages with interface types
+// https://github.com/golang/go/issues/22550
+//
+// The intention to have an atomic value store for errors. However, running this code panics:
+// panic: sync/atomic: store of inconsistently typed value into Value
+// This is because atomic.Value requires that the underlying concrete type be the same (which is a reasonable expectation for its implementation).
+// When going through the atomic.Value.Store method call, the fact that both these are of the error interface is lost.
+type tValue[T any] struct {
+	value T
+}
+
+func (t *TypedValue[T]) Load() T {
+	value := t.value.Load()
+	if value == nil {
+		return DefaultValue[T]()
+	}
+	return value.(tValue[T]).value
+}
+
+func (t *TypedValue[T]) Store(value T) {
+	t.value.Store(tValue[T]{value})
+}
+
+func (t *TypedValue[T]) Swap(new T) T {
+	old := t.value.Swap(tValue[T]{new})
+	if old == nil {
+		return DefaultValue[T]()
+	}
+	return old.(tValue[T]).value
+}
+
+func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
+	return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new})
+}
+
+func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
+	return json.Marshal(t.Load())
+}
+
+func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
+	var v T
+	if err := json.Unmarshal(b, &v); err != nil {
+		return err
+	}
+	t.Store(v)
+	return nil
+}
+
+func NewTypedValue[T any](t T) (v TypedValue[T]) {
+	v.Store(t)
+	return
+}
+
+type noCopy struct{}
+
+// Lock is a no-op used by -copylocks checker from `go vet`.
+func (*noCopy) Lock()   {}
+func (*noCopy) Unlock() {}

+ 105 - 0
core/Clash.Meta/common/batch/batch.go

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

+ 83 - 0
core/Clash.Meta/common/batch/batch_test.go

@@ -0,0 +1,83 @@
+package batch
+
+import (
+	"context"
+	"errors"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBatch(t *testing.T) {
+	b, _ := New[string](context.Background())
+
+	now := time.Now()
+	b.Go("foo", func() (string, error) {
+		time.Sleep(time.Millisecond * 100)
+		return "foo", nil
+	})
+	b.Go("bar", func() (string, error) {
+		time.Sleep(time.Millisecond * 150)
+		return "bar", nil
+	})
+	result, err := b.WaitAndGetResult()
+
+	assert.Nil(t, err)
+
+	duration := time.Since(now)
+	assert.Less(t, duration, time.Millisecond*200)
+	assert.Equal(t, 2, len(result))
+
+	for k, v := range result {
+		assert.NoError(t, v.Err)
+		assert.Equal(t, k, v.Value)
+	}
+}
+
+func TestBatchWithConcurrencyNum(t *testing.T) {
+	b, _ := New[string](
+		context.Background(),
+		WithConcurrencyNum[string](3),
+	)
+
+	now := time.Now()
+	for i := 0; i < 7; i++ {
+		idx := i
+		b.Go(strconv.Itoa(idx), func() (string, error) {
+			time.Sleep(time.Millisecond * 100)
+			return strconv.Itoa(idx), nil
+		})
+	}
+	result, _ := b.WaitAndGetResult()
+	duration := time.Since(now)
+	assert.Greater(t, duration, time.Millisecond*260)
+	assert.Equal(t, 7, len(result))
+
+	for k, v := range result {
+		assert.NoError(t, v.Err)
+		assert.Equal(t, k, v.Value)
+	}
+}
+
+func TestBatchContext(t *testing.T) {
+	b, ctx := New[string](context.Background())
+
+	b.Go("error", func() (string, error) {
+		time.Sleep(time.Millisecond * 100)
+		return "", errors.New("test error")
+	})
+
+	b.Go("ctx", func() (string, error) {
+		<-ctx.Done()
+		return "", ctx.Err()
+	})
+
+	result, err := b.WaitAndGetResult()
+
+	assert.NotNil(t, err)
+	assert.Equal(t, "error", err.Key)
+
+	assert.Equal(t, ctx.Err(), result["ctx"].Err)
+}

+ 21 - 0
core/Clash.Meta/common/buf/sing.go

@@ -0,0 +1,21 @@
+package buf
+
+import (
+	"github.com/sagernet/sing/common"
+	"github.com/sagernet/sing/common/buf"
+)
+
+const BufferSize = buf.BufferSize
+
+type Buffer = buf.Buffer
+
+var New = buf.New
+var NewPacket = buf.NewPacket
+var NewSize = buf.NewSize
+var With = buf.With
+var As = buf.As
+
+var (
+	Must  = common.Must
+	Error = common.Error
+)

+ 55 - 0
core/Clash.Meta/common/callback/callback.go

@@ -0,0 +1,55 @@
+package callback
+
+import (
+	"github.com/metacubex/mihomo/common/buf"
+	N "github.com/metacubex/mihomo/common/net"
+	C "github.com/metacubex/mihomo/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) WriteBuffer(buffer *buf.Buffer) (err error) {
+	defer func() {
+		if !c.written {
+			c.written = true
+			c.callback(err)
+		}
+	}()
+	return c.Conn.WriteBuffer(buffer)
+}
+
+func (c *firstWriteCallBackConn) Upstream() any {
+	return c.Conn
+}
+
+func (c *firstWriteCallBackConn) WriterReplaceable() bool {
+	return c.written
+}
+
+func (c *firstWriteCallBackConn) ReaderReplaceable() bool {
+	return true
+}
+
+var _ N.ExtendedConn = (*firstWriteCallBackConn)(nil)
+
+func NewFirstWriteCallBackConn(c C.Conn, callback func(error)) C.Conn {
+	return &firstWriteCallBackConn{
+		Conn:     c,
+		callback: callback,
+		written:  false,
+	}
+}

+ 61 - 0
core/Clash.Meta/common/callback/close_callback.go

@@ -0,0 +1,61 @@
+package callback
+
+import (
+	"sync"
+
+	C "github.com/metacubex/mihomo/constant"
+)
+
+type closeCallbackConn struct {
+	C.Conn
+	closeFunc func()
+	closeOnce sync.Once
+}
+
+func (w *closeCallbackConn) Close() error {
+	w.closeOnce.Do(w.closeFunc)
+	return w.Conn.Close()
+}
+
+func (w *closeCallbackConn) ReaderReplaceable() bool {
+	return true
+}
+
+func (w *closeCallbackConn) WriterReplaceable() bool {
+	return true
+}
+
+func (w *closeCallbackConn) Upstream() any {
+	return w.Conn
+}
+
+func NewCloseCallbackConn(conn C.Conn, callback func()) C.Conn {
+	return &closeCallbackConn{Conn: conn, closeFunc: callback}
+}
+
+type closeCallbackPacketConn struct {
+	C.PacketConn
+	closeFunc func()
+	closeOnce sync.Once
+}
+
+func (w *closeCallbackPacketConn) Close() error {
+	w.closeOnce.Do(w.closeFunc)
+	return w.PacketConn.Close()
+}
+
+func (w *closeCallbackPacketConn) ReaderReplaceable() bool {
+	return true
+}
+
+func (w *closeCallbackPacketConn) WriterReplaceable() bool {
+	return true
+}
+
+func (w *closeCallbackPacketConn) Upstream() any {
+	return w.PacketConn
+}
+
+func NewCloseCallbackPacketConn(conn C.PacketConn, callback func()) C.PacketConn {
+	return &closeCallbackPacketConn{PacketConn: conn, closeFunc: callback}
+}

+ 36 - 0
core/Clash.Meta/common/cmd/cmd.go

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

+ 11 - 0
core/Clash.Meta/common/cmd/cmd_other.go

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

+ 40 - 0
core/Clash.Meta/common/cmd/cmd_test.go

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

+ 12 - 0
core/Clash.Meta/common/cmd/cmd_windows.go

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

+ 45 - 0
core/Clash.Meta/common/convert/base64.go

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

+ 534 - 0
core/Clash.Meta/common/convert/converter.go

@@ -0,0 +1,534 @@
+package convert
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+
+	"github.com/metacubex/mihomo/log"
+)
+
+// ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo 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")
+			if alpn := query.Get("alpn"); alpn != "" {
+				hysteria["alpn"] = strings.Split(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 "hysteria2", "hy2":
+			urlHysteria2, err := url.Parse(line)
+			if err != nil {
+				continue
+			}
+
+			query := urlHysteria2.Query()
+			name := uniqueName(names, urlHysteria2.Fragment)
+			hysteria2 := make(map[string]any, 20)
+
+			hysteria2["name"] = name
+			hysteria2["type"] = "hysteria2"
+			hysteria2["server"] = urlHysteria2.Hostname()
+			if port := urlHysteria2.Port(); port != "" {
+				hysteria2["port"] = port
+			} else {
+				hysteria2["port"] = "443"
+			}
+			hysteria2["obfs"] = query.Get("obfs")
+			hysteria2["obfs-password"] = query.Get("obfs-password")
+			hysteria2["sni"] = query.Get("sni")
+			hysteria2["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
+			if alpn := query.Get("alpn"); alpn != "" {
+				hysteria2["alpn"] = strings.Split(alpn, ",")
+			}
+			if auth := urlHysteria2.User.String(); auth != "" {
+				hysteria2["password"] = auth
+			}
+			hysteria2["fingerprint"] = query.Get("pinSHA256")
+			hysteria2["down"] = query.Get("down")
+			hysteria2["up"] = query.Get("up")
+
+			proxies = append(proxies, hysteria2)
+
+		case "tuic":
+			// A temporary unofficial TUIC share link standard
+			// Modified from https://github.com/daeuniverse/dae/discussions/182
+			// Changes:
+			//   1. Support TUICv4, just replace uuid:password with token
+			//   2. Remove `allow_insecure` field
+			urlTUIC, err := url.Parse(line)
+			if err != nil {
+				continue
+			}
+			query := urlTUIC.Query()
+
+			tuic := make(map[string]any, 20)
+			tuic["name"] = uniqueName(names, urlTUIC.Fragment)
+			tuic["type"] = scheme
+			tuic["server"] = urlTUIC.Hostname()
+			tuic["port"] = urlTUIC.Port()
+			tuic["udp"] = true
+			password, v5 := urlTUIC.User.Password()
+			if v5 {
+				tuic["uuid"] = urlTUIC.User.Username()
+				tuic["password"] = password
+			} else {
+				tuic["token"] = urlTUIC.User.Username()
+			}
+			if cc := query.Get("congestion_control"); cc != "" {
+				tuic["congestion-controller"] = cc
+			}
+			if alpn := query.Get("alpn"); alpn != "" {
+				tuic["alpn"] = strings.Split(alpn, ",")
+			}
+			if sni := query.Get("sni"); sni != "" {
+				tuic["sni"] = sni
+			}
+			if query.Get("disable_sni") == "1" {
+				tuic["disable-sni"] = true
+			}
+			if udpRelayMode := query.Get("udp_relay_mode"); udpRelayMode != "" {
+				tuic["udp-relay-mode"] = udpRelayMode
+			}
+
+			proxies = append(proxies, tuic)
+
+		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"], _ = strconv.ParseBool(query.Get("allowInsecure"))
+
+			if sni := query.Get("sni"); sni != "" {
+				trojan["sni"] = sni
+			}
+			if alpn := query.Get("alpn"); alpn != "" {
+				trojan["alpn"] = strings.Split(alpn, ",")
+			}
+
+			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, _ := values["net"].(string)
+			network = strings.ToLower(network)
+			if values["type"] == "http" {
+				network = "http"
+			} else if network == "http" {
+				network = "h2"
+			}
+			vmess["network"] = network
+
+			tls, ok := values["tls"].(string)
+			if ok {
+				tls = strings.ToLower(tls)
+				if strings.HasSuffix(tls, "tls") {
+					vmess["tls"] = true
+				}
+				if alpn, ok := values["alpn"].(string); ok {
+					vmess["alpn"] = strings.Split(alpn, ",")
+				}
+			}
+
+			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", "httpupgrade":
+				headers := make(map[string]any)
+				wsOpts := make(map[string]any)
+				wsOpts["path"] = "/"
+				if host, ok := values["host"]; ok && host != "" {
+					headers["Host"] = host.(string)
+				}
+				if path, ok := values["path"]; ok && path != "" {
+					path := path.(string)
+					pathURL, err := url.Parse(path)
+					if err == nil {
+						query := pathURL.Query()
+						if earlyData := query.Get("ed"); earlyData != "" {
+							med, err := strconv.Atoi(earlyData)
+							if err == nil {
+								switch network {
+								case "ws":
+									wsOpts["max-early-data"] = med
+									wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol"
+								case "httpupgrade":
+									wsOpts["v2ray-http-upgrade-fast-open"] = true
+								}
+								query.Del("ed")
+								pathURL.RawQuery = query.Encode()
+								path = pathURL.String()
+							}
+						}
+						if earlyDataHeader := query.Get("eh"); earlyDataHeader != "" {
+							wsOpts["early-data-header-name"] = earlyDataHeader
+						}
+					}
+					wsOpts["path"] = path
+				}
+				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
+			}
+			plugin := query.Get("plugin")
+			if strings.Contains(plugin, ";") {
+				pluginInfo, _ := url.ParseQuery("pluginName=" + strings.ReplaceAll(plugin, ";", "&"))
+				pluginName := pluginInfo.Get("pluginName")
+				if strings.Contains(pluginName, "obfs") {
+					ss["plugin"] = "obfs"
+					ss["plugin-opts"] = map[string]any{
+						"mode": pluginInfo.Get("obfs"),
+						"host": pluginInfo.Get("obfs-host"),
+					}
+				} else if strings.Contains(pluginName, "v2ray-plugin") {
+					ss["plugin"] = "v2ray-plugin"
+					ss["plugin-opts"] = map[string]any{
+						"mode": pluginInfo.Get("mode"),
+						"host": pluginInfo.Get("host"),
+						"path": pluginInfo.Get("path"),
+						"tls":  strings.Contains(plugin, "tls"),
+					}
+				}
+			}
+
+			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
+}

+ 35 - 0
core/Clash.Meta/common/convert/converter_test.go

@@ -0,0 +1,35 @@
+package convert
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// https://v2.hysteria.network/zh/docs/developers/URI-Scheme/
+func TestConvertsV2Ray_normal(t *testing.T) {
+	hy2test := "hysteria2://letmein@example.com:8443/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com&up=114&down=514&alpn=h3,h4#hy2test"
+
+	expected := []map[string]interface{}{
+		{
+			"name":             "hy2test",
+			"type":             "hysteria2",
+			"server":           "example.com",
+			"port":             "8443",
+			"sni":              "real.example.com",
+			"obfs":             "salamander",
+			"obfs-password":    "gawrgura",
+			"alpn":             []string{"h3", "h4"},
+			"password":         "letmein",
+			"up":               "114",
+			"down":             "514",
+			"skip-cert-verify": true,
+			"fingerprint":      "deadbeef",
+		},
+	}
+
+	proxies, err := ConvertsV2Ray([]byte(hy2test))
+
+	assert.Nil(t, err)
+	assert.Equal(t, expected, proxies)
+}

+ 323 - 0
core/Clash.Meta/common/convert/util.go

@@ -0,0 +1,323 @@
+package convert
+
+import (
+	"encoding/base64"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/metacubex/mihomo/common/utils"
+
+	"github.com/metacubex/randv2"
+	"github.com/metacubex/sing-shadowsocks/shadowimpl"
+)
+
+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 {
+	base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().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[randv2.IntN(hostsLen)]
+}
+
+func RandUserAgent() string {
+	return userAgents[randv2.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, time.Now)
+	return
+}

+ 136 - 0
core/Clash.Meta/common/convert/v.go

@@ -0,0 +1,136 @@
+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
+	tls := strings.ToLower(query.Get("security"))
+	if strings.HasSuffix(tls, "tls") || tls == "reality" {
+		proxy["tls"] = true
+		if fingerprint := query.Get("fp"); fingerprint == "" {
+			proxy["client-fingerprint"] = "chrome"
+		} else {
+			proxy["client-fingerprint"] = fingerprint
+		}
+		if alpn := query.Get("alpn"); alpn != "" {
+			proxy["alpn"] = strings.Split(alpn, ",")
+		}
+	}
+	if sni := query.Get("sni"); sni != "" {
+		proxy["servername"] = sni
+	}
+	if realityPublicKey := query.Get("pbk"); realityPublicKey != "" {
+		proxy["reality-opts"] = map[string]any{
+			"public-key": realityPublicKey,
+			"short-id":   query.Get("sid"),
+		}
+	}
+
+	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", "httpupgrade":
+		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)
+			}
+			switch network {
+			case "ws":
+				wsOpts["max-early-data"] = med
+				wsOpts["early-data-header-name"] = "Sec-WebSocket-Protocol"
+			case "httpupgrade":
+				wsOpts["v2ray-http-upgrade-fast-open"] = true
+			}
+		}
+		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
+}

+ 294 - 0
core/Clash.Meta/common/lru/lrucache.go

@@ -0,0 +1,294 @@
+package lru
+
+// Modified by https://github.com/die-net/lrucache
+
+import (
+	"sync"
+	"time"
+
+	list "github.com/bahlo/generic-list-go"
+	"github.com/samber/lo"
+)
+
+// Option is part of Functional Options Pattern
+type Option[K comparable, V any] func(*LruCache[K, V])
+
+// EvictCallback is used to get a callback when a cache entry is evicted
+type EvictCallback[K comparable, V any] func(key K, value V)
+
+// WithEvict set the evict callback
+func WithEvict[K comparable, V any](cb EvictCallback[K, V]) Option[K, V] {
+	return func(l *LruCache[K, V]) {
+		l.onEvict = cb
+	}
+}
+
+// WithUpdateAgeOnGet update expires when Get element
+func WithUpdateAgeOnGet[K comparable, V any]() Option[K, V] {
+	return func(l *LruCache[K, V]) {
+		l.updateAgeOnGet = true
+	}
+}
+
+// WithAge defined element max age (second)
+func WithAge[K comparable, V any](maxAge int64) Option[K, V] {
+	return func(l *LruCache[K, V]) {
+		l.maxAge = maxAge
+	}
+}
+
+// WithSize defined max length of LruCache
+func WithSize[K comparable, V any](maxSize int) Option[K, V] {
+	return func(l *LruCache[K, V]) {
+		l.maxSize = maxSize
+	}
+}
+
+// WithStale decide whether Stale return is enabled.
+// If this feature is enabled, element will not get Evicted according to `WithAge`.
+func WithStale[K comparable, V any](stale bool) Option[K, V] {
+	return func(l *LruCache[K, V]) {
+		l.staleReturn = stale
+	}
+}
+
+// LruCache is a thread-safe, in-memory lru-cache that evicts the
+// least recently used entries from memory when (if set) the entries are
+// older than maxAge (in seconds).  Use the New constructor to create one.
+type LruCache[K comparable, V any] struct {
+	maxAge         int64
+	maxSize        int
+	mu             sync.Mutex
+	cache          map[K]*list.Element[*entry[K, V]]
+	lru            *list.List[*entry[K, V]] // Front is least-recent
+	updateAgeOnGet bool
+	staleReturn    bool
+	onEvict        EvictCallback[K, V]
+}
+
+// New creates an LruCache
+func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
+	lc := &LruCache[K, V]{
+		lru:   list.New[*entry[K, V]](),
+		cache: make(map[K]*list.Element[*entry[K, V]]),
+	}
+
+	for _, option := range options {
+		option(lc)
+	}
+
+	return lc
+}
+
+// Get returns any representation of a cached response and a bool
+// set to true if the key was found.
+func (c *LruCache[K, V]) Get(key K) (V, bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	el := c.get(key)
+	if el == nil {
+		return lo.Empty[V](), false
+	}
+	value := el.value
+
+	return value, true
+}
+
+func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	el := c.get(key)
+	if el == nil {
+		value := constructor()
+		c.set(key, value)
+		return value, false
+	}
+	value := el.value
+
+	return value, true
+}
+
+// GetWithExpire returns any representation of a cached response,
+// a time.Time Give expected expires,
+// and a bool set to true if the key was found.
+// This method will NOT check the maxAge of element and will NOT update the expires.
+func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	el := c.get(key)
+	if el == nil {
+		return lo.Empty[V](), time.Time{}, false
+	}
+
+	return el.value, time.Unix(el.expires, 0), true
+}
+
+// Exist returns if key exist in cache but not put item to the head of linked list
+func (c *LruCache[K, V]) Exist(key K) bool {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	_, ok := c.cache[key]
+	return ok
+}
+
+// Set stores any representation of a response for a given key.
+func (c *LruCache[K, V]) Set(key K, value V) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.set(key, value)
+}
+
+func (c *LruCache[K, V]) set(key K, value V) {
+	expires := int64(0)
+	if c.maxAge > 0 {
+		expires = time.Now().Unix() + c.maxAge
+	}
+	c.setWithExpire(key, value, time.Unix(expires, 0))
+}
+
+// SetWithExpire stores any representation of a response for a given key and given expires.
+// The expires time will round to second.
+func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.setWithExpire(key, value, expires)
+}
+
+func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) {
+	if le, ok := c.cache[key]; ok {
+		c.lru.MoveToBack(le)
+		e := le.Value
+		e.value = value
+		e.expires = expires.Unix()
+	} else {
+		e := &entry[K, V]{key: key, value: value, expires: expires.Unix()}
+		c.cache[key] = c.lru.PushBack(e)
+
+		if c.maxSize > 0 {
+			if elLen := c.lru.Len(); elLen > c.maxSize {
+				c.deleteElement(c.lru.Front())
+			}
+		}
+	}
+
+	c.maybeDeleteOldest()
+}
+
+// CloneTo clone and overwrite elements to another LruCache
+func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	n.mu.Lock()
+	defer n.mu.Unlock()
+
+	n.lru = list.New[*entry[K, V]]()
+	n.cache = make(map[K]*list.Element[*entry[K, V]])
+
+	for e := c.lru.Front(); e != nil; e = e.Next() {
+		elm := e.Value
+		n.cache[elm.key] = n.lru.PushBack(elm)
+	}
+}
+
+func (c *LruCache[K, V]) get(key K) *entry[K, V] {
+	le, ok := c.cache[key]
+	if !ok {
+		return nil
+	}
+
+	if !c.staleReturn && c.maxAge > 0 && le.Value.expires <= time.Now().Unix() {
+		c.deleteElement(le)
+		c.maybeDeleteOldest()
+
+		return nil
+	}
+
+	c.lru.MoveToBack(le)
+	el := le.Value
+	if c.maxAge > 0 && c.updateAgeOnGet {
+		el.expires = time.Now().Unix() + c.maxAge
+	}
+	return el
+}
+
+// Delete removes the value associated with a key.
+func (c *LruCache[K, V]) Delete(key K) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.delete(key)
+}
+
+func (c *LruCache[K, V]) delete(key K) {
+	if le, ok := c.cache[key]; ok {
+		c.deleteElement(le)
+	}
+}
+
+func (c *LruCache[K, V]) maybeDeleteOldest() {
+	if !c.staleReturn && c.maxAge > 0 {
+		now := time.Now().Unix()
+		for le := c.lru.Front(); le != nil && le.Value.expires <= now; le = c.lru.Front() {
+			c.deleteElement(le)
+		}
+	}
+}
+
+func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
+	c.lru.Remove(le)
+	e := le.Value
+	delete(c.cache, e.key)
+	if c.onEvict != nil {
+		c.onEvict(e.key, e.value)
+	}
+}
+
+func (c *LruCache[K, V]) Clear() error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	c.cache = make(map[K]*list.Element[*entry[K, V]])
+
+	return nil
+}
+
+// Compute either sets the computed new value for the key or deletes
+// the value for the key. When the delete result of the valueFn function
+// is set to true, the value will be deleted, if it exists. When delete
+// is set to false, the value is updated to the newValue.
+// The ok result indicates whether value was computed and stored, thus, is
+// present in the map. The actual result contains the new value in cases where
+// the value was computed and stored.
+func (c *LruCache[K, V]) Compute(
+	key K,
+	valueFn func(oldValue V, loaded bool) (newValue V, delete bool),
+) (actual V, ok bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if el := c.get(key); el != nil {
+		actual, ok = el.value, true
+	}
+	if newValue, del := valueFn(actual, ok); del {
+		if ok { // data not in cache, so needn't delete
+			c.delete(key)
+		}
+		return lo.Empty[V](), false
+	} else {
+		c.set(key, newValue)
+		return newValue, true
+	}
+}
+
+type entry[K comparable, V any] struct {
+	key     K
+	value   V
+	expires int64
+}

+ 184 - 0
core/Clash.Meta/common/lru/lrucache_test.go

@@ -0,0 +1,184 @@
+package lru
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var entries = []struct {
+	key   string
+	value string
+}{
+	{"1", "one"},
+	{"2", "two"},
+	{"3", "three"},
+	{"4", "four"},
+	{"5", "five"},
+}
+
+func TestLRUCache(t *testing.T) {
+	c := New[string, string]()
+
+	for _, e := range entries {
+		c.Set(e.key, e.value)
+	}
+
+	c.Delete("missing")
+	_, ok := c.Get("missing")
+	assert.False(t, ok)
+
+	for _, e := range entries {
+		value, ok := c.Get(e.key)
+		if assert.True(t, ok) {
+			assert.Equal(t, e.value, value)
+		}
+	}
+
+	for _, e := range entries {
+		c.Delete(e.key)
+
+		_, ok := c.Get(e.key)
+		assert.False(t, ok)
+	}
+}
+
+func TestLRUMaxAge(t *testing.T) {
+	c := New[string, string](WithAge[string, string](86400))
+
+	now := time.Now().Unix()
+	expected := now + 86400
+
+	// Add one expired entry
+	c.Set("foo", "bar")
+	c.lru.Back().Value.expires = now
+
+	// Reset
+	c.Set("foo", "bar")
+	e := c.lru.Back().Value
+	assert.True(t, e.expires >= now)
+	c.lru.Back().Value.expires = now
+
+	// Set a few and verify expiration times
+	for _, s := range entries {
+		c.Set(s.key, s.value)
+		e := c.lru.Back().Value
+		assert.True(t, e.expires >= expected && e.expires <= expected+10)
+	}
+
+	// Make sure we can get them all
+	for _, s := range entries {
+		_, ok := c.Get(s.key)
+		assert.True(t, ok)
+	}
+
+	// Expire all entries
+	for _, s := range entries {
+		le, ok := c.cache[s.key]
+		if assert.True(t, ok) {
+			le.Value.expires = now
+		}
+	}
+
+	// Get one expired entry, which should clear all expired entries
+	_, ok := c.Get("3")
+	assert.False(t, ok)
+	assert.Equal(t, c.lru.Len(), 0)
+}
+
+func TestLRUpdateOnGet(t *testing.T) {
+	c := New[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
+
+	now := time.Now().Unix()
+	expires := now + 86400/2
+
+	// Add one expired entry
+	c.Set("foo", "bar")
+	c.lru.Back().Value.expires = expires
+
+	_, ok := c.Get("foo")
+	assert.True(t, ok)
+	assert.True(t, c.lru.Back().Value.expires > expires)
+}
+
+func TestMaxSize(t *testing.T) {
+	c := New[string, string](WithSize[string, string](2))
+	// Add one expired entry
+	c.Set("foo", "bar")
+	_, ok := c.Get("foo")
+	assert.True(t, ok)
+
+	c.Set("bar", "foo")
+	c.Set("baz", "foo")
+
+	_, ok = c.Get("foo")
+	assert.False(t, ok)
+}
+
+func TestExist(t *testing.T) {
+	c := New[int, int](WithSize[int, int](1))
+	c.Set(1, 2)
+	assert.True(t, c.Exist(1))
+	c.Set(2, 3)
+	assert.False(t, c.Exist(1))
+}
+
+func TestEvict(t *testing.T) {
+	temp := 0
+	evict := func(key int, value int) {
+		temp = key + value
+	}
+
+	c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
+	c.Set(1, 2)
+	c.Set(2, 3)
+
+	assert.Equal(t, temp, 3)
+}
+
+func TestSetWithExpire(t *testing.T) {
+	c := New[int, *struct{}](WithAge[int, *struct{}](1))
+	now := time.Now().Unix()
+
+	tenSecBefore := time.Unix(now-10, 0)
+	c.SetWithExpire(1, &struct{}{}, tenSecBefore)
+
+	// res is expected not to exist, and expires should be empty time.Time
+	res, expires, exist := c.GetWithExpire(1)
+
+	assert.True(t, nil == res)
+	assert.Equal(t, time.Time{}, expires)
+	assert.Equal(t, false, exist)
+}
+
+func TestStale(t *testing.T) {
+	c := New[int, int](WithAge[int, int](1), WithStale[int, int](true))
+	now := time.Now().Unix()
+
+	tenSecBefore := time.Unix(now-10, 0)
+	c.SetWithExpire(1, 2, tenSecBefore)
+
+	res, expires, exist := c.GetWithExpire(1)
+	assert.Equal(t, 2, res)
+	assert.Equal(t, tenSecBefore, expires)
+	assert.Equal(t, true, exist)
+}
+
+func TestCloneTo(t *testing.T) {
+	o := New[string, int](WithSize[string, int](10))
+	o.Set("1", 1)
+	o.Set("2", 2)
+
+	n := New[string, int](WithSize[string, int](2))
+	n.Set("3", 3)
+	n.Set("4", 4)
+
+	o.CloneTo(n)
+
+	assert.False(t, n.Exist("3"))
+	assert.True(t, n.Exist("1"))
+
+	n.Set("5", 5)
+	assert.False(t, n.Exist("1"))
+}

+ 50 - 0
core/Clash.Meta/common/murmur3/murmur.go

@@ -0,0 +1,50 @@
+package murmur3
+
+type bmixer interface {
+	bmix(p []byte) (tail []byte)
+	Size() (n int)
+	reset()
+}
+
+type digest struct {
+	clen int      // Digested input cumulative length.
+	tail []byte   // 0 to Size()-1 bytes view of `buf'.
+	buf  [16]byte // Expected (but not required) to be Size() large.
+	seed uint32   // Seed for initializing the hash.
+	bmixer
+}
+
+func (d *digest) BlockSize() int { return 1 }
+
+func (d *digest) Write(p []byte) (n int, err error) {
+	n = len(p)
+	d.clen += n
+
+	if len(d.tail) > 0 {
+		// Stick back pending bytes.
+		nfree := d.Size() - len(d.tail) // nfree ∈ [1, d.Size()-1].
+		if nfree < len(p) {
+			// One full block can be formed.
+			block := append(d.tail, p[:nfree]...)
+			p = p[nfree:]
+			_ = d.bmix(block) // No tail.
+		} else {
+			// Tail's buf is large enough to prevent reallocs.
+			p = append(d.tail, p...)
+		}
+	}
+
+	d.tail = d.bmix(p)
+
+	// Keep own copy of the 0 to Size()-1 pending bytes.
+	nn := copy(d.buf[:], d.tail)
+	d.tail = d.buf[:nn]
+
+	return n, nil
+}
+
+func (d *digest) Reset() {
+	d.clen = 0
+	d.tail = nil
+	d.bmixer.reset()
+}

+ 144 - 0
core/Clash.Meta/common/murmur3/murmur32.go

@@ -0,0 +1,144 @@
+package murmur3
+
+// https://github.com/spaolacci/murmur3/blob/master/murmur32.go
+
+import (
+	"hash"
+	"math/bits"
+	"unsafe"
+)
+
+// Make sure interfaces are correctly implemented.
+var (
+	_ hash.Hash32 = new(digest32)
+	_ bmixer      = new(digest32)
+)
+
+const (
+	c1_32 uint32 = 0xcc9e2d51
+	c2_32 uint32 = 0x1b873593
+)
+
+// digest32 represents a partial evaluation of a 32 bites hash.
+type digest32 struct {
+	digest
+	h1 uint32 // Unfinalized running hash.
+}
+
+// New32 returns new 32-bit hasher
+func New32() hash.Hash32 { return New32WithSeed(0) }
+
+// New32WithSeed returns new 32-bit hasher set with explicit seed value
+func New32WithSeed(seed uint32) hash.Hash32 {
+	d := new(digest32)
+	d.seed = seed
+	d.bmixer = d
+	d.Reset()
+	return d
+}
+
+func (d *digest32) Size() int { return 4 }
+
+func (d *digest32) reset() { d.h1 = d.seed }
+
+func (d *digest32) Sum(b []byte) []byte {
+	h := d.Sum32()
+	return append(b, byte(h>>24), byte(h>>16), byte(h>>8), byte(h))
+}
+
+// Digest as many blocks as possible.
+func (d *digest32) bmix(p []byte) (tail []byte) {
+	h1 := d.h1
+
+	nblocks := len(p) / 4
+	for i := 0; i < nblocks; i++ {
+		k1 := *(*uint32)(unsafe.Pointer(&p[i*4]))
+
+		k1 *= c1_32
+		k1 = bits.RotateLeft32(k1, 15)
+		k1 *= c2_32
+
+		h1 ^= k1
+		h1 = bits.RotateLeft32(h1, 13)
+		h1 = h1*4 + h1 + 0xe6546b64
+	}
+	d.h1 = h1
+	return p[nblocks*d.Size():]
+}
+
+func (d *digest32) Sum32() (h1 uint32) {
+	h1 = d.h1
+
+	var k1 uint32
+	switch len(d.tail) & 3 {
+	case 3:
+		k1 ^= uint32(d.tail[2]) << 16
+		fallthrough
+	case 2:
+		k1 ^= uint32(d.tail[1]) << 8
+		fallthrough
+	case 1:
+		k1 ^= uint32(d.tail[0])
+		k1 *= c1_32
+		k1 = bits.RotateLeft32(k1, 15)
+		k1 *= c2_32
+		h1 ^= k1
+	}
+
+	h1 ^= uint32(d.clen)
+
+	h1 ^= h1 >> 16
+	h1 *= 0x85ebca6b
+	h1 ^= h1 >> 13
+	h1 *= 0xc2b2ae35
+	h1 ^= h1 >> 16
+
+	return h1
+}
+
+func Sum32(data []byte) uint32 { return Sum32WithSeed(data, 0) }
+
+func Sum32WithSeed(data []byte, seed uint32) uint32 {
+	h1 := seed
+
+	nblocks := len(data) / 4
+	for i := 0; i < nblocks; i++ {
+		k1 := *(*uint32)(unsafe.Pointer(&data[i*4]))
+
+		k1 *= c1_32
+		k1 = bits.RotateLeft32(k1, 15)
+		k1 *= c2_32
+
+		h1 ^= k1
+		h1 = bits.RotateLeft32(h1, 13)
+		h1 = h1*4 + h1 + 0xe6546b64
+	}
+
+	tail := data[nblocks*4:]
+
+	var k1 uint32
+	switch len(tail) & 3 {
+	case 3:
+		k1 ^= uint32(tail[2]) << 16
+		fallthrough
+	case 2:
+		k1 ^= uint32(tail[1]) << 8
+		fallthrough
+	case 1:
+		k1 ^= uint32(tail[0])
+		k1 *= c1_32
+		k1 = bits.RotateLeft32(k1, 15)
+		k1 *= c2_32
+		h1 ^= k1
+	}
+
+	h1 ^= uint32(len(data))
+
+	h1 ^= h1 >> 16
+	h1 *= 0x85ebca6b
+	h1 ^= h1 >> 13
+	h1 *= 0xc2b2ae35
+	h1 ^= h1 >> 16
+
+	return h1
+}

+ 36 - 0
core/Clash.Meta/common/net/addr.go

@@ -0,0 +1,36 @@
+package net
+
+import (
+	"net"
+)
+
+type CustomAddr interface {
+	net.Addr
+	RawAddr() net.Addr
+}
+
+type customAddr struct {
+	networkStr string
+	addrStr    string
+	rawAddr    net.Addr
+}
+
+func (a customAddr) Network() string {
+	return a.networkStr
+}
+
+func (a customAddr) String() string {
+	return a.addrStr
+}
+
+func (a customAddr) RawAddr() net.Addr {
+	return a.rawAddr
+}
+
+func NewCustomAddr(networkStr string, addrStr string, rawAddr net.Addr) CustomAddr {
+	return customAddr{
+		networkStr: networkStr,
+		addrStr:    addrStr,
+		rawAddr:    rawAddr,
+	}
+}

+ 45 - 0
core/Clash.Meta/common/net/bind.go

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

+ 106 - 0
core/Clash.Meta/common/net/bufconn.go

@@ -0,0 +1,106 @@
+package net
+
+import (
+	"bufio"
+	"net"
+
+	"github.com/metacubex/mihomo/common/buf"
+)
+
+var _ ExtendedConn = (*BufferedConn)(nil)
+
+type BufferedConn struct {
+	r *bufio.Reader
+	ExtendedConn
+	peeked bool
+}
+
+func NewBufferedConn(c net.Conn) *BufferedConn {
+	if bc, ok := c.(*BufferedConn); ok {
+		return bc
+	}
+	return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false}
+}
+
+func WarpConnWithBioReader(c net.Conn, br *bufio.Reader) net.Conn {
+	if br != nil && br.Buffered() > 0 {
+		if bc, ok := c.(*BufferedConn); ok && bc.r == br {
+			return bc
+		}
+		return &BufferedConn{br, NewExtendedConn(c), true}
+	}
+	return c
+}
+
+// Reader returns the internal bufio.Reader.
+func (c *BufferedConn) Reader() *bufio.Reader {
+	return c.r
+}
+
+func (c *BufferedConn) ResetPeeked() {
+	c.peeked = false
+}
+
+func (c *BufferedConn) Peeked() bool {
+	return c.peeked
+}
+
+// Peek returns the next n bytes without advancing the reader.
+func (c *BufferedConn) Peek(n int) ([]byte, error) {
+	c.peeked = true
+	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) {
+	return c.r.Read(p)
+}
+
+func (c *BufferedConn) ReadByte() (byte, error) {
+	return c.r.ReadByte()
+}
+
+func (c *BufferedConn) UnreadByte() error {
+	return c.r.UnreadByte()
+}
+
+func (c *BufferedConn) Buffered() int {
+	return c.r.Buffered()
+}
+
+func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) {
+	if c.r != nil && c.r.Buffered() > 0 {
+		_, err = buffer.ReadOnceFrom(c.r)
+		return
+	}
+	return c.ExtendedConn.ReadBuffer(buffer)
+}
+
+func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy
+	if c.r != nil && c.r.Buffered() > 0 {
+		length := c.r.Buffered()
+		b, _ := c.r.Peek(length)
+		_, _ = c.r.Discard(length)
+		return buf.As(b)
+	}
+	c.r = nil // drop bufio.Reader to let gc can clean up its internal buf
+	return nil
+}
+
+func (c *BufferedConn) Upstream() any {
+	return c.ExtendedConn
+}
+
+func (c *BufferedConn) ReaderReplaceable() bool {
+	if c.r != nil && c.r.Buffered() > 0 {
+		return false
+	}
+	return true
+}
+
+func (c *BufferedConn) WriterReplaceable() bool {
+	return true
+}

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