mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2025-12-20 17:10:08 +08:00
Compare commits
No commits in common. "main" and "v1.19.13" have entirely different histories.
105
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
105
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,5 +1,5 @@
|
||||
name: Bug Report
|
||||
description: "Report Mihomo bug"
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
@ -7,74 +7,77 @@ body:
|
||||
id: ensure
|
||||
attributes:
|
||||
label: Verify steps
|
||||
description: Before submitting, please check all the options below to confirm that you have read and understood the following requirements; otherwise, this issue will be closed.
|
||||
description: "
|
||||
在提交之前,请确认
|
||||
Please verify that you've followed these steps
|
||||
"
|
||||
options:
|
||||
- label: I have read the [documentation](https://wiki.metacubex.one/) and understand the meaning of all the configuration items I have written, rather than just piling up seemingly useful options or default values.
|
||||
- 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: I have carefully reviewed the [documentation](https://wiki.metacubex.one/) and have not resolved the issue.
|
||||
required: false
|
||||
- label: I have searched the [Issue Tracker](……/) for the issue I want to raise and did not find it.
|
||||
required: false
|
||||
- label: I am a non-Chinese user.
|
||||
required: false
|
||||
- label: I have tested with the latest Alpha branch version, and the issue still persists.
|
||||
- label: "
|
||||
我已经在 [Issue Tracker](……/) 中找过我要提出的问题
|
||||
I have searched on the [issue tracker](……/) for a related issue.
|
||||
"
|
||||
required: true
|
||||
- label: I have provided the server and client configuration files and processes that can reproduce the issue locally, rather than a sanitized complex client configuration file.
|
||||
- label: "
|
||||
我已经使用 Alpha 分支版本测试过,问题依旧存在
|
||||
I have tested using the dev branch, and the issue still exists.
|
||||
"
|
||||
required: true
|
||||
- label: I provided the simplest configuration that can be used to reproduce the errors in my report, rather than relying on remote servers or piling on a lot of unnecessary configurations for reproduction.
|
||||
- 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: I have provided complete logs, rather than just the parts I think are useful out of confidence in my own intelligence.
|
||||
- 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
|
||||
- label: I have directly reproduced the error using the Mihomo command-line program, rather than using other tools or scripts.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: "Please provide the type of operating system."
|
||||
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
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
- OpenBSD/FreeBSD
|
||||
- Android
|
||||
- type: input
|
||||
attributes:
|
||||
label: System Version
|
||||
description: "Please provide the version of the operating system where the issue occurred."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Mihomo Version
|
||||
description: "Provide the output of the `mihomo -v` command."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
render: yaml
|
||||
label: Configuration File
|
||||
description: |-
|
||||
Please attach the Mihomo configuration file below.
|
||||
Make sure there is no sensitive information in the configuration file (such as server addresses, passwords, ports, etc.)
|
||||
Also, ensure that the configuration file can reproduce the error using the Mihomo command-line program locally (if it's a proxy protocol issue, make sure the local server can be used for reproduction).
|
||||
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
|
||||
description: "Please provide a detailed description of the error."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
description: "Please provide the steps to reproduce the error."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
description: "Attach the running logs of Mihomo Core below, with `log-level` set to `DEBUG`."
|
||||
render: shell
|
||||
80
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
80
.github/ISSUE_TEMPLATE/bug_report_zh.yml
vendored
@ -1,80 +0,0 @@
|
||||
name: 错误反馈
|
||||
description: "提交 Mihomo 漏洞"
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: ensure
|
||||
attributes:
|
||||
label: 验证步骤
|
||||
description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||
options:
|
||||
- label: 我已经阅读了 [文档](https://wiki.metacubex.one/),了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
|
||||
required: false
|
||||
- label: 我仔细看过 [文档](https://wiki.metacubex.one/) 并未解决问题
|
||||
required: false
|
||||
- label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的问题,并且没有找到
|
||||
required: false
|
||||
- label: 我是中文用户,而非其他语言用户
|
||||
required: false
|
||||
- label: 我已经使用最新的 Alpha 分支版本测试过,问题依旧存在
|
||||
required: true
|
||||
- label: 我提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
|
||||
required: true
|
||||
- label: 我提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器或者堆砌大量对于复现无用的配置等。
|
||||
required: true
|
||||
- label: 我提供了完整的日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。
|
||||
required: true
|
||||
- label: 我直接使用 Mihomo 命令行程序重现了错误,而不是使用其他工具或脚本。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: 操作系统
|
||||
description: 请提供操作系统类型
|
||||
multiple: true
|
||||
options:
|
||||
- MacOS
|
||||
- Windows
|
||||
- Linux
|
||||
- OpenBSD/FreeBSD
|
||||
- Android
|
||||
- type: input
|
||||
attributes:
|
||||
label: 系统版本
|
||||
description: 请提供出现问题的操作系统版本
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Mihomo 版本
|
||||
description: 提供 `mihomo -v` 命令的输出
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
render: yaml
|
||||
label: 配置文件
|
||||
description: |-
|
||||
在下方附上 Mihomo 配置文件
|
||||
请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等)
|
||||
以及确保可以在本地(如果是代理协议问题,请确保本地服务器可用于复现)使用 Mihomo 原始命令行程序重现错误的配置文件
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 描述
|
||||
description: 请提供错误的详细描述。
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 重现方式
|
||||
description: 请提供重现错误的步骤
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 日志
|
||||
description: 在下方附上 Mihomo Core 的运行日志,`log-level` 使用 `DEBUG`
|
||||
render: shell
|
||||
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,23 +1,37 @@
|
||||
name: Feature Request
|
||||
description: Suggest improvements for this project
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: ensure
|
||||
attributes:
|
||||
label: Verification Steps
|
||||
description: Before submitting, please check the following options to confirm that you have read and understood the requirements below; otherwise, this issue will be closed.
|
||||
label: Verify steps
|
||||
description: "
|
||||
在提交之前,请确认
|
||||
Please verify that you've followed these steps
|
||||
"
|
||||
options:
|
||||
- label: I have read the [documentation](https://wiki.metacubex.one/) and confirmed that this feature is not implemented
|
||||
required: false
|
||||
- label: I have searched for the feature request I want to propose in the [Issue Tracker](……/) and did not find it
|
||||
required: false
|
||||
- label: I am a non-Chinese user.
|
||||
required: false
|
||||
- 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: Please provide a detailed description of the feature, rather than vague statements.
|
||||
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
|
||||
"
|
||||
23
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
23
.github/ISSUE_TEMPLATE/feature_request_zh.yml
vendored
@ -1,23 +0,0 @@
|
||||
name: 功能请求
|
||||
description: 为该项目提出建议
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: ensure
|
||||
attributes:
|
||||
label: 验证步骤
|
||||
description: 在提交之前,请勾选以下选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。
|
||||
options:
|
||||
- label: 我已经阅读了 [文档](https://wiki.metacubex.one/),确认了该功能没有实现
|
||||
required: false
|
||||
- label: 我已在 [Issue Tracker](……/) 中寻找过我要提出的功能请求,并且没有找到
|
||||
required: false
|
||||
- label: 我是中文用户,而非其他语言用户
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 描述
|
||||
description: 请提供对于该功能的详细描述,而不是莫名其妙的话术。
|
||||
validations:
|
||||
required: true
|
||||
32
.github/genReleaseNote.sh
vendored
Executable file
32
.github/genReleaseNote.sh
vendored
Executable file
@ -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
|
||||
26
.github/release.sh
vendored
Normal file
26
.github/release.sh
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILENAMES=$(ls)
|
||||
for FILENAME in $FILENAMES
|
||||
do
|
||||
if [[ ! ($FILENAME =~ ".exe" || $FILENAME =~ ".sh")]];then
|
||||
gzip -S ".gz" $FILENAME
|
||||
elif [[ $FILENAME =~ ".exe" ]];then
|
||||
zip -m ${FILENAME%.*}.zip $FILENAME
|
||||
else echo "skip $FILENAME"
|
||||
fi
|
||||
done
|
||||
|
||||
FILENAMES=$(ls)
|
||||
for FILENAME in $FILENAMES
|
||||
do
|
||||
if [[ $FILENAME =~ ".zip" ]];then
|
||||
echo "rename $FILENAME"
|
||||
mv $FILENAME ${FILENAME%.*}-${VERSION}.zip
|
||||
elif [[ $FILENAME =~ ".gz" ]];then
|
||||
echo "rename $FILENAME"
|
||||
mv $FILENAME ${FILENAME%.*}-${VERSION}.gz
|
||||
else
|
||||
echo "skip $FILENAME"
|
||||
fi
|
||||
done
|
||||
18
.github/release/.fpm_systemd
vendored
Normal file
18
.github/release/.fpm_systemd
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
-s dir
|
||||
--name mihomo
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://wiki.metacubex.one/"
|
||||
--maintainer "MetaCubeX <none@example.com>"
|
||||
--deb-field "Bug: https://github.com/MetaCubeX/mihomo/issues"
|
||||
--no-deb-generate-changes
|
||||
--config-files /etc/mihomo/config.yaml
|
||||
|
||||
.github/release/config.yaml=/etc/mihomo/config.yaml
|
||||
|
||||
.github/release/mihomo.service=/usr/lib/systemd/system/mihomo.service
|
||||
.github/release/mihomo@.service=/usr/lib/systemd/system/mihomo@.service
|
||||
|
||||
|
||||
LICENSE=/usr/share/licenses/mihomo/LICENSE
|
||||
15
.github/release/config.yaml
vendored
Normal file
15
.github/release/config.yaml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
mixed-port: 7890
|
||||
|
||||
dns:
|
||||
enable: true
|
||||
ipv6: true
|
||||
enhanced-mode: fake-ip
|
||||
fake-ip-filter:
|
||||
- "*"
|
||||
- "+.lan"
|
||||
- "+.local"
|
||||
nameserver:
|
||||
- system
|
||||
|
||||
rules:
|
||||
- MATCH,DIRECT
|
||||
17
.github/release/mihomo.service
vendored
Normal file
17
.github/release/mihomo.service
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=mihomo Daemon, Another Clash Kernel.
|
||||
Documentation=https://wiki.metacubex.one
|
||||
After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
LimitNOFILE=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
17
.github/release/mihomo@.service
vendored
Normal file
17
.github/release/mihomo@.service
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=mihomo Daemon, Another Clash Kernel.
|
||||
Documentation=https://wiki.metacubex.one
|
||||
After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
LimitNOFILE=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
35
.github/rename-cgo.sh
vendored
Normal file
35
.github/rename-cgo.sh
vendored
Normal file
@ -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
.github/rename-go120.sh
vendored
Normal file
12
.github/rename-go120.sh
vendored
Normal file
@ -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
|
||||
17
.github/workflows/Delete.yml
vendored
17
.github/workflows/Delete.yml
vendored
@ -1,17 +0,0 @@
|
||||
name: Delete old workflow
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
# 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
|
||||
585
.github/workflows/build.yml
vendored
585
.github/workflows/build.yml
vendored
@ -5,31 +5,582 @@ on:
|
||||
version:
|
||||
description: "Tag version to release"
|
||||
required: true
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
branches:
|
||||
- Alpha
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- Alpha
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REGISTRY: docker.io
|
||||
jobs:
|
||||
release_archive:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'KT-Yeh/mihomo'
|
||||
strategy:
|
||||
matrix:
|
||||
jobs:
|
||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
|
||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1 }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2 }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3 }
|
||||
- { goos: darwin, goarch: arm64, output: arm64 }
|
||||
|
||||
- { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386}
|
||||
- { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' }
|
||||
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible} # old style file name will be removed in next released
|
||||
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, debian: amd64, rpm: x86_64, pacman: x86_64}
|
||||
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1, debian: amd64, rpm: x86_64, pacman: x86_64, test: test }
|
||||
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2, debian: amd64, rpm: x86_64, pacman: x86_64}
|
||||
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3, debian: amd64, rpm: x86_64, pacman: x86_64}
|
||||
- { goos: linux, goarch: arm64, output: arm64, debian: arm64, rpm: aarch64, pacman: aarch64}
|
||||
- { goos: linux, goarch: arm, goarm: '5', output: armv5 }
|
||||
- { goos: linux, goarch: arm, goarm: '6', output: armv6, debian: armel, rpm: armv6hl}
|
||||
- { goos: linux, goarch: arm, goarm: '7', output: armv7, debian: armhf, rpm: armv7hl, pacman: armv7hl}
|
||||
- { goos: linux, goarch: mips, gomips: hardfloat, output: mips-hardfloat }
|
||||
- { goos: linux, goarch: mips, gomips: softfloat, output: mips-softfloat }
|
||||
- { goos: linux, goarch: mipsle, gomips: hardfloat, output: mipsle-hardfloat }
|
||||
- { goos: linux, goarch: mipsle, gomips: softfloat, output: mipsle-softfloat }
|
||||
- { goos: linux, goarch: mips64, output: mips64 }
|
||||
- { goos: linux, goarch: mips64le, output: mips64le, debian: mips64el, rpm: mips64el }
|
||||
- { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1', debian: loongarch64, rpm: loongarch64 }
|
||||
- { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2', debian: loong64, rpm: loong64 }
|
||||
- { goos: linux, goarch: riscv64, output: riscv64, debian: riscv64, rpm: riscv64 }
|
||||
- { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x }
|
||||
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||
|
||||
- { goos: windows, goarch: '386', output: '386' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1 }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2 }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3 }
|
||||
- { goos: windows, goarch: arm64, output: arm64 }
|
||||
|
||||
- { goos: freebsd, goarch: '386', output: '386' }
|
||||
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
|
||||
- { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 }
|
||||
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-v1 }
|
||||
- { goos: freebsd, goarch: amd64, goamd64: v2, output: amd64-v2 }
|
||||
- { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64-v3 }
|
||||
- { goos: freebsd, goarch: arm64, output: arm64 }
|
||||
|
||||
- { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' }
|
||||
- { 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.24 with special patch can work on Windows 7
|
||||
# https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
||||
- { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' }
|
||||
|
||||
# Go 1.23 with special patch can work on Windows 7
|
||||
# https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||
- { goos: windows, goarch: '386', output: '386-go123', goversion: '1.23' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' }
|
||||
|
||||
# Go 1.22 with special patch can work on Windows 7
|
||||
# https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
|
||||
- { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' }
|
||||
|
||||
# Go 1.21 can revert commit `9e4385` to work on Windows 7
|
||||
# https://github.com/golang/go/issues/64622#issuecomment-1847475161
|
||||
# (OR we can just use golang1.21.4 which unneeded any patch)
|
||||
- { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go121, goversion: '1.21' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go121, goversion: '1.21' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go121, goversion: '1.21' }
|
||||
|
||||
# Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
|
||||
- { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
|
||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' }
|
||||
|
||||
# Go 1.24 is the last release that will run on macOS 11 Big Sur. Go 1.25 will require macOS 12 Monterey or later.
|
||||
- { goos: darwin, goarch: arm64, output: arm64-go124, goversion: '1.24' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go124, goversion: '1.24' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go124, goversion: '1.24' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go124, goversion: '1.24' }
|
||||
|
||||
# Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later.
|
||||
- { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go122, goversion: '1.22' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go122, goversion: '1.22' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go122, goversion: '1.22' }
|
||||
|
||||
# Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
|
||||
- { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
|
||||
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' }
|
||||
|
||||
# Go 1.23 is the last release that requires Linux kernel version 2.6.32 or later. Go 1.24 will require Linux kernel version 3.2 or later.
|
||||
- { goos: linux, goarch: '386', output: '386-go123', goversion: '1.23' }
|
||||
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go123, goversion: '1.23', test: test }
|
||||
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go123, goversion: '1.23' }
|
||||
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go123, goversion: '1.23' }
|
||||
|
||||
# only for test
|
||||
- { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' }
|
||||
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-v1-go120, goversion: '1.20', test: test }
|
||||
- { goos: linux, goarch: amd64, goamd64: v2, output: amd64-v2-go120, goversion: '1.20' }
|
||||
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-v3-go120, goversion: '1.20' }
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Archive Release
|
||||
uses: thedoctor0/zip-release@0.7.1
|
||||
- name: Set up Go
|
||||
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
type: zip
|
||||
filename: 'mihomo_${{ github.ref_name }}.zip'
|
||||
exclusions: '*.git*'
|
||||
go-version: '1.25'
|
||||
|
||||
- name: Release
|
||||
- name: Set up Go
|
||||
if: ${{ matrix.jobs.goversion != '' && matrix.jobs.abi != '1' }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.jobs.goversion }}
|
||||
|
||||
- name: Set up Go1.24 loongarch abi1
|
||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||
run: |
|
||||
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/go1.24.0.linux-amd64-abi1.tar.gz
|
||||
sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local
|
||||
echo "/usr/local/go/bin" >> $GITHUB_PATH
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.25.x
|
||||
# that means after golang1.26 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.25 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.24.x
|
||||
# that means after golang1.25 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.24 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.24' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.23.x
|
||||
# that means after golang1.24 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.23 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.23' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.22.x
|
||||
# that means after golang1.23 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.22 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.22' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
- name: Revert Golang1.21 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.21' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
|
||||
|
||||
- name: Set variables
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)"
|
||||
VERSION="${VERSION//\//-}"
|
||||
PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}"
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
PackageVersion="${VERSION#v}"
|
||||
fi
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV
|
||||
|
||||
echo "BUILDTIME=$(date)" >> $GITHUB_ENV
|
||||
echo "CGO_ENABLED=0" >> $GITHUB_ENV
|
||||
echo "BUILDTAG=-extldflags --static" >> $GITHUB_ENV
|
||||
echo "GOTOOLCHAIN=local" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup NDK
|
||||
if: ${{ matrix.jobs.goos == 'android' }}
|
||||
uses: nttld/setup-ndk@v1
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r29-beta1
|
||||
|
||||
- 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 ./...
|
||||
echo "---test with_gvisor---"
|
||||
go test ./... -tags "with_gvisor" -count=1
|
||||
|
||||
- name: Update CA
|
||||
run: |
|
||||
sudo apt-get update && 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}}
|
||||
GO386: ${{matrix.jobs.go386}}
|
||||
GOARM: ${{matrix.jobs.goarm}}
|
||||
GOMIPS: ${{matrix.jobs.gomips}}
|
||||
run: |
|
||||
go env
|
||||
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: Package DEB
|
||||
if: matrix.jobs.debian != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .github/release/.fpm_systemd .fpm
|
||||
|
||||
fpm -t deb \
|
||||
-v "${PackageVersion}" \
|
||||
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb" \
|
||||
--architecture ${{ matrix.jobs.debian }} \
|
||||
mihomo=/usr/bin/mihomo
|
||||
|
||||
- name: Package RPM
|
||||
if: matrix.jobs.rpm != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
cp .github/release/.fpm_systemd .fpm
|
||||
|
||||
fpm -t rpm \
|
||||
-v "${PackageVersion}" \
|
||||
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm" \
|
||||
--architecture ${{ matrix.jobs.rpm }} \
|
||||
mihomo=/usr/bin/mihomo
|
||||
|
||||
- name: Package Pacman
|
||||
if: matrix.jobs.pacman != ''
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
sudo apt-get update && sudo apt-get install -y libarchive-tools
|
||||
cp .github/release/.fpm_systemd .fpm
|
||||
|
||||
fpm -t pacman \
|
||||
-v "${PackageVersion}" \
|
||||
-p "mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.jobs.pacman }} \
|
||||
mihomo=/usr/bin/mihomo
|
||||
|
||||
- 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*.pkg.tar.zst
|
||||
mihomo*.zip
|
||||
version.txt
|
||||
checksums.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: Calculate checksums
|
||||
run: |
|
||||
cd bin/
|
||||
find . -type f -not -name "checksums.*" -not -name "version.txt" | sort | xargs sha256sum > checksums.txt
|
||||
cat checksums.txt
|
||||
shell: bash
|
||||
|
||||
- name: Delete current release assets
|
||||
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
|
||||
files: 'mihomo_${{ github.ref_name }}.zip'
|
||||
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: Force push Alpha branch to 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 push origin Alpha:Meta --force
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Tag the commit on Alpha
|
||||
run: |
|
||||
git checkout Alpha
|
||||
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 }}
|
||||
139
.github/workflows/test.yml
vendored
Normal file
139
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,139 @@
|
||||
name: Test
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
branches:
|
||||
- Alpha
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- Alpha
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest' # amd64 linux
|
||||
- 'windows-latest' # amd64 windows
|
||||
- 'macos-latest' # arm64 macos
|
||||
- 'ubuntu-24.04-arm' # arm64 linux
|
||||
- 'macos-13' # amd64 macos
|
||||
go-version:
|
||||
- '1.25'
|
||||
- '1.24'
|
||||
- '1.23'
|
||||
- '1.22'
|
||||
- '1.21'
|
||||
- '1.20'
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOTOOLCHAIN: local
|
||||
# Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919
|
||||
MSYS_NO_PATHCONV: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.25.x
|
||||
# that means after golang1.26 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.25 commit for Windows7/8
|
||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.24.x
|
||||
# that means after golang1.25 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.24 commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.24' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.23.x
|
||||
# that means after golang1.24 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.23 commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.23' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
# this patch file only works on golang1.22.x
|
||||
# that means after golang1.23 release it must be changed
|
||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
|
||||
# revert:
|
||||
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
|
||||
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
|
||||
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
|
||||
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
|
||||
- name: Revert Golang1.22 commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.22' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1
|
||||
curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1
|
||||
|
||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
||||
- name: Revert Golang1.21 commit for Windows7/8
|
||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.21' }}
|
||||
run: |
|
||||
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
|
||||
cd $(go env GOROOT)
|
||||
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
|
||||
|
||||
- name: Test
|
||||
run: go test ./... -v -count=1
|
||||
|
||||
- name: Test with tag with_gvisor
|
||||
run: go test ./... -v -count=1 -tags "with_gvisor"
|
||||
30
.github/workflows/trigger-cmfa-update.yml
vendored
Normal file
30
.github/workflows/trigger-cmfa-update.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Trigger CMFA Update
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
- ".github/ISSUE_TEMPLATE/**"
|
||||
branches:
|
||||
- Alpha
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
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"}'
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@ -1,5 +1,28 @@
|
||||
__pycache__
|
||||
venv
|
||||
build
|
||||
mihomo.egg-info
|
||||
*.ipynb
|
||||
# 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
.golangci.yaml
Normal file
17
.golangci.yaml
Normal file
@ -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'
|
||||
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -0,0 +1,26 @@
|
||||
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 && chmod +x mihomo && 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
|
||||
ENTRYPOINT [ "/mihomo" ]
|
||||
675
LICENSE
675
LICENSE
@ -1,7 +1,674 @@
|
||||
Copyright 2023 KT
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
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.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
Preamble
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
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>.
|
||||
|
||||
206
Makefile
Normal file
206
Makefile
Normal file
@ -0,0 +1,206 @@
|
||||
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-386 \
|
||||
darwin-amd64-compatible \
|
||||
darwin-amd64 \
|
||||
darwin-amd64-v1 \
|
||||
darwin-amd64-v2 \
|
||||
darwin-amd64-v3 \
|
||||
darwin-arm64 \
|
||||
linux-386 \
|
||||
linux-amd64-compatible \
|
||||
linux-amd64 \
|
||||
linux-amd64-v1 \
|
||||
linux-amd64-v2 \
|
||||
linux-amd64-v3 \
|
||||
linux-armv5 \
|
||||
linux-armv6 \
|
||||
linux-armv7 \
|
||||
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-amd64-v1 \
|
||||
windows-amd64-v2 \
|
||||
windows-amd64-v3 \
|
||||
windows-arm64 \
|
||||
windows-arm32v7
|
||||
|
||||
all:linux-amd64-v3 linux-arm64\
|
||||
darwin-amd64-v3 darwin-arm64\
|
||||
windows-amd64-v3 windows-arm64\
|
||||
|
||||
|
||||
darwin-all: darwin-amd64-v3 darwin-arm64
|
||||
|
||||
docker:
|
||||
GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-386:
|
||||
GOARCH=386 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-compatible:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-v1:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-v2:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-amd64-v3:
|
||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
darwin-arm64:
|
||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-386:
|
||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-compatible:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-v1:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-v2:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-amd64-v3:
|
||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-arm64:
|
||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
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-compatible:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64-v1:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64-v2:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-amd64-v3:
|
||||
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
windows-arm64:
|
||||
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
||||
|
||||
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)
|
||||
|
||||
184
README.md
184
README.md
@ -1,115 +1,101 @@
|
||||
# mihomo
|
||||
A simple python pydantic model (type hint and autocompletion support) for Honkai: Star Rail parsed data from the Mihomo API.
|
||||
<h1 align="center">
|
||||
<img src="Meta.png" alt="Meta Kennel" width="200">
|
||||
<br>Meta Kernel<br>
|
||||
</h1>
|
||||
|
||||
API url: https://api.mihomo.me/sr_info_parsed/{UID}?lang={LANG}
|
||||
<h3 align="center">Another Mihomo Kernel.</h3>
|
||||
|
||||
## Installation
|
||||
```
|
||||
pip install -U git+https://github.com/KT-Yeh/mihomo.git
|
||||
<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
|
||||
```
|
||||
|
||||
## Usage
|
||||
Set go proxy if a connection to GitHub is not possible:
|
||||
|
||||
### Basic
|
||||
There are two parsed data formats:
|
||||
- V1:
|
||||
- URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en&version=v1
|
||||
- Fetching: use `client.fetch_user_v1(800333171)`
|
||||
- Data model: `mihomo.models.v1.StarrailInfoParsedV1`
|
||||
- All models defined in `mihomo/models/v1` directory.
|
||||
- V2:
|
||||
- URL: https://api.mihomo.me/sr_info_parsed/800333171?lang=en
|
||||
- Fetching: use `client.fetch_user(800333171)`
|
||||
- Data model: `mihomo.models.StarrailInfoParsed`
|
||||
- All models defined in `mihomo/models` directory.
|
||||
|
||||
If you don't want to use `client.get_icon_url` to get the image url everytime, you can use `client.fetch_user(800333171, replace_icon_name_with_url=True)` to get the parsed data with asset urls.
|
||||
|
||||
### Example
|
||||
```py
|
||||
import asyncio
|
||||
|
||||
from mihomo import Language, MihomoAPI
|
||||
from mihomo.models import StarrailInfoParsed
|
||||
from mihomo.models.v1 import StarrailInfoParsedV1
|
||||
|
||||
client = MihomoAPI(language=Language.EN)
|
||||
|
||||
|
||||
async def v1():
|
||||
data: StarrailInfoParsedV1 = await client.fetch_user_v1(800333171)
|
||||
|
||||
print(f"Name: {data.player.name}")
|
||||
print(f"Level: {data.player.level}")
|
||||
print(f"Signature: {data.player.signature}")
|
||||
print(f"Achievements: {data.player_details.achievements}")
|
||||
print(f"Characters count: {data.player_details.characters}")
|
||||
print(f"Profile picture url: {client.get_icon_url(data.player.icon)}")
|
||||
for character in data.characters:
|
||||
print("-----------")
|
||||
print(f"Name: {character.name}")
|
||||
print(f"Rarity: {character.rarity}")
|
||||
print(f"Level: {character.level}")
|
||||
print(f"Avatar url: {client.get_icon_url(character.icon)}")
|
||||
print(f"Preview url: {client.get_icon_url(character.preview)}")
|
||||
print(f"Portrait url: {client.get_icon_url(character.portrait)}")
|
||||
|
||||
|
||||
async def v2():
|
||||
data: StarrailInfoParsed = await client.fetch_user(800333171, replace_icon_name_with_url=True)
|
||||
|
||||
print(f"Name: {data.player.name}")
|
||||
print(f"Level: {data.player.level}")
|
||||
print(f"Signature: {data.player.signature}")
|
||||
print(f"Profile picture url: {data.player.avatar.icon}")
|
||||
for character in data.characters:
|
||||
print("-----------")
|
||||
print(f"Name: {character.name}")
|
||||
print(f"Rarity: {character.rarity}")
|
||||
print(f"Portrait url: {character.portrait}")
|
||||
|
||||
asyncio.run(v1())
|
||||
asyncio.run(v2())
|
||||
```shell
|
||||
go env -w GOPROXY=https://goproxy.io,direct
|
||||
```
|
||||
|
||||
### Tools
|
||||
`from mihomo import tools`
|
||||
#### Remove Duplicate Character
|
||||
```py
|
||||
data = await client.fetch_user(800333171)
|
||||
data = tools.remove_duplicate_character(data)
|
||||
Build with gvisor tun stack:
|
||||
|
||||
```shell
|
||||
go build -tags with_gvisor
|
||||
```
|
||||
|
||||
#### Merge Character Data
|
||||
```py
|
||||
old_data = await client.fetch_user(800333171)
|
||||
### IPTABLES configuration
|
||||
|
||||
# Change characters in game and wait for the API to refresh
|
||||
# ...
|
||||
Work on Linux OS which supported `iptables`
|
||||
|
||||
new_data = await client.fetch_user(800333171)
|
||||
data = tools.merge_character_data(new_data, old_data)
|
||||
```yaml
|
||||
# Enable the TPROXY listener
|
||||
tproxy-port: 9898
|
||||
|
||||
iptables:
|
||||
enable: true # default is false
|
||||
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
|
||||
```
|
||||
|
||||
### Data Persistence
|
||||
Take pickle and json as an example
|
||||
```py
|
||||
import pickle
|
||||
import zlib
|
||||
from mihomo import MihomoAPI, Language, StarrailInfoParsed
|
||||
## Debugging
|
||||
|
||||
client = MihomoAPI(language=Language.EN)
|
||||
data = await client.fetch_user(800333171)
|
||||
Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug
|
||||
API.
|
||||
|
||||
# Save
|
||||
pickle_data = zlib.compress(pickle.dumps(data))
|
||||
print(len(pickle_data))
|
||||
json_data = data.json(by_alias=True, ensure_ascii=False)
|
||||
print(len(json_data))
|
||||
## Credits
|
||||
|
||||
# Load
|
||||
data_from_pickle = pickle.loads(zlib.decompress(pickle_data))
|
||||
data_from_json = StarrailInfoParsed.parse_raw(json_data)
|
||||
print(type(data_from_pickle))
|
||||
print(type(data_from_json))
|
||||
```
|
||||
- [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.
|
||||
|
||||
**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.**
|
||||
320
adapter/adapter.go
Normal file
320
adapter/adapter.go
Normal file
@ -0,0 +1,320 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/atomic"
|
||||
"github.com/metacubex/mihomo/common/queue"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/common/xsync"
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
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.Map[string, *internalProxyState]
|
||||
}
|
||||
|
||||
// Adapter implements C.Proxy
|
||||
func (p *Proxy) Adapter() C.ProxyAdapter {
|
||||
return p.ProxyAdapter
|
||||
}
|
||||
|
||||
// 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) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||
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) (C.PacketConn, error) {
|
||||
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||
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["uot"] = p.SupportUOT()
|
||||
|
||||
proxyInfo := p.ProxyInfo()
|
||||
mapping["xudp"] = proxyInfo.XUDP
|
||||
mapping["tfo"] = proxyInfo.TFO
|
||||
mapping["mptcp"] = proxyInfo.MPTCP
|
||||
mapping["smux"] = proxyInfo.SMUX
|
||||
mapping["interface"] = proxyInfo.Interface
|
||||
mapping["dialer-proxy"] = proxyInfo.DialerProxy
|
||||
mapping["routing-mark"] = proxyInfo.RoutingMark
|
||||
|
||||
return json.Marshal(mapping)
|
||||
}
|
||||
|
||||
// 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()
|
||||
var ignoredErr error
|
||||
var secondResp *http.Response
|
||||
secondResp, ignoredErr = client.Do(req)
|
||||
if ignoredErr == nil {
|
||||
resp = secondResp
|
||||
_ = resp.Body.Close()
|
||||
start = second
|
||||
} else {
|
||||
if strings.HasPrefix(url, "http://") {
|
||||
log.Errorln("%s failed to get the second response from %s: %v", p.Name(), url, ignoredErr)
|
||||
log.Warnln("It is recommended to use HTTPS for provider.health-check.url and group.url to ensure better reliability. Due to some proxy providers hijacking test addresses and not being compatible with repeated HEAD requests, using HTTP may result in failed tests.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port))
|
||||
return
|
||||
}
|
||||
73
adapter/inbound/addition.go
Normal file
73
adapter/inbound/addition.go
Normal file
@ -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) {}
|
||||
38
adapter/inbound/auth.go
Normal file
38
adapter/inbound/auth.go
Normal file
@ -0,0 +1,38 @@
|
||||
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 {
|
||||
return prefixesContains(skipAuthPrefixes, addr)
|
||||
}
|
||||
20
adapter/inbound/http.go
Normal file
20
adapter/inbound/http.go
Normal file
@ -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
|
||||
}
|
||||
19
adapter/inbound/https.go
Normal file
19
adapter/inbound/https.go
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
metadata.RawSrcAddr = conn.RemoteAddr()
|
||||
metadata.RawDstAddr = conn.LocalAddr()
|
||||
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr()))
|
||||
ApplyAdditions(metadata, additions...)
|
||||
return conn, metadata
|
||||
}
|
||||
47
adapter/inbound/ipfilter.go
Normal file
47
adapter/inbound/ipfilter.go
Normal file
@ -0,0 +1,47 @@
|
||||
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
|
||||
}
|
||||
ipAddr := m.AddrPort().Addr()
|
||||
if ipAddr.IsValid() {
|
||||
return isAllowed(ipAddr) && !isDisAllowed(ipAddr)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAllowed(addr netip.Addr) bool {
|
||||
return prefixesContains(lanAllowedIPs, addr)
|
||||
}
|
||||
|
||||
func isDisAllowed(addr netip.Addr) bool {
|
||||
return prefixesContains(lanDisAllowedIPs, addr)
|
||||
}
|
||||
106
adapter/inbound/listen.go
Normal file
106
adapter/inbound/listen.go
Normal file
@ -0,0 +1,106 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/metacubex/mihomo/component/keepalive"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
)
|
||||
|
||||
var (
|
||||
lc = tfo.ListenConfig{
|
||||
DisableTFO: true,
|
||||
}
|
||||
mutex sync.RWMutex
|
||||
)
|
||||
|
||||
func SetTfo(open bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
lc.DisableTFO = !open
|
||||
}
|
||||
|
||||
func Tfo() bool {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
return !lc.DisableTFO
|
||||
}
|
||||
|
||||
func SetMPTCP(open bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
setMultiPathTCP(&lc.ListenConfig, open)
|
||||
}
|
||||
|
||||
func MPTCP() bool {
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
return getMultiPathTCP(&lc.ListenConfig)
|
||||
}
|
||||
|
||||
func preResolve(network, address string) (string, error) {
|
||||
switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr
|
||||
case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6":
|
||||
if host, port, err := net.SplitHostPort(address); err == nil {
|
||||
switch host {
|
||||
case "localhost":
|
||||
switch network {
|
||||
case "tcp6", "udp6", "ip6":
|
||||
address = net.JoinHostPort("::1", port)
|
||||
default:
|
||||
address = net.JoinHostPort("127.0.0.1", port)
|
||||
}
|
||||
case "": // internetAddrList can handle this special case
|
||||
break
|
||||
default:
|
||||
if _, err := netip.ParseAddr(host); err != nil { // not ip
|
||||
return "", fmt.Errorf("invalid network address: %s", address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func ListenContext(ctx context.Context, network, address string) (net.Listener, error) {
|
||||
address, err := preResolve(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
return lc.Listen(ctx, network, address)
|
||||
}
|
||||
|
||||
func Listen(network, address string) (net.Listener, error) {
|
||||
return ListenContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func ListenPacketContext(ctx context.Context, network, address string) (net.PacketConn, error) {
|
||||
address, err := preResolve(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mutex.RLock()
|
||||
defer mutex.RUnlock()
|
||||
return lc.ListenPacket(ctx, network, address)
|
||||
}
|
||||
|
||||
func ListenPacket(network, address string) (net.PacketConn, error) {
|
||||
return ListenPacketContext(context.Background(), network, address)
|
||||
}
|
||||
|
||||
func init() {
|
||||
keepalive.SetDisableKeepAliveCallback.Register(func(b bool) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
keepalive.SetNetListenConfig(&lc.ListenConfig)
|
||||
})
|
||||
}
|
||||
14
adapter/inbound/listen_notwindows.go
Normal file
14
adapter/inbound/listen_notwindows.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
const SupportNamedPipe = false
|
||||
|
||||
func ListenNamedPipe(path string) (net.Listener, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
32
adapter/inbound/listen_windows.go
Normal file
32
adapter/inbound/listen_windows.go
Normal file
@ -0,0 +1,32 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/metacubex/wireguard-go/ipc/namedpipe"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const SupportNamedPipe = true
|
||||
|
||||
// windowsSDDL is the Security Descriptor set on the namedpipe.
|
||||
// It provides read/write access to all users and the local system.
|
||||
const windowsSDDL = "D:PAI(A;OICI;GWGR;;;BU)(A;OICI;GWGR;;;SY)"
|
||||
|
||||
func ListenNamedPipe(path string) (net.Listener, error) {
|
||||
sddl := os.Getenv("LISTEN_NAMEDPIPE_SDDL")
|
||||
if sddl == "" {
|
||||
sddl = windowsSDDL
|
||||
}
|
||||
securityDescriptor, err := windows.SecurityDescriptorFromString(sddl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namedpipeLC := namedpipe.ListenConfig{
|
||||
SecurityDescriptor: securityDescriptor,
|
||||
InputBufferSize: 256 * 1024,
|
||||
OutputBufferSize: 256 * 1024,
|
||||
}
|
||||
return namedpipeLC.Listen(path)
|
||||
}
|
||||
14
adapter/inbound/mptcp_go120.go
Normal file
14
adapter/inbound/mptcp_go120.go
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package inbound
|
||||
|
||||
import "net"
|
||||
|
||||
const multipathTCPAvailable = false
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
||||
}
|
||||
|
||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
||||
return false
|
||||
}
|
||||
15
adapter/inbound/mptcp_go121.go
Normal file
15
adapter/inbound/mptcp_go121.go
Normal file
@ -0,0 +1,15 @@
|
||||
//go:build go1.21
|
||||
|
||||
package inbound
|
||||
|
||||
import "net"
|
||||
|
||||
const multipathTCPAvailable = true
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig, open bool) {
|
||||
listenConfig.SetMultipathTCP(open)
|
||||
}
|
||||
|
||||
func getMultiPathTCP(listenConfig *net.ListenConfig) bool {
|
||||
return listenConfig.MultipathTCP()
|
||||
}
|
||||
22
adapter/inbound/packet.go
Normal file
22
adapter/inbound/packet.go
Normal file
@ -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
adapter/inbound/socket.go
Normal file
18
adapter/inbound/socket.go
Normal file
@ -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
|
||||
}
|
||||
62
adapter/inbound/util.go
Normal file
62
adapter/inbound/util.go
Normal file
@ -0,0 +1,62 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
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, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len])
|
||||
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
|
||||
case socks5.AtypIPv6:
|
||||
metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len])
|
||||
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
|
||||
}
|
||||
metadata.DstIP = metadata.DstIP.Unmap()
|
||||
|
||||
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, ".")
|
||||
|
||||
metadata := &C.Metadata{}
|
||||
_ = metadata.SetRemoteAddress(net.JoinHostPort(host, port))
|
||||
return metadata
|
||||
}
|
||||
|
||||
func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool {
|
||||
if len(prefixes) == 0 {
|
||||
return false
|
||||
}
|
||||
if !addr.IsValid() {
|
||||
return false
|
||||
}
|
||||
addr = addr.Unmap().WithZone("") // netip.Prefix.Contains returns false if ip has an IPv6 zone
|
||||
for _, prefix := range prefixes {
|
||||
if prefix.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
135
adapter/outbound/anytls.go
Normal file
135
adapter/outbound/anytls.go
Normal file
@ -0,0 +1,135 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
CN "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/anytls"
|
||||
"github.com/metacubex/mihomo/transport/vmess"
|
||||
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
"github.com/metacubex/sing/common/uot"
|
||||
)
|
||||
|
||||
type AnyTLS struct {
|
||||
*Base
|
||||
client *anytls.Client
|
||||
dialer proxydialer.SingDialer
|
||||
option *AnyTLSOption
|
||||
}
|
||||
|
||||
type AnyTLSOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
|
||||
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
|
||||
MinIdleSession int `proxy:"min-idle-session,omitempty"`
|
||||
}
|
||||
|
||||
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := t.client.CreateProxy(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c, t), nil
|
||||
}
|
||||
|
||||
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create tcp
|
||||
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create uot on tcp
|
||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (t *AnyTLS) SupportUOT() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (t *AnyTLS) ProxyInfo() C.ProxyInfo {
|
||||
info := t.Base.ProxyInfo()
|
||||
info.DialerProxy = t.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (t *AnyTLS) Close() error {
|
||||
return t.client.Close()
|
||||
}
|
||||
|
||||
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
outbound := &AnyTLS{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.AnyTLS,
|
||||
udp: option.UDP,
|
||||
tfo: option.TFO,
|
||||
mpTcp: option.MPTCP,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||
},
|
||||
option: &option,
|
||||
}
|
||||
|
||||
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...))
|
||||
outbound.dialer = singDialer
|
||||
|
||||
tOption := anytls.ClientConfig{
|
||||
Password: option.Password,
|
||||
Server: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
|
||||
Dialer: singDialer,
|
||||
IdleSessionCheckInterval: time.Duration(option.IdleSessionCheckInterval) * time.Second,
|
||||
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
|
||||
MinIdleSession: option.MinIdleSession,
|
||||
}
|
||||
echConfig, err := option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := &vmess.TLSConfig{
|
||||
Host: option.SNI,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
NextProtos: option.ALPN,
|
||||
FingerPrint: option.Fingerprint,
|
||||
ClientFingerprint: option.ClientFingerprint,
|
||||
ECH: echConfig,
|
||||
}
|
||||
if tlsConfig.Host == "" {
|
||||
tlsConfig.Host = option.Server
|
||||
}
|
||||
tOption.TLSConfig = tlsConfig
|
||||
|
||||
client := anytls.NewClient(context.TODO(), tOption)
|
||||
outbound.client = client
|
||||
|
||||
return outbound, nil
|
||||
}
|
||||
396
adapter/outbound/base.go
Normal file
396
adapter/outbound/base.go
Normal file
@ -0,0 +1,396 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type ProxyAdapter interface {
|
||||
C.ProxyAdapter
|
||||
DialOptions() []dialer.Option
|
||||
ResolveUDP(ctx context.Context, metadata *C.Metadata) error
|
||||
}
|
||||
|
||||
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) (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) (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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (b *Base) ProxyInfo() (info C.ProxyInfo) {
|
||||
info.XUDP = b.xudp
|
||||
info.TFO = b.tfo
|
||||
info.MPTCP = b.mpTcp
|
||||
info.SMUX = false
|
||||
info.Interface = b.iface
|
||||
info.RoutingMark = b.rmark
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
||||
func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
if !metadata.Resolved() {
|
||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't resolve ip: %w", err)
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Base) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type BasicOption struct {
|
||||
TFO bool `proxy:"tfo,omitempty"`
|
||||
MPTCP bool `proxy:"mptcp,omitempty"`
|
||||
Interface string `proxy:"interface-name,omitempty"`
|
||||
RoutingMark int `proxy:"routing-mark,omitempty"`
|
||||
IPVersion string `proxy:"ip-version,omitempty"`
|
||||
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
|
||||
}
|
||||
|
||||
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
|
||||
adapterAddr string
|
||||
}
|
||||
|
||||
func (c *conn) RemoteDestination() string {
|
||||
if remoteAddr := c.RemoteAddr(); remoteAddr != nil {
|
||||
m := C.Metadata{}
|
||||
if err := m.SetRemoteAddr(remoteAddr); err == nil {
|
||||
if m.Valid() {
|
||||
return m.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(c.adapterAddr)
|
||||
return host
|
||||
}
|
||||
|
||||
// Chains implements C.Connection
|
||||
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 (c *conn) AddRef(ref any) {
|
||||
c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter
|
||||
}
|
||||
|
||||
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||
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()}, a.Addr()}
|
||||
}
|
||||
|
||||
type packetConn struct {
|
||||
N.EnhancePacketConn
|
||||
chain C.Chain
|
||||
adapterName string
|
||||
connID string
|
||||
adapterAddr string
|
||||
resolveUDP func(ctx context.Context, metadata *C.Metadata) error
|
||||
}
|
||||
|
||||
func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
return c.resolveUDP(ctx, metadata)
|
||||
}
|
||||
|
||||
func (c *packetConn) RemoteDestination() string {
|
||||
host, _, _ := net.SplitHostPort(c.adapterAddr)
|
||||
return host
|
||||
}
|
||||
|
||||
// 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 (c *packetConn) AddRef(ref any) {
|
||||
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
|
||||
}
|
||||
|
||||
func newPacketConn(pc net.PacketConn, a ProxyAdapter) C.PacketConn {
|
||||
epc := N.NewEnhancePacketConn(pc)
|
||||
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
|
||||
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
|
||||
}
|
||||
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
|
||||
}
|
||||
|
||||
type AddRef interface {
|
||||
AddRef(ref any)
|
||||
}
|
||||
|
||||
type autoCloseProxyAdapter struct {
|
||||
ProxyAdapter
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c, ok := c.(AddRef); ok {
|
||||
c.AddRef(p)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (p *autoCloseProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := p.ProxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c, ok := c.(AddRef); ok {
|
||||
c.AddRef(p)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc, ok := pc.(AddRef); ok {
|
||||
pc.AddRef(p)
|
||||
}
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func (p *autoCloseProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
pc, err := p.ProxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc, ok := pc.(AddRef); ok {
|
||||
pc.AddRef(p)
|
||||
}
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
func (p *autoCloseProxyAdapter) Close() error {
|
||||
p.closeOnce.Do(func() {
|
||||
log.Debugln("Closing outdated proxy [%s]", p.Name())
|
||||
runtime.SetFinalizer(p, nil)
|
||||
p.closeErr = p.ProxyAdapter.Close()
|
||||
})
|
||||
return p.closeErr
|
||||
}
|
||||
|
||||
func NewAutoCloseProxyAdapter(adapter ProxyAdapter) ProxyAdapter {
|
||||
proxy := &autoCloseProxyAdapter{
|
||||
ProxyAdapter: adapter,
|
||||
}
|
||||
// auto close ProxyAdapter
|
||||
runtime.SetFinalizer(proxy, (*autoCloseProxyAdapter).Close)
|
||||
return proxy
|
||||
}
|
||||
105
adapter/outbound/direct.go
Normal file
105
adapter/outbound/direct.go
Normal file
@ -0,0 +1,105 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/loopback"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
)
|
||||
|
||||
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) (C.Conn, error) {
|
||||
if err := d.loopBack.CheckConn(metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := d.DialOptions()
|
||||
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
|
||||
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.loopBack.NewConn(NewConn(c, d)), nil
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
if err := d.loopBack.CheckPacketConn(metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
|
||||
}
|
||||
|
||||
func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" {
|
||||
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't resolve ip: %w", err)
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
|
||||
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
169
adapter/outbound/dns.go
Normal file
169
adapter/outbound/dns.go
Normal file
@ -0,0 +1,169 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"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) (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) (C.PacketConn, error) {
|
||||
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
|
||||
if err := d.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return newPacketConn(&dnsPacketConn{
|
||||
response: make(chan dnsPacket, 1),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}, d), nil
|
||||
}
|
||||
|
||||
func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
if !metadata.Resolved() {
|
||||
metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dnsPacket struct {
|
||||
data []byte
|
||||
put func()
|
||||
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),
|
||||
},
|
||||
}
|
||||
}
|
||||
36
adapter/outbound/ech.go
Normal file
36
adapter/outbound/ech.go
Normal file
@ -0,0 +1,36 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ech"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
)
|
||||
|
||||
type ECHOptions struct {
|
||||
Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"`
|
||||
Config string `proxy:"config,omitempty" obfs:"config,omitempty"`
|
||||
}
|
||||
|
||||
func (o ECHOptions) Parse() (*ech.Config, error) {
|
||||
if !o.Enable {
|
||||
return nil, nil
|
||||
}
|
||||
echConfig := &ech.Config{}
|
||||
if o.Config != "" {
|
||||
list, err := base64.StdEncoding.DecodeString(o.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("base64 decode ech config string failed: %v", err)
|
||||
}
|
||||
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
|
||||
return list, nil
|
||||
}
|
||||
} else {
|
||||
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
|
||||
return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver)
|
||||
}
|
||||
}
|
||||
return echConfig, nil
|
||||
}
|
||||
195
adapter/outbound/http.go
Normal file
195
adapter/outbound/http.go
Normal file
@ -0,0 +1,195 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"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"
|
||||
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.shakeHandContext(ctx, c, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
return h.DialContextWithDialer(ctx, dialer.NewDialer(h.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (h *Http) ProxyInfo() C.ProxyInfo {
|
||||
info := h.Base.ProxyInfo()
|
||||
info.DialerProxy = h.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
addr := metadata.RemoteAddress()
|
||||
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 = c.Write([]byte(HeaderString))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(c), 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
|
||||
}
|
||||
329
adapter/outbound/hysteria.go
Normal file
329
adapter/outbound/hysteria.go
Normal file
@ -0,0 +1,329 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/ech"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"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"
|
||||
|
||||
"github.com/metacubex/quic-go"
|
||||
"github.com/metacubex/quic-go/congestion"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
tlsConfig *tlsC.Config
|
||||
echConfig *ech.Config
|
||||
}
|
||||
|
||||
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(tcpConn, h), nil
|
||||
}
|
||||
|
||||
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
if err := h.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
udpConn, err := h.client.DialUDP(h.genHdc(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPacketConn(&hyPacketConn{udpConn}, h), nil
|
||||
}
|
||||
|
||||
func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer {
|
||||
return &hyDialerWithContext{
|
||||
ctx: context.Background(),
|
||||
hyDialer: func(network string, rAddr net.Addr) (net.PacketConn, error) {
|
||||
var err error
|
||||
var cDialer C.Dialer = dialer.NewDialer(h.DialOptions()...)
|
||||
if len(h.option.DialerProxy) > 0 {
|
||||
cDialer, err = proxydialer.NewByName(h.option.DialerProxy, cDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rAddrPort, _ := netip.ParseAddrPort(rAddr.String())
|
||||
return cDialer.ListenPacket(ctx, network, "", rAddrPort)
|
||||
},
|
||||
remoteAddr: func(addr string) (net.Addr, error) {
|
||||
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = h.echConfig.ClientHandle(ctx, h.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpAddr, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (h *Hysteria) ProxyInfo() C.ProxyInfo {
|
||||
info := h.Base.ProxyInfo()
|
||||
info.DialerProxy = h.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
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"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
CustomCA string `proxy:"ca,omitempty"`
|
||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `proxy:"fast-open,omitempty"`
|
||||
HopInterval int `proxy:"hop-interval,omitempty"`
|
||||
}
|
||||
|
||||
func (c *HysteriaOption) Speed() (uint64, uint64, error) {
|
||||
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{}
|
||||
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 option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
tlsConfig.NextProtos = option.ALPN
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||
}
|
||||
|
||||
echConfig, err := option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
||||
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
|
||||
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, tlsClientConfig, 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)
|
||||
}
|
||||
outbound := &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,
|
||||
tlsConfig: tlsClientConfig,
|
||||
echConfig: echConfig,
|
||||
}
|
||||
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (h *Hysteria) Close() error {
|
||||
if h.client != nil {
|
||||
return h.client.Close()
|
||||
}
|
||||
return 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, rAddr net.Addr) (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, rAddr)
|
||||
}
|
||||
|
||||
func (h *hyDialerWithContext) Context() context.Context {
|
||||
return h.ctx
|
||||
}
|
||||
|
||||
func (h *hyDialerWithContext) RemoteAddr(host string) (net.Addr, error) {
|
||||
return h.remoteAddr(host)
|
||||
}
|
||||
237
adapter/outbound/hysteria2.go
Normal file
237
adapter/outbound/hysteria2.go
Normal file
@ -0,0 +1,237 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"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"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
|
||||
|
||||
"github.com/metacubex/quic-go"
|
||||
"github.com/metacubex/sing-quic/hysteria2"
|
||||
M "github.com/metacubex/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"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
CustomCA string `proxy:"ca,omitempty"`
|
||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||
CWND int `proxy:"cwnd,omitempty"`
|
||||
UdpMTU int `proxy:"udp-mtu,omitempty"`
|
||||
|
||||
// quic-go special config
|
||||
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"`
|
||||
InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"`
|
||||
MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c, h), nil
|
||||
}
|
||||
|
||||
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if err = h.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err := h.client.ListenPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc == nil {
|
||||
return nil, errors.New("packetConn is nil")
|
||||
}
|
||||
return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (h *Hysteria2) Close() error {
|
||||
if h.client != nil {
|
||||
return h.client.CloseWithError(errors.New("proxy removed"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (h *Hysteria2) ProxyInfo() C.ProxyInfo {
|
||||
info := h.Base.ProxyInfo()
|
||||
info.DialerProxy = h.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
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,
|
||||
}
|
||||
|
||||
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...))
|
||||
outbound.dialer = singDialer
|
||||
|
||||
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 option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
tlsConfig.NextProtos = option.ALPN
|
||||
}
|
||||
|
||||
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||
echConfig, err := option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if option.UdpMTU == 0 {
|
||||
// "1200" from quic-go's MaxDatagramSize
|
||||
// "-3" from quic-go's DatagramFrame.MaxDataLen
|
||||
option.UdpMTU = 1200 - 3
|
||||
}
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: option.InitialStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: option.MaxStreamReceiveWindow,
|
||||
InitialConnectionReceiveWindow: option.InitialConnectionReceiveWindow,
|
||||
MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow,
|
||||
}
|
||||
|
||||
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: tlsClientConfig,
|
||||
QUICConfig: quicConfig,
|
||||
UDPDisabled: false,
|
||||
CWND: option.CWND,
|
||||
UdpMTU: option.UdpMTU,
|
||||
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
|
||||
udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = echConfig.ClientHandle(ctx, tlsClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpAddr, nil
|
||||
},
|
||||
}
|
||||
|
||||
var ranges utils.IntRanges[uint16]
|
||||
var serverPorts []uint16
|
||||
if option.Ports != "" {
|
||||
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ranges.Range(func(port uint16) bool {
|
||||
serverPorts = append(serverPorts, port)
|
||||
return true
|
||||
})
|
||||
if len(serverPorts) > 0 {
|
||||
if option.HopInterval == 0 {
|
||||
option.HopInterval = defaultHopInterval
|
||||
} else if option.HopInterval < minHopInterval {
|
||||
option.HopInterval = minHopInterval
|
||||
}
|
||||
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
|
||||
clientOptions.ServerPorts = serverPorts
|
||||
}
|
||||
}
|
||||
if option.Port == 0 && len(serverPorts) == 0 {
|
||||
return nil, errors.New("invalid port")
|
||||
}
|
||||
|
||||
client, err := hysteria2.NewClient(clientOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outbound.client = client
|
||||
|
||||
return outbound, nil
|
||||
}
|
||||
313
adapter/outbound/mieru.go
Normal file
313
adapter/outbound/mieru.go
Normal file
@ -0,0 +1,313 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
CN "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
|
||||
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type Mieru struct {
|
||||
*Base
|
||||
option *MieruOption
|
||||
client mieruclient.Client
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type MieruOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port,omitempty"`
|
||||
PortRange string `proxy:"port-range,omitempty"`
|
||||
Transport string `proxy:"transport"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
UserName string `proxy:"username"`
|
||||
Password string `proxy:"password"`
|
||||
Multiplexing string `proxy:"multiplexing,omitempty"`
|
||||
HandshakeMode string `proxy:"handshake-mode,omitempty"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
if err := m.ensureClientIsRunning(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr := metadataToMieruNetAddrSpec(metadata)
|
||||
c, err := m.client.DialContext(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial to %s failed: %w", addr, err)
|
||||
}
|
||||
return NewConn(c, m), nil
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if err = m.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.ensureClientIsRunning(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := m.client.DialContext(ctx, metadata.UDPAddr())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
|
||||
}
|
||||
return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (m *Mieru) SupportUOT() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (m *Mieru) ProxyInfo() C.ProxyInfo {
|
||||
info := m.Base.ProxyInfo()
|
||||
info.DialerProxy = m.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func (m *Mieru) ensureClientIsRunning() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.client.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a dialer and add it to the client config, before starting the client.
|
||||
var dialer C.Dialer = dialer.NewDialer(m.DialOptions()...)
|
||||
var err error
|
||||
if len(m.option.DialerProxy) > 0 {
|
||||
dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
config, err := m.client.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Dialer = dialer
|
||||
if err := m.client.Store(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.client.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start mieru client: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMieru(option MieruOption) (*Mieru, error) {
|
||||
config, err := buildMieruClientConfig(option)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build mieru client config: %w", err)
|
||||
}
|
||||
c := mieruclient.NewClient()
|
||||
if err := c.Store(config); err != nil {
|
||||
return nil, fmt.Errorf("failed to store mieru client config: %w", err)
|
||||
}
|
||||
// Client is started lazily on the first use.
|
||||
|
||||
var addr string
|
||||
if option.Port != 0 {
|
||||
addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
} else {
|
||||
beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange)
|
||||
addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort))
|
||||
}
|
||||
outbound := &Mieru{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
iface: option.Interface,
|
||||
tp: C.Mieru,
|
||||
udp: option.UDP,
|
||||
xudp: false,
|
||||
rmark: option.RoutingMark,
|
||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||
},
|
||||
option: &option,
|
||||
client: c,
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (m *Mieru) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.client != nil && m.client.IsRunning() {
|
||||
return m.client.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {
|
||||
if metadata.Host != "" {
|
||||
return mierumodel.NetAddrSpec{
|
||||
AddrSpec: mierumodel.AddrSpec{
|
||||
FQDN: metadata.Host,
|
||||
Port: int(metadata.DstPort),
|
||||
},
|
||||
Net: "tcp",
|
||||
}
|
||||
} else {
|
||||
return mierumodel.NetAddrSpec{
|
||||
AddrSpec: mierumodel.AddrSpec{
|
||||
IP: metadata.DstIP.AsSlice(),
|
||||
Port: int(metadata.DstPort),
|
||||
},
|
||||
Net: "tcp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) {
|
||||
if err := validateMieruOption(option); err != nil {
|
||||
return nil, fmt.Errorf("failed to validate mieru option: %w", err)
|
||||
}
|
||||
|
||||
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
|
||||
var server *mierupb.ServerEndpoint
|
||||
if net.ParseIP(option.Server) != nil {
|
||||
// server is an IP address
|
||||
if option.PortRange != "" {
|
||||
server = &mierupb.ServerEndpoint{
|
||||
IpAddress: proto.String(option.Server),
|
||||
PortBindings: []*mierupb.PortBinding{
|
||||
{
|
||||
PortRange: proto.String(option.PortRange),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
server = &mierupb.ServerEndpoint{
|
||||
IpAddress: proto.String(option.Server),
|
||||
PortBindings: []*mierupb.PortBinding{
|
||||
{
|
||||
Port: proto.Int32(int32(option.Port)),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// server is a domain name
|
||||
if option.PortRange != "" {
|
||||
server = &mierupb.ServerEndpoint{
|
||||
DomainName: proto.String(option.Server),
|
||||
PortBindings: []*mierupb.PortBinding{
|
||||
{
|
||||
PortRange: proto.String(option.PortRange),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
server = &mierupb.ServerEndpoint{
|
||||
DomainName: proto.String(option.Server),
|
||||
PortBindings: []*mierupb.PortBinding{
|
||||
{
|
||||
Port: proto.Int32(int32(option.Port)),
|
||||
Protocol: transportProtocol,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
config := &mieruclient.ClientConfig{
|
||||
Profile: &mierupb.ClientProfile{
|
||||
ProfileName: proto.String(option.Name),
|
||||
User: &mierupb.User{
|
||||
Name: proto.String(option.UserName),
|
||||
Password: proto.String(option.Password),
|
||||
},
|
||||
Servers: []*mierupb.ServerEndpoint{server},
|
||||
},
|
||||
}
|
||||
if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok {
|
||||
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||
}
|
||||
}
|
||||
if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
|
||||
config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func validateMieruOption(option MieruOption) error {
|
||||
if option.Name == "" {
|
||||
return fmt.Errorf("name is empty")
|
||||
}
|
||||
if option.Server == "" {
|
||||
return fmt.Errorf("server is empty")
|
||||
}
|
||||
if option.Port == 0 && option.PortRange == "" {
|
||||
return fmt.Errorf("either port or port-range must be set")
|
||||
}
|
||||
if option.Port != 0 && option.PortRange != "" {
|
||||
return fmt.Errorf("port and port-range cannot be set at the same time")
|
||||
}
|
||||
if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
|
||||
return fmt.Errorf("port must be between 1 and 65535")
|
||||
}
|
||||
if option.PortRange != "" {
|
||||
begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port-range format")
|
||||
}
|
||||
if begin < 1 || begin > 65535 {
|
||||
return fmt.Errorf("begin port must be between 1 and 65535")
|
||||
}
|
||||
if end < 1 || end > 65535 {
|
||||
return fmt.Errorf("end port must be between 1 and 65535")
|
||||
}
|
||||
if begin > end {
|
||||
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||
}
|
||||
}
|
||||
|
||||
if option.Transport != "TCP" {
|
||||
return fmt.Errorf("transport must be TCP")
|
||||
}
|
||||
if option.UserName == "" {
|
||||
return fmt.Errorf("username is empty")
|
||||
}
|
||||
if option.Password == "" {
|
||||
return fmt.Errorf("password is empty")
|
||||
}
|
||||
if option.Multiplexing != "" {
|
||||
if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok {
|
||||
return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing)
|
||||
}
|
||||
}
|
||||
if option.HandshakeMode != "" {
|
||||
if _, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; !ok {
|
||||
return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
|
||||
var begin, end int
|
||||
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
|
||||
return begin, end, err
|
||||
}
|
||||
92
adapter/outbound/mieru_test.go
Normal file
92
adapter/outbound/mieru_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package outbound
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewMieru(t *testing.T) {
|
||||
testCases := []struct {
|
||||
option MieruOption
|
||||
wantBaseAddr string
|
||||
}{
|
||||
{
|
||||
option: MieruOption{
|
||||
Name: "test",
|
||||
Server: "1.2.3.4",
|
||||
Port: 10000,
|
||||
Transport: "TCP",
|
||||
UserName: "test",
|
||||
Password: "test",
|
||||
},
|
||||
wantBaseAddr: "1.2.3.4:10000",
|
||||
},
|
||||
{
|
||||
option: MieruOption{
|
||||
Name: "test",
|
||||
Server: "2001:db8::1",
|
||||
PortRange: "10001-10002",
|
||||
Transport: "TCP",
|
||||
UserName: "test",
|
||||
Password: "test",
|
||||
},
|
||||
wantBaseAddr: "[2001:db8::1]:10001",
|
||||
},
|
||||
{
|
||||
option: MieruOption{
|
||||
Name: "test",
|
||||
Server: "example.com",
|
||||
Port: 10003,
|
||||
Transport: "TCP",
|
||||
UserName: "test",
|
||||
Password: "test",
|
||||
},
|
||||
wantBaseAddr: "example.com:10003",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
mieru, err := NewMieru(testCase.option)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if mieru.addr != testCase.wantBaseAddr {
|
||||
t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeginAndEndPortFromPortRange(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
begin int
|
||||
end int
|
||||
hasErr bool
|
||||
}{
|
||||
{"1-10", 1, 10, false},
|
||||
{"1000-2000", 1000, 2000, false},
|
||||
{"65535-65535", 65535, 65535, false},
|
||||
{"1", 0, 0, true},
|
||||
{"1-", 0, 0, true},
|
||||
{"-10", 0, 0, true},
|
||||
{"a-b", 0, 0, true},
|
||||
{"1-b", 0, 0, true},
|
||||
{"a-10", 0, 0, true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
begin, end, err := beginAndEndPortFromPortRange(testCase.input)
|
||||
if testCase.hasErr {
|
||||
if err == nil {
|
||||
t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err)
|
||||
}
|
||||
if begin != testCase.begin {
|
||||
t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin)
|
||||
}
|
||||
if end != testCase.end {
|
||||
t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
adapter/outbound/reality.go
Normal file
47
adapter/outbound/reality.go
Normal file
@ -0,0 +1,47 @@
|
||||
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"`
|
||||
|
||||
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
|
||||
}
|
||||
|
||||
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
|
||||
if o.PublicKey != "" {
|
||||
config := new(tlsC.RealityConfig)
|
||||
config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768
|
||||
|
||||
const x25519ScalarSize = 32
|
||||
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)
|
||||
if err != nil || len(publicKey) != 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 := hex.DecodedLen(len(o.ShortID))
|
||||
if n > tlsC.RealityMaxShortIDLen {
|
||||
return nil, errors.New("invalid REALITY short id")
|
||||
}
|
||||
n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID))
|
||||
if err != nil || n > tlsC.RealityMaxShortIDLen {
|
||||
return nil, errors.New("invalid REALITY short ID")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
138
adapter/outbound/reject.go
Normal file
138
adapter/outbound/reject.go
Normal file
@ -0,0 +1,138 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/buf"
|
||||
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) (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) (C.PacketConn, error) {
|
||||
if err := r.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPacketConn(&nopPacketConn{}, r), nil
|
||||
}
|
||||
|
||||
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
if !metadata.Resolved() {
|
||||
metadata.DstIP = netip.IPv4Unspecified()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRejectWithOption(option RejectOption) *Reject {
|
||||
return &Reject{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
tp: C.Reject,
|
||||
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 }
|
||||
404
adapter/outbound/shadowsocks.go
Normal file
404
adapter/outbound/shadowsocks.go
Normal file
@ -0,0 +1,404 @@
|
||||
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"
|
||||
gost "github.com/metacubex/mihomo/transport/gost-plugin"
|
||||
"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"
|
||||
|
||||
shadowsocks "github.com/metacubex/sing-shadowsocks2"
|
||||
"github.com/metacubex/sing/common/bufio"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
"github.com/metacubex/sing/common/uot"
|
||||
)
|
||||
|
||||
type ShadowSocks struct {
|
||||
*Base
|
||||
method shadowsocks.Method
|
||||
|
||||
option *ShadowSocksOption
|
||||
// obfs
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
v2rayOption *v2rayObfs.Option
|
||||
gostOption *gost.Option
|
||||
shadowTLSOption *shadowtls.ShadowTLSOption
|
||||
restlsConfig *restls.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"`
|
||||
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"`
|
||||
V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"`
|
||||
}
|
||||
|
||||
type gostObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
type shadowTLSOption struct {
|
||||
Password string `obfs:"password,omitempty"`
|
||||
Host string `obfs:"host"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Version int `obfs:"version,omitempty"`
|
||||
ALPN []string `obfs:"alpn,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, err 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":
|
||||
if ss.v2rayOption != nil {
|
||||
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
|
||||
} else if ss.gostOption != nil {
|
||||
c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption)
|
||||
} else {
|
||||
return nil, fmt.Errorf("plugin options is required")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
case shadowtls.Mode:
|
||||
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
useEarly = true
|
||||
case restls.Mode:
|
||||
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 !useEarly {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
}
|
||||
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) (_ C.Conn, err error) {
|
||||
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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) (C.PacketConn, error) {
|
||||
return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
|
||||
info := ss.Base.ProxyInfo()
|
||||
info.DialerProxy = ss.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if ss.option.UDPOverTCP {
|
||||
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destination := M.SocksaddrFromNet(metadata.UDPAddr())
|
||||
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
|
||||
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
|
||||
} 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 cipher: %s initialize error: %w", addr, option.Cipher, err)
|
||||
}
|
||||
|
||||
var v2rayOption *v2rayObfs.Option
|
||||
var gostOption *gost.Option
|
||||
var obfsOption *simpleObfsOption
|
||||
var shadowTLSOpt *shadowtls.ShadowTLSOption
|
||||
var restlsConfig *restls.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
|
||||
|
||||
echConfig, err := opts.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
|
||||
}
|
||||
v2rayOption.ECHConfig = echConfig
|
||||
}
|
||||
} else if option.Plugin == "gost-plugin" {
|
||||
opts := gostObfsOption{Host: "bing.com", Mux: true}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
|
||||
}
|
||||
|
||||
if opts.Mode != "websocket" {
|
||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
gostOption = &gost.Option{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
Mux: opts.Mux,
|
||||
}
|
||||
|
||||
if opts.TLS {
|
||||
gostOption.TLS = true
|
||||
gostOption.SkipCertVerify = opts.SkipCertVerify
|
||||
gostOption.Fingerprint = opts.Fingerprint
|
||||
|
||||
echConfig, err := opts.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
|
||||
}
|
||||
gostOption.ECHConfig = echConfig
|
||||
}
|
||||
} else if option.Plugin == shadowtls.Mode {
|
||||
obfsMode = shadowtls.Mode
|
||||
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,
|
||||
}
|
||||
|
||||
if opt.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
shadowTLSOpt.ALPN = opt.ALPN
|
||||
} else {
|
||||
shadowTLSOpt.ALPN = shadowtls.DefaultALPN
|
||||
}
|
||||
} 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 = restls.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,
|
||||
gostOption: gostOption,
|
||||
obfsOption: obfsOption,
|
||||
shadowTLSOption: shadowTLSOpt,
|
||||
restlsConfig: restlsConfig,
|
||||
}, nil
|
||||
}
|
||||
265
adapter/outbound/shadowsocksr.go
Normal file
265
adapter/outbound/shadowsocksr.go
Normal file
@ -0,0 +1,265 @@
|
||||
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, err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
c = ssr.obfs.StreamConn(c)
|
||||
c = ssr.cipher.StreamConn(c)
|
||||
var (
|
||||
iv []byte
|
||||
)
|
||||
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) (_ C.Conn, err error) {
|
||||
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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) (C.PacketConn, error) {
|
||||
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
if err = ssr.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (ssr *ShadowSocksR) ProxyInfo() C.ProxyInfo {
|
||||
info := ssr.Base.ProxyInfo()
|
||||
info.DialerProxy = ssr.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||
// 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 cipher: %s initialize error: %w", addr, cipher, 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
|
||||
}
|
||||
124
adapter/outbound/singmux.go
Normal file
124
adapter/outbound/singmux.go
Normal file
@ -0,0 +1,124 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
CN "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
mux "github.com/metacubex/sing-mux"
|
||||
E "github.com/metacubex/sing/common/exceptions"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
)
|
||||
|
||||
type SingMux struct {
|
||||
ProxyAdapter
|
||||
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"`
|
||||
}
|
||||
|
||||
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c, s), err
|
||||
}
|
||||
|
||||
func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if s.onlyTcp {
|
||||
return s.ProxyAdapter.ListenPacketContext(ctx, metadata)
|
||||
}
|
||||
if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc == nil {
|
||||
return nil, E.New("packetConn is nil")
|
||||
}
|
||||
return newPacketConn(CN.NewThreadSafePacketConn(pc), s), 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 (s *SingMux) ProxyInfo() C.ProxyInfo {
|
||||
info := s.ProxyAdapter.ProxyInfo()
|
||||
info.SMUX = true
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (s *SingMux) Close() error {
|
||||
if s.client != nil {
|
||||
_ = s.client.Close()
|
||||
}
|
||||
return s.ProxyAdapter.Close()
|
||||
}
|
||||
|
||||
func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error) {
|
||||
// TODO
|
||||
// "TCP Brutal is only supported on Linux-based systems"
|
||||
|
||||
singDialer := proxydialer.NewSingDialer(proxy, dialer.NewDialer(proxy.DialOptions()...), 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,
|
||||
client: client,
|
||||
dialer: singDialer,
|
||||
onlyTcp: option.OnlyTcp,
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
229
adapter/outbound/snell.go
Normal file
229
adapter/outbound/snell.go
Normal file
@ -0,0 +1,229 @@
|
||||
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 snellStreamConn(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 = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||
err := s.writeHeaderContext(ctx, c, metadata)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
|
||||
if metadata.NetWork == C.UDP {
|
||||
err = snell.WriteUDPHeader(c, s.version)
|
||||
return
|
||||
}
|
||||
err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
|
||||
return
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
if s.version == snell.Version2 {
|
||||
c, err := s.pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.writeHeaderContext(ctx, c, metadata); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c, s), err
|
||||
}
|
||||
|
||||
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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) (C.PacketConn, error) {
|
||||
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
if err = s.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err = s.StreamConnContext(ctx, c, metadata)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (s *Snell) ProxyInfo() C.ProxyInfo {
|
||||
info := s.Base.ProxyInfo()
|
||||
info.DialerProxy = s.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func NewSnell(option SnellOption) (*Snell, error) {
|
||||
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.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
|
||||
}
|
||||
|
||||
return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
|
||||
})
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
266
adapter/outbound/socks5.go
Normal file
266
adapter/outbound/socks5.go
Normal file
@ -0,0 +1,266 @@
|
||||
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 := ss.clientHandshakeContext(ctx, 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) (_ C.Conn, err error) {
|
||||
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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) (_ C.PacketConn, err error) {
|
||||
var cDialer C.Dialer = dialer.NewDialer(ss.DialOptions()...)
|
||||
if len(ss.option.DialerProxy) > 0 {
|
||||
cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = ss.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
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)
|
||||
|
||||
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 := ss.clientHandshakeContext(ctx, 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(), C.IPv4Prefer)
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (ss *Socks5) ProxyInfo() C.ProxyInfo {
|
||||
info := ss.Base.ProxyInfo()
|
||||
info.DialerProxy = ss.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr socks5.Addr, command socks5.Command, user *socks5.User) (_ socks5.Addr, err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
return socks5.ClientHandshake(c, addr, command, user)
|
||||
}
|
||||
|
||||
func NewSocks5(option Socks5Option) (*Socks5, error) {
|
||||
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
adapter/outbound/ssh.go
Normal file
208
adapter/outbound/ssh.go
Normal file
@ -0,0 +1,208 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"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
|
||||
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
cMutex sync.Mutex
|
||||
}
|
||||
|
||||
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) (_ C.Conn, err error) {
|
||||
var cDialer C.Dialer = dialer.NewDialer(s.DialOptions()...)
|
||||
if len(s.option.DialerProxy) > 0 {
|
||||
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
client, err := s.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(c, s), nil
|
||||
}
|
||||
|
||||
func (s *Ssh) 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (s *Ssh) ProxyInfo() C.ProxyInfo {
|
||||
info := s.Base.ProxyInfo()
|
||||
info.DialerProxy = s.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (s *Ssh) Close() error {
|
||||
s.cMutex.Lock()
|
||||
defer s.cMutex.Unlock()
|
||||
if s.client != nil {
|
||||
return s.client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
path := C.Path.Resolve(option.PrivateKey)
|
||||
if !C.Path.IsSafePath(path) {
|
||||
return nil, C.Path.ErrNotSafePath(path)
|
||||
}
|
||||
b, err = os.ReadFile(path)
|
||||
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,
|
||||
config: &config,
|
||||
}
|
||||
|
||||
return outbound, nil
|
||||
}
|
||||
390
adapter/outbound/trojan.go
Normal file
390
adapter/outbound/trojan.go
Normal file
@ -0,0 +1,390 @@
|
||||
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/ech"
|
||||
"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"
|
||||
"github.com/metacubex/mihomo/transport/vmess"
|
||||
)
|
||||
|
||||
type Trojan struct {
|
||||
*Base
|
||||
option *TrojanOption
|
||||
hexPassword [trojan.KeyLength]byte
|
||||
|
||||
// for gun mux
|
||||
gunTLSConfig *tls.Config
|
||||
gunConfig *gun.Config
|
||||
transport *gun.TransportWrap
|
||||
|
||||
realityConfig *tlsC.RealityConfig
|
||||
echConfig *ech.Config
|
||||
|
||||
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"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,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"`
|
||||
}
|
||||
|
||||
// StreamConnContext implements C.ProxyAdapter
|
||||
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
||||
switch t.option.Network {
|
||||
case "ws":
|
||||
host, port, _ := net.SplitHostPort(t.addr)
|
||||
|
||||
wsOpts := &vmess.WebsocketConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: t.option.WSOpts.Path,
|
||||
MaxEarlyData: t.option.WSOpts.MaxEarlyData,
|
||||
EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName,
|
||||
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
|
||||
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
|
||||
ClientFingerprint: t.option.ClientFingerprint,
|
||||
ECHConfig: t.echConfig,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
alpn := trojan.DefaultWebsocketALPN
|
||||
if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
alpn = t.option.ALPN
|
||||
}
|
||||
|
||||
wsOpts.TLS = true
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: alpn,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: t.option.SkipCertVerify,
|
||||
ServerName: t.option.SNI,
|
||||
}
|
||||
|
||||
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
|
||||
case "grpc":
|
||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
|
||||
default:
|
||||
// default tcp network
|
||||
// handle TLS
|
||||
alpn := trojan.DefaultALPN
|
||||
if t.option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
|
||||
alpn = t.option.ALPN
|
||||
}
|
||||
c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{
|
||||
Host: t.option.SNI,
|
||||
SkipCertVerify: t.option.SkipCertVerify,
|
||||
FingerPrint: t.option.Fingerprint,
|
||||
ClientFingerprint: t.option.ClientFingerprint,
|
||||
NextProtos: alpn,
|
||||
ECH: t.echConfig,
|
||||
Reality: t.realityConfig,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
|
||||
return t.streamConnContext(ctx, c, metadata)
|
||||
}
|
||||
|
||||
func (t *Trojan) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
||||
if t.ssCipher != nil {
|
||||
c = t.ssCipher.StreamConn(c)
|
||||
}
|
||||
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
command := trojan.CommandTCP
|
||||
if metadata.NetWork == C.UDP {
|
||||
command = trojan.CommandUDP
|
||||
}
|
||||
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
command := trojan.CommandTCP
|
||||
if metadata.NetWork == C.UDP {
|
||||
command = trojan.CommandUDP
|
||||
}
|
||||
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
|
||||
return err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if t.transport != nil {
|
||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = t.streamConnContext(ctx, c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, t), nil
|
||||
}
|
||||
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), 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)
|
||||
}
|
||||
|
||||
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) (_ C.PacketConn, err error) {
|
||||
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c net.Conn
|
||||
|
||||
// grpc transport
|
||||
if t.transport != nil {
|
||||
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)
|
||||
|
||||
c, err = t.streamConnContext(ctx, c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := trojan.NewPacketConn(c)
|
||||
return newPacketConn(pc, t), err
|
||||
}
|
||||
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
c, err = t.StreamConnContext(ctx, c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := trojan.NewPacketConn(c)
|
||||
return newPacketConn(pc, t), err
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (t *Trojan) SupportWithDialer() C.NetWork {
|
||||
return C.ALLNet
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (t *Trojan) SupportUOT() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (t *Trojan) ProxyInfo() C.ProxyInfo {
|
||||
info := t.Base.ProxyInfo()
|
||||
info.DialerProxy = t.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (t *Trojan) Close() error {
|
||||
if t.transport != nil {
|
||||
return t.transport.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
|
||||
if option.SNI == "" {
|
||||
option.SNI = option.Server
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
option: &option,
|
||||
hexPassword: trojan.Key(option.Password),
|
||||
}
|
||||
|
||||
var err error
|
||||
t.realityConfig, err = option.RealityOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.echConfig, err = option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if option.SSOpts.Enabled {
|
||||
if option.SSOpts.Password == "" {
|
||||
return nil, errors.New("empty password")
|
||||
}
|
||||
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(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var err error
|
||||
var cDialer C.Dialer = dialer.NewDialer(t.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(ctx, "tcp", t.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: option.ALPN,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: option.SNI,
|
||||
}
|
||||
|
||||
var err error
|
||||
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
|
||||
|
||||
t.gunTLSConfig = tlsConfig
|
||||
t.gunConfig = &gun.Config{
|
||||
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||
Host: option.SNI,
|
||||
ClientFingerprint: option.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
336
adapter/outbound/tuic.go
Normal file
336
adapter/outbound/tuic.go
Normal file
@ -0,0 +1,336 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/ech"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/tuic"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/metacubex/quic-go"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
"github.com/metacubex/sing/common/uot"
|
||||
)
|
||||
|
||||
type Tuic struct {
|
||||
*Base
|
||||
option *TuicOption
|
||||
client *tuic.PoolClient
|
||||
|
||||
tlsConfig *tlsC.Config
|
||||
echConfig *ech.Config
|
||||
}
|
||||
|
||||
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"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,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) (C.Conn, error) {
|
||||
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), 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) (_ C.PacketConn, err error) {
|
||||
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
|
||||
}
|
||||
|
||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t.option.UDPOverStream {
|
||||
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
|
||||
uotMetadata := *metadata
|
||||
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
|
||||
|
||||
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 := resolveUDPAddr(ctx, "udp", t.addr, t.prefer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
addr = udpAddr
|
||||
var pc net.PacketConn
|
||||
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (t *Tuic) ProxyInfo() C.ProxyInfo {
|
||||
info := t.Base.ProxyInfo()
|
||||
info.DialerProxy = t.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
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
|
||||
}
|
||||
|
||||
tlsClientConfig := tlsC.UConfig(tlsConfig)
|
||||
echConfig, err := option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch option.UDPOverStreamVersion {
|
||||
case uot.Version, uot.LegacyVersion:
|
||||
case 0:
|
||||
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,
|
||||
tlsConfig: tlsClientConfig,
|
||||
echConfig: echConfig,
|
||||
}
|
||||
|
||||
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: tlsClientConfig,
|
||||
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: tlsClientConfig,
|
||||
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
|
||||
}
|
||||
105
adapter/outbound/util.go
Normal file
105
adapter/outbound/util.go
Normal file
@ -0,0 +1,105 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
)
|
||||
|
||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||
var buf [][]byte
|
||||
addrType := metadata.AddrType()
|
||||
p := uint(metadata.DstPort)
|
||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
||||
switch addrType {
|
||||
case C.AtypDomainName:
|
||||
lenM := uint8(len(metadata.Host))
|
||||
host := []byte(metadata.Host)
|
||||
buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port}
|
||||
case C.AtypIPv4:
|
||||
host := metadata.DstIP.AsSlice()
|
||||
buf = [][]byte{{socks5.AtypIPv4}, host, port}
|
||||
case C.AtypIPv6:
|
||||
host := metadata.DstIP.AsSlice()
|
||||
buf = [][]byte{{socks5.AtypIPv6}, host, port}
|
||||
}
|
||||
return bytes.Join(buf, nil)
|
||||
}
|
||||
|
||||
func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ip netip.Addr
|
||||
switch prefer {
|
||||
case C.IPv4Only:
|
||||
ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
|
||||
case C.IPv6Only:
|
||||
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
|
||||
case C.IPv6Prefer:
|
||||
ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
|
||||
default:
|
||||
ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, port = resolver.LookupIP4P(ip, port)
|
||||
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
|
||||
}
|
||||
522
adapter/outbound/vless.go
Normal file
522
adapter/outbound/vless.go
Normal file
@ -0,0 +1,522 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/metacubex/mihomo/common/convert"
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/component/dialer"
|
||||
"github.com/metacubex/mihomo/component/ech"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/transport/gun"
|
||||
"github.com/metacubex/mihomo/transport/vless"
|
||||
"github.com/metacubex/mihomo/transport/vless/encryption"
|
||||
"github.com/metacubex/mihomo/transport/vmess"
|
||||
|
||||
vmessSing "github.com/metacubex/sing-vmess"
|
||||
"github.com/metacubex/sing-vmess/packetaddr"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
)
|
||||
|
||||
type Vless struct {
|
||||
*Base
|
||||
client *vless.Client
|
||||
option *VlessOption
|
||||
|
||||
encryption *encryption.ClientInstance
|
||||
|
||||
// for gun mux
|
||||
gunTLSConfig *tls.Config
|
||||
gunConfig *gun.Config
|
||||
transport *gun.TransportWrap
|
||||
|
||||
realityConfig *tlsC.RealityConfig
|
||||
echConfig *ech.Config
|
||||
}
|
||||
|
||||
type VlessOption struct {
|
||||
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"`
|
||||
Encryption string `proxy:"encryption,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
|
||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||
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, err error) {
|
||||
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,
|
||||
ECHConfig: v.echConfig,
|
||||
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(ctx, c, h2Opts)
|
||||
case "grpc":
|
||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
|
||||
default:
|
||||
// default tcp network
|
||||
// handle TLS
|
||||
c, err = v.streamTLSConn(ctx, c, false)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.streamConnContext(ctx, c, metadata)
|
||||
}
|
||||
|
||||
func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
if v.encryption != nil {
|
||||
c, err = v.encryption.Handshake(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if metadata.NetWork == C.UDP {
|
||||
if v.option.PacketAddr {
|
||||
metadata = &C.Metadata{
|
||||
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))
|
||||
} 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,
|
||||
ECH: v.echConfig,
|
||||
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) (_ C.Conn, err error) {
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
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.streamConnContext(ctx, c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, v), nil
|
||||
}
|
||||
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), 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())
|
||||
}
|
||||
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) (_ C.PacketConn, err error) {
|
||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
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.streamConnContext(ctx, 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.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
|
||||
if err = v.ResolveUDP(ctx, metadata); 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())
|
||||
}
|
||||
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) {
|
||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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(v.client.PacketConn(c, metadata.UDPAddr()),
|
||||
M.SocksaddrFromNet(metadata.UDPAddr())),
|
||||
), v), nil
|
||||
}
|
||||
return newPacketConn(N.NewThreadSafePacketConn(v.client.PacketConn(c, metadata.UDPAddr())), v), nil
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (v *Vless) SupportUOT() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (v *Vless) ProxyInfo() C.ProxyInfo {
|
||||
info := v.Base.ProxyInfo()
|
||||
info.DialerProxy = v.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (v *Vless) Close() error {
|
||||
if v.transport != nil {
|
||||
return v.transport.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
|
||||
var addrType byte
|
||||
var addr []byte
|
||||
switch metadata.AddrType() {
|
||||
case C.AtypIPv4:
|
||||
addrType = vless.AtypIPv4
|
||||
addr = make([]byte, net.IPv4len)
|
||||
copy(addr[:], metadata.DstIP.AsSlice())
|
||||
case C.AtypIPv6:
|
||||
addrType = vless.AtypIPv6
|
||||
addr = make([]byte, net.IPv6len)
|
||||
copy(addr[:], metadata.DstIP.AsSlice())
|
||||
case C.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,
|
||||
}
|
||||
}
|
||||
|
||||
func NewVless(option VlessOption) (*Vless, error) {
|
||||
var addons *vless.Addons
|
||||
if option.Network != "ws" && len(option.Flow) >= 16 {
|
||||
option.Flow = option.Flow[:16]
|
||||
if option.Flow != vless.XRV {
|
||||
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
|
||||
}
|
||||
addons = &vless.Addons{
|
||||
Flow: 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.encryption, err = encryption.NewClient(option.Encryption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.realityConfig, err = v.option.RealityOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.echConfig, err = v.option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch option.Network {
|
||||
case "h2":
|
||||
if len(option.HTTP2Opts.Host) == 0 {
|
||||
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
||||
}
|
||||
case "grpc":
|
||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var err error
|
||||
var cDialer C.Dialer = dialer.NewDialer(v.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(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
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, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
}, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.echConfig, v.realityConfig)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
548
adapter/outbound/vmess.go
Normal file
548
adapter/outbound/vmess.go
Normal file
@ -0,0 +1,548 @@
|
||||
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/ech"
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
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/metacubex/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
|
||||
echConfig *ech.Config
|
||||
}
|
||||
|
||||
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"`
|
||||
ECHOpts ECHOptions `proxy:"ech-opts,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, err error) {
|
||||
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,
|
||||
ECHConfig: v.echConfig,
|
||||
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,
|
||||
ECH: v.echConfig,
|
||||
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(ctx, c, h2Opts)
|
||||
case "grpc":
|
||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, 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,
|
||||
ECH: v.echConfig,
|
||||
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.streamConnContext(ctx, c, metadata)
|
||||
}
|
||||
|
||||
func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
|
||||
useEarly := N.NeedHandshake(c)
|
||||
if !useEarly {
|
||||
if ctx.Done() != nil {
|
||||
done := N.SetupContextForConn(ctx, c)
|
||||
defer done(&err)
|
||||
}
|
||||
}
|
||||
if metadata.NetWork == C.UDP {
|
||||
if v.option.XUDP {
|
||||
var globalID [8]byte
|
||||
if metadata.SourceValid() {
|
||||
globalID = utils.GlobalID(metadata.SourceAddress())
|
||||
}
|
||||
if useEarly {
|
||||
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 useEarly {
|
||||
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 useEarly {
|
||||
conn = v.client.DialEarlyPacketConn(c,
|
||||
M.SocksaddrFromNet(metadata.UDPAddr()))
|
||||
} else {
|
||||
conn, err = v.client.DialPacketConn(c,
|
||||
M.SocksaddrFromNet(metadata.UDPAddr()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if useEarly {
|
||||
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) (_ C.Conn, err error) {
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
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.streamConnContext(ctx, c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, v), nil
|
||||
}
|
||||
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), 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())
|
||||
}
|
||||
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) (_ C.PacketConn, err error) {
|
||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
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.streamConnContext(ctx, 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.DialOptions()...), 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
|
||||
}
|
||||
}
|
||||
|
||||
if err = v.ResolveUDP(ctx, metadata); 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())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// ProxyInfo implements C.ProxyAdapter
|
||||
func (v *Vmess) ProxyInfo() C.ProxyInfo {
|
||||
info := v.Base.ProxyInfo()
|
||||
info.DialerProxy = v.option.DialerProxy
|
||||
return info
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (v *Vmess) Close() error {
|
||||
if v.transport != nil {
|
||||
return v.transport.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
v.realityConfig, err = v.option.RealityOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v.echConfig, err = v.option.ECHOpts.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch option.Network {
|
||||
case "h2":
|
||||
if len(option.HTTP2Opts.Host) == 0 {
|
||||
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
||||
}
|
||||
case "grpc":
|
||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var err error
|
||||
var cDialer C.Dialer = dialer.NewDialer(v.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(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
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, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
}, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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.echConfig, v.realityConfig)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
609
adapter/outbound/wireguard.go
Normal file
609
adapter/outbound/wireguard.go
Normal file
@ -0,0 +1,609 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/atomic"
|
||||
"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"
|
||||
|
||||
amnezia "github.com/metacubex/amneziawg-go/device"
|
||||
wireguard "github.com/metacubex/sing-wireguard"
|
||||
"github.com/metacubex/wireguard-go/device"
|
||||
|
||||
"github.com/metacubex/sing/common/debug"
|
||||
E "github.com/metacubex/sing/common/exceptions"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
)
|
||||
|
||||
type wireguardGoDevice interface {
|
||||
Close()
|
||||
IpcSet(uapiConf string) error
|
||||
}
|
||||
|
||||
type WireGuard struct {
|
||||
*Base
|
||||
bind *wireguard.ClientBind
|
||||
device wireguardGoDevice
|
||||
tunDevice wireguard.Device
|
||||
dialer proxydialer.SingDialer
|
||||
resolver resolver.Resolver
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
AmneziaWGOption *AmneziaWGOption `proxy:"amnezia-wg-option,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 AmneziaWGOption struct {
|
||||
JC int `proxy:"jc,omitempty"`
|
||||
JMin int `proxy:"jmin,omitempty"`
|
||||
JMax int `proxy:"jmax,omitempty"`
|
||||
S1 int `proxy:"s1,omitempty"`
|
||||
S2 int `proxy:"s2,omitempty"`
|
||||
H1 uint32 `proxy:"h1,omitempty"`
|
||||
H2 uint32 `proxy:"h2,omitempty"`
|
||||
H3 uint32 `proxy:"h3,omitempty"`
|
||||
H4 uint32 `proxy:"h4,omitempty"`
|
||||
|
||||
// AmneziaWG v1.5
|
||||
I1 string `proxy:"i1,omitempty"`
|
||||
I2 string `proxy:"i2,omitempty"`
|
||||
I3 string `proxy:"i3,omitempty"`
|
||||
I4 string `proxy:"i4,omitempty"`
|
||||
I5 string `proxy:"i5,omitempty"`
|
||||
J1 string `proxy:"j1,omitempty"`
|
||||
J2 string `proxy:"j2,omitempty"`
|
||||
J3 string `proxy:"j3,omitempty"`
|
||||
Itime int64 `proxy:"itime,omitempty"`
|
||||
}
|
||||
|
||||
type wgSingErrorHandler struct {
|
||||
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),
|
||||
},
|
||||
}
|
||||
singDialer := proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer(outbound.DialOptions()...)), slowdown.New())
|
||||
outbound.dialer = singDialer
|
||||
|
||||
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")
|
||||
}
|
||||
logger := &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...)))
|
||||
},
|
||||
}
|
||||
if option.AmneziaWGOption != nil {
|
||||
outbound.bind.SetParseReserved(false) // AmneziaWG don't need parse reserved
|
||||
outbound.device = amnezia.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers)
|
||||
} else {
|
||||
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, logger, option.Workers)
|
||||
}
|
||||
|
||||
var has6 bool
|
||||
for _, address := range outbound.localPrefixes {
|
||||
if !address.Addr().Unmap().Is4() {
|
||||
has6 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 = outbound
|
||||
}
|
||||
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 := resolveUDPAddr(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 w.option.AmneziaWGOption != nil {
|
||||
if w.option.AmneziaWGOption.JC != 0 {
|
||||
ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.JMin != 0 {
|
||||
ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.JMax != 0 {
|
||||
ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.S1 != 0 {
|
||||
ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.S2 != 0 {
|
||||
ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.H1 != 0 {
|
||||
ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.H2 != 0 {
|
||||
ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.H3 != 0 {
|
||||
ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.H4 != 0 {
|
||||
ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.I1 != "" {
|
||||
ipcConf += "i1=" + w.option.AmneziaWGOption.I1 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.I2 != "" {
|
||||
ipcConf += "i2=" + w.option.AmneziaWGOption.I2 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.I3 != "" {
|
||||
ipcConf += "i3=" + w.option.AmneziaWGOption.I3 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.I4 != "" {
|
||||
ipcConf += "i4=" + w.option.AmneziaWGOption.I4 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.I5 != "" {
|
||||
ipcConf += "i5=" + w.option.AmneziaWGOption.I5 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.J1 != "" {
|
||||
ipcConf += "j1=" + w.option.AmneziaWGOption.J1 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.J2 != "" {
|
||||
ipcConf += "j2=" + w.option.AmneziaWGOption.J2 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.J3 != "" {
|
||||
ipcConf += "j3=" + w.option.AmneziaWGOption.J3 + "\n"
|
||||
}
|
||||
if w.option.AmneziaWGOption.Itime != 0 {
|
||||
ipcConf += "itime=" + strconv.FormatInt(int64(w.option.AmneziaWGOption.Itime), 10) + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(w.option.Peers) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
// Close implements C.ProxyAdapter
|
||||
func (w *WireGuard) Close() error {
|
||||
if w.device != nil {
|
||||
w.device.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
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 {
|
||||
r = w.resolver
|
||||
}
|
||||
options := w.DialOptions()
|
||||
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(conn, w), nil
|
||||
}
|
||||
|
||||
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
var pc net.PacketConn
|
||||
if err = w.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = w.ResolveUDP(ctx, metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pc == nil {
|
||||
return nil, E.New("packetConn is nil")
|
||||
}
|
||||
return newPacketConn(pc, w), nil
|
||||
}
|
||||
|
||||
func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
|
||||
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
|
||||
r := resolver.DefaultResolver
|
||||
if w.resolver != nil {
|
||||
r = w.resolver
|
||||
}
|
||||
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't resolve ip: %w", err)
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsL3Protocol implements C.ProxyAdapter
|
||||
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
|
||||
return true
|
||||
}
|
||||
171
adapter/outboundgroup/fallback.go
Normal file
171
adapter/outboundgroup/fallback.go
Normal file
@ -0,0 +1,171 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/callback"
|
||||
N "github.com/metacubex/mihomo/common/net"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
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) (C.Conn, error) {
|
||||
proxy := f.findAliveProxy(true)
|
||||
c, err := proxy.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(f)
|
||||
} else {
|
||||
f.onDialFailed(proxy.Type(), err, f.healthCheck)
|
||||
}
|
||||
|
||||
if N.NeedHandshake(c) {
|
||||
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
|
||||
if err == nil {
|
||||
f.onDialSuccess()
|
||||
} else {
|
||||
f.onDialFailed(proxy.Type(), err, f.healthCheck)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (f *Fallback) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
proxy := f.findAliveProxy(true)
|
||||
pc, err := proxy.ListenPacketContext(ctx, metadata)
|
||||
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{
|
||||
Name: option.Name,
|
||||
Type: C.Fallback,
|
||||
Filter: option.Filter,
|
||||
ExcludeFilter: option.ExcludeFilter,
|
||||
ExcludeType: option.ExcludeType,
|
||||
TestTimeout: option.TestTimeout,
|
||||
MaxFailedTimes: option.MaxFailedTimes,
|
||||
Providers: providers,
|
||||
}),
|
||||
disableUDP: option.DisableUDP,
|
||||
testUrl: option.URL,
|
||||
expectedStatus: option.ExpectedStatus,
|
||||
Hidden: option.Hidden,
|
||||
Icon: option.Icon,
|
||||
}
|
||||
}
|
||||
308
adapter/outboundgroup/groupbase.go
Normal file
308
adapter/outboundgroup/groupbase.go
Normal file
@ -0,0 +1,308 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type GroupBase struct {
|
||||
*outbound.Base
|
||||
filterRegs []*regexp2.Regexp
|
||||
excludeFilterRegs []*regexp2.Regexp
|
||||
excludeTypeArray []string
|
||||
providers []provider.ProxyProvider
|
||||
failedTestMux sync.Mutex
|
||||
failedTimes int
|
||||
failedTime time.Time
|
||||
failedTesting atomic.Bool
|
||||
TestTimeout int
|
||||
maxFailedTimes int
|
||||
|
||||
// for GetProxies
|
||||
getProxiesMutex sync.Mutex
|
||||
providerVersions []uint32
|
||||
providerProxies []C.Proxy
|
||||
}
|
||||
|
||||
type GroupBaseOption struct {
|
||||
Name string
|
||||
Type C.AdapterType
|
||||
Filter string
|
||||
ExcludeFilter string
|
||||
ExcludeType string
|
||||
TestTimeout int
|
||||
MaxFailedTimes int
|
||||
Providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
func NewGroupBase(opt GroupBaseOption) *GroupBase {
|
||||
var excludeTypeArray []string
|
||||
if opt.ExcludeType != "" {
|
||||
excludeTypeArray = strings.Split(opt.ExcludeType, "|")
|
||||
}
|
||||
|
||||
var excludeFilterRegs []*regexp2.Regexp
|
||||
if opt.ExcludeFilter != "" {
|
||||
for _, excludeFilter := range strings.Split(opt.ExcludeFilter, "`") {
|
||||
excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None)
|
||||
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
|
||||
}
|
||||
}
|
||||
|
||||
var filterRegs []*regexp2.Regexp
|
||||
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(outbound.BaseOption{Name: opt.Name, Type: opt.Type}),
|
||||
filterRegs: filterRegs,
|
||||
excludeFilterRegs: excludeFilterRegs,
|
||||
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
|
||||
}
|
||||
|
||||
return gb
|
||||
}
|
||||
|
||||
func (gb *GroupBase) Touch() {
|
||||
for _, pd := range gb.providers {
|
||||
pd.Touch()
|
||||
}
|
||||
}
|
||||
|
||||
func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
|
||||
providerVersions := make([]uint32, len(gb.providers))
|
||||
for i, pd := range gb.providers {
|
||||
if touch { // touch first
|
||||
pd.Touch()
|
||||
}
|
||||
providerVersions[i] = pd.Version()
|
||||
}
|
||||
|
||||
// thread safe
|
||||
gb.getProxiesMutex.Lock()
|
||||
defer gb.getProxiesMutex.Unlock()
|
||||
|
||||
// return the cached proxies if version not changed
|
||||
if slices.Equal(providerVersions, gb.providerVersions) {
|
||||
return gb.providerProxies
|
||||
}
|
||||
|
||||
var proxies []C.Proxy
|
||||
if len(gb.filterRegs) == 0 {
|
||||
for _, pd := range gb.providers {
|
||||
proxies = append(proxies, pd.Proxies()...)
|
||||
}
|
||||
} else {
|
||||
for _, pd := range gb.providers {
|
||||
if pd.VehicleType() == types.Compatible { // compatible provider unneeded filter
|
||||
proxies = append(proxies, pd.Proxies()...)
|
||||
continue
|
||||
}
|
||||
|
||||
var newProxies []C.Proxy
|
||||
proxiesSet := map[string]struct{}{}
|
||||
for _, filterReg := range gb.filterRegs {
|
||||
for _, p := range pd.Proxies() {
|
||||
name := p.Name()
|
||||
if mat, _ := filterReg.MatchString(name); mat {
|
||||
if _, ok := proxiesSet[name]; !ok {
|
||||
proxiesSet[name] = struct{}{}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proxies = append(proxies, newProxies...)
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple filers means that proxies are sorted in the order in which the filers appear.
|
||||
// Although the filter has been performed once in the previous process,
|
||||
// when there are multiple providers, the array needs to be reordered as a whole.
|
||||
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
|
||||
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 len(gb.excludeFilterRegs) > 0 {
|
||||
var newProxies []C.Proxy
|
||||
LOOP1:
|
||||
for _, p := range proxies {
|
||||
name := p.Name()
|
||||
for _, excludeFilterReg := range gb.excludeFilterRegs {
|
||||
if mat, _ := excludeFilterReg.MatchString(name); mat {
|
||||
continue LOOP1
|
||||
}
|
||||
}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
proxies = newProxies
|
||||
}
|
||||
|
||||
if gb.excludeTypeArray != nil {
|
||||
var newProxies []C.Proxy
|
||||
LOOP2:
|
||||
for _, p := range proxies {
|
||||
mType := p.Type().String()
|
||||
for _, excludeType := range gb.excludeTypeArray {
|
||||
if strings.EqualFold(mType, excludeType) {
|
||||
continue LOOP2
|
||||
}
|
||||
}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
proxies = newProxies
|
||||
}
|
||||
|
||||
if len(proxies) == 0 {
|
||||
return []C.Proxy{tunnel.Proxies()["COMPATIBLE"]}
|
||||
}
|
||||
|
||||
// only cache when proxies not empty
|
||||
gb.providerVersions = providerVersions
|
||||
gb.providerProxies = proxies
|
||||
|
||||
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, fn func()) {
|
||||
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, C.ErrNotSupport) {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
if strings.Contains(err.Error(), "connection refused") {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
fn()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
272
adapter/outboundgroup/loadbalance.go
Normal file
272
adapter/outboundgroup/loadbalance.go
Normal file
@ -0,0 +1,272 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
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) (c C.Conn, err error) {
|
||||
proxy := lb.Unwrap(metadata, true)
|
||||
c, err = proxy.DialContext(ctx, metadata)
|
||||
|
||||
if err == nil {
|
||||
c.AppendToChains(lb)
|
||||
} else {
|
||||
lb.onDialFailed(proxy.Type(), err, lb.healthCheck)
|
||||
}
|
||||
|
||||
if N.NeedHandshake(c) {
|
||||
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
|
||||
if err == nil {
|
||||
lb.onDialSuccess()
|
||||
} else {
|
||||
lb.onDialFailed(proxy.Type(), err, lb.healthCheck)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
pc.AppendToChains(lb)
|
||||
}
|
||||
}()
|
||||
|
||||
proxy := lb.Unwrap(metadata, true)
|
||||
return proxy.ListenPacketContext(ctx, metadata)
|
||||
}
|
||||
|
||||
// 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 !has || 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{
|
||||
Name: option.Name,
|
||||
Type: C.LoadBalance,
|
||||
Filter: option.Filter,
|
||||
ExcludeFilter: option.ExcludeFilter,
|
||||
ExcludeType: option.ExcludeType,
|
||||
TestTimeout: option.TestTimeout,
|
||||
MaxFailedTimes: option.MaxFailedTimes,
|
||||
Providers: providers,
|
||||
}),
|
||||
strategyFn: strategyFn,
|
||||
disableUDP: option.DisableUDP,
|
||||
testUrl: option.URL,
|
||||
expectedStatus: option.ExpectedStatus,
|
||||
Hidden: option.Hidden,
|
||||
Icon: option.Icon,
|
||||
}, nil
|
||||
}
|
||||
233
adapter/outboundgroup/parser.go
Normal file
233
adapter/outboundgroup/parser.go
Normal file
@ -0,0 +1,233 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
|
||||
"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"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
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 {
|
||||
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"`
|
||||
|
||||
// removed configs, only for error logging
|
||||
Interface string `group:"interface-name,omitempty"`
|
||||
RoutingMark int `group:"routing-mark,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
|
||||
}
|
||||
|
||||
if groupOption.RoutingMark != 0 {
|
||||
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||
}
|
||||
if groupOption.Interface != "" {
|
||||
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||
}
|
||||
|
||||
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 {
|
||||
groupOption.Proxies = []string{"COMPATIBLE"}
|
||||
}
|
||||
}
|
||||
|
||||
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
adapter/outboundgroup/patch_android.go
Normal file
64
adapter/outboundgroup/patch_android.go
Normal file
@ -0,0 +1,64 @@
|
||||
//go:build android && cmfa
|
||||
|
||||
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)
|
||||
}
|
||||
163
adapter/outboundgroup/relay.go
Normal file
163
adapter/outboundgroup/relay.go
Normal file
@ -0,0 +1,163 @@
|
||||
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) (C.Conn, error) {
|
||||
proxies, chainProxies := r.proxies(metadata, true)
|
||||
|
||||
switch len(proxies) {
|
||||
case 0:
|
||||
return outbound.NewDirect().DialContext(ctx, metadata)
|
||||
case 1:
|
||||
return proxies[0].DialContext(ctx, metadata)
|
||||
}
|
||||
var d C.Dialer
|
||||
d = dialer.NewDialer()
|
||||
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) (_ C.PacketConn, err error) {
|
||||
proxies, chainProxies := r.proxies(metadata, true)
|
||||
|
||||
switch len(proxies) {
|
||||
case 0:
|
||||
return outbound.NewDirect().ListenPacketContext(ctx, metadata)
|
||||
case 1:
|
||||
return proxies[0].ListenPacketContext(ctx, metadata)
|
||||
}
|
||||
|
||||
var d C.Dialer
|
||||
d = dialer.NewDialer()
|
||||
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{
|
||||
Name: option.Name,
|
||||
Type: C.Relay,
|
||||
Providers: providers,
|
||||
}),
|
||||
Hidden: option.Hidden,
|
||||
Icon: option.Icon,
|
||||
}
|
||||
}
|
||||
129
adapter/outboundgroup/selector.go
Normal file
129
adapter/outboundgroup/selector.go
Normal file
@ -0,0 +1,129 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/constant/provider"
|
||||
)
|
||||
|
||||
type Selector struct {
|
||||
*GroupBase
|
||||
disableUDP bool
|
||||
selected string
|
||||
testUrl string
|
||||
Hidden bool
|
||||
Icon string
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(s)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata)
|
||||
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())
|
||||
}
|
||||
// When testurl is the default value
|
||||
// do not append a value to ensure that the web dashboard follows the settings of the dashboard
|
||||
var url string
|
||||
if s.testUrl != C.DefaultTestURL {
|
||||
url = s.testUrl
|
||||
}
|
||||
|
||||
return json.Marshal(map[string]any{
|
||||
"type": s.Type().String(),
|
||||
"now": s.Now(),
|
||||
"all": all,
|
||||
"testUrl": url,
|
||||
"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{
|
||||
Name: option.Name,
|
||||
Type: C.Selector,
|
||||
Filter: option.Filter,
|
||||
ExcludeFilter: option.ExcludeFilter,
|
||||
ExcludeType: option.ExcludeType,
|
||||
TestTimeout: option.TestTimeout,
|
||||
MaxFailedTimes: option.MaxFailedTimes,
|
||||
Providers: providers,
|
||||
}),
|
||||
selected: "COMPATIBLE",
|
||||
disableUDP: option.DisableUDP,
|
||||
testUrl: option.URL,
|
||||
Hidden: option.Hidden,
|
||||
Icon: option.Icon,
|
||||
}
|
||||
}
|
||||
230
adapter/outboundgroup/urltest.go
Normal file
230
adapter/outboundgroup/urltest.go
Normal file
@ -0,0 +1,230 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
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.ForceSet(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *URLTest) ForceSet(name string) {
|
||||
u.selected = name
|
||||
u.fastSingle.Reset()
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
proxy := u.fast(true)
|
||||
c, err = proxy.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(u)
|
||||
} else {
|
||||
u.onDialFailed(proxy.Type(), err, u.healthCheck)
|
||||
}
|
||||
|
||||
if N.NeedHandshake(c) {
|
||||
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
|
||||
if err == nil {
|
||||
u.onDialSuccess()
|
||||
} else {
|
||||
u.onDialFailed(proxy.Type(), err, u.healthCheck)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
|
||||
proxy := u.fast(true)
|
||||
pc, err := proxy.ListenPacketContext(ctx, metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(u)
|
||||
} else {
|
||||
u.onDialFailed(proxy.Type(), err, u.healthCheck)
|
||||
}
|
||||
|
||||
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) healthCheck() {
|
||||
u.fastSingle.Reset()
|
||||
u.GroupBase.healthCheck()
|
||||
u.fastSingle.Reset()
|
||||
}
|
||||
|
||||
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
|
||||
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, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
|
||||
}
|
||||
|
||||
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{
|
||||
Name: option.Name,
|
||||
Type: C.URLTest,
|
||||
Filter: option.Filter,
|
||||
ExcludeFilter: option.ExcludeFilter,
|
||||
ExcludeType: option.ExcludeType,
|
||||
TestTimeout: option.TestTimeout,
|
||||
MaxFailedTimes: option.MaxFailedTimes,
|
||||
Providers: 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
|
||||
}
|
||||
10
adapter/outboundgroup/util.go
Normal file
10
adapter/outboundgroup/util.go
Normal file
@ -0,0 +1,10 @@
|
||||
package outboundgroup
|
||||
|
||||
type SelectAble interface {
|
||||
Set(string) error
|
||||
ForceSet(name string)
|
||||
}
|
||||
|
||||
var _ SelectAble = (*Fallback)(nil)
|
||||
var _ SelectAble = (*URLTest)(nil)
|
||||
var _ SelectAble = (*Selector)(nil)
|
||||
179
adapter/parser.go
Normal file
179
adapter/parser.go
Normal file
@ -0,0 +1,179 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 outbound.ProxyAdapter
|
||||
err error
|
||||
)
|
||||
switch proxyType {
|
||||
case "ss":
|
||||
ssOption := &outbound.ShadowSocksOption{}
|
||||
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{"/"},
|
||||
},
|
||||
}
|
||||
|
||||
err = decoder.Decode(mapping, vmessOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewVmess(*vmessOption)
|
||||
case "vless":
|
||||
vlessOption := &outbound.VlessOption{}
|
||||
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{}
|
||||
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)
|
||||
case "mieru":
|
||||
mieruOption := &outbound.MieruOption{}
|
||||
err = decoder.Decode(mapping, mieruOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewMieru(*mieruOption)
|
||||
case "anytls":
|
||||
anytlsOption := &outbound.AnyTLSOption{}
|
||||
err = decoder.Decode(mapping, anytlsOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewAnyTLS(*anytlsOption)
|
||||
default:
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proxy = outbound.NewAutoCloseProxyAdapter(proxy)
|
||||
return NewProxy(proxy), nil
|
||||
}
|
||||
217
adapter/provider/healthcheck.go
Normal file
217
adapter/provider/healthcheck.go
Normal file
@ -0,0 +1,217 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/atomic"
|
||||
"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"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type HealthCheckOption struct {
|
||||
URL string
|
||||
Interval uint
|
||||
}
|
||||
|
||||
type extraOption struct {
|
||||
expectedStatus utils.IntRanges[uint16]
|
||||
filters map[string]struct{}
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
url string
|
||||
extra map[string]*extraOption
|
||||
mu sync.Mutex
|
||||
proxies []C.Proxy
|
||||
interval time.Duration
|
||||
lazy bool
|
||||
expectedStatus utils.IntRanges[uint16]
|
||||
lastTouch atomic.TypedValue[time.Time]
|
||||
singleDo *singledo.Single[struct{}]
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) process() {
|
||||
ticker := time.NewTicker(hc.interval)
|
||||
go hc.check()
|
||||
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.ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) setProxies(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
|
||||
}
|
||||
|
||||
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) 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 := new(errgroup.Group)
|
||||
b.SetLimit(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 *errgroup.Group, 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(func() error {
|
||||
ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout)
|
||||
defer cancel()
|
||||
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
|
||||
_, _ = p.URLTest(ctx, url, expectedStatus)
|
||||
log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) close() {
|
||||
hc.ctxCancel()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &HealthCheck{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
proxies: proxies,
|
||||
url: url,
|
||||
timeout: time.Duration(timeout) * time.Millisecond,
|
||||
extra: map[string]*extraOption{},
|
||||
interval: time.Duration(interval) * time.Second,
|
||||
lazy: lazy,
|
||||
expectedStatus: expectedStatus,
|
||||
singleDo: singledo.NewSingle[struct{}](time.Second),
|
||||
}
|
||||
}
|
||||
133
adapter/provider/parser.go
Normal file
133
adapter/provider/parser.go
Normal file
@ -0,0 +1,133 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"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"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
)
|
||||
|
||||
var (
|
||||
errVehicleType = errors.New("unsupport vehicle type")
|
||||
)
|
||||
|
||||
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 OverrideProxyNameSchema struct {
|
||||
// matching expression for regex replacement
|
||||
Pattern *regexp2.Regexp `provider:"pattern"`
|
||||
// the new content after regex matching
|
||||
Target string `provider:"target"`
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*regexp2.Regexp)(nil) // ensure *regexp2.Regexp can decode direct by structure package
|
||||
|
||||
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"`
|
||||
|
||||
ProxyName []OverrideProxyNameSchema `provider:"proxy-name,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"`
|
||||
SizeLimit int64 `provider:"size-limit,omitempty"`
|
||||
Payload []map[string]any `provider:"payload,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)
|
||||
|
||||
parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var vehicle types.Vehicle
|
||||
switch schema.Type {
|
||||
case "file":
|
||||
path := C.Path.Resolve(schema.Path)
|
||||
if !C.Path.IsSafePath(path) {
|
||||
return nil, C.Path.ErrNotSafePath(path)
|
||||
}
|
||||
vehicle = resource.NewFileVehicle(path)
|
||||
case "http":
|
||||
path := C.Path.GetPathByHash("proxies", schema.URL)
|
||||
if schema.Path != "" {
|
||||
path = C.Path.Resolve(schema.Path)
|
||||
if !C.Path.IsSafePath(path) {
|
||||
return nil, C.Path.ErrNotSafePath(path)
|
||||
}
|
||||
}
|
||||
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
|
||||
case "inline":
|
||||
return NewInlineProvider(name, schema.Payload, parser, hc)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||
}
|
||||
|
||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||
|
||||
return NewProxySetProvider(name, interval, schema.Payload, parser, vehicle, hc)
|
||||
}
|
||||
467
adapter/provider/provider.go
Normal file
467
adapter/provider/provider.go
Normal file
@ -0,0 +1,467 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"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"
|
||||
"github.com/metacubex/mihomo/component/profile/cachefile"
|
||||
"github.com/metacubex/mihomo/component/resource"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
types "github.com/metacubex/mihomo/constant/provider"
|
||||
"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"`
|
||||
}
|
||||
|
||||
type providerForApi struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
VehicleType string `json:"vehicleType"`
|
||||
Proxies []C.Proxy `json:"proxies"`
|
||||
TestUrl string `json:"testUrl"`
|
||||
ExpectedStatus string `json:"expectedStatus"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitempty"`
|
||||
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"`
|
||||
}
|
||||
|
||||
type baseProvider struct {
|
||||
name string
|
||||
proxies []C.Proxy
|
||||
healthCheck *HealthCheck
|
||||
version uint32
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Name() string {
|
||||
return bp.name
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Version() uint32 {
|
||||
return bp.version
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Initial() error {
|
||||
if bp.healthCheck.auto() {
|
||||
go bp.healthCheck.process()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bp *baseProvider) HealthCheck() {
|
||||
bp.healthCheck.check()
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Type() types.ProviderType {
|
||||
return types.Proxy
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Proxies() []C.Proxy {
|
||||
return bp.proxies
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Count() int {
|
||||
return len(bp.proxies)
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Touch() {
|
||||
bp.healthCheck.touch()
|
||||
}
|
||||
|
||||
func (bp *baseProvider) HealthCheckURL() string {
|
||||
return bp.healthCheck.url
|
||||
}
|
||||
|
||||
func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
|
||||
bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
|
||||
}
|
||||
|
||||
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
|
||||
bp.proxies = proxies
|
||||
bp.version += 1
|
||||
bp.healthCheck.setProxies(proxies)
|
||||
if bp.healthCheck.auto() {
|
||||
go bp.healthCheck.check()
|
||||
}
|
||||
}
|
||||
|
||||
func (bp *baseProvider) Close() error {
|
||||
bp.healthCheck.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProxySetProvider for auto gc
|
||||
type ProxySetProvider struct {
|
||||
*proxySetProvider
|
||||
}
|
||||
|
||||
type proxySetProvider struct {
|
||||
baseProvider
|
||||
*resource.Fetcher[[]C.Proxy]
|
||||
subscriptionInfo *SubscriptionInfo
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(providerForApi{
|
||||
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) Name() string {
|
||||
return pp.Fetcher.Name()
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Update() error {
|
||||
_, _, err := pp.Fetcher.Update()
|
||||
return err
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Initial() error {
|
||||
if err := pp.baseProvider.Initial(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := pp.Fetcher.Initial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" {
|
||||
pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo)
|
||||
}
|
||||
pp.closeAllConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
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 (pp *proxySetProvider) Close() error {
|
||||
_ = pp.baseProvider.Close()
|
||||
return pp.Fetcher.Close()
|
||||
}
|
||||
|
||||
func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
|
||||
pd := &proxySetProvider{
|
||||
baseProvider: baseProvider{
|
||||
name: name,
|
||||
proxies: []C.Proxy{},
|
||||
healthCheck: hc,
|
||||
},
|
||||
}
|
||||
|
||||
if len(payload) > 0 { // using as fallback proxies
|
||||
ps := ProxySchema{Proxies: payload}
|
||||
buf, err := yaml.Marshal(ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxies, err := parser(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pd.proxies = proxies
|
||||
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
|
||||
hc.setProxies(proxies)
|
||||
}
|
||||
|
||||
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
|
||||
pd.Fetcher = fetcher
|
||||
if httpVehicle, ok := vehicle.(*resource.HTTPVehicle); ok {
|
||||
httpVehicle.SetInRead(func(resp *http.Response) {
|
||||
if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" {
|
||||
cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo)
|
||||
pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wrapper := &ProxySetProvider{pd}
|
||||
runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close)
|
||||
return wrapper, nil
|
||||
}
|
||||
|
||||
func (pp *ProxySetProvider) Close() error {
|
||||
runtime.SetFinalizer(pp, nil)
|
||||
return pp.proxySetProvider.Close()
|
||||
}
|
||||
|
||||
// InlineProvider for auto gc
|
||||
type InlineProvider struct {
|
||||
*inlineProvider
|
||||
}
|
||||
|
||||
type inlineProvider struct {
|
||||
baseProvider
|
||||
updateAt time.Time
|
||||
}
|
||||
|
||||
func (ip *inlineProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(providerForApi{
|
||||
Name: ip.Name(),
|
||||
Type: ip.Type().String(),
|
||||
VehicleType: ip.VehicleType().String(),
|
||||
Proxies: ip.Proxies(),
|
||||
TestUrl: ip.healthCheck.url,
|
||||
ExpectedStatus: ip.healthCheck.expectedStatus.String(),
|
||||
UpdatedAt: ip.updateAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (ip *inlineProvider) VehicleType() types.VehicleType {
|
||||
return types.Inline
|
||||
}
|
||||
|
||||
func (ip *inlineProvider) Update() error {
|
||||
// make api update happy
|
||||
ip.updateAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
|
||||
ps := ProxySchema{Proxies: payload}
|
||||
buf, err := yaml.Marshal(ps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxies, err := parser(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
|
||||
hc.setProxies(proxies)
|
||||
|
||||
ip := &inlineProvider{
|
||||
baseProvider: baseProvider{
|
||||
name: name,
|
||||
proxies: proxies,
|
||||
healthCheck: hc,
|
||||
},
|
||||
updateAt: time.Now(),
|
||||
}
|
||||
wrapper := &InlineProvider{ip}
|
||||
runtime.SetFinalizer(wrapper, (*InlineProvider).Close)
|
||||
return wrapper, nil
|
||||
}
|
||||
|
||||
func (ip *InlineProvider) Close() error {
|
||||
runtime.SetFinalizer(ip, nil)
|
||||
return ip.baseProvider.Close()
|
||||
}
|
||||
|
||||
// CompatibleProvider for auto gc
|
||||
type CompatibleProvider struct {
|
||||
*compatibleProvider
|
||||
}
|
||||
|
||||
type compatibleProvider struct {
|
||||
baseProvider
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(providerForApi{
|
||||
Name: cp.Name(),
|
||||
Type: cp.Type().String(),
|
||||
VehicleType: cp.VehicleType().String(),
|
||||
Proxies: cp.Proxies(),
|
||||
TestUrl: cp.healthCheck.url,
|
||||
ExpectedStatus: cp.healthCheck.expectedStatus.String(),
|
||||
})
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) VehicleType() types.VehicleType {
|
||||
return types.Compatible
|
||||
}
|
||||
|
||||
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("provider need one proxy at least")
|
||||
}
|
||||
|
||||
pd := &compatibleProvider{
|
||||
baseProvider: baseProvider{
|
||||
name: name,
|
||||
proxies: proxies,
|
||||
healthCheck: hc,
|
||||
},
|
||||
}
|
||||
|
||||
wrapper := &CompatibleProvider{pd}
|
||||
runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close)
|
||||
return wrapper, nil
|
||||
}
|
||||
|
||||
func (cp *CompatibleProvider) Close() error {
|
||||
runtime.SetFinalizer(cp, nil)
|
||||
return cp.compatibleProvider.Close()
|
||||
}
|
||||
|
||||
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
|
||||
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
case "proxy-name":
|
||||
// Iterate through all naming replacement rules and perform the replacements.
|
||||
for _, expr := range override.ProxyName {
|
||||
name := mapping["name"].(string)
|
||||
newName, err := expr.Pattern.Replace(name, expr.Target, 0, -1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy name replace error: %w", err)
|
||||
}
|
||||
mapping["name"] = newName
|
||||
}
|
||||
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
|
||||
}, nil
|
||||
}
|
||||
58
adapter/provider/subscription_info.go
Normal file
58
adapter/provider/subscription_info.go
Normal file
@ -0,0 +1,58 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type SubscriptionInfo struct {
|
||||
Upload int64
|
||||
Download int64
|
||||
Total int64
|
||||
Expire int64
|
||||
}
|
||||
|
||||
func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) {
|
||||
userinfo = strings.ReplaceAll(strings.ToLower(userinfo), " ", "")
|
||||
si = new(SubscriptionInfo)
|
||||
|
||||
for _, field := range strings.Split(userinfo, ";") {
|
||||
name, value, ok := strings.Cut(field, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
intValue, err := parseValue(value)
|
||||
if err != nil {
|
||||
log.Warnln("[Provider] get subscription-userinfo: %e", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "upload":
|
||||
si.Upload = intValue
|
||||
case "download":
|
||||
si.Download = intValue
|
||||
case "total":
|
||||
si.Total = intValue
|
||||
case "expire":
|
||||
si.Expire = intValue
|
||||
}
|
||||
}
|
||||
return si
|
||||
}
|
||||
|
||||
func parseValue(value string) (int64, error) {
|
||||
if intValue, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
return intValue, nil
|
||||
}
|
||||
|
||||
if floatValue, err := strconv.ParseFloat(value, 64); err == nil {
|
||||
return int64(floatValue), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("failed to parse value '%s'", value)
|
||||
}
|
||||
21
android_tz.go
Normal file
21
android_tz.go
Normal file
@ -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(¤tT)
|
||||
C.localtime_r(¤tT, ¤tTM)
|
||||
tzOffset := int(currentTM.tm_gmtoff)
|
||||
tz := C.GoString(currentTM.tm_zone)
|
||||
time.Local = time.FixedZone(tz, tzOffset)
|
||||
}
|
||||
28
check_amd64.sh
Normal file
28
check_amd64.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
flags=$(grep '^flags\b' </proc/cpuinfo | head -n 1)
|
||||
flags=" ${flags#*:} "
|
||||
|
||||
has_flags () {
|
||||
for flag; do
|
||||
case "$flags" in
|
||||
*" $flag "*) :;;
|
||||
*) return 1;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
determine_level () {
|
||||
level=0
|
||||
has_flags lm cmov cx8 fpu fxsr mmx syscall sse2 || return 0
|
||||
level=1
|
||||
has_flags cx16 lahf_lm popcnt sse4_1 sse4_2 ssse3 || return 0
|
||||
level=2
|
||||
has_flags avx avx2 bmi1 bmi2 f16c fma abm movbe xsave || return 0
|
||||
level=3
|
||||
has_flags avx512f avx512bw avx512cd avx512dq avx512vl || return 0
|
||||
level=4
|
||||
}
|
||||
|
||||
determine_level
|
||||
echo "Your CPU supports amd64-v$level"
|
||||
return $level
|
||||
241
common/arc/arc.go
Normal file
241
common/arc/arc.go
Normal file
@ -0,0 +1,241 @@
|
||||
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]{}
|
||||
arc.Clear()
|
||||
|
||||
for _, option := range options {
|
||||
option(arc)
|
||||
}
|
||||
return arc
|
||||
}
|
||||
|
||||
func (a *ARC[K, V]) Clear() {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
a.p = 0
|
||||
a.t1 = list.New[*entry[K, V]]()
|
||||
a.b1 = list.New[*entry[K, V]]()
|
||||
a.t2 = list.New[*entry[K, V]]()
|
||||
a.b2 = list.New[*entry[K, V]]()
|
||||
a.len = 0
|
||||
a.cache = make(map[K]*entry[K, V])
|
||||
}
|
||||
|
||||
// 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
common/arc/arc_test.go
Normal file
105
common/arc/arc_test.go
Normal file
@ -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
common/arc/entry.go
Normal file
32
common/arc/entry.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
63
common/atomic/enum.go
Normal file
63
common/atomic/enum.go
Normal file
@ -0,0 +1,63 @@
|
||||
package atomic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Int32Enum[T ~int32] struct {
|
||||
value atomic.Int32
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.Load())
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) UnmarshalJSON(b []byte) error {
|
||||
var v T
|
||||
if err := json.Unmarshal(b, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v T
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) String() string {
|
||||
return fmt.Sprint(i.Load())
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) Store(v T) {
|
||||
i.value.Store(int32(v))
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) Load() T {
|
||||
return T(i.value.Load())
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) Swap(new T) T {
|
||||
return T(i.value.Swap(int32(new)))
|
||||
}
|
||||
|
||||
func (i *Int32Enum[T]) CompareAndSwap(old, new T) bool {
|
||||
return i.value.CompareAndSwap(int32(old), int32(new))
|
||||
}
|
||||
|
||||
func NewInt32Enum[T ~int32](v T) *Int32Enum[T] {
|
||||
a := &Int32Enum[T]{}
|
||||
a.Store(v)
|
||||
return a
|
||||
}
|
||||
289
common/atomic/type.go
Normal file
289
common/atomic/type.go
Normal file
@ -0,0 +1,289 @@
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Bool) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v bool
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Bool) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatBool(v)
|
||||
}
|
||||
|
||||
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]) MarshalYAML() (any, error) {
|
||||
return p.Load(), nil
|
||||
}
|
||||
|
||||
func (p *Pointer[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v *T
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Pointer[T]) String() string {
|
||||
return fmt.Sprint(p.Load())
|
||||
}
|
||||
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Int32) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v int32
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Int32) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Int64) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v int64
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Int64) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatInt(int64(v), 10)
|
||||
}
|
||||
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Uint32) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v uint32
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Uint32) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Uint64) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v uint64
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Uint64) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
|
||||
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) MarshalYAML() (any, error) {
|
||||
return i.Load(), nil
|
||||
}
|
||||
|
||||
func (i *Uintptr) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v uintptr
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Uintptr) String() string {
|
||||
v := i.Load()
|
||||
return strconv.FormatUint(uint64(v), 10)
|
||||
}
|
||||
83
common/atomic/value.go
Normal file
83
common/atomic/value.go
Normal file
@ -0,0 +1,83 @@
|
||||
package atomic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type TypedValue[T any] struct {
|
||||
value atomic.Pointer[T]
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) Load() (v T) {
|
||||
v, _ = t.LoadOk()
|
||||
return
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) LoadOk() (v T, ok bool) {
|
||||
value := t.value.Load()
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
return *value, true
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) Store(value T) {
|
||||
t.value.Store(&value)
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) Swap(new T) (v T) {
|
||||
old := t.value.Swap(&new)
|
||||
if old == nil {
|
||||
return
|
||||
}
|
||||
return *old
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
|
||||
for {
|
||||
currentP := t.value.Load()
|
||||
var currentValue T
|
||||
if currentP != nil {
|
||||
currentValue = *currentP
|
||||
}
|
||||
// Compare old and current via runtime equality check.
|
||||
if any(currentValue) != any(old) {
|
||||
return false
|
||||
}
|
||||
if t.value.CompareAndSwap(currentP, &new) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
|
||||
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 (t *TypedValue[T]) MarshalYAML() (any, error) {
|
||||
return t.Load(), nil
|
||||
}
|
||||
|
||||
func (t *TypedValue[T]) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var v T
|
||||
if err := unmarshal(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Store(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTypedValue[T any](t T) (v TypedValue[T]) {
|
||||
v.Store(t)
|
||||
return
|
||||
}
|
||||
169
common/atomic/value_test.go
Normal file
169
common/atomic/value_test.go
Normal file
@ -0,0 +1,169 @@
|
||||
package atomic
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTypedValue(t *testing.T) {
|
||||
{
|
||||
var v TypedValue[int]
|
||||
got, gotOk := v.LoadOk()
|
||||
if got != 0 || gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
|
||||
}
|
||||
v.Store(1)
|
||||
got, gotOk = v.LoadOk()
|
||||
if got != 1 || !gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var v TypedValue[error]
|
||||
got, gotOk := v.LoadOk()
|
||||
if got != nil || gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
|
||||
}
|
||||
v.Store(io.EOF)
|
||||
got, gotOk = v.LoadOk()
|
||||
if got != io.EOF || !gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
|
||||
}
|
||||
err := &os.PathError{}
|
||||
v.Store(err)
|
||||
got, gotOk = v.LoadOk()
|
||||
if got != err || !gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
|
||||
}
|
||||
v.Store(nil)
|
||||
got, gotOk = v.LoadOk()
|
||||
if got != nil || !gotOk {
|
||||
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
e1, e2, e3 := io.EOF, &os.PathError{}, &os.PathError{}
|
||||
var v TypedValue[error]
|
||||
if v.CompareAndSwap(e1, e2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != nil {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, nil)
|
||||
}
|
||||
if v.CompareAndSwap(nil, e1) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != e1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, e1)
|
||||
}
|
||||
if v.CompareAndSwap(e2, e3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != e1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, e1)
|
||||
}
|
||||
if v.CompareAndSwap(e1, e2) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != e2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, e2)
|
||||
}
|
||||
if v.CompareAndSwap(e3, e2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != e2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, e2)
|
||||
}
|
||||
if v.CompareAndSwap(nil, e3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != e2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, e2)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
|
||||
var v TypedValue[chan struct{}]
|
||||
if v.CompareAndSwap(c1, c2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != nil {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, nil)
|
||||
}
|
||||
if v.CompareAndSwap(nil, c1) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != c1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c1)
|
||||
}
|
||||
if v.CompareAndSwap(c2, c3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c1)
|
||||
}
|
||||
if v.CompareAndSwap(c1, c2) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
if v.CompareAndSwap(c3, c2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
if v.CompareAndSwap(nil, c3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
c1, c2, c3 := &io.LimitedReader{}, &io.SectionReader{}, &io.SectionReader{}
|
||||
var v TypedValue[io.Reader]
|
||||
if v.CompareAndSwap(c1, c2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != nil {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, nil)
|
||||
}
|
||||
if v.CompareAndSwap(nil, c1) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != c1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c1)
|
||||
}
|
||||
if v.CompareAndSwap(c2, c3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c1 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c1)
|
||||
}
|
||||
if v.CompareAndSwap(c1, c2) != true {
|
||||
t.Fatalf("CompareAndSwap = false, want true")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
if v.CompareAndSwap(c3, c2) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
if v.CompareAndSwap(nil, c3) != false {
|
||||
t.Fatalf("CompareAndSwap = true, want false")
|
||||
}
|
||||
if value := v.Load(); value != c2 {
|
||||
t.Fatalf("Load = (%v), want (%v)", value, c2)
|
||||
}
|
||||
}
|
||||
}
|
||||
105
common/batch/batch.go
Normal file
105
common/batch/batch.go
Normal file
@ -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
common/batch/batch_test.go
Normal file
83
common/batch/batch_test.go
Normal file
@ -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)
|
||||
}
|
||||
22
common/buf/sing.go
Normal file
22
common/buf/sing.go
Normal file
@ -0,0 +1,22 @@
|
||||
package buf
|
||||
|
||||
import (
|
||||
"github.com/metacubex/sing/common"
|
||||
"github.com/metacubex/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 ReleaseMulti = buf.ReleaseMulti
|
||||
|
||||
var (
|
||||
Must = common.Must
|
||||
Error = common.Error
|
||||
)
|
||||
55
common/callback/callback.go
Normal file
55
common/callback/callback.go
Normal file
@ -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
common/callback/close_callback.go
Normal file
61
common/callback/close_callback.go
Normal file
@ -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
common/cmd/cmd.go
Normal file
36
common/cmd/cmd.go
Normal file
@ -0,0 +1,36 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ExecCmd(cmdStr string) (string, error) {
|
||||
args := splitArgs(cmdStr)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if len(args) == 1 {
|
||||
cmd = exec.Command(args[0])
|
||||
} else {
|
||||
cmd = exec.Command(args[0], args[1:]...)
|
||||
|
||||
}
|
||||
prepareBackgroundCommand(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%v, %s", err, string(out))
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func splitArgs(cmd string) []string {
|
||||
args := strings.Split(cmd, " ")
|
||||
|
||||
// use in pipeline
|
||||
if len(args) > 2 && strings.ContainsAny(cmd, "|") {
|
||||
suffix := strings.Join(args[2:], " ")
|
||||
args = append(args[:2], suffix)
|
||||
}
|
||||
return args
|
||||
}
|
||||
11
common/cmd/cmd_other.go
Normal file
11
common/cmd/cmd_other.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build !windows
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func prepareBackgroundCommand(cmd *exec.Cmd) {
|
||||
|
||||
}
|
||||
40
common/cmd/cmd_test.go
Normal file
40
common/cmd/cmd_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitArgs(t *testing.T) {
|
||||
args := splitArgs("ls")
|
||||
args1 := splitArgs("ls -la")
|
||||
args2 := splitArgs("bash -c ls")
|
||||
args3 := splitArgs("bash -c ls -lahF | grep 'cmd'")
|
||||
|
||||
assert.Equal(t, 1, len(args))
|
||||
assert.Equal(t, 2, len(args1))
|
||||
assert.Equal(t, 3, len(args2))
|
||||
assert.Equal(t, 3, len(args3))
|
||||
}
|
||||
|
||||
func TestExecCmd(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
_, err := ExecCmd("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
common/cmd/cmd_windows.go
Normal file
12
common/cmd/cmd_windows.go
Normal file
@ -0,0 +1,12 @@
|
||||
//go:build windows
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func prepareBackgroundCommand(cmd *exec.Cmd) {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
31
common/contextutils/afterfunc_compact.go
Normal file
31
common/contextutils/afterfunc_compact.go
Normal file
@ -0,0 +1,31 @@
|
||||
package contextutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func afterFunc(ctx context.Context, f func()) (stop func() bool) {
|
||||
stopc := make(chan struct{})
|
||||
once := sync.Once{} // either starts running f or stops f from running
|
||||
if ctx.Done() != nil {
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
once.Do(func() {
|
||||
go f()
|
||||
})
|
||||
case <-stopc:
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return func() bool {
|
||||
stopped := false
|
||||
once.Do(func() {
|
||||
stopped = true
|
||||
close(stopc)
|
||||
})
|
||||
return stopped
|
||||
}
|
||||
}
|
||||
11
common/contextutils/afterfunc_go120.go
Normal file
11
common/contextutils/afterfunc_go120.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package contextutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
|
||||
return afterFunc(ctx, f)
|
||||
}
|
||||
9
common/contextutils/afterfunc_go121.go
Normal file
9
common/contextutils/afterfunc_go121.go
Normal file
@ -0,0 +1,9 @@
|
||||
//go:build go1.21
|
||||
|
||||
package contextutils
|
||||
|
||||
import "context"
|
||||
|
||||
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
|
||||
return context.AfterFunc(ctx, f)
|
||||
}
|
||||
100
common/contextutils/afterfunc_test.go
Normal file
100
common/contextutils/afterfunc_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package contextutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test
|
||||
veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time
|
||||
)
|
||||
|
||||
func TestAfterFuncCalledAfterCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
donec := make(chan struct{})
|
||||
stop := afterFunc(ctx, func() {
|
||||
close(donec)
|
||||
})
|
||||
select {
|
||||
case <-donec:
|
||||
t.Fatalf("AfterFunc called before context is done")
|
||||
case <-time.After(shortDuration):
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-donec:
|
||||
case <-time.After(veryLongDuration):
|
||||
t.Fatalf("AfterFunc not called after context is canceled")
|
||||
}
|
||||
if stop() {
|
||||
t.Fatalf("stop() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterFuncCalledAfterTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
|
||||
defer cancel()
|
||||
donec := make(chan struct{})
|
||||
afterFunc(ctx, func() {
|
||||
close(donec)
|
||||
})
|
||||
select {
|
||||
case <-donec:
|
||||
case <-time.After(veryLongDuration):
|
||||
t.Fatalf("AfterFunc not called after context is canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterFuncCalledImmediately(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
donec := make(chan struct{})
|
||||
afterFunc(ctx, func() {
|
||||
close(donec)
|
||||
})
|
||||
select {
|
||||
case <-donec:
|
||||
case <-time.After(veryLongDuration):
|
||||
t.Fatalf("AfterFunc not called for already-canceled context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAfterFuncNotCalledAfterStop(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
donec := make(chan struct{})
|
||||
stop := afterFunc(ctx, func() {
|
||||
close(donec)
|
||||
})
|
||||
if !stop() {
|
||||
t.Fatalf("stop() = false, want true")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-donec:
|
||||
t.Fatalf("AfterFunc called for already-canceled context")
|
||||
case <-time.After(shortDuration):
|
||||
}
|
||||
if stop() {
|
||||
t.Fatalf("stop() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// This test verifies that canceling a context does not block waiting for AfterFuncs to finish.
|
||||
func TestAfterFuncCalledAsynchronously(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
donec := make(chan struct{})
|
||||
stop := afterFunc(ctx, func() {
|
||||
// The channel send blocks until donec is read from.
|
||||
donec <- struct{}{}
|
||||
})
|
||||
defer stop()
|
||||
cancel()
|
||||
// After cancel returns, read from donec and unblock the AfterFunc.
|
||||
select {
|
||||
case <-donec:
|
||||
case <-time.After(veryLongDuration):
|
||||
t.Fatalf("AfterFunc not called after context is canceled")
|
||||
}
|
||||
}
|
||||
65
common/convert/base64.go
Normal file
65
common/convert/base64.go
Normal file
@ -0,0 +1,65 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"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)
|
||||
}
|
||||
|
||||
func TryDecodeBase64(s string) (decoded []byte, err error) {
|
||||
if len(s)%4 == 0 {
|
||||
if decoded, err = base64.StdEncoding.DecodeString(s); err == nil {
|
||||
return
|
||||
}
|
||||
if decoded, err = base64.URLEncoding.DecodeString(s); err == nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if decoded, err = base64.RawStdEncoding.DecodeString(s); err == nil {
|
||||
return
|
||||
}
|
||||
if decoded, err = base64.RawURLEncoding.DecodeString(s); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("invalid base64-encoded string")
|
||||
}
|
||||
637
common/convert/converter.go
Normal file
637
common/convert/converter.go
Normal file
@ -0,0 +1,637 @@
|
||||
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
|
||||
}
|
||||
if decodedHost, err := tryDecodeBase64([]byte(urlVLess.Host)); err == nil {
|
||||
urlVLess.Host = string(decodedHost)
|
||||
}
|
||||
query := urlVLess.Query()
|
||||
vless := make(map[string]any, 20)
|
||||
err = handleVShareLink(names, urlVLess, scheme, vless)
|
||||
if err != nil {
|
||||
log.Warnln("error:%s line:%s", err.Error(), line)
|
||||
continue
|
||||
}
|
||||
if flow := query.Get("flow"); flow != "" {
|
||||
vless["flow"] = strings.ToLower(flow)
|
||||
}
|
||||
if encryption := query.Get("encryption"); encryption != "" {
|
||||
vless["encryption"] = encryption
|
||||
}
|
||||
proxies = append(proxies, vless)
|
||||
|
||||
case "vmess":
|
||||
// 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"].(string); ok && cipher != "" {
|
||||
vmess["cipher"] = cipher
|
||||
}
|
||||
|
||||
if sni, ok := values["sni"].(string); ok && sni != "" {
|
||||
vmess["servername"] = sni
|
||||
}
|
||||
|
||||
network, ok := values["net"].(string)
|
||||
if ok {
|
||||
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"].(string); ok && host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
httpOpts["path"] = []string{"/"}
|
||||
if path, ok := values["path"].(string); ok && path != "" {
|
||||
httpOpts["path"] = []string{path}
|
||||
}
|
||||
httpOpts["headers"] = headers
|
||||
|
||||
vmess["http-opts"] = httpOpts
|
||||
|
||||
case "h2":
|
||||
headers := make(map[string]any)
|
||||
h2Opts := make(map[string]any)
|
||||
if host, ok := values["host"].(string); ok && host != "" {
|
||||
headers["Host"] = []string{host}
|
||||
}
|
||||
|
||||
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"].(string); ok && host != "" {
|
||||
headers["Host"] = host
|
||||
}
|
||||
if path, ok := values["path"].(string); ok && path != "" {
|
||||
path := path
|
||||
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 := TryDecodeBase64(body)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64param&protoparam=urlsafebase64param&remarks=urlsafebase64remarks&group=urlsafebase64group&udpport=0&uot=1
|
||||
|
||||
before, after, ok := strings.Cut(string(dcBuf), "/?")
|
||||
if !ok {
|
||||
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 := decodeUrlSafe(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)
|
||||
|
||||
case "socks", "socks5", "socks5h", "http", "https":
|
||||
link, err := url.Parse(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
continue
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
continue
|
||||
}
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
name := uniqueName(names, remarks)
|
||||
encodeStr := link.User.String()
|
||||
var username, password string
|
||||
if encodeStr != "" {
|
||||
decodeStr := string(DecodeBase64([]byte(encodeStr)))
|
||||
splitStr := strings.Split(decodeStr, ":")
|
||||
|
||||
// todo: should use url.QueryUnescape ?
|
||||
username = splitStr[0]
|
||||
if len(splitStr) == 2 {
|
||||
password = splitStr[1]
|
||||
}
|
||||
}
|
||||
socks := make(map[string]any, 10)
|
||||
socks["name"] = name
|
||||
socks["type"] = func() string {
|
||||
switch scheme {
|
||||
case "socks", "socks5", "socks5h":
|
||||
return "socks5"
|
||||
case "http", "https":
|
||||
return "http"
|
||||
}
|
||||
return scheme
|
||||
}()
|
||||
socks["server"] = server
|
||||
socks["port"] = portStr
|
||||
socks["username"] = username
|
||||
socks["password"] = password
|
||||
socks["skip-cert-verify"] = true
|
||||
if scheme == "https" {
|
||||
socks["tls"] = true
|
||||
}
|
||||
|
||||
proxies = append(proxies, socks)
|
||||
|
||||
case "anytls":
|
||||
// https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
|
||||
link, err := url.Parse(line)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
username := link.User.Username()
|
||||
password, exist := link.User.Password()
|
||||
if !exist {
|
||||
password = username
|
||||
}
|
||||
query := link.Query()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
continue
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
continue
|
||||
}
|
||||
insecure, sni := query.Get("insecure"), query.Get("sni")
|
||||
insecureBool := insecure == "1"
|
||||
fingerprint := query.Get("hpkp")
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
name := uniqueName(names, remarks)
|
||||
anytls := make(map[string]any, 10)
|
||||
anytls["name"] = name
|
||||
anytls["type"] = "anytls"
|
||||
anytls["server"] = server
|
||||
anytls["port"] = portStr
|
||||
anytls["username"] = username
|
||||
anytls["password"] = password
|
||||
anytls["sni"] = sni
|
||||
anytls["fingerprint"] = fingerprint
|
||||
anytls["skip-cert-verify"] = insecureBool
|
||||
anytls["udp"] = true
|
||||
|
||||
proxies = append(proxies, anytls)
|
||||
}
|
||||
}
|
||||
|
||||
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
common/convert/converter_test.go
Normal file
35
common/convert/converter_test.go
Normal file
@ -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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user