Compare commits

..

2 Commits

Author SHA1 Message Date
世界
637a8b6ed5 refactor: udp 2022-06-09 21:16:42 +08:00
世界
cd466f05d3 chore: reformat code 2022-06-09 21:08:46 +08:00
739 changed files with 16892 additions and 57987 deletions

View File

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

View File

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

View File

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

View File

@@ -1,32 +0,0 @@
#!/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
View File

@@ -1,26 +0,0 @@
#!/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

View File

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

View File

@@ -1,15 +0,0 @@
mixed-port: 7890
dns:
enable: true
ipv6: true
enhanced-mode: fake-ip
fake-ip-filter:
- "*"
- "+.lan"
- "+.local"
nameserver:
- system
rules:
- MATCH,DIRECT

View File

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

View File

@@ -1,17 +0,0 @@
[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
View File

@@ -1,35 +0,0 @@
#!/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

View File

@@ -1,12 +0,0 @@
#!/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

20
.github/workflows/build.yaml vendored Normal file
View File

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

View File

@@ -1,529 +0,0 @@
name: Build
on:
workflow_dispatch:
inputs:
version:
description: "Tag version to release"
required: true
push:
paths-ignore:
- "docs/**"
- "README.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- Alpha
tags:
- "v*"
pull_request:
branches:
- Alpha
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
env:
REGISTRY: docker.io
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
jobs:
- { goos: darwin, goarch: arm64, output: arm64 }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: linux, goarch: '386', go386: sse2, output: '386', debian: i386, rpm: i386}
- { goos: linux, goarch: '386', go386: softfloat, output: '386-softfloat' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64, 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 }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: windows, goarch: arm64, output: arm64 }
- { goos: freebsd, goarch: '386', output: '386' }
- { goos: freebsd, goarch: amd64, goamd64: v1, output: amd64-compatible }
- { goos: freebsd, goarch: amd64, goamd64: v3, output: amd64 }
- { goos: freebsd, goarch: arm64, output: arm64 }
- { goos: android, goarch: '386', ndk: i686-linux-android34, output: '386' }
- { goos: android, goarch: amd64, ndk: x86_64-linux-android34, output: amd64 }
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
# Go 1.23 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
- { goos: windows, goarch: '386', output: '386-go123', goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' }
# Go 1.22 with special patch can work on Windows 7
# https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
- { goos: windows, goarch: '386', output: '386-go122', goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' }
# Go 1.21 can revert commit `9e4385` to work on Windows 7
# https://github.com/golang/go/issues/64622#issuecomment-1847475161
# (OR we can just use golang1.21.4 which unneeded any patch)
- { goos: windows, goarch: '386', output: '386-go121', goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go121, goversion: '1.21' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go121, goversion: '1.21' }
# Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016.
- { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
# Go 1.22 is the last release that will run on macOS 10.15 Catalina. Go 1.23 will require macOS 11 Big Sur or later.
- { goos: darwin, goarch: arm64, output: arm64-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go122, goversion: '1.22' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go122, goversion: '1.22' }
# Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later.
- { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' }
- { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' }
- { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
# Go 1.23 is the last release that requires Linux kernel version 2.6.32 or later. Go 1.24 will require Linux kernel version 3.2 or later.
- { goos: linux, goarch: '386', output: '386-go123', goversion: '1.23' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go123, goversion: '1.23', test: test }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go123, goversion: '1.23' }
# only for test
- { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' }
- { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test }
- { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' }
steps:
- uses: actions/checkout@v4
- name: Set up Go
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
uses: actions/setup-go@v5
with:
go-version: '1.24'
- 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.24.x
# that means after golang1.25 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
- name: Revert Golang1.24 commit for Windows7/8
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
run: |
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: |
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: |
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: |
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)"
PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}"
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }}
PackageVersion="${VERSION#v}" >> $GITHUB_ENV
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
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 }}

61
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Docker
on:
push:
branches:
- Beta
tags:
- "v*"
env:
REGISTRY: docker.io
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
- name: Log into registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: |
linux/386
linux/amd64
linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

70
.github/workflows/prerelease.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Prerelease
on:
push:
branches:
- Alpha
- Beta
pull_request:
branches:
- Alpha
- Beta
jobs:
Build:
runs-on: ubuntu-latest
steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Cache go module
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test
if: ${{github.ref_name=='Beta'}}
run: |
go test ./...
- name: Build
if: success()
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j$(($(nproc) + 1)) releases
- name: Delete current release assets
uses: andreaswilli/delete-release-assets-action@v2.0.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
tag: Prerelease-${{ github.ref_name }}
deleteOnlyFromDrafts: false
- name: Tag Repo
uses: richardsimko/update-tag@v1
with:
tag_name: Prerelease-${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Alpha
uses: softprops/action-gh-release@v1
if: ${{ success() }}
with:
tag: ${{ github.ref_name }}
tag_name: Prerelease-${{ github.ref_name }}
files: bin/*
prerelease: true
generate_release_notes: true

44
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
Build:
runs-on: ubuntu-latest
steps:
- name: Get latest go version
id: version
run: |
echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g')
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ steps.version.outputs.go_version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Cache go module
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test
run: |
go test ./...
- name: Build
if: success()
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j$(($(nproc) + 1)) releases
- name: Upload Release
uses: softprops/action-gh-release@v1
if: ${{ success() && startsWith(github.ref, 'refs/tags/')}}
with:
tag: ${{ github.ref }}
files: bin/*
generate_release_notes: true

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

@@ -1,26 +1,26 @@
FROM alpine:latest as builder FROM golang:alpine as builder
ARG TARGETPLATFORM
RUN echo "I'm building for $TARGETPLATFORM"
RUN apk add --no-cache gzip && \ RUN apk add --no-cache make git && \
mkdir /mihomo-config && \ mkdir /clash-config && \
wget -O /mihomo-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \ wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /mihomo-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \ wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \
wget -O /mihomo-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat wget -O /clash-config/geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
COPY . /clash-src
WORKDIR /clash-src
RUN go mod download &&\
make docker &&\
mv ./bin/Clash.Meta-docker /clash
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 FROM alpine:latest
LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/mihomo" LABEL org.opencontainers.image.source="https://github.com/MetaCubeX/Clash.Meta"
RUN apk add --no-cache ca-certificates tzdata iptables RUN apk add --no-cache ca-certificates tzdata
VOLUME ["/root/.config/mihomo/"] VOLUME ["/root/.config/clash/"]
COPY --from=builder /mihomo-config/ /root/.config/mihomo/ COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /mihomo/mihomo /mihomo COPY --from=builder /clash /clash
ENTRYPOINT [ "/mihomo" ] RUN chmod +x /clash
ENTRYPOINT [ "/clash" ]

View File

@@ -1,4 +1,4 @@
NAME=mihomo NAME=Clash.Meta
BINDIR=bin BINDIR=bin
BRANCH=$(shell git branch --show-current) BRANCH=$(shell git branch --show-current)
ifeq ($(BRANCH),Alpha) ifeq ($(BRANCH),Alpha)
@@ -12,12 +12,11 @@ VERSION=$(shell git rev-parse --short HEAD)
endif endif
BUILDTIME=$(shell date -u) BUILDTIME=$(shell date -u)
GOBUILD=CGO_ENABLED=0 go build -tags with_gvisor -trimpath -ldflags '-X "github.com/metacubex/mihomo/constant.Version=$(VERSION)" \ GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/Dreamacro/clash/constant.Version=$(VERSION)" \
-X "github.com/metacubex/mihomo/constant.BuildTime=$(BUILDTIME)" \ -X "github.com/Dreamacro/clash/constant.BuildTime=$(BUILDTIME)" \
-w -s -buildid=' -w -s -buildid='
PLATFORM_LIST = \ PLATFORM_LIST = \
darwin-amd64-compatible \
darwin-amd64 \ darwin-amd64 \
darwin-arm64 \ darwin-arm64 \
linux-amd64-compatible \ linux-amd64-compatible \
@@ -32,8 +31,6 @@ PLATFORM_LIST = \
linux-mips-hardfloat \ linux-mips-hardfloat \
linux-mipsle-softfloat \ linux-mipsle-softfloat \
linux-mipsle-hardfloat \ linux-mipsle-hardfloat \
linux-riscv64 \
linux-loong64 \
android-arm64 \ android-arm64 \
freebsd-386 \ freebsd-386 \
freebsd-amd64 \ freebsd-amd64 \
@@ -50,17 +47,14 @@ all:linux-amd64 linux-arm64\
darwin-amd64 darwin-arm64\ darwin-amd64 darwin-arm64\
windows-amd64 windows-arm64\ windows-amd64 windows-arm64\
darwin-all: darwin-amd64 darwin-arm64
docker: docker:
GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64: darwin-amd64:
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-amd64-compatible: darwin-amd64-compatible:
GOARCH=amd64 GOOS=darwin GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=darwin GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
darwin-arm64: darwin-arm64:
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@@ -72,7 +66,7 @@ linux-amd64:
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-amd64-compatible: linux-amd64-compatible:
GOARCH=amd64 GOOS=linux GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=amd64 GOOS=linux GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
linux-arm64: linux-arm64:
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@@ -104,12 +98,6 @@ linux-mips64:
linux-mips64le: linux-mips64le:
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ 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: android-arm64:
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
@@ -129,7 +117,7 @@ windows-amd64:
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-compatible: windows-amd64-compatible:
GOARCH=amd64 GOOS=windows GOAMD64=v1 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=amd64 GOOS=windows GOAMD64=v2 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
windows-arm64: windows-arm64:
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
@@ -159,7 +147,3 @@ lint:
clean: clean:
rm $(BINDIR)/* rm $(BINDIR)/*
CLANG ?= clang-14
CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)

289
README.md
View File

@@ -3,17 +3,17 @@
<br>Meta Kernel<br> <br>Meta Kernel<br>
</h1> </h1>
<h3 align="center">Another Mihomo Kernel.</h3> <h3 align="center">Another Clash Kernel.</h3>
<p align="center"> <p align="center">
<a href="https://goreportcard.com/report/github.com/MetaCubeX/mihomo"> <a href="https://goreportcard.com/report/github.com/Clash-Mini/Clash.Meta">
<img src="https://goreportcard.com/badge/github.com/MetaCubeX/mihomo?style=flat-square"> <img src="https://goreportcard.com/badge/github.com/Clash-Mini/Clash.Meta?style=flat-square">
</a> </a>
<img src="https://img.shields.io/github/go-mod/go-version/MetaCubeX/mihomo/Alpha?style=flat-square"> <img src="https://img.shields.io/github/go-mod/go-version/Dreamacro/clash?style=flat-square">
<a href="https://github.com/MetaCubeX/mihomo/releases"> <a href="https://github.com/Clash-Mini/Clash.Meta/releases">
<img src="https://img.shields.io/github/release/MetaCubeX/mihomo/all.svg?style=flat-square"> <img src="https://img.shields.io/github/release/Clash-Mini/Clash.Meta/all.svg?style=flat-square">
</a> </a>
<a href="https://github.com/MetaCubeX/mihomo"> <a href="https://github.com/Clash-Mini/Clash.Meta">
<img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square"> <img src="https://img.shields.io/badge/release-Meta-00b4f0?style=flat-square">
</a> </a>
</p> </p>
@@ -21,55 +21,199 @@
## Features ## Features
- Local HTTP/HTTPS/SOCKS server with authentication support - Local HTTP/HTTPS/SOCKS server with authentication support
- VMess, VLESS, Shadowsocks, Trojan, Snell, TUIC, Hysteria protocol support - VMess, Shadowsocks, Trojan, Snell protocol support for remote connections
- Built-in DNS server that aims to minimize DNS pollution attack impact, supports DoH/DoT upstream and fake IP. - 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 - 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 - Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
based off latency - Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Remote providers, allowing users to get node lists remotely instead of hard-coding in config - Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Netfilter TCP redirecting. Deploy Mihomo on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller - Comprehensive HTTP RESTful API controller
## Dashboard ## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
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). ## Advanced usage for this branch
## Configration example ### DNS configuration
Configuration example is located at [/docs/config.yaml](https://github.com/MetaCubeX/mihomo/blob/Alpha/docs/config.yaml). Support `geosite` with `fallback-filter`.
## Docs Restore `Redir remote resolution`.
Documentation can be found in [mihomo Docs](https://wiki.metacubex.one/). Support resolve ip with a `Proxy Tunnel`.
## For development ```yaml
proxy-groups:
Requirements: - name: DNS
[Go 1.20 or newer](https://go.dev/dl/) type: url-test
use:
Build mihomo: - HK
url: http://cp.cloudflare.com
```shell interval: 180
git clone https://github.com/MetaCubeX/mihomo.git lazy: true
cd mihomo && go mod download ```
go build ```yaml
dns:
enable: true
use-hosts: true
ipv6: false
enhanced-mode: redir-host
fake-ip-range: 198.18.0.1/16
listen: 127.0.0.1:6868
default-nameserver:
- 119.29.29.29
- 114.114.114.114
nameserver:
- https://doh.pub/dns-query
- tls://223.5.5.5:853
fallback:
- 'https://1.0.0.1/dns-query#DNS' # append the proxy adapter name or group name to the end of DNS URL with '#' prefix.
- 'tls://8.8.4.4:853#DNS'
fallback-filter:
geoip: false
geosite:
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
domain:
- +.example.com
ipcidr:
- 0.0.0.0/32
``` ```
Set go proxy if a connection to GitHub is not possible: ### TUN configuration
```shell Supports macOS, Linux and Windows.
go env -w GOPROXY=https://goproxy.io,direct
Built-in [Wintun](https://www.wintun.net) driver.
```yaml
# Enable the TUN listener
tun:
enable: true
stack: gvisor # only gvisor
dns-hijack:
- 0.0.0.0:53 # additional dns server listen on TUN
auto-route: true # auto set global route
```
### Rules configuration
- Support rule `GEOSITE`.
- Support rule-providers `RULE-SET`.
- Support `multiport` condition for rule `SRC-PORT` and `DST-PORT`.
- Support `network` condition for all rules.
- Support source IPCIDR condition for all rules, just append to the end.
- The `GEOSITE` databases via https://github.com/Loyalsoldier/v2ray-rules-dat.
```yaml
rules:
# network(tcp/udp) condition for all rules
- DOMAIN-SUFFIX,bilibili.com,DIRECT,tcp
- DOMAIN-SUFFIX,bilibili.com,REJECT,udp
# multiport condition for rules SRC-PORT and DST-PORT
- DST-PORT,123/136/137-139,DIRECT,udp
# rule GEOSITE
- GEOSITE,category-ads-all,REJECT
- GEOSITE,icloud@cn,DIRECT
- GEOSITE,apple@cn,DIRECT
- GEOSITE,apple-cn,DIRECT
- GEOSITE,microsoft@cn,DIRECT
- GEOSITE,facebook,PROXY
- GEOSITE,youtube,PROXY
- GEOSITE,geolocation-cn,DIRECT
- GEOSITE,geolocation-!cn,PROXY
# source IPCIDR condition for all rules in gateway proxy
#- GEOSITE,geolocation-!cn,REJECT,192.168.1.88/32,192.168.1.99/32
- GEOIP,telegram,PROXY,no-resolve
- GEOIP,private,DIRECT,no-resolve
- GEOIP,cn,DIRECT
- MATCH,PROXY
``` ```
Build with gvisor tun stack:
```shell ### Proxies configuration
go build -tags with_gvisor
Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node)
Support `Policy Group Filter`
```yaml
proxy-groups:
- name: 🚀 HK Group
type: select
use:
- ALL
filter: 'HK'
- name: 🚀 US Group
type: select
use:
- ALL
filter: 'US'
proxy-providers:
ALL:
type: http
url: "xxxxx"
interval: 3600
path: "xxxxx"
health-check:
enable: true
interval: 600
url: http://www.gstatic.com/generate_204
```
Support outbound transport protocol `VLESS`.
The XTLS support (TCP/UDP) transport by the XRAY-CORE.
```yaml
proxies:
- name: "vless"
type: vless
server: server
port: 443
uuid: uuid
servername: example.com # AKA SNI
# flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS
# skip-cert-verify: true
- name: "vless-ws"
type: vless
server: server
port: 443
uuid: uuid
tls: true
udp: true
network: ws
servername: example.com # priority over wss host
# skip-cert-verify: true
ws-opts:
path: /path
headers: { Host: example.com, Edge: "12a00c4.fm.huawei.com:82897" }
- name: "vless-grpc"
type: vless
server: server
port: 443
uuid: uuid
tls: true
udp: true
network: grpc
servername: example.com # priority over wss host
# skip-cert-verify: true
grpc-opts:
grpc-service-name: grpcname
``` ```
### IPTABLES configuration ### IPTABLES configuration
Work on Linux OS who's supported `iptables`
Work on Linux OS which supported `iptables`
```yaml ```yaml
# Enable the TPROXY listener # Enable the TPROXY listener
@@ -80,22 +224,75 @@ iptables:
inbound-interface: eth0 # detect the inbound interface, default is 'lo' inbound-interface: eth0 # detect the inbound interface, default is 'lo'
``` ```
## Debugging
Check [wiki](https://wiki.metacubex.one/api/#debug) to get an instruction on using debug ### General installation guide for Linux
API. + Create user given name `clash-meta`
+ Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
+ Rename executable file to `Clash-Meta` and move to `/usr/local/bin/`
+ Create folder `/etc/Clash-Meta/` as working directory
Run Meta Kernel by user `clash-meta` as a daemon.
Create the systemd configuration file at `/etc/systemd/system/Clash-Meta.service`:
```
[Unit]
Description=Clash-Meta Daemon, Another Clash Kernel.
After=network.target NetworkManager.service systemd-networkd.service iwd.service
[Service]
Type=simple
User=clash-meta
Group=clash-meta
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=cap_net_admin
AmbientCapabilities=cap_net_admin
Restart=always
ExecStartPre=/usr/bin/sleep 1s
ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
[Install]
WantedBy=multi-user.target
```
Launch clashd on system startup with:
```shell
$ systemctl enable Clash-Meta
```
Launch clashd immediately with:
```shell
$ systemctl start Clash-Meta
```
### Display Process name
Clash add field `Process` to `Metadata` and prepare to get process name for Restful API `GET /connections`.
To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard).
![img.png](https://github.com/Clash-Mini/Dashboard/raw/master/View/Dashboard-Process.png)
## Development
If you want to build an application that uses clash as a library, check out the
the [GitHub Wiki](https://github.com/Dreamacro/clash/wiki/use-clash-as-a-library)
## Credits ## Credits
- [Dreamacro/clash](https://github.com/Dreamacro/clash) * [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)
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2) * [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core) * [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go) * [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
- [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
## License ## License
This software is released under the GPL-3.0 license. This software is released under the GPL-3.0 license.
**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.** [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FDreamacro%2Fclash.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FDreamacro%2Fclash?ref=badge_large)

View File

@@ -2,55 +2,31 @@ package adapter
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
"github.com/metacubex/mihomo/common/atomic" "github.com/Dreamacro/clash/common/queue"
"github.com/metacubex/mihomo/common/queue" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/common/utils" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/ca" "go.uber.org/atomic"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
"github.com/puzpuzpuz/xsync/v3"
) )
var UnifiedDelay = atomic.NewBool(false) var UnifiedDelay = atomic.NewBool(false)
const (
defaultHistoriesNum = 10
)
type internalProxyState struct {
alive atomic.Bool
history *queue.Queue[C.DelayHistory]
}
type Proxy struct { type Proxy struct {
C.ProxyAdapter C.ProxyAdapter
alive atomic.Bool
history *queue.Queue[C.DelayHistory] history *queue.Queue[C.DelayHistory]
extra *xsync.MapOf[string, *internalProxyState] alive *atomic.Bool
} }
// Adapter implements C.Proxy // Alive implements C.Proxy
func (p *Proxy) Adapter() C.ProxyAdapter { func (p *Proxy) Alive() bool {
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() return p.alive.Load()
} }
@@ -62,8 +38,13 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata) conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
return conn, err return conn, err
} }
@@ -75,8 +56,9 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata) pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err return pc, err
} }
@@ -90,60 +72,17 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
return histories return histories
} }
// DelayHistoryForTestUrl implements C.Proxy // LastDelay return last history record. if proxy is not alive, return the max value of uint16.
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 // implements C.Proxy
func (p *Proxy) ExtraDelayHistories() map[string]C.ProxyState { func (p *Proxy) LastDelay() (delay uint16) {
histories := map[string]C.ProxyState{} var max uint16 = 0xffff
if !p.alive.Load() {
p.extra.Range(func(k string, v *internalProxyState) bool { return max
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 { history := p.history.Last()
return maxDelay if history.Delay == 0 {
return max
} }
return history.Delay return history.Delay
} }
@@ -158,62 +97,24 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping := map[string]any{} mapping := map[string]any{}
_ = json.Unmarshal(inner, &mapping) _ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory() mapping["history"] = p.DelayHistory()
mapping["extra"] = p.ExtraDelayHistories()
mapping["alive"] = p.alive.Load()
mapping["name"] = p.Name() mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP() 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) return json.Marshal(mapping)
} }
// URLTest get the delay for the specified URL // URLTest get the delay for the specified URL
// implements C.Proxy // implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (t uint16, err error) { func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
var satisfied bool
defer func() { defer func() {
alive := err == nil p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()} record := C.DelayHistory{Time: time.Now()}
if alive { if err == nil {
record.Delay = t record.Delay = t
} }
p.alive.Store(alive)
p.history.Put(record) p.history.Put(record)
if p.history.Len() > defaultHistoriesNum { if p.history.Len() > 10 {
p.history.Pop() 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() unifiedDelay := UnifiedDelay.Load()
@@ -247,55 +148,34 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}),
} }
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second,
Transport: transport, Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}, },
} }
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return return
} }
_ = resp.Body.Close()
if unifiedDelay { if unifiedDelay {
second := time.Now() start = time.Now()
var ignoredErr error resp, err = client.Do(req)
var secondResp *http.Response if err != nil {
secondResp, ignoredErr = client.Do(req) return
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.")
}
} }
} }
_ = resp.Body.Close()
satisfied = resp != nil && (expectedStatus == nil || expectedStatus.Check(uint16(resp.StatusCode)))
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{ return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
ProxyAdapter: adapter,
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
extra: xsync.NewMapOf[string, *internalProxyState]()}
} }
func urlToMetadata(rawURL string) (addr C.Metadata, err error) { func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@@ -316,15 +196,12 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return return
} }
} }
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{ addr = C.Metadata{
Host: u.Hostname(), AddrType: C.AtypDomainName,
DstIP: netip.Addr{}, Host: u.Hostname(),
DstPort: uint16(uintPort), DstIP: netip.Addr{},
DstPort: port,
} }
return return
} }

View File

@@ -1,73 +0,0 @@
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) {}

View File

@@ -1,38 +0,0 @@
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)
}

View File

@@ -3,18 +3,19 @@ package inbound
import ( import (
"net" "net"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
) )
// NewHTTP receive normal http request and return HTTPContext // 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) { func NewHTTP(target socks5.Addr, source net.Addr, conn net.Conn) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = C.HTTP metadata.Type = C.HTTP
metadata.RawSrcAddr = srcConn.RemoteAddr() if ip, port, err := parseAddr(source.String()); err == nil {
metadata.RawDstAddr = srcConn.LocalAddr() metadata.SrcIP = ip
ApplyAdditions(metadata, WithSrcAddr(srcConn.RemoteAddr()), WithInAddr(srcConn.LocalAddr())) metadata.SrcPort = port
ApplyAdditions(metadata, additions...) }
return conn, metadata return context.NewConnContext(conn, metadata)
} }

View File

@@ -4,16 +4,17 @@ import (
"net" "net"
"net/http" "net/http"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
) )
// NewHTTPS receive CONNECT request and return ConnContext // NewHTTPS receive CONNECT request and return ConnContext
func NewHTTPS(request *http.Request, conn net.Conn, additions ...Addition) (net.Conn, *C.Metadata) { func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
metadata := parseHTTPAddr(request) metadata := parseHTTPAddr(request)
metadata.Type = C.HTTPS metadata.Type = C.HTTPS
metadata.RawSrcAddr = conn.RemoteAddr() if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
metadata.RawDstAddr = conn.LocalAddr() metadata.SrcIP = ip
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) metadata.SrcPort = port
ApplyAdditions(metadata, additions...) }
return conn, metadata return context.NewConnContext(conn, metadata)
} }

View File

@@ -1,47 +0,0 @@
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)
}

View File

@@ -1,106 +0,0 @@
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)
})
}

View File

@@ -1,14 +0,0 @@
//go:build !windows
package inbound
import (
"net"
"os"
)
const SupportNamedPipe = false
func ListenNamedPipe(path string) (net.Listener, error) {
return nil, os.ErrInvalid
}

View File

@@ -1,32 +0,0 @@
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)
}

View File

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

View File

@@ -1,15 +0,0 @@
//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()
}

View File

@@ -1,22 +1,33 @@
package inbound package inbound
import ( import (
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/transport/socks5" M "github.com/sagernet/sing/common/metadata"
) )
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
type PacketAdapter struct {
C.UDPPacket
metadata *C.Metadata
}
// Metadata returns destination metadata
func (s *PacketAdapter) Metadata() *C.Metadata {
return s.metadata
}
// NewPacket is PacketAdapter generator // NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) (C.UDPPacket, *C.Metadata) { func NewPacket(target M.Socksaddr, packet C.UDPPacket, source C.Type) *PacketAdapter {
metadata := parseSocksAddr(target) metadata := socksAddrToMetadata(target)
metadata.NetWork = C.UDP metadata.NetWork = C.UDP
metadata.Type = source metadata.Type = source
metadata.RawSrcAddr = packet.LocalAddr() if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
metadata.RawDstAddr = metadata.UDPAddr() metadata.SrcIP = ip
ApplyAdditions(metadata, WithSrcAddr(packet.LocalAddr())) metadata.SrcPort = port
if p, ok := packet.(C.UDPPacketInAddr); ok {
ApplyAdditions(metadata, WithInAddr(p.InAddr()))
} }
ApplyAdditions(metadata, additions...)
return packet, metadata return &PacketAdapter{
UDPPacket: packet,
metadata: metadata,
}
} }

View File

@@ -2,17 +2,50 @@ package inbound
import ( import (
"net" "net"
"net/netip"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/transport/socks5"
) )
// NewSocket receive TCP inbound and return ConnContext // NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) (net.Conn, *C.Metadata) { func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
metadata := parseSocksAddr(target) metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP metadata.NetWork = C.TCP
metadata.Type = source metadata.Type = source
ApplyAdditions(metadata, WithSrcAddr(conn.RemoteAddr()), WithInAddr(conn.LocalAddr())) remoteAddr := conn.RemoteAddr()
ApplyAdditions(metadata, additions...) // Filter when net.Addr interface is nil
return conn, metadata if remoteAddr != nil {
if ip, port, err := parseAddr(remoteAddr.String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
}
return context.NewConnContext(conn, metadata)
}
func NewInner(conn net.Conn, dst string, host string) *context.ConnContext {
metadata := &C.Metadata{}
metadata.NetWork = C.TCP
metadata.Type = C.INNER
metadata.DNSMode = C.DNSMapping
metadata.Host = host
metadata.AddrType = C.AtypDomainName
metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(dst); err == nil {
metadata.DstPort = port
if host == "" {
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
metadata.AddrType = C.AtypIPv4
if ip.Is6() {
metadata.AddrType = C.AtypIPv6
}
}
}
}
return context.NewConnContext(conn, metadata)
} }

View File

@@ -7,26 +7,46 @@ import (
"strconv" "strconv"
"strings" "strings"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/common/nnip"
"github.com/metacubex/mihomo/transport/socks5" C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
M "github.com/sagernet/sing/common/metadata"
) )
func parseSocksAddr(target socks5.Addr) *C.Metadata { func socksAddrToMetadata(addr M.Socksaddr) *C.Metadata {
metadata := &C.Metadata{} metadata := &C.Metadata{}
switch addr.Family() {
case M.AddressFamilyIPv4:
metadata.AddrType = C.AtypIPv4
metadata.DstIP = addr.Addr
case M.AddressFamilyIPv6:
metadata.AddrType = C.AtypIPv6
metadata.DstIP = addr.Addr
case M.AddressFamilyFqdn:
metadata.AddrType = C.AtypDomainName
metadata.Host = addr.Fqdn
}
metadata.DstPort = strconv.Itoa(int(addr.Port))
return metadata
}
func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata := &C.Metadata{
AddrType: int(target[0]),
}
switch target[0] { switch target[0] {
case socks5.AtypDomainName: case socks5.AtypDomainName:
// trim for FQDN // trim for FQDN
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4: case socks5.AtypIPv4:
metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len]) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv6len]))
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
metadata.DstIP = metadata.DstIP.Unmap()
return metadata return metadata
} }
@@ -41,38 +61,34 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737) // trim FQDN (#737)
host = strings.TrimRight(host, ".") host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{ metadata := &C.Metadata{
NetWork: C.TCP, NetWork: C.TCP,
Host: host, AddrType: C.AtypDomainName,
DstIP: netip.Addr{}, Host: host,
DstPort: uint16Port, DstIP: netip.Addr{},
DstPort: port,
} }
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
if err == nil { if err == nil {
switch {
case ip.Is6():
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip metadata.DstIP = ip
} }
return metadata return metadata
} }
func prefixesContains(prefixes []netip.Prefix, addr netip.Addr) bool { func parseAddr(addr string) (netip.Addr, string, error) {
if len(prefixes) == 0 { host, port, err := net.SplitHostPort(addr)
return false if err != nil {
return netip.Addr{}, "", err
} }
if !addr.IsValid() {
return false ip, err := netip.ParseAddr(host)
} return ip, port, err
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
} }

View File

@@ -1,134 +0,0 @@
package outbound
import (
"context"
"errors"
"net"
"strconv"
"time"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls"
"github.com/metacubex/mihomo/transport/vmess"
M "github.com/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"`
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) {
// create tcp
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
if err != nil {
return nil, err
}
// create uot on tcp
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
}
// SupportUOT implements C.ProxyAdapter
func (t *AnyTLS) SupportUOT() bool {
return true
}
// ProxyInfo implements C.ProxyAdapter
func (t *AnyTLS) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (t *AnyTLS) Close() error {
return t.client.Close()
}
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
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,
}
tlsConfig := &vmess.TLSConfig{
Host: option.SNI,
SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN,
FingerPrint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
}
if tlsConfig.Host == "" {
tlsConfig.Host = option.Server
}
tOption.TLSConfig = tlsConfig
client := anytls.NewClient(context.TODO(), tOption)
outbound.client = client
return outbound, nil
}

View File

@@ -3,36 +3,27 @@ package outbound
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"net" "net"
"runtime"
"strings" "strings"
"sync"
"syscall"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/common/utils" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/dialer" "github.com/gofrs/uuid"
C "github.com/metacubex/mihomo/constant" "github.com/sagernet/sing/common/buf"
"github.com/metacubex/mihomo/log" "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
) )
type ProxyAdapter interface {
C.ProxyAdapter
DialOptions() []dialer.Option
}
type Base struct { type Base struct {
name string name string
addr string addr string
iface string iface string
tp C.AdapterType tp C.AdapterType
udp bool udp bool
xudp bool rmark int
tfo bool id string
mpTcp bool
rmark int
id string
prefer C.DNSPrefer
} }
// Name implements C.ProxyAdapter // Name implements C.ProxyAdapter
@@ -43,7 +34,12 @@ func (b *Base) Name() string {
// Id implements C.ProxyAdapter // Id implements C.ProxyAdapter
func (b *Base) Id() string { func (b *Base) Id() string {
if b.id == "" { if b.id == "" {
b.id = utils.NewUUIDV6().String() id, err := uuid.NewV6()
if err != nil {
b.id = b.name
} else {
b.id = id.String()
}
} }
return b.id return b.id
@@ -54,33 +50,23 @@ func (b *Base) Type() C.AdapterType {
return b.tp return b.tp
} }
// StreamConnContext implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, C.ErrNotSupport return c, errors.New("no support")
} }
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return nil, C.ErrNotSupport return nil, errors.New("no support")
}
// DialContextWithDialer implements C.ProxyAdapter
func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
return nil, C.ErrNotSupport
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return nil, C.ErrNotSupport return nil, errors.New("no support")
} }
// ListenPacketWithDialer implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, C.ErrNotSupport return nil, errors.New("no support")
}
// SupportWithDialer implements C.ProxyAdapter
func (b *Base) SupportWithDialer() C.NetWork {
return C.InvalidNet
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@@ -93,22 +79,6 @@ func (b *Base) SupportUDP() bool {
return b.udp 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 // MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) { func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{ return json.Marshal(map[string]string{
@@ -123,12 +93,12 @@ func (b *Base) Addr() string {
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (b *Base) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
return nil return nil
} }
// DialOptions return []dialer.Option from struct // DialOptions return []dialer.Option from struct
func (b *Base) DialOptions() (opts []dialer.Option) { func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
if b.iface != "" { if b.iface != "" {
opts = append(opts, dialer.WithInterface(b.iface)) opts = append(opts, dialer.WithInterface(b.iface))
} }
@@ -137,40 +107,12 @@ func (b *Base) DialOptions() (opts []dialer.Option) {
opts = append(opts, dialer.WithRoutingMark(b.rmark)) opts = append(opts, dialer.WithRoutingMark(b.rmark))
} }
switch b.prefer {
case C.IPv4Only:
opts = append(opts, dialer.WithOnlySingleStack(true))
case C.IPv6Only:
opts = append(opts, dialer.WithOnlySingleStack(false))
case C.IPv4Prefer:
opts = append(opts, dialer.WithPreferIPv4())
case C.IPv6Prefer:
opts = append(opts, dialer.WithPreferIPv6())
default:
}
if b.tfo {
opts = append(opts, dialer.WithTFO(true))
}
if b.mpTcp {
opts = append(opts, dialer.WithMPTCP(true))
}
return opts return opts
} }
func (b *Base) Close() error {
return nil
}
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,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 { type BaseOption struct {
@@ -178,31 +120,23 @@ type BaseOption struct {
Addr string Addr string
Type C.AdapterType Type C.AdapterType
UDP bool UDP bool
XUDP bool
TFO bool
MPTCP bool
Interface string Interface string
RoutingMark int RoutingMark int
Prefer C.DNSPrefer
} }
func NewBase(opt BaseOption) *Base { func NewBase(opt BaseOption) *Base {
return &Base{ return &Base{
name: opt.Name, name: opt.Name,
addr: opt.Addr, addr: opt.Addr,
tp: opt.Type, tp: opt.Type,
udp: opt.UDP, udp: opt.UDP,
xudp: opt.XUDP, iface: opt.Interface,
tfo: opt.TFO, rmark: opt.RoutingMark,
mpTcp: opt.MPTCP,
iface: opt.Interface,
rmark: opt.RoutingMark,
prefer: opt.Prefer,
} }
} }
type conn struct { type conn struct {
N.ExtendedConn net.Conn
chain C.Chain chain C.Chain
actualRemoteDestination string actualRemoteDestination string
} }
@@ -221,34 +155,14 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) c.chain = append(c.chain, a.Name())
} }
func (c *conn) Upstream() any {
return c.ExtendedConn
}
func (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 { func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn return &conn{c, []string{a.Name()}, parseRemoteDestination(a.Addr())}
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
} }
type packetConn struct { type packetConn struct {
N.EnhancePacketConn net.PacketConn
nc N.PacketConn
chain C.Chain chain C.Chain
adapterName string
connID string
actualRemoteDestination string actualRemoteDestination string
} }
@@ -266,33 +180,22 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name()) c.chain = append(c.chain, a.Name())
} }
func (c *packetConn) LocalAddr() net.Addr { func (c *packetConn) ReadPacket(buffer *buf.Buffer) (addr M.Socksaddr, err error) {
lAddr := c.EnhancePacketConn.LocalAddr() return c.nc.ReadPacket(buffer)
return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy
} }
func (c *packetConn) Upstream() any { func (c *packetConn) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
return c.EnhancePacketConn return c.nc.WritePacket(buffer, addr)
}
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 C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc) var nc N.PacketConn
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn if n, isNc := pc.(N.PacketConn); isNc {
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly nc = n
} else {
nc = &bufio.PacketConnWrapper{PacketConn: pc}
} }
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())} return &packetConn{pc, nc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
} }
func parseRemoteDestination(addr string) string { func parseRemoteDestination(addr string) string {
@@ -306,75 +209,3 @@ func parseRemoteDestination(addr string) string {
} }
} }
} }
type AddRef interface {
AddRef(ref any)
}
type autoCloseProxyAdapter struct {
ProxyAdapter
closeOnce sync.Once
closeErr error
}
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata) (_ 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
}

View File

@@ -2,97 +2,57 @@ package outbound
import ( import (
"context" "context"
"errors" "net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/loopback" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/resolver" C "github.com/Dreamacro/clash/constant"
C "github.com/metacubex/mihomo/constant"
) )
type Direct struct { type Direct struct {
*Base *Base
loopBack *loopback.Detector
}
type DirectOption struct {
BasicOption
Name string `proxy:"name"`
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if err := d.loopBack.CheckConn(metadata); err != nil { opts = append(opts, dialer.WithDirect())
return nil, err c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
}
opts := d.DialOptions()
opts = append(opts, dialer.WithResolver(resolver.DirectHostResolver))
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return d.loopBack.NewConn(NewConn(c, d)), nil tcpKeepAlive(c)
return NewConn(c, d), nil
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
if err := d.loopBack.CheckPacketConn(metadata); err != nil { opts = append(opts, dialer.WithDirect())
return nil, err pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
}
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil return newPacketConn(&directPacketConn{pc}, d), nil
} }
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool { type directPacketConn struct {
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver net.PacketConn
}
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 { func NewDirect() *Direct {
return &Direct{ return &Direct{
Base: &Base{ Base: &Base{
name: "DIRECT", name: "DIRECT",
tp: C.Direct, tp: C.Direct,
udp: true, udp: true,
prefer: C.DualStack,
}, },
loopBack: loopback.NewDetector(),
} }
} }
func NewCompatible() *Direct { func NewCompatible() *Direct {
return &Direct{ return &Direct{
Base: &Base{ Base: &Base{
name: "COMPATIBLE", name: "COMPATIBLE",
tp: C.Compatible, tp: C.Compatible,
udp: true, udp: true,
prefer: C.DualStack,
}, },
loopBack: loopback.NewDetector(),
} }
} }

View File

@@ -1,158 +0,0 @@
package outbound
import (
"context"
"net"
"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())
ctx, cancel := context.WithCancel(context.Background())
return newPacketConn(&dnsPacketConn{
response: make(chan dnsPacket, 1),
ctx: ctx,
cancel: cancel,
}, d), nil
}
type dnsPacket struct {
data []byte
put func()
addr net.Addr
}
// dnsPacketConn implements net.PacketConn
type dnsPacketConn struct {
response chan dnsPacket
ctx context.Context
cancel context.CancelFunc
}
func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
select {
case packet := <-d.response:
return packet.data, packet.put, packet.addr, nil
case <-d.ctx.Done():
return nil, nil, nil, net.ErrClosed
}
}
func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
select {
case packet := <-d.response:
n = copy(p, packet.data)
if packet.put != nil {
packet.put()
}
return n, packet.addr, nil
case <-d.ctx.Done():
return 0, nil, net.ErrClosed
}
}
func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
select {
case <-d.ctx.Done():
return 0, net.ErrClosed
default:
}
if len(p) > resolver.SafeDnsPacketSize {
// wtf???
return len(p), nil
}
buf := pool.Get(resolver.SafeDnsPacketSize)
put := func() { _ = pool.Put(buf) }
copy(buf, p) // avoid p be changed after WriteTo returned
go func() { // don't block the WriteTo function
ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout)
defer cancel()
buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf)
if err != nil {
put()
return
}
packet := dnsPacket{
data: buf,
put: put,
addr: addr,
}
select {
case d.response <- packet:
break
case <-d.ctx.Done():
put()
}
}()
return len(p), nil
}
func (d *dnsPacketConn) Close() error {
d.cancel()
return nil
}
func (*dnsPacketConn) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 53,
Zone: "",
}
}
func (*dnsPacketConn) SetDeadline(t time.Time) error {
return nil
}
func (*dnsPacketConn) SetReadDeadline(t time.Time) error {
return nil
}
func (*dnsPacketConn) SetWriteDeadline(t time.Time) error {
return nil
}
func NewDnsWithOption(option DnsOption) *Dns {
return &Dns{
Base: &Base{
name: option.Name,
tp: C.Dns,
udp: true,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
}
}

View File

@@ -7,15 +7,14 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/ca" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
) )
type Http struct { type Http struct {
@@ -36,50 +35,37 @@ type HttpOption struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
} }
// StreamConnContext implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if h.tlsConfig != nil { if h.tlsConfig != nil {
cc := tls.Client(c, h.tlsConfig) cc := tls.Client(c, h.tlsConfig)
err := cc.HandshakeContext(ctx) err := cc.Handshake()
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
} }
if err := h.shakeHandContext(ctx, c, metadata); err != nil { if err := h.shakeHand(metadata, c); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return h.DialContextWithDialer(ctx, dialer.NewDialer(h.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", h.addr, err) return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = h.StreamConnContext(ctx, c, metadata) c, err = h.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -87,55 +73,36 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
return NewConn(c, h), nil return NewConn(c, h), nil
} }
// SupportWithDialer implements C.ProxyAdapter func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
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() addr := metadata.RemoteAddress()
HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" req := &http.Request{
tempHeaders := map[string]string{ Method: http.MethodConnect,
"Host": addr, URL: &url.URL{
"User-Agent": "Go-http-client/1.1", Host: addr,
"Proxy-Connection": "Keep-Alive", },
Host: addr,
Header: http.Header{
"Proxy-Connection": []string{"Keep-Alive"},
},
} }
for key, value := range h.option.Headers { // 增加headers
tempHeaders[key] = value if len(h.option.Headers) != 0 {
for key, value := range h.option.Headers {
req.Header.Add(key, value)
}
} }
if h.user != "" && h.pass != "" { if h.user != "" && h.pass != "" {
auth := h.user + ":" + h.pass auth := h.user + ":" + h.pass
tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
} }
for key, value := range tempHeaders { if err := req.Write(rw); err != nil {
HeaderString += key + ": " + value + "\r\n"
}
HeaderString += "\r\n"
_, err = c.Write([]byte(HeaderString))
if err != nil {
return err return err
} }
resp, err := http.ReadResponse(bufio.NewReader(c), nil) resp, err := http.ReadResponse(bufio.NewReader(rw), req)
if err != nil { if err != nil {
return err return err
} }
@@ -159,37 +126,30 @@ func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Met
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
} }
func NewHttp(option HttpOption) (*Http, error) { func NewHttp(option HttpOption) *Http {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
sni := option.Server sni := option.Server
if option.SNI != "" { if option.SNI != "" {
sni = option.SNI sni = option.SNI
} }
var err error tlsConfig = &tls.Config{
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: sni, ServerName: sni,
}, option.Fingerprint)
if err != nil {
return nil, err
} }
} }
return &Http{ return &Http{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Http, tp: C.Http,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
option: &option, option: &option,
}, nil }
} }

View File

@@ -3,249 +3,241 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"net/netip" "regexp"
"strconv" "strconv"
"time" "time"
"github.com/metacubex/mihomo/component/ca" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/Dreamacro/clash/log"
tlsC "github.com/metacubex/mihomo/component/tls" "github.com/lucas-clemente/quic-go"
C "github.com/metacubex/mihomo/constant" "github.com/lucas-clemente/quic-go/congestion"
"github.com/metacubex/mihomo/log" M "github.com/sagernet/sing/common/metadata"
hyCongestion "github.com/metacubex/mihomo/transport/hysteria/congestion" hyCongestion "github.com/tobyxdd/hysteria/pkg/congestion"
"github.com/metacubex/mihomo/transport/hysteria/core" "github.com/tobyxdd/hysteria/pkg/core"
"github.com/metacubex/mihomo/transport/hysteria/obfs" "github.com/tobyxdd/hysteria/pkg/obfs"
"github.com/metacubex/mihomo/transport/hysteria/pmtud_fix" "github.com/tobyxdd/hysteria/pkg/pmtud_fix"
"github.com/metacubex/mihomo/transport/hysteria/transport" "github.com/tobyxdd/hysteria/pkg/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 ( const (
mbpsToBps = 125000 mbpsToBps = 125000
minSpeedBPS = 16384
DefaultStreamReceiveWindow = 15728640 // 15 MB/s DefaultStreamReceiveWindow = 15728640 // 15 MB/s
DefaultConnectionReceiveWindow = 67108864 // 64 MB/s DefaultConnectionReceiveWindow = 67108864 // 64 MB/s
DefaultMaxIncomingStreams = 1024
DefaultALPN = "hysteria" DefaultALPN = "hysteria"
DefaultProtocol = "udp" DefaultProtocol = "udp"
DefaultHopInterval = 10
) )
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
type Hysteria struct { type Hysteria struct {
*Base *Base
option *HysteriaOption client *core.Client
client *core.Client clientTransport *transport.ClientTransport
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
tcpConn, err := h.client.DialTCP(metadata.String(), metadata.DstPort, h.genHdc(ctx)) tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), hyDialer(func() (net.PacketConn, error) {
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
}))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(tcpConn, h), nil return NewConn(tcpConn, h), nil
} }
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
udpConn, err := h.client.DialUDP(h.genHdc(ctx)) udpConn, err := h.client.DialUDP(hyDialer(func() (net.PacketConn, error) {
return dialer.ListenPacket(ctx, "udp", "", h.Base.DialOptions(opts...)...)
}))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(&hyPacketConn{udpConn}, h), nil 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) {
return resolveUDPAddr(ctx, "udp", addr, h.prefer)
},
}
}
// ProxyInfo implements C.ProxyAdapter
func (h *Hysteria) ProxyInfo() C.ProxyInfo {
info := h.Base.ProxyInfo()
info.DialerProxy = h.option.DialerProxy
return info
}
type HysteriaOption struct { type HysteriaOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port,omitempty"` Port int `proxy:"port"`
Ports string `proxy:"ports,omitempty"` Protocol string `proxy:"protocol,omitempty"`
Protocol string `proxy:"protocol,omitempty"` Up string `proxy:"up,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash UpMbps int `proxy:"up_mbps,omitempty"`
Up string `proxy:"up"` Down string `proxy:"down,omitempty"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash DownMbps int `proxy:"down_mbps,omitempty"`
Down string `proxy:"down"` Auth string `proxy:"auth,omitempty"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash AuthString string `proxy:"auth_str,omitempty"`
Auth string `proxy:"auth,omitempty"` Obfs string `proxy:"obfs,omitempty"`
AuthString string `proxy:"auth-str,omitempty"` SNI string `proxy:"sni,omitempty"`
Obfs string `proxy:"obfs,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
SNI string `proxy:"sni,omitempty"` ALPN string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` CustomCA string `proxy:"ca,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` CustomCAString string `proxy:"ca_str,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` ReceiveWindowConn int `proxy:"recv_window_conn,omitempty"`
CustomCA string `proxy:"ca,omitempty"` ReceiveWindow int `proxy:"recv_window,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"` DisableMTUDiscovery bool `proxy:"disable_mtu_discovery,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) { func (c *HysteriaOption) Speed() (uint64, uint64, error) {
var up, down uint64 var up, down uint64
up = StringToBps(c.Up) if len(c.Up) > 0 {
if up == 0 { up = stringToBps(c.Up)
return 0, 0, fmt.Errorf("invaild upload speed: %s", c.Up) if up == 0 {
return 0, 0, errors.New("invalid speed format")
}
} else {
up = uint64(c.UpMbps) * mbpsToBps
} }
if len(c.Down) > 0 {
down = StringToBps(c.Down) down = stringToBps(c.Down)
if down == 0 { if down == 0 {
return 0, 0, fmt.Errorf("invaild download speed: %s", c.Down) return 0, 0, errors.New("invalid speed format")
}
} else {
down = uint64(c.DownMbps) * mbpsToBps
} }
return up, down, nil return up, down, nil
} }
func NewHysteria(option HysteriaOption) (*Hysteria, error) { func NewHysteria(option HysteriaOption) (*Hysteria, error) {
clientTransport := &transport.ClientTransport{} clientTransport := &transport.ClientTransport{
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) Dialer: &net.Dialer{
ports := option.Ports Timeout: 8 * time.Second,
},
serverName := option.Server
if option.SNI != "" {
serverName = option.SNI
} }
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server
if option.SNI != "" {
serverName = option.Server
}
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: serverName, ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13, MinVersion: tls.VersionTLS13,
} }
var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if len(option.ALPN) > 0 { if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN tlsConfig.NextProtos = []string{option.ALPN}
} else { } else {
tlsConfig.NextProtos = []string{DefaultALPN} tlsConfig.NextProtos = []string{DefaultALPN}
} }
if len(option.CustomCA) > 0 {
bs, err := ioutil.ReadFile(option.CustomCA)
if err != nil {
return nil, fmt.Errorf("hysteria %s load ca error: %w", addr, err)
}
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(bs) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
}
tlsConfig.RootCAs = cp
} else if option.CustomCAString != "" {
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM([]byte(option.CustomCAString)) {
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
}
tlsConfig.RootCAs = cp
}
quicConfig := &quic.Config{ quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow), InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow), MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
KeepAlivePeriod: 10 * time.Second, KeepAlive: true,
DisablePathMTUDiscovery: option.DisableMTUDiscovery, DisablePathMTUDiscovery: option.DisableMTUDiscovery,
EnableDatagrams: true, EnableDatagrams: true,
} }
if option.ObfsProtocol != "" {
option.Protocol = option.ObfsProtocol
}
if option.Protocol == "" { if option.Protocol == "" {
option.Protocol = DefaultProtocol option.Protocol = DefaultProtocol
} }
if option.HopInterval == 0 { if option.ReceiveWindowConn == 0 {
option.HopInterval = DefaultHopInterval quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow
}
hopInterval := time.Duration(int64(option.HopInterval)) * time.Second
if option.ReceiveWindow == 0 {
quicConfig.InitialStreamReceiveWindow = DefaultStreamReceiveWindow / 10
quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow quicConfig.MaxStreamReceiveWindow = DefaultStreamReceiveWindow
} }
if option.ReceiveWindow == 0 { if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow / 10 quicConfig.InitialConnectionReceiveWindow = DefaultConnectionReceiveWindow
quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow quicConfig.MaxConnectionReceiveWindow = DefaultConnectionReceiveWindow
} }
if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery { if !quicConfig.DisablePathMTUDiscovery && pmtud_fix.DisablePathMTUDiscovery {
log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform") log.Infoln("hysteria: Path MTU Discovery is not yet supported on this platform")
} }
var auth []byte
var auth = []byte(option.AuthString)
if option.Auth != "" { if option.Auth != "" {
auth, err = base64.StdEncoding.DecodeString(option.Auth) authBytes, err := base64.StdEncoding.DecodeString(option.Auth)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("hysteria %s parse auth error: %w", addr, err)
} }
auth = authBytes
} else {
auth = []byte(option.AuthString)
} }
var obfuscator obfs.Obfuscator var obfuscator obfs.Obfuscator
if len(option.Obfs) > 0 { if len(option.Obfs) > 0 {
obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs)) obfuscator = obfs.NewXPlusObfuscator([]byte(option.Obfs))
} }
up, down, _ := option.Speed()
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( client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { addr, option.Protocol, auth, tlsConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen, }, obfuscator,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("hysteria %s create error: %w", addr, err) return nil, fmt.Errorf("hysteria %s create error: %w", addr, err)
} }
outbound := &Hysteria{ return &Hysteria{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Hysteria, tp: C.Hysteria,
udp: true, udp: true,
tfo: option.FastOpen, iface: option.Interface,
iface: option.Interface, rmark: option.RoutingMark,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, client: client,
client: client, clientTransport: clientTransport,
} }, nil
return outbound, nil
} }
// Close implements C.ProxyAdapter func stringToBps(s string) uint64 {
func (h *Hysteria) Close() error { if s == "" {
if h.client != nil { return 0
return h.client.Close()
} }
return nil m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
switch m[2] {
case "K":
n = 1 << 10
case "M":
n = 1 << 20
case "G":
n = 1 << 30
case "T":
n = 1 << 40
default:
n = 1
}
v, _ := strconv.ParseUint(m[1], 10, 64)
n = v * n
if m[3] == "b" {
// Bits, need to convert to bytes
n = n >> 3
}
return n
} }
type hyPacketConn struct { type hyPacketConn struct {
@@ -262,16 +254,6 @@ func (c *hyPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
return 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) { func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String()) err = c.UDPConn.WriteTo(p, M.SocksaddrFromNet(addr).String())
if err != nil { if err != nil {
@@ -281,24 +263,8 @@ func (c *hyPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return return
} }
type hyDialerWithContext struct { type hyDialer func() (net.PacketConn, error)
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) { func (h hyDialer) ListenPacket() (net.PacketConn, error) {
network := "udp" return h()
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)
} }

View File

@@ -1,223 +0,0 @@
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/randv2"
"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"`
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) {
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 len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN
}
if option.UdpMTU == 0 {
// "1200" from quic-go's MaxDatagramSize
// "-3" from quic-go's DatagramFrame.MaxDataLen
option.UdpMTU = 1200 - 3
}
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: tlsC.UConfig(tlsConfig),
QUICConfig: quicConfig,
UDPDisabled: false,
CWND: option.CWND,
UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
},
}
var ranges utils.IntRanges[uint16]
var serverAddress []string
if option.Ports != "" {
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
if err != nil {
return nil, err
}
ranges.Range(func(port uint16) bool {
serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port))))
return true
})
if len(serverAddress) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
}
if option.HopInterval == 0 {
option.HopInterval = defaultHopInterval
} else if option.HopInterval < minHopInterval {
option.HopInterval = minHopInterval
}
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
}
}
if option.Port == 0 && len(serverAddress) == 0 {
return nil, errors.New("invalid port")
}
client, err := hysteria2.NewClient(clientOptions)
if err != nil {
return nil, err
}
outbound.client = client
return outbound, nil
}

View File

@@ -1,301 +0,0 @@
package outbound
import (
"context"
"fmt"
"net"
"strconv"
"sync"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
mieruclient "github.com/enfein/mieru/v3/apis/client"
mierucommon "github.com/enfein/mieru/v3/apis/common"
mierumodel "github.com/enfein/mieru/v3/apis/model"
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
"google.golang.org/protobuf/proto"
)
type Mieru struct {
*Base
option *MieruOption
client mieruclient.Client
mu sync.Mutex
}
type MieruOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port,omitempty"`
PortRange string `proxy:"port-range,omitempty"`
Transport string `proxy:"transport"`
UDP bool `proxy:"udp,omitempty"`
UserName string `proxy:"username"`
Password string `proxy:"password"`
Multiplexing string `proxy:"multiplexing,omitempty"`
}
// DialContext implements C.ProxyAdapter
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (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.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(),
}
}
return config, nil
}
func validateMieruOption(option MieruOption) error {
if option.Name == "" {
return fmt.Errorf("name is empty")
}
if option.Server == "" {
return fmt.Errorf("server is empty")
}
if option.Port == 0 && option.PortRange == "" {
return fmt.Errorf("either port or port-range must be set")
}
if option.Port != 0 && option.PortRange != "" {
return fmt.Errorf("port and port-range cannot be set at the same time")
}
if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
return fmt.Errorf("port must be between 1 and 65535")
}
if option.PortRange != "" {
begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
if err != nil {
return fmt.Errorf("invalid port-range format")
}
if begin < 1 || begin > 65535 {
return fmt.Errorf("begin port must be between 1 and 65535")
}
if end < 1 || end > 65535 {
return fmt.Errorf("end port must be between 1 and 65535")
}
if begin > end {
return fmt.Errorf("begin port must be less than or equal to end port")
}
}
if option.Transport != "TCP" {
return fmt.Errorf("transport must be TCP")
}
if option.UserName == "" {
return fmt.Errorf("username is empty")
}
if option.Password == "" {
return fmt.Errorf("password is empty")
}
if option.Multiplexing != "" {
if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok {
return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing)
}
}
return nil
}
func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
var begin, end int
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
return begin, end, err
}

View File

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

View File

@@ -1,44 +0,0 @@
package outbound
import (
"crypto/ecdh"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
)
type RealityOptions struct {
PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"`
}
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
if o.PublicKey != "" {
config := new(tlsC.RealityConfig)
const x25519ScalarSize = 32
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
}

View File

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

View File

@@ -7,51 +7,38 @@ import (
"net" "net"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/common/structure"
"github.com/metacubex/mihomo/common/structure" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/proxydialer" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/metacubex/mihomo/component/resolver" "github.com/Dreamacro/clash/transport/socks5"
C "github.com/metacubex/mihomo/constant" v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
gost "github.com/metacubex/mihomo/transport/gost-plugin" "github.com/sagernet/sing-shadowsocks"
"github.com/metacubex/mihomo/transport/restls" "github.com/sagernet/sing-shadowsocks/shadowimpl"
obfs "github.com/metacubex/mihomo/transport/simple-obfs" "github.com/sagernet/sing/common/bufio"
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" M "github.com/sagernet/sing/common/metadata"
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 { type ShadowSocks struct {
*Base *Base
method shadowsocks.Method method shadowsocks.Method
option *ShadowSocksOption
// obfs // obfs
obfsMode string obfsMode string
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option v2rayOption *v2rayObfs.Option
gostOption *gost.Option
shadowTLSOption *shadowtls.ShadowTLSOption
restlsConfig *restls.Config
} }
type ShadowSocksOption struct { type ShadowSocksOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Password string `proxy:"password"` Password string `proxy:"password"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"` Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"` PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `proxy:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
type simpleObfsOption struct { type simpleObfsOption struct {
@@ -60,48 +47,17 @@ type simpleObfsOption struct {
} }
type v2rayObfsOption struct { type v2rayObfsOption struct {
Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"`
V2rayHttpUpgrade bool `obfs:"v2ray-http-upgrade,omitempty"`
V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"`
}
type gostObfsOption struct {
Mode string `obfs:"mode"` Mode string `obfs:"mode"`
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Mux bool `obfs:"mux,omitempty"` Mux bool `obfs:"mux,omitempty"`
} }
type shadowTLSOption struct { // StreamConn implements C.ProxyAdapter
Password string `obfs:"password,omitempty"` func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
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 { switch ss.obfsMode {
case "tls": case "tls":
c = obfs.NewTLSObfs(c, ss.obfsOption.Host) c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
@@ -109,163 +65,54 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
_, port, _ := net.SplitHostPort(ss.addr) _, port, _ := net.SplitHostPort(ss.addr)
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket": case "websocket":
if ss.v2rayOption != nil { var err error
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
} else if ss.gostOption != nil {
c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption)
} else {
return nil, fmt.Errorf("plugin options is required")
}
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
case shadowtls.Mode:
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))
} }
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ss.StreamConnContext(ctx, c, metadata) c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err return NewConn(c, ss), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata)
}
if len(ss.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
pc = ss.method.DialPacketConn(bufio.NewBindPacketConn(pc, addr)) pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
return newPacketConn(pc, ss), nil 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 {
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
} else {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), ss), nil
}
}
return nil, C.ErrNotSupport
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
Password: option.Password,
})
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
} }
var v2rayOption *v2rayObfs.Option var v2rayOption *v2rayObfs.Option
var gostOption *gost.Option
var obfsOption *simpleObfsOption var obfsOption *simpleObfsOption
var shadowTLSOpt *shadowtls.ShadowTLSOption
var restlsConfig *restls.Config
obfsMode := "" obfsMode := ""
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
@@ -291,30 +138,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
} }
obfsMode = opts.Mode obfsMode = opts.Mode
v2rayOption = &v2rayObfs.Option{ v2rayOption = &v2rayObfs.Option{
Host: opts.Host,
Path: opts.Path,
Headers: opts.Headers,
Mux: opts.Mux,
V2rayHttpUpgrade: opts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: opts.V2rayHttpUpgradeFastOpen,
}
if opts.TLS {
v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint
}
} else if option.Plugin == "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, Host: opts.Host,
Path: opts.Path, Path: opts.Path,
Headers: opts.Headers, Headers: opts.Headers,
@@ -322,74 +145,57 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
} }
if opts.TLS { if opts.TLS {
gostOption.TLS = true v2rayOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint
} }
} else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode
opt := &shadowTLSOption{
Version: 2,
}
if err := decoder.Decode(option.PluginOpts, opt); err != nil {
return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err)
}
shadowTLSOpt = &shadowtls.ShadowTLSOption{
Password: opt.Password,
Host: opt.Host,
Fingerprint: opt.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
SkipCertVerify: opt.SkipCertVerify,
Version: opt.Version,
}
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{ return &ShadowSocks{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Shadowsocks, tp: C.Shadowsocks,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
method: method, method: method,
option: &option, obfsMode: obfsMode,
obfsMode: obfsMode, v2rayOption: v2rayOption,
v2rayOption: v2rayOption, obfsOption: obfsOption,
gostOption: gostOption,
obfsOption: obfsOption,
shadowTLSOption: shadowTLSOpt,
restlsConfig: restlsConfig,
}, nil }, nil
} }
type ssPacketConn struct {
net.PacketConn
rAddr net.Addr
}
func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
if err != nil {
return
}
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.PacketConn.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
}

View File

@@ -2,26 +2,21 @@ package outbound
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/Dreamacro/clash/transport/shadowsocks/core"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/metacubex/mihomo/transport/shadowsocks/shadowaead" "github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/metacubex/mihomo/transport/shadowsocks/shadowstream" "github.com/Dreamacro/clash/transport/ssr/protocol"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/transport/ssr/obfs"
"github.com/metacubex/mihomo/transport/ssr/protocol"
) )
type ShadowSocksR struct { type ShadowSocksR struct {
*Base *Base
option *ShadowSocksROption
cipher core.Cipher cipher core.Cipher
obfs obfs.Obfs obfs obfs.Obfs
protocol protocol.Protocol protocol protocol.Protocol
@@ -41,16 +36,13 @@ type ShadowSocksROption struct {
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
} }
// StreamConnContext implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
c = ssr.obfs.StreamConn(c) c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c) c = ssr.cipher.StreamConn(c)
var ( var (
iv []byte iv []byte
err error
) )
switch conn := c.(type) { switch conn := c.(type) {
case *shadowstream.Conn: case *shadowstream.Conn:
@@ -67,74 +59,40 @@ func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, meta
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err) return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ssr.StreamConnContext(ctx, c, metadata) c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err return NewConn(c, ssr), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.DialOptions()...), metadata) pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(ssr.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil { if err != nil {
pc.Close()
return nil, err return nil, err
} }
epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc)) pc = ssr.cipher.PacketConn(pc)
epc = ssr.protocol.PacketConn(epc) pc = ssr.protocol.PacketConn(pc)
return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil return newPacketConn(&ssPacketConn{PacketConn: pc, 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) { func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
// SSR protocol compatibility // SSR protocol compatibility
// https://github.com/metacubex/mihomo/pull/2056 // https://github.com/Dreamacro/clash/pull/2056
if option.Cipher == "none" { if option.Cipher == "none" {
option.Cipher = "dummy" option.Cipher = "dummy"
} }
@@ -144,7 +102,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
password := option.Password password := option.Password
coreCiph, err := core.PickCipher(cipher, nil, password) coreCiph, err := core.PickCipher(cipher, nil, password)
if err != nil { if err != nil {
return nil, fmt.Errorf("ssr %s cipher: %s initialize error: %w", addr, cipher, err) return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
} }
var ( var (
ivSize int ivSize int
@@ -185,78 +143,15 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{ return &ShadowSocksR{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.ShadowsocksR, tp: C.ShadowsocksR,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option,
cipher: coreCiph, cipher: coreCiph,
obfs: obfs, obfs: obfs,
protocol: protocol, protocol: protocol,
}, nil }, 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
}

View File

@@ -1,133 +0,0 @@
package outbound
import (
"context"
"errors"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
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)
}
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
if err != nil {
return nil, err
}
if pc == nil {
return nil, E.New("packetConn is nil")
}
return newPacketConn(CN.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
}

View File

@@ -6,18 +6,15 @@ import (
"net" "net"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/common/structure"
"github.com/metacubex/mihomo/common/structure" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/proxydialer" obfs "github.com/Dreamacro/clash/transport/simple-obfs"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/transport/snell"
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
"github.com/metacubex/mihomo/transport/snell"
) )
type Snell struct { type Snell struct {
*Base *Base
option *SnellOption
psk []byte psk []byte
pool *snell.Pool pool *snell.Pool
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
@@ -42,7 +39,7 @@ type streamOption struct {
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
} }
func snellStreamConn(c net.Conn, option streamOption) *snell.Snell { func streamConn(c net.Conn, option streamOption) *snell.Snell {
switch option.obfsOption.Mode { switch option.obfsOption.Mode {
case "tls": case "tls":
c = obfs.NewTLSObfs(c, option.obfsOption.Host) c = obfs.NewTLSObfs(c, option.obfsOption.Host)
@@ -53,94 +50,68 @@ func snellStreamConn(c net.Conn, option streamOption) *snell.Snell {
return snell.StreamConn(c, option.psk, option.version) return snell.StreamConn(c, option.psk, option.version)
} }
// StreamConnContext implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err := s.writeHeaderContext(ctx, c, metadata) if metadata.NetWork == C.UDP {
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err return c, err
} }
func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP {
err = snell.WriteUDPHeader(c, s.version)
return
}
err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 { if s.version == snell.Version2 && len(opts) == 0 {
c, err := s.pool.Get() c, err := s.pool.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = s.writeHeaderContext(ctx, c, metadata); err != nil { port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
_ = c.Close() if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err return nil, err
} }
return NewConn(c, s), err return NewConn(c, s), err
} }
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err) return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = s.StreamConnContext(ctx, c, metadata) c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err return NewConn(c, s), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
var err error
if len(s.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
c, err = s.StreamConnContext(ctx, c, metadata) err = snell.WriteUDPHeader(c, s.version)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c) pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil return newPacketConn(pc, s), nil
} }
// SupportWithDialer implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (s *Snell) SupportWithDialer() C.NetWork { func (s *Snell) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return C.ALLNet pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@@ -148,13 +119,6 @@ func (s *Snell) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter
func (s *Snell) ProxyInfo() C.ProxyInfo {
info := s.Base.ProxyInfo()
info.DialerProxy = s.option.DialerProxy
return info
}
func NewSnell(option SnellOption) (*Snell, error) { func NewSnell(option SnellOption) (*Snell, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
psk := []byte(option.Psk) psk := []byte(option.Psk)
@@ -188,17 +152,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
s := &Snell{ s := &Snell{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Snell, tp: C.Snell,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option,
psk: psk, psk: psk,
obfsOption: obfsOption, obfsOption: obfsOption,
version: option.Version, version: option.Version,
@@ -206,20 +166,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 { if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) { s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
var err error c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
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 { if err != nil {
return nil, err return nil, err
} }
return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil tcpKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
}) })
} }
return s, nil return s, nil

View File

@@ -7,20 +7,15 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/ca" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/dialer" "github.com/Dreamacro/clash/transport/socks5"
"github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/socks5"
) )
type Socks5 struct { type Socks5 struct {
*Base *Base
option *Socks5Option
user string user string
pass string pass string
tls bool tls bool
@@ -38,14 +33,13 @@ type Socks5Option struct {
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
} }
// StreamConnContext implements C.ProxyAdapter // StreamConn implements C.ProxyAdapter
func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
err := cc.HandshakeContext(ctx) err := cc.Handshake()
c = cc c = cc
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
@@ -59,35 +53,23 @@ func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C
Password: ss.pass, Password: ss.pass,
} }
} }
if _, err := ss.clientHandshakeContext(ctx, c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.DialOptions()...), metadata) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
}
// 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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = ss.StreamConnContext(ctx, c, metadata) c, err = ss.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -95,21 +77,9 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
return NewConn(c, ss), nil return NewConn(c, ss), nil
} }
// SupportWithDialer implements C.ProxyAdapter
func (ss *Socks5) SupportWithDialer() C.NetWork {
return C.TCP
}
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var cDialer C.Dialer = dialer.NewDialer(ss.DialOptions()...) c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if len(ss.option.DialerProxy) > 0 {
cDialer, err = proxydialer.NewByName(ss.option.DialerProxy, cDialer)
if err != nil {
return nil, err
}
}
c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err) err = fmt.Errorf("%s connect error: %w", ss.addr, err)
return return
@@ -117,16 +87,13 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if ss.tls { if ss.tls {
cc := tls.Client(c, ss.tlsConfig) cc := tls.Client(c, ss.tlsConfig)
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout) err = cc.Handshake()
defer cancel()
err = cc.HandshakeContext(ctx)
c = cc c = cc
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
var user *socks5.User var user *socks5.User
if ss.user != "" { if ss.user != "" {
user = &socks5.User{ user = &socks5.User{
@@ -135,28 +102,13 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
} }
} }
udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
bindAddr, err := ss.clientHandshakeContext(ctx, c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
if err != nil { if err != nil {
err = fmt.Errorf("client hanshake error: %w", err) err = fmt.Errorf("client hanshake error: %w", err)
return return
} }
// Support unspecified UDP bind address. pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
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 { if err != nil {
return return
} }
@@ -169,58 +121,47 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
pc.Close() pc.Close()
}() }()
// Support unspecified UDP bind address.
bindUDPAddr := bindAddr.UDPAddr()
if bindUDPAddr == nil {
err = errors.New("invalid UDP bind address")
return
} else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
if err != nil {
return nil, err
}
bindUDPAddr.IP = serverAddr.IP
}
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
} }
// ProxyInfo implements C.ProxyAdapter func NewSocks5(option Socks5Option) *Socks5 {
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 var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: option.Server, ServerName: option.Server,
} }
var err error
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
}
} }
return &Socks5{ return &Socks5{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Socks5, tp: C.Socks5,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option,
user: option.UserName, user: option.UserName,
pass: option.Password, pass: option.Password,
tls: option.TLS, tls: option.TLS,
skipCertVerify: option.SkipCertVerify, skipCertVerify: option.SkipCertVerify,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
}, nil }
} }
type socksPacketConn struct { type socksPacketConn struct {

View File

@@ -1,208 +0,0 @@
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, fmt.Errorf("path is not subpath of home directory: %s", 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
}

View File

@@ -3,81 +3,53 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/ca" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/dialer" "github.com/Dreamacro/clash/transport/gun"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/Dreamacro/clash/transport/trojan"
tlsC "github.com/metacubex/mihomo/component/tls" "github.com/Dreamacro/clash/transport/vless"
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 { type Trojan struct {
*Base *Base
option *TrojanOption instance *trojan.Trojan
hexPassword [trojan.KeyLength]byte option *TrojanOption
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
ssCipher core.Cipher
} }
type TrojanOption struct { type TrojanOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Password string `proxy:"password"` Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` UDP bool `proxy:"udp,omitempty"`
UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"`
Network string `proxy:"network,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` Flow string `proxy:"flow,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` FlowShow bool `proxy:"flow-show,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 func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
type TrojanSSOption struct { if t.option.Network == "ws" {
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) host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
wsOpts := &vmess.WebsocketConfig{ Host: host,
Host: host, Port: port,
Port: port, Path: t.option.WSOpts.Path,
Path: t.option.WSOpts.Path,
MaxEarlyData: t.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint,
Headers: http.Header{},
} }
if t.option.SNI != "" { if t.option.SNI != "" {
@@ -85,126 +57,77 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
} }
if len(t.option.WSOpts.Headers) != 0 { if len(t.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range t.option.WSOpts.Headers { for key, value := range t.option.WSOpts.Headers {
wsOpts.Headers.Add(key, value) header.Add(key, value)
} }
wsOpts.Headers = header
} }
alpn := trojan.DefaultWebsocketALPN return t.instance.StreamWebsocketConn(c, wsOpts)
if len(t.option.ALPN) != 0 {
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.realityConfig)
default:
// default tcp network
// handle TLS
alpn := trojan.DefaultALPN
if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
}
c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{
Host: t.option.SNI,
SkipCertVerify: t.option.SkipCertVerify,
FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn,
Reality: t.realityConfig,
})
} }
return t.instance.StreamConn(c)
}
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.plainStream(c)
}
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
return t.streamConnContext(ctx, c, metadata) c, err = t.instance.PresetXTLSConn(c)
} if err != nil {
return nil, err
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 { if metadata.NetWork == C.UDP {
command = trojan.CommandUDP err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
} }
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata)) err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err 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 // DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if t.transport != nil { if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.streamConnContext(ctx, c, metadata) c, err = t.instance.PresetXTLSConn(c)
if err != nil { if err != nil {
c.Close()
return nil, err
}
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
c.Close()
return nil, err return nil, err
} }
return NewConn(c, t), nil return NewConn(c, t), nil
} }
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.DialOptions()...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
tcpKeepAlive(c)
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = t.StreamConnContext(ctx, c, metadata) c, err = t.StreamConn(c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -213,62 +136,41 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
var c net.Conn var c net.Conn
// grpc transport // grpc transport
if t.transport != nil { if t.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err) } else {
}(c) c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
c, err = t.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
defer safeConnClose(c, err)
pc := trojan.NewPacketConn(c) tcpKeepAlive(c)
return newPacketConn(pc, t), err c, err = t.plainStream(c)
}
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 { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
} }
} }
c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil { err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
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 { if err != nil {
return nil, err return nil, err
} }
pc := trojan.NewPacketConn(c) pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
// SupportWithDialer implements C.ProxyAdapter
func (t *Trojan) SupportWithDialer() C.NetWork {
return C.ALLNet
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := trojan.NewPacketConn(c) pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
@@ -277,101 +179,71 @@ func (t *Trojan) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter
func (t *Trojan) ProxyInfo() C.ProxyInfo {
info := t.Base.ProxyInfo()
info.DialerProxy = t.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (t *Trojan) Close() error {
if t.transport != nil {
return t.transport.Close()
}
return nil
}
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
if option.SNI == "" { tOption := &trojan.Option{
option.SNI = option.Server Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
FlowShow: option.FlowShow,
}
if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
tOption.Flow = option.Flow
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
}
if option.SNI != "" {
tOption.ServerName = option.SNI
} }
t := &Trojan{ t := &Trojan{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: addr, addr: addr,
tp: C.Trojan, tp: C.Trojan,
udp: option.UDP, udp: option.UDP,
tfo: option.TFO, iface: option.Interface,
mpTcp: option.MPTCP, rmark: option.RoutingMark,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, instance: trojan.New(tOption),
hexPassword: trojan.Key(option.Password), option: &option,
}
var err error
t.realityConfig, err = option.RealityOpts.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" { if option.Network == "grpc" {
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) {
var err error c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
tcpKeepAlive(c)
return c, nil return c, nil
} }
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
NextProtos: option.ALPN, NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
InsecureSkipVerify: option.SkipCertVerify, InsecureSkipVerify: tOption.SkipCertVerify,
ServerName: option.SNI, ServerName: tOption.ServerName,
} }
var err error if t.option.Flow != "" {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
if err != nil { } else {
return nil, err t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName, ServiceName: option.GrpcOpts.GrpcServiceName,
Host: option.SNI, Host: tOption.ServerName,
ClientFingerprint: option.ClientFingerprint,
} }
} }

View File

@@ -1,324 +0,0 @@
package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"math"
"net"
"strconv"
"time"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
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
}
type TuicOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Token string `proxy:"token,omitempty"`
UUID string `proxy:"uuid,omitempty"`
Password string `proxy:"password,omitempty"`
Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
RequestTimeout int `proxy:"request-timeout,omitempty"`
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
}
// DialContext implements C.ProxyAdapter
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata) (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 t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata
uotMetadata.Host = uotDestination.Fqdn
uotMetadata.DstPort = uotDestination.Port
c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata)
if err != nil {
return nil, err
}
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil
} else {
return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil
}
}
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
}
return newPacketConn(pc, t), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (t *Tuic) SupportWithDialer() C.NetWork {
return C.ALLNet
}
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) {
if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
if err != nil {
return nil, nil, err
}
}
udpAddr, err := resolveUDPAddr(ctx, "udp", t.addr, t.prefer)
if err != nil {
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, nil, err
}
transport = &quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
return
}
// 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
}
switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion:
case 0:
option.UDPOverStreamVersion = uot.LegacyVersion
default:
return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion)
}
t := &Tuic{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Tuic,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
}
clientMaxOpenStreams := int64(option.MaxOpenStreams)
// to avoid tuic's "too many open streams", decrease to 0.9x
if clientMaxOpenStreams == 100 {
clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0))
}
if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1
}
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsC.UConfig(tlsConfig),
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV4(clientOption)
} else {
maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsC.UConfig(tlsConfig),
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV5(clientOption)
}
return t, nil
}

View File

@@ -2,104 +2,80 @@ package outbound
import ( import (
"bytes" "bytes"
"context" "crypto/tls"
"fmt"
"net" "net"
"net/netip"
"regexp"
"strconv" "strconv"
"sync"
"time"
"github.com/metacubex/mihomo/component/resolver" "github.com/Dreamacro/clash/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/Dreamacro/clash/transport/socks5"
xtls "github.com/xtls/go"
) )
var (
globalClientSessionCache tls.ClientSessionCache
globalClientXSessionCache xtls.ClientSessionCache
once sync.Once
)
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func getClientXSessionCache() xtls.ClientSessionCache {
once.Do(func() {
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
})
return globalClientXSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() aType := uint8(metadata.AddrType)
p := uint(metadata.DstPort) p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch metadata.AddrType {
case C.AtypDomainName: case socks5.AtypDomainName:
lenM := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port} buf = [][]byte{{aType, lenM}, host, port}
case C.AtypIPv4: case socks5.AtypIPv4:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{socks5.AtypIPv4}, host, port} buf = [][]byte{{aType}, host, port}
case C.AtypIPv6: case socks5.AtypIPv6:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{socks5.AtypIPv6}, host, port} buf = [][]byte{{aType}, host, port}
} }
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
} }
func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) { func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
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)
}
ip, err := resolver.ResolveProxyServerHost(host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ip, port = resolver.LookupIP4P(ip, port)
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
} }
func safeConnClose(c net.Conn, err error) { func safeConnClose(c net.Conn, err error) {
if err != nil && c != nil { if err != nil {
_ = c.Close() _ = 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
}

View File

@@ -12,22 +12,13 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/metacubex/mihomo/common/convert" "github.com/Dreamacro/clash/common/convert"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/common/utils" "github.com/Dreamacro/clash/component/resolver"
"github.com/metacubex/mihomo/component/ca" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/dialer" "github.com/Dreamacro/clash/transport/gun"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/Dreamacro/clash/transport/vless"
"github.com/metacubex/mihomo/component/resolver" "github.com/Dreamacro/clash/transport/vmess"
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/vmess"
vmessSing "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/metacubex/sing/common/metadata"
) )
const ( const (
@@ -44,87 +35,70 @@ type Vless struct {
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
} }
type VlessOption struct { type VlessOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"` Flow string `proxy:"flow,omitempty"`
TLS bool `proxy:"tls,omitempty"` FlowShow bool `proxy:"flow-show,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"` Network string `proxy:"network,omitempty"`
XUDP bool `proxy:"xudp,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
Network string `proxy:"network,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` WSPath string `proxy:"ws-path,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` ServerName string `proxy:"servername,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) { func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{ wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
for key, value := range v.option.WSOpts.Headers { for key, value := range v.option.WSOpts.Headers {
wsOpts.Headers.Add(key, value) header.Add(key, value)
} }
wsOpts.Headers = header
} }
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) wsOpts.TLS = true
if err != nil { wsOpts.TLSConfig = &tls.Config{
return nil, err MinVersion: tls.VersionTLS12,
} ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
if v.option.ServerName != "" { NextProtos: []string{"http/1.1"},
wsOpts.TLSConfig.ServerName = v.option.ServerName }
} else if host := wsOpts.Headers.Get("Host"); host != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = v.option.ServerName
} } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
} else { } else {
if host := wsOpts.Headers.Get("Host"); host == "" { wsOpts.Headers.Set("Host", convert.RandHost())
wsOpts.Headers.Set("Host", convert.RandHost()) convert.SetUserAgent(wsOpts.Headers)
convert.SetUserAgent(wsOpts.Headers)
}
} }
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
c, err = v.streamTLSConn(ctx, c, false) c, err = v.streamTLSOrXTLSConn(c, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -139,7 +113,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = vmess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
c, err = v.streamTLSConn(ctx, c, true) c, err = v.streamTLSOrXTLSConn(c, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -149,65 +123,49 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = vmess.StreamH2Conn(ctx, c, h2Opts) c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) if v.isXTLSEnabled() {
c, err = gun.StreamGunWithXTLSConn(c, v.gunTLSConfig, v.gunConfig)
} else {
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
}
default: default:
// default tcp network // default tcp network
// handle TLS // handle TLS And XTLS
c, err = v.streamTLSConn(ctx, c, false) c, err = v.streamTLSOrXTLSConn(c, false)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return v.streamConnContext(ctx, c, metadata) return v.client.StreamConn(c, parseVlessAddr(metadata))
} }
func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
if ctx.Done() != nil { host, _, _ := net.SplitHostPort(v.addr)
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP {
if v.option.PacketAddr {
metadata = &C.Metadata{
NetWork: C.UDP,
Host: packetaddr.SeqPacketMagicAddress,
DstPort: 443,
}
} else {
metadata = &C.Metadata{ // a clear metadata only contains ip
NetWork: C.UDP,
DstIP: metadata.DstIP,
DstPort: metadata.DstPort,
}
}
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
if v.option.PacketAddr {
conn = packetaddr.NewBindConn(conn)
}
} else {
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false))
}
if err != nil {
conn = nil
}
return
}
func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) { if v.isXTLSEnabled() {
if v.option.TLS { xtlsOpts := vless.XTLSConfig{
host, _, _ := net.SplitHostPort(v.addr) Host: host,
SkipCertVerify: v.option.SkipCertVerify,
}
if isH2 {
xtlsOpts.NextProtos = []string{"h2"}
}
if v.option.ServerName != "" {
xtlsOpts.Host = v.option.ServerName
}
return vless.StreamXTLSConn(conn, &xtlsOpts)
} else if v.option.TLS {
tlsOpts := vmess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if isH2 { if isH2 {
@@ -218,157 +176,87 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
return vmess.StreamTLSConn(ctx, conn, &tlsOpts) return vmess.StreamTLSConn(conn, &tlsOpts)
} }
return conn, nil return conn, nil
} }
func (v *Vless) isXTLSEnabled() bool {
return v.client.Addons != nil
}
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
defer func(c net.Conn) { tcpKeepAlive(c)
safeConnClose(c, err) defer safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata) c, err = v.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
return NewConn(c, v), err return NewConn(c, v), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr // vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata) c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
defer safeConnClose(c, err)
return v.ListenPacketOnStreamConn(ctx, c, metadata) c, err = v.StreamConn(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
}
} }
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
return v.ListenPacketOnStreamConn(ctx, c, metadata) return v.ListenPacketOnStreamConn(c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter
func (v *Vless) SupportWithDialer() C.NetWork {
return C.ALLNet
} }
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
return newPacketConn(N.NewThreadSafePacketConn(
vmessSing.NewXUDPConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
} else if v.option.PacketAddr {
return newPacketConn(N.NewThreadSafePacketConn(
packetaddr.NewConn(&vlessPacketConn{
Conn: c, rAddr: metadata.UDPAddr(),
}, M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
}
return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@@ -376,25 +264,10 @@ func (v *Vless) SupportUOT() bool {
return true return true
} }
// ProxyInfo implements C.ProxyAdapter func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
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 addrType byte
var addr []byte var addr []byte
switch metadata.AddrType() { switch metadata.AddrType {
case C.AtypIPv4: case C.AtypIPv4:
addrType = vless.AtypIPv4 addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
@@ -410,12 +283,12 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
copy(addr[1:], metadata.Host) copy(addr[1:], metadata.Host)
} }
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{ return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP, UDP: metadata.NetWork == C.UDP,
AddrType: addrType, AddrType: addrType,
Addr: addr, Addr: addr,
Port: metadata.DstPort, Port: uint(port),
Mux: metadata.NetWork == C.UDP && xudp,
} }
} }
@@ -515,103 +388,70 @@ func NewVless(option VlessOption) (*Vless, error) {
var addons *vless.Addons var addons *vless.Addons
if option.Network != "ws" && len(option.Flow) >= 16 { if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16] option.Flow = option.Flow[:16]
if option.Flow != vless.XRV { switch option.Flow {
case vless.XRO, vless.XRD, vless.XRS:
addons = &vless.Addons{
Flow: option.Flow,
}
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
} }
addons = &vless.Addons{
Flow: option.Flow,
}
} }
switch option.PacketEncoding { client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
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 { if err != nil {
return nil, err return nil, err
} }
v := &Vless{ v := &Vless{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless, tp: C.Vless,
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, iface: option.Interface,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
} }
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) {
var err error c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
return c, nil return c, nil
} }
gunConfig := &gun.Config{ gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName, ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
} }
if option.ServerName == "" { tlsConfig := &tls.Config{
gunConfig.Host = v.addr InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
} }
var tlsConfig *tls.Config
if option.TLS { if v.option.ServerName == "" {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ host, _, _ := net.SplitHostPort(v.addr)
InsecureSkipVerify: v.option.SkipCertVerify, tlsConfig.ServerName = host
ServerName: v.option.ServerName, gunConfig.Host = host
}, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
}
} }
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
if v.isXTLSEnabled() {
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) v.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else {
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
} }
return v, nil return v, nil

View File

@@ -9,27 +9,15 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"sync"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/common/convert"
"github.com/metacubex/mihomo/common/utils" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/ca" "github.com/Dreamacro/clash/component/resolver"
"github.com/metacubex/mihomo/component/dialer" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/Dreamacro/clash/transport/gun"
"github.com/metacubex/mihomo/component/resolver" "github.com/Dreamacro/clash/transport/vmess"
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 { type Vmess struct {
*Base *Base
client *vmess.Client client *vmess.Client
@@ -39,36 +27,29 @@ type Vmess struct {
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
gunConfig *gun.Config gunConfig *gun.Config
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
} }
type VmessOption struct { type VmessOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
UUID string `proxy:"uuid"` UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"` AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"` Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"` TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` ServerName string `proxy:"servername,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
ServerName string `proxy:"servername,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` // TODO: compatible with VMESS WS older version configurations
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"` WSPath string `proxy:"ws-path,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 { type HTTPOptions struct {
@@ -87,29 +68,26 @@ type GrpcOptions struct {
} }
type WSOptions struct { type WSOptions struct {
Path string `proxy:"path,omitempty"` Path string `proxy:"path,omitempty"`
Headers map[string]string `proxy:"headers,omitempty"` Headers map[string]string `proxy:"headers,omitempty"`
MaxEarlyData int `proxy:"max-early-data,omitempty"` MaxEarlyData int `proxy:"max-early-data,omitempty"`
EarlyDataHeaderName string `proxy:"early-data-header-name,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 // StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &mihomoVMess.WebsocketConfig{ wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: v.option.WSOpts.Path, Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData, MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, Headers: make(http.Header),
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
} }
if len(v.option.WSOpts.Headers) != 0 { if len(v.option.WSOpts.Headers) != 0 {
@@ -120,305 +98,171 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
if v.option.TLS { if v.option.TLS {
wsOpts.TLS = true wsOpts.TLS = true
tlsConfig := &tls.Config{ wsOpts.TLSConfig = &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"}, NextProtos: []string{"http/1.1"},
} }
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
if v.option.ServerName != "" { if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" { } else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host wsOpts.TLSConfig.ServerName = host
} }
} else {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
} }
c, err = mihomoVMess.StreamWebsocketConn(ctx, c, wsOpts) c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http": case "http":
// readability first, so just copy default TLS logic // readability first, so just copy default TLS logic
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts)
c, err = vmess.StreamTLSConn(c, tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
http.Header(v.option.HTTPOpts.Headers).Set("Host", convert.RandHost())
convert.SetUserAgent(v.option.HTTPOpts.Headers)
} }
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
httpOpts := &mihomoVMess.HTTPConfig{ httpOpts := &vmess.HTTPConfig{
Host: host, Host: host,
Method: v.option.HTTPOpts.Method, Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path, Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers, Headers: v.option.HTTPOpts.Headers,
} }
c = mihomoVMess.StreamHTTPConn(c, httpOpts) c = vmess.StreamHTTPConn(c, httpOpts)
case "h2": case "h2":
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := mihomoVMess.TLSConfig{ tlsOpts := vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, NextProtos: []string{"h2"},
NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = mihomoVMess.StreamTLSConn(ctx, c, &tlsOpts) c, err = vmess.StreamTLSConn(c, &tlsOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
h2Opts := &mihomoVMess.H2Config{ h2Opts := &vmess.H2Config{
Hosts: v.option.HTTP2Opts.Host, Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &mihomoVMess.TLSConfig{ tlsOpts := &vmess.TLSConfig{
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
} }
if v.option.ServerName != "" { if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName tlsOpts.Host = v.option.ServerName
} }
c, err = mihomoVMess.StreamTLSConn(ctx, c, tlsOpts) c, err = vmess.StreamTLSConn(c, tlsOpts)
} }
} }
if err != nil { if err != nil {
return nil, err 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) { return v.client.StreamConn(c, parseVmessAddr(metadata))
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 // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(c, v), nil return NewConn(c, v), nil
} }
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
defer func(c net.Conn) { tcpKeepAlive(c)
safeConnClose(c, err) defer safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata) c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err return NewConn(c, v), err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() { if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host) ip, err := resolver.ResolveIP(metadata.Host)
if err != nil { if err != nil {
return nil, errors.New("can't resolve ip") return nil, errors.New("can't resolve ip")
} }
metadata.DstIP = ip metadata.DstIP = ip
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil { if v.transport != nil && len(opts) == 0 {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(c net.Conn) { defer safeConnClose(c, err)
safeConnClose(c, err)
}(c)
c, err = v.streamConnContext(ctx, c, metadata) c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
} else {
c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
return v.ListenPacketOnStreamConn(ctx, c, metadata) tcpKeepAlive(c)
} defer safeConnClose(c, err)
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.DialOptions()...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter c, err = v.StreamConn(c, metadata)
func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(v.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
} }
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
return v.ListenPacketOnStreamConn(ctx, c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter return v.ListenPacketOnStreamConn(c, metadata)
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 // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
}
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
} }
@@ -429,121 +273,110 @@ func (v *Vmess) SupportUOT() bool {
func NewVmess(option VmessOption) (*Vmess, error) { func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher) security := strings.ToLower(option.Cipher)
var options []vmess.ClientOption client, err := vmess.NewClient(vmess.Config{
if option.GlobalPadding { UUID: option.UUID,
options = append(options, vmess.ClientWithGlobalPadding()) AlterID: uint16(option.AlterID),
} Security: security,
if option.AuthenticatedLength { HostName: option.Server,
options = append(options, vmess.ClientWithAuthenticatedLength()) Port: strconv.Itoa(option.Port),
} IsAead: option.AlterID == 0,
options = append(options, vmess.ClientWithTimeFunc(ntp.Now)) })
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
switch option.PacketEncoding { switch option.Network {
case "packetaddr", "packet": case "h2", "grpc":
option.PacketAddr = true if !option.TLS {
case "xudp": return nil, fmt.Errorf("TLS must be true with h2/grpc network")
option.XUDP = true }
}
if option.XUDP {
option.PacketAddr = false
} }
v := &Vmess{ v := &Vmess{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess, tp: C.Vmess,
udp: option.UDP, udp: option.UDP,
xudp: option.XUDP, iface: option.Interface,
tfo: option.TFO, rmark: option.RoutingMark,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
}, },
client: client, client: client,
option: &option, option: &option,
} }
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) { dialFn := func(network, addr string) (net.Conn, error) {
var err error c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
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 { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
} }
tcpKeepAlive(c)
return c, nil return c, nil
} }
gunConfig := &gun.Config{ gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName, ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName, Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
} }
if option.ServerName == "" { tlsConfig := &tls.Config{
gunConfig.Host = v.addr InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
} }
var tlsConfig *tls.Config
if option.TLS { if v.option.ServerName == "" {
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ host, _, _ := net.SplitHostPort(v.addr)
InsecureSkipVerify: v.option.SkipCertVerify, tlsConfig.ServerName = host
ServerName: v.option.ServerName, gunConfig.Host = host
}, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
}
} }
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
} }
return v, nil return v, nil
} }
type vmessPacketConn struct { func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
net.Conn var addrType byte
rAddr net.Addr var addr []byte
access sync.Mutex switch metadata.AddrType {
case C.AtypIPv4:
addrType = byte(vmess.AtypIPv4)
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypIPv6:
addrType = byte(vmess.AtypIPv6)
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice())
case C.AtypDomainName:
addrType = byte(vmess.AtypDomainName)
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], []byte(metadata.Host))
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vmess.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: uint(port),
}
}
type vmessPacketConn struct {
net.Conn
rAddr net.Addr
} }
// WriteTo implments C.PacketConn.WriteTo
// Since VMess doesn't support full cone NAT by design, we verify if addr matches uc.rAddr, and drop the packet if not.
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
allowedAddr := uc.rAddr
destAddr := addr
if allowedAddr.String() != destAddr.String() {
return 0, ErrUDPRemoteAddrMismatch
}
uc.access.Lock()
defer uc.access.Unlock()
return uc.Conn.Write(b) return uc.Conn.Write(b)
} }

View File

@@ -1,547 +0,0 @@
package outbound
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"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"`
JMin int `proxy:"jmin"`
JMax int `proxy:"jmax"`
S1 int `proxy:"s1"`
S2 int `proxy:"s2"`
H1 uint32 `proxy:"h1"`
H2 uint32 `proxy:"h2"`
H3 uint32 `proxy:"h3"`
H4 uint32 `proxy:"h4"`
}
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 {
ipcConf += "jc=" + strconv.Itoa(w.option.AmneziaWGOption.JC) + "\n"
ipcConf += "jmin=" + strconv.Itoa(w.option.AmneziaWGOption.JMin) + "\n"
ipcConf += "jmax=" + strconv.Itoa(w.option.AmneziaWGOption.JMax) + "\n"
ipcConf += "s1=" + strconv.Itoa(w.option.AmneziaWGOption.S1) + "\n"
ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n"
ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n"
ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n"
ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n"
ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n"
}
}
if 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 (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
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
}
// IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true
}

View File

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

View File

@@ -2,220 +2,107 @@ package outboundgroup
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/metacubex/mihomo/common/atomic" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/common/utils" types "github.com/Dreamacro/clash/constant/provider"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/constant/provider"
"github.com/metacubex/mihomo/constant/provider" "github.com/Dreamacro/clash/log"
types "github.com/metacubex/mihomo/constant/provider" "github.com/Dreamacro/clash/tunnel"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
"github.com/dlclark/regexp2" "github.com/dlclark/regexp2"
"golang.org/x/exp/slices" "go.uber.org/atomic"
) )
type GroupBase struct { type GroupBase struct {
*outbound.Base *outbound.Base
filterRegs []*regexp2.Regexp filter *regexp2.Regexp
excludeFilterRegs []*regexp2.Regexp providers []provider.ProxyProvider
excludeTypeArray []string versions sync.Map // map[string]uint
providers []provider.ProxyProvider proxies sync.Map // map[string][]C.Proxy
failedTestMux sync.Mutex failedTestMux sync.Mutex
failedTimes int failedTimes int
failedTime time.Time failedTime time.Time
failedTesting atomic.Bool failedTesting *atomic.Bool
TestTimeout int
maxFailedTimes int
// for GetProxies
getProxiesMutex sync.Mutex
providerVersions []uint32
providerProxies []C.Proxy
} }
type GroupBaseOption struct { type GroupBaseOption struct {
Name string outbound.BaseOption
Type C.AdapterType filter string
Filter string providers []provider.ProxyProvider
ExcludeFilter string
ExcludeType string
TestTimeout int
MaxFailedTimes int
Providers []provider.ProxyProvider
} }
func NewGroupBase(opt GroupBaseOption) *GroupBase { func NewGroupBase(opt GroupBaseOption) *GroupBase {
var excludeTypeArray []string var filter *regexp2.Regexp = nil
if opt.ExcludeType != "" { if opt.filter != "" {
excludeTypeArray = strings.Split(opt.ExcludeType, "|") filter = regexp2.MustCompile(opt.filter, 0)
} }
return &GroupBase{
var excludeFilterRegs []*regexp2.Regexp Base: outbound.NewBase(opt.BaseOption),
if opt.ExcludeFilter != "" { filter: filter,
for _, excludeFilter := range strings.Split(opt.ExcludeFilter, "`") { providers: opt.providers,
excludeFilterReg := regexp2.MustCompile(excludeFilter, regexp2.None) failedTesting: atomic.NewBool(false),
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 { func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
providerVersions := make([]uint32, len(gb.providers)) if gb.filter == nil {
for i, pd := range gb.providers { var proxies []C.Proxy
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 { for _, pd := range gb.providers {
if touch {
pd.Touch()
}
proxies = append(proxies, pd.Proxies()...) proxies = append(proxies, pd.Proxies()...)
} }
} else { if len(proxies) == 0 {
for _, pd := range gb.providers { return append(proxies, tunnel.Proxies()["COMPATIBLE"])
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...)
} }
return proxies
} }
// Multiple filers means that proxies are sorted in the order in which the filers appear. for _, pd := range gb.providers {
// Although the filter has been performed once in the previous process, if touch {
// when there are multiple providers, the array needs to be reordered as a whole. pd.Touch()
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 { }
var newProxies []C.Proxy
proxiesSet := map[string]struct{}{} if pd.VehicleType() == types.Compatible {
for _, filterReg := range gb.filterRegs { gb.proxies.Store(pd.Name(), pd.Proxies())
gb.versions.Store(pd.Name(), pd.Version())
continue
}
if version, ok := gb.versions.Load(pd.Name()); !ok || version != pd.Version() {
var (
proxies []C.Proxy
newProxies []C.Proxy
)
proxies = pd.Proxies()
for _, p := range proxies { for _, p := range proxies {
name := p.Name() if mat, _ := gb.filter.FindStringMatch(p.Name()); mat != nil {
if mat, _ := filterReg.MatchString(name); mat { newProxies = append(newProxies, p)
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 { gb.proxies.Store(pd.Name(), newProxies)
var newProxies []C.Proxy gb.versions.Store(pd.Name(), pd.Version())
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
} }
var proxies []C.Proxy
if gb.excludeTypeArray != nil { gb.proxies.Range(func(key, value any) bool {
var newProxies []C.Proxy proxies = append(proxies, value.([]C.Proxy)...)
LOOP2: return true
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 { if len(proxies) == 0 {
return []C.Proxy{tunnel.Proxies()["COMPATIBLE"]} return append(proxies, tunnel.Proxies()["COMPATIBLE"])
} }
// only cache when proxies not empty
gb.providerVersions = providerVersions
gb.providerProxies = proxies
return proxies return proxies
} }
func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) { func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
var wg sync.WaitGroup var wg sync.WaitGroup
var lock sync.Mutex var lock sync.Mutex
mp := map[string]uint16{} mp := map[string]uint16{}
@@ -224,12 +111,12 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus uti
proxy := proxy proxy := proxy
wg.Add(1) wg.Add(1)
go func() { go func() {
delay, err := proxy.URLTest(ctx, url, expectedStatus) delay, err := proxy.URLTest(ctx, url)
lock.Lock()
if err == nil { if err == nil {
lock.Lock()
mp[proxy.Name()] = delay mp[proxy.Name()] = delay
lock.Unlock()
} }
lock.Unlock()
wg.Done() wg.Done()
}() }()
@@ -243,21 +130,12 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus uti
} }
} }
func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func()) { func (gb *GroupBase) onDialFailed() {
if adapterType == C.Direct || adapterType == C.Compatible || adapterType == C.Reject || adapterType == C.Pass || adapterType == C.RejectDrop { if gb.failedTesting.Load() {
return
}
if errors.Is(err, C.ErrNotSupport) {
return return
} }
go func() { go func() {
if strings.Contains(err.Error(), "connection refused") {
fn()
return
}
gb.failedTestMux.Lock() gb.failedTestMux.Lock()
defer gb.failedTestMux.Unlock() defer gb.failedTestMux.Unlock()
@@ -266,39 +144,34 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func(
log.Debugln("ProxyGroup: %s first failed", gb.Name()) log.Debugln("ProxyGroup: %s first failed", gb.Name())
gb.failedTime = time.Now() gb.failedTime = time.Now()
} else { } else {
if time.Since(gb.failedTime) > time.Duration(gb.TestTimeout)*time.Millisecond { if time.Since(gb.failedTime) > gb.failedTimeoutInterval() {
gb.failedTimes = 0
return return
} }
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
if gb.failedTimes >= gb.maxFailedTimes { if gb.failedTimes >= gb.maxFailedTimes() {
gb.failedTesting.Store(true)
log.Warnln("because %s failed multiple times, active health check", gb.Name()) log.Warnln("because %s failed multiple times, active health check", gb.Name())
fn() 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) healthCheck() { func (gb *GroupBase) failedIntervalTime() int64 {
if gb.failedTesting.Load() { return 5 * time.Second.Milliseconds()
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() { func (gb *GroupBase) onDialSuccess() {
@@ -306,3 +179,11 @@ func (gb *GroupBase) onDialSuccess() {
gb.failedTimes = 0 gb.failedTimes = 0
} }
} }
func (gb *GroupBase) maxFailedTimes() int {
return 5
}
func (gb *GroupBase) failedTimeoutInterval() time.Duration {
return 5 * time.Second
}

View File

@@ -6,36 +6,32 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"sync"
"time" "time"
"github.com/metacubex/mihomo/common/callback" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/metacubex/mihomo/common/lru" "github.com/Dreamacro/clash/common/cache"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/common/murmur3"
"github.com/metacubex/mihomo/common/utils" "github.com/Dreamacro/clash/component/dialer"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/constant/provider" "github.com/Dreamacro/clash/constant/provider"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
type LoadBalance struct { type LoadBalance struct {
*GroupBase *GroupBase
disableUDP bool disableUDP bool
strategyFn strategyFn strategyFn strategyFn
testUrl string
expectedStatus string
Hidden bool
Icon string
} }
var errStrategy = errors.New("unsupported strategy") var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string { func parseStrategy(config map[string]any) string {
if strategy, ok := config["strategy"].(string); ok { if elm, ok := config["strategy"]; ok {
return strategy if strategy, ok := elm.(string); ok {
return strategy
}
} }
return "consistent-hashing" return "consistent-hashing"
} }
@@ -86,39 +82,32 @@ func jumpHash(key uint64, buckets int32) int32 {
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := lb.Unwrap(metadata, true) defer func() {
c, err = proxy.DialContext(ctx, metadata) if err == nil {
c.AppendToChains(lb)
lb.onDialSuccess()
} else {
lb.onDialFailed()
}
}()
if err == nil { proxy := lb.Unwrap(metadata)
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)
}
})
}
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
return return
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (pc C.PacketConn, err error) { func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (pc C.PacketConn, err error) {
defer func() { defer func() {
if err == nil { if err == nil {
pc.AppendToChains(lb) pc.AppendToChains(lb)
} }
}() }()
proxy := lb.Unwrap(metadata, true) proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata) return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
} }
// SupportUDP implements C.ProxyAdapter // SupportUDP implements C.ProxyAdapter
@@ -126,32 +115,14 @@ func (lb *LoadBalance) SupportUDP() bool {
return !lb.disableUDP return !lb.disableUDP
} }
// IsL3Protocol implements C.ProxyAdapter func strategyRoundRobin() strategyFn {
func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool {
return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
}
func strategyRoundRobin(url string) strategyFn {
idx := 0 idx := 0
idxMutex := sync.Mutex{} return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
idxMutex.Lock()
defer idxMutex.Unlock()
i := 0
length := len(proxies) length := len(proxies)
for i := 0; i < length; i++ {
if touch { idx = (idx + 1) % length
defer func() { proxy := proxies[idx]
idx = (idx + i) % length if proxy.Alive() {
}()
}
for ; i < length; i++ {
id := (idx + i) % length
proxy := proxies[id]
if proxy.AliveForTestUrl(url) {
i++
return proxy return proxy
} }
} }
@@ -160,22 +131,15 @@ func strategyRoundRobin(url string) strategyFn {
} }
} }
func strategyConsistentHashing(url string) strategyFn { func strategyConsistentHashing() strategyFn {
maxRetry := 5 maxRetry := 5
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := utils.MapHash(getKey(metadata)) key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(proxies)) buckets := int32(len(proxies))
for i := 0; i < maxRetry; i, key = i+1, key+1 { for i := 0; i < maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets) idx := jumpHash(key, buckets)
proxy := proxies[idx] proxy := proxies[idx]
if proxy.AliveForTestUrl(url) { if proxy.Alive() {
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 proxy
} }
} }
@@ -184,14 +148,14 @@ func strategyConsistentHashing(url string) strategyFn {
} }
} }
func strategyStickySessions(url string) strategyFn { func strategyStickySessions() strategyFn {
ttl := time.Minute * 10 ttl := time.Minute * 10
maxRetry := 5 maxRetry := 5
lruCache := lru.New[uint64, int]( lruCache := cache.NewLRUCache[uint64, int](
lru.WithAge[uint64, int](int64(ttl.Seconds())), cache.WithAge[uint64, int](int64(ttl.Seconds())),
lru.WithSize[uint64, int](1000)) cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy { return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := utils.MapHash(getKeyWithSrcAndDst(metadata)) key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
length := len(proxies) length := len(proxies)
idx, has := lruCache.Get(key) idx, has := lruCache.Get(key)
if !has { if !has {
@@ -201,8 +165,9 @@ func strategyStickySessions(url string) strategyFn {
nowIdx := idx nowIdx := idx
for i := 1; i < maxRetry; i++ { for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx] proxy := proxies[nowIdx]
if proxy.AliveForTestUrl(url) { if proxy.Alive() {
if !has || nowIdx != idx { if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx) lruCache.Set(key, nowIdx)
} }
@@ -212,15 +177,16 @@ func strategyStickySessions(url string) strategyFn {
} }
} }
lruCache.Delete(key)
lruCache.Set(key, 0) lruCache.Set(key, 0)
return proxies[0] return proxies[0]
} }
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.GetProxies(touch) proxies := lb.GetProxies(true)
return lb.strategyFn(proxies, metadata, touch) return lb.strategyFn(proxies, metadata)
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
@@ -230,12 +196,8 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"type": lb.Type().String(), "type": lb.Type().String(),
"all": all, "all": all,
"testUrl": lb.testUrl,
"expectedStatus": lb.expectedStatus,
"hidden": lb.Hidden,
"icon": lb.Icon,
}) })
} }
@@ -243,30 +205,26 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
var strategyFn strategyFn var strategyFn strategyFn
switch strategy { switch strategy {
case "consistent-hashing": case "consistent-hashing":
strategyFn = strategyConsistentHashing(option.URL) strategyFn = strategyConsistentHashing()
case "round-robin": case "round-robin":
strategyFn = strategyRoundRobin(option.URL) strategyFn = strategyRoundRobin()
case "sticky-sessions": case "sticky-sessions":
strategyFn = strategyStickySessions(option.URL) strategyFn = strategyStickySessions()
default: default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy) return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
} }
return &LoadBalance{ return &LoadBalance{
GroupBase: NewGroupBase(GroupBaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.LoadBalance, Name: option.Name,
Filter: option.Filter, Type: C.LoadBalance,
ExcludeFilter: option.ExcludeFilter, Interface: option.Interface,
ExcludeType: option.ExcludeType, RoutingMark: option.RoutingMark,
TestTimeout: option.TestTimeout, },
MaxFailedTimes: option.MaxFailedTimes, option.Filter,
Providers: providers, providers,
}), }),
strategyFn: strategyFn, strategyFn: strategyFn,
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
Hidden: option.Hidden,
Icon: option.Icon,
}, nil }, nil
} }

View File

@@ -3,52 +3,36 @@ package outboundgroup
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/dlclark/regexp2" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/metacubex/mihomo/adapter/provider" "github.com/Dreamacro/clash/common/structure"
"github.com/metacubex/mihomo/common/structure" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/common/utils" types "github.com/Dreamacro/clash/constant/provider"
C "github.com/metacubex/mihomo/constant"
types "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log"
) )
var ( var (
errFormat = errors.New("format error") errFormat = errors.New("format error")
errType = errors.New("unsupported type") errType = errors.New("unsupport type")
errMissProxy = errors.New("`use` or `proxies` missing") errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("duplicate provider name") errDuplicateProvider = errors.New("duplicate provider name")
) )
type GroupCommonOption struct { type GroupCommonOption struct {
Name string `group:"name"` outbound.BasicOption
Type string `group:"type"` Name string `group:"name"`
Proxies []string `group:"proxies,omitempty"` Type string `group:"type"`
Use []string `group:"use,omitempty"` Proxies []string `group:"proxies,omitempty"`
URL string `group:"url,omitempty"` Use []string `group:"use,omitempty"`
Interval int `group:"interval,omitempty"` URL string `group:"url,omitempty"`
TestTimeout int `group:"timeout,omitempty"` Interval int `group:"interval,omitempty"`
MaxFailedTimes int `group:"max-failed-times,omitempty"` Lazy bool `group:"lazy,omitempty"`
Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` Filter string `group:"filter,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
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) { func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{ groupOption := &GroupCommonOption{
@@ -62,115 +46,62 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
return nil, errFormat 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 groupName := groupOption.Name
providers := []types.ProxyProvider{} 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 { if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy) return nil, 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 { if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies) ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err) return nil, err
} }
if _, ok := providersMap[groupName]; ok { if _, ok := providersMap[groupName]; ok {
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider) return nil, errDuplicateProvider
} }
if groupOption.URL == "" { // select don't need health check
groupOption.URL = C.DefaultTestURL if groupOption.Type == "select" || groupOption.Type == "relay" {
} hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} else {
if groupOption.URL == "" {
groupOption.URL = "http://www.gstatic.com/generate_204"
}
// select don't need auto health check
if groupOption.Type != "select" && groupOption.Type != "relay" {
if groupOption.Interval == 0 { if groupOption.Interval == 0 {
groupOption.Interval = 300 groupOption.Interval = 300
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
providers = append(providers, pd)
providersMap[groupName] = pd
} }
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.TestTimeout), uint(groupOption.Interval), groupOption.Lazy, expectedStatus) if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err) return nil, err
} }
providers = append(providers, list...)
providers = append([]types.ProxyProvider{pd}, providers...) } else {
providersMap[groupName] = pd groupOption.Filter = ""
} }
var group C.ProxyAdapter var group C.ProxyAdapter
@@ -221,13 +152,3 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
} }
return ps, nil 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)
}
}

View File

@@ -1,64 +0,0 @@
//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)
}

View File

@@ -3,41 +3,59 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/metacubex/mihomo/component/dialer" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" C "github.com/Dreamacro/clash/constant"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/constant/provider"
"github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log"
) )
type Relay struct { type Relay struct {
*GroupBase *GroupBase
Hidden bool
Icon string
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
proxies, chainProxies := r.proxies(metadata, true) proxies, chainProxies := r.proxies(metadata, true)
switch len(proxies) { switch len(proxies) {
case 0: case 0:
return outbound.NewDirect().DialContext(ctx, metadata) return outbound.NewDirect().DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1: case 1:
return proxies[0].DialContext(ctx, metadata) return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var d C.Dialer
d = dialer.NewDialer()
for _, proxy := range proxies[:len(proxies)-1] {
d = proxydialer.New(proxy, d, false)
} }
first := proxies[0]
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
conn, err := last.DialContextWithDialer(ctx, d, metadata)
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
tcpKeepAlive(c)
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
}
c, err = last.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
conn := outbound.NewConn(c, last)
for i := len(chainProxies) - 2; i >= 0; i-- { for i := len(chainProxies) - 2; i >= 0; i-- {
conn.AppendToChains(chainProxies[i]) conn.AppendToChains(chainProxies[i])
@@ -49,25 +67,49 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
proxies, chainProxies := r.proxies(metadata, true) proxies, chainProxies := r.proxies(metadata, true)
switch len(proxies) { switch len(proxies) {
case 0: case 0:
return outbound.NewDirect().ListenPacketContext(ctx, metadata) return outbound.NewDirect().ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
case 1: case 1:
return proxies[0].ListenPacketContext(ctx, metadata) return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
} }
var d C.Dialer first := proxies[0]
d = dialer.NewDialer()
for _, proxy := range proxies[:len(proxies)-1] {
d = proxydialer.New(proxy, d, false)
}
last := proxies[len(proxies)-1] last := proxies[len(proxies)-1]
pc, err := last.ListenPacketWithDialer(ctx, d, metadata)
c, err := dialer.DialContext(ctx, "tcp", first.Addr(), r.Base.DialOptions(opts...)...)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
tcpKeepAlive(c)
var currentMeta *C.Metadata
for _, proxy := range proxies[1:] {
currentMeta, err = addrToMetadata(proxy.Addr())
if err != nil {
return nil, err
}
c, err = first.StreamConn(c, currentMeta)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
}
first = proxy
}
c, err = last.StreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
}
var pc C.PacketConn
pc, err = last.ListenPacketOnStreamConn(c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
} }
for i := len(chainProxies) - 2; i >= 0; i-- { for i := len(chainProxies) - 2; i >= 0; i-- {
@@ -85,22 +127,8 @@ func (r *Relay) SupportUDP() bool {
if len(proxies) == 0 { // C.Direct if len(proxies) == 0 { // C.Direct
return true return true
} }
for i := len(proxies) - 1; i >= 0; i-- { last := proxies[len(proxies)-1]
proxy := proxies[i] return last.SupportUDP() && last.SupportUOT()
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 // MarshalJSON implements C.ProxyAdapter
@@ -110,10 +138,8 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"type": r.Type().String(), "type": r.Type().String(),
"all": all, "all": all,
"hidden": r.Hidden,
"icon": r.Icon,
}) })
} }
@@ -127,11 +153,11 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy)
for n, proxy := range rawProxies { for n, proxy := range rawProxies {
proxies = append(proxies, proxy) proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy) chainProxies = append(chainProxies, proxy)
subproxy := proxy.Unwrap(metadata, touch) subproxy := proxy.Unwrap(metadata)
for subproxy != nil { for subproxy != nil {
chainProxies = append(chainProxies, subproxy) chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata, touch) subproxy = subproxy.Unwrap(metadata)
} }
} }
@@ -145,19 +171,21 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy)
} }
func (r *Relay) Addr() string { func (r *Relay) Addr() string {
proxies, _ := r.proxies(nil, false) proxies, _ := r.proxies(nil, true)
return proxies[len(proxies)-1].Addr() return proxies[len(proxies)-1].Addr()
} }
func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Relay { 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{ return &Relay{
GroupBase: NewGroupBase(GroupBaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.Relay, Name: option.Name,
Providers: providers, Type: C.Relay,
Interface: option.Interface,
RoutingMark: option.RoutingMark,
},
"",
providers,
}), }),
Hidden: option.Hidden,
Icon: option.Icon,
} }
} }

View File

@@ -5,22 +5,21 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/metacubex/mihomo/constant/provider" "github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
) )
type Selector struct { type Selector struct {
*GroupBase *GroupBase
disableUDP bool disableUDP bool
selected string selected string
testUrl string
Hidden bool
Icon string
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
c, err := s.selectedProxy(true).DialContext(ctx, metadata) c, err := s.selectedProxy(true).DialContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
c.AppendToChains(s) c.AppendToChains(s)
} }
@@ -28,8 +27,8 @@ func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (s *Selector) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata) pc, err := s.selectedProxy(true).ListenPacketContext(ctx, metadata, s.Base.DialOptions(opts...)...)
if err == nil { if err == nil {
pc.AppendToChains(s) pc.AppendToChains(s)
} }
@@ -45,31 +44,17 @@ func (s *Selector) SupportUDP() bool {
return s.selectedProxy(false).SupportUDP() 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 // MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) { func (s *Selector) MarshalJSON() ([]byte, error) {
all := []string{} all := []string{}
for _, proxy := range s.GetProxies(false) { for _, proxy := range s.GetProxies(false) {
all = append(all, proxy.Name()) 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{ return json.Marshal(map[string]any{
"type": s.Type().String(), "type": s.Type().String(),
"now": s.Now(), "now": s.Now(),
"all": all, "all": all,
"testUrl": url,
"hidden": s.Hidden,
"icon": s.Icon,
}) })
} }
@@ -88,13 +73,9 @@ func (s *Selector) Set(name string) error {
return errors.New("proxy not exist") return errors.New("proxy not exist")
} }
func (s *Selector) ForceSet(name string) {
s.selected = name
}
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
return s.selectedProxy(touch) return s.selectedProxy(true)
} }
func (s *Selector) selectedProxy(touch bool) C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
@@ -111,19 +92,16 @@ func (s *Selector) selectedProxy(touch bool) C.Proxy {
func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector { func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
return &Selector{ return &Selector{
GroupBase: NewGroupBase(GroupBaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.Selector, Name: option.Name,
Filter: option.Filter, Type: C.Selector,
ExcludeFilter: option.ExcludeFilter, Interface: option.Interface,
ExcludeType: option.ExcludeType, RoutingMark: option.RoutingMark,
TestTimeout: option.TestTimeout, },
MaxFailedTimes: option.MaxFailedTimes, option.Filter,
Providers: providers, providers,
}), }),
selected: "COMPATIBLE", selected: "COMPATIBLE",
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL,
Hidden: option.Hidden,
Icon: option.Icon,
} }
} }

View File

@@ -3,15 +3,13 @@ package outboundgroup
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"time" "time"
"github.com/metacubex/mihomo/common/callback" "github.com/Dreamacro/clash/adapter/outbound"
N "github.com/metacubex/mihomo/common/net" "github.com/Dreamacro/clash/common/singledo"
"github.com/metacubex/mihomo/common/singledo" "github.com/Dreamacro/clash/component/dialer"
"github.com/metacubex/mihomo/common/utils" C "github.com/Dreamacro/clash/constant"
C "github.com/metacubex/mihomo/constant" "github.com/Dreamacro/clash/constant/provider"
"github.com/metacubex/mihomo/constant/provider"
) )
type urlTestOption func(*URLTest) type urlTestOption func(*URLTest)
@@ -24,105 +22,48 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct { type URLTest struct {
*GroupBase *GroupBase
selected string tolerance uint16
testUrl string disableUDP bool
expectedStatus string fastNode C.Proxy
tolerance uint16 fastSingle *singledo.Single[C.Proxy]
disableUDP bool
Hidden bool
Icon string
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
return u.fast(false).Name() 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 // DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := u.fast(true) c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
c, err = proxy.DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
u.onDialSuccess()
} else { } else {
u.onDialFailed(proxy.Type(), err, u.healthCheck) u.onDialFailed()
} }
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 return c, err
} }
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
proxy := u.fast(true) pc, err := u.fast(true).ListenPacketContext(ctx, metadata, u.Base.DialOptions(opts...)...)
pc, err := proxy.ListenPacketContext(ctx, metadata)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
} else {
u.onDialFailed(proxy.Type(), err, u.healthCheck)
} }
return pc, err return pc, err
} }
// Unwrap implements C.ProxyAdapter // Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy { func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(touch) return u.fast(true)
}
func (u *URLTest) healthCheck() {
u.fastSingle.Reset()
u.GroupBase.healthCheck()
u.fastSingle.Reset()
} }
func (u *URLTest) fast(touch bool) C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) { elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch) 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] fast := proxies[0]
minDelay := fast.LastDelayForTestUrl(u.testUrl) min := fast.LastDelay()
fastNotExist := true fastNotExist := true
for _, proxy := range proxies[1:] { for _, proxy := range proxies[1:] {
@@ -130,26 +71,24 @@ func (u *URLTest) fast(touch bool) C.Proxy {
fastNotExist = false fastNotExist = false
} }
if !proxy.AliveForTestUrl(u.testUrl) { if !proxy.Alive() {
continue continue
} }
delay := proxy.LastDelayForTestUrl(u.testUrl) delay := proxy.LastDelay()
if delay < minDelay { if delay < min {
fast = proxy fast = proxy
minDelay = delay min = delay
} }
} }
// tolerance // tolerance
if u.fastNode == nil || fastNotExist || !u.fastNode.AliveForTestUrl(u.testUrl) || u.fastNode.LastDelayForTestUrl(u.testUrl) > fast.LastDelayForTestUrl(u.testUrl)+u.tolerance { if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
u.fastNode = fast u.fastNode = fast
} }
return u.fastNode, nil return u.fastNode, nil
}) })
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
u.Touch()
}
return elm return elm
} }
@@ -159,12 +98,8 @@ func (u *URLTest) SupportUDP() bool {
if u.disableUDP { if u.disableUDP {
return false return false
} }
return u.fast(false).SupportUDP()
}
// IsL3Protocol implements C.ProxyAdapter return u.fast(false).SupportUDP()
func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool {
return u.fast(false).IsL3Protocol(metadata)
} }
// MarshalJSON implements C.ProxyAdapter // MarshalJSON implements C.ProxyAdapter
@@ -174,21 +109,12 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"type": u.Type().String(), "type": u.Type().String(),
"now": u.Now(), "now": u.Now(),
"all": all, "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 { func parseURLTestOption(config map[string]any) []urlTestOption {
opts := []urlTestOption{} opts := []urlTestOption{}
@@ -205,21 +131,18 @@ func parseURLTestOption(config map[string]any) []urlTestOption {
func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest { func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
urlTest := &URLTest{ urlTest := &URLTest{
GroupBase: NewGroupBase(GroupBaseOption{ GroupBase: NewGroupBase(GroupBaseOption{
Name: option.Name, outbound.BaseOption{
Type: C.URLTest, Name: option.Name,
Filter: option.Filter, Type: C.URLTest,
ExcludeFilter: option.ExcludeFilter, Interface: option.Interface,
ExcludeType: option.ExcludeType, RoutingMark: option.RoutingMark,
TestTimeout: option.TestTimeout, },
MaxFailedTimes: option.MaxFailedTimes,
Providers: providers, option.Filter,
providers,
}), }),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP, disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
Hidden: option.Hidden,
Icon: option.Icon,
} }
for _, option := range options { for _, option := range options {

View File

@@ -1,10 +1,57 @@
package outboundgroup package outboundgroup
import (
"fmt"
"net"
"net/netip"
"time"
C "github.com/Dreamacro/clash/constant"
)
func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
host, port, err := net.SplitHostPort(rawAddress)
if err != nil {
err = fmt.Errorf("addrToMetadata failed: %w", err)
return
}
ip, err := netip.ParseAddr(host)
if err != nil {
addr = &C.Metadata{
AddrType: C.AtypDomainName,
Host: host,
DstIP: netip.Addr{},
DstPort: port,
}
err = nil
return
} else if ip.Is4() {
addr = &C.Metadata{
AddrType: C.AtypIPv4,
Host: "",
DstIP: ip,
DstPort: port,
}
return
}
addr = &C.Metadata{
AddrType: C.AtypIPv6,
Host: "",
DstIP: ip,
DstPort: port,
}
return
}
func tcpKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(30 * time.Second)
}
}
type SelectAble interface { type SelectAble interface {
Set(string) error Set(string) error
ForceSet(name string)
} }
var _ SelectAble = (*Fallback)(nil)
var _ SelectAble = (*URLTest)(nil)
var _ SelectAble = (*Selector)(nil)

View File

@@ -3,20 +3,20 @@ package adapter
import ( import (
"fmt" "fmt"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/Dreamacro/clash/adapter/outbound"
"github.com/metacubex/mihomo/common/structure" "github.com/Dreamacro/clash/common/structure"
C "github.com/metacubex/mihomo/constant" C "github.com/Dreamacro/clash/constant"
) )
func ParseProxy(mapping map[string]any) (C.Proxy, error) { func ParseProxy(mapping map[string]any) (C.Proxy, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true, KeyReplacer: structure.DefaultKeyReplacer}) decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
proxyType, existType := mapping["type"].(string) proxyType, existType := mapping["type"].(string)
if !existType { if !existType {
return nil, fmt.Errorf("missing type") return nil, fmt.Errorf("missing type")
} }
var ( var (
proxy outbound.ProxyAdapter proxy C.ProxyAdapter
err error err error
) )
switch proxyType { switch proxyType {
@@ -40,14 +40,14 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil { if err != nil {
break break
} }
proxy, err = outbound.NewSocks5(*socksOption) proxy = outbound.NewSocks5(*socksOption)
case "http": case "http":
httpOption := &outbound.HttpOption{} httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption) err = decoder.Decode(mapping, httpOption)
if err != nil { if err != nil {
break break
} }
proxy, err = outbound.NewHttp(*httpOption) proxy = outbound.NewHttp(*httpOption)
case "vmess": case "vmess":
vmessOption := &outbound.VmessOption{ vmessOption := &outbound.VmessOption{
HTTPOpts: outbound.HTTPOptions{ HTTPOpts: outbound.HTTPOptions{
@@ -55,7 +55,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
Path: []string{"/"}, Path: []string{"/"},
}, },
} }
err = decoder.Decode(mapping, vmessOption) err = decoder.Decode(mapping, vmessOption)
if err != nil { if err != nil {
break break
@@ -89,69 +88,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break break
} }
proxy, err = outbound.NewHysteria(*hyOption) 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: default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
} }
@@ -160,20 +96,5 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
return nil, err 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 return NewProxy(proxy), nil
} }

198
adapter/provider/fetcher.go Normal file
View File

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

View File

@@ -2,18 +2,15 @@ package provider
import ( import (
"context" "context"
"strings"
"sync"
"time" "time"
"github.com/metacubex/mihomo/common/atomic" "github.com/Dreamacro/clash/common/batch"
"github.com/metacubex/mihomo/common/singledo" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/common/utils" "go.uber.org/atomic"
C "github.com/metacubex/mihomo/constant" )
"github.com/metacubex/mihomo/log"
"github.com/dlclark/regexp2" const (
"golang.org/x/sync/errgroup" defaultURLTestTimeout = time.Second * 5
) )
type HealthCheckOption struct { type HealthCheckOption struct {
@@ -21,48 +18,32 @@ type HealthCheckOption struct {
Interval uint Interval uint
} }
type extraOption struct {
expectedStatus utils.IntRanges[uint16]
filters map[string]struct{}
}
type HealthCheck struct { type HealthCheck struct {
ctx context.Context url string
ctxCancel context.CancelFunc proxies []C.Proxy
url string interval uint
extra map[string]*extraOption lazy bool
mu sync.Mutex lastTouch *atomic.Int64
started atomic.Bool done chan struct{}
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() { func (hc *HealthCheck) process() {
if hc.started.Load() { ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
log.Warnln("Skip start health check timer due to it's started")
return go func() {
} time.Sleep(30 * time.Second)
hc.check()
}()
ticker := time.NewTicker(hc.interval)
hc.start()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
lastTouch := hc.lastTouch.Load() now := time.Now().Unix()
since := time.Since(lastTouch) if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
if !hc.lazy || since < hc.interval {
hc.check() hc.check()
} else {
log.Debugln("Skip once health check because we are lazy")
} }
case <-hc.ctx.Done(): case <-hc.done:
ticker.Stop() ticker.Stop()
hc.stop()
return return
} }
} }
@@ -72,165 +53,39 @@ func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies hc.proxies = proxies
} }
func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
url = strings.TrimSpace(url)
if len(url) == 0 || url == hc.url {
log.Debugln("ignore invalid health check url: %s", url)
return
}
hc.mu.Lock()
defer hc.mu.Unlock()
// if the provider has not set up health checks, then modify it to be the same as the group's interval
if hc.interval == 0 {
hc.interval = time.Duration(interval) * time.Second
}
if hc.extra == nil {
hc.extra = make(map[string]*extraOption)
}
// prioritize the use of previously registered configurations, especially those from provider
if _, ok := hc.extra[url]; ok {
// provider default health check does not set filter
if url != hc.url && len(filter) != 0 {
splitAndAddFiltersToExtra(filter, hc.extra[url])
}
log.Debugln("health check url: %s exists", url)
return
}
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
filter = strings.TrimSpace(filter)
if len(filter) != 0 {
for _, regex := range strings.Split(filter, "`") {
regex = strings.TrimSpace(regex)
if len(regex) != 0 {
option.filters[regex] = struct{}{}
}
}
}
}
func (hc *HealthCheck) auto() bool { func (hc *HealthCheck) auto() bool {
return hc.interval != 0 return hc.interval != 0
} }
func (hc *HealthCheck) touch() { func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now()) hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
} }
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
if len(hc.proxies) == 0 { b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
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 { 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 p := proxy
b.Go(func() error { b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(hc.ctx, hc.timeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel() defer cancel()
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) _, _ = p.URLTest(ctx, hc.url)
_, _ = p.URLTest(ctx, url, expectedStatus) return false, nil
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
}) })
} }
b.Wait()
} }
func (hc *HealthCheck) close() { func (hc *HealthCheck) close() {
hc.ctxCancel() hc.done <- struct{}{}
} }
func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
if url == "" {
expectedStatus = nil
interval = 0
}
if timeout == 0 {
timeout = 5000
}
ctx, cancel := context.WithCancel(context.Background())
return &HealthCheck{ return &HealthCheck{
ctx: ctx, proxies: proxies,
ctxCancel: cancel, url: url,
proxies: proxies, interval: interval,
url: url, lazy: lazy,
timeout: time.Duration(timeout) * time.Millisecond, lastTouch: atomic.NewInt64(0),
extra: map[string]*extraOption{}, done: make(chan struct{}, 1),
interval: time.Duration(interval) * time.Second,
lazy: lazy,
expectedStatus: expectedStatus,
singleDo: singledo.NewSingle[struct{}](time.Second),
} }
} }

View File

@@ -1,77 +1,31 @@
package provider package provider
import ( import (
"encoding"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"github.com/metacubex/mihomo/common/structure" "github.com/Dreamacro/clash/common/structure"
"github.com/metacubex/mihomo/common/utils" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/resource" types "github.com/Dreamacro/clash/constant/provider"
C "github.com/metacubex/mihomo/constant"
types "github.com/metacubex/mihomo/constant/provider"
"github.com/dlclark/regexp2"
) )
var ( var errVehicleType = errors.New("unsupport vehicle type")
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
type healthCheckSchema struct { type healthCheckSchema struct {
Enable bool `provider:"enable"` Enable bool `provider:"enable"`
URL string `provider:"url"` URL string `provider:"url"`
Interval int `provider:"interval"` Interval int `provider:"interval"`
TestTimeout int `provider:"timeout,omitempty"` Lazy bool `provider:"lazy,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 proxyProviderSchema struct {
Type string `provider:"type"` Type string `provider:"type"`
Path string `provider:"path,omitempty"` Path string `provider:"path"`
URL string `provider:"url,omitempty"` URL string `provider:"url,omitempty"`
Proxy string `provider:"proxy,omitempty"` Interval int `provider:"interval,omitempty"`
Interval int `provider:"interval,omitempty"` Filter string `provider:"filter,omitempty"`
Filter string `provider:"filter,omitempty"` HealthCheck healthCheckSchema `provider:"health-check,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) { func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
@@ -86,46 +40,25 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
return nil, err return nil, err
} }
expectedStatus, err := utils.NewUnsignedRanges[uint16](schema.HealthCheck.ExpectedStatus)
if err != nil {
return nil, err
}
var hcInterval uint var hcInterval uint
if schema.HealthCheck.Enable { if schema.HealthCheck.Enable {
if schema.HealthCheck.Interval == 0 {
schema.HealthCheck.Interval = 300
}
hcInterval = uint(schema.HealthCheck.Interval) hcInterval = uint(schema.HealthCheck.Interval)
} }
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, uint(schema.HealthCheck.TestTimeout), hcInterval, schema.HealthCheck.Lazy, expectedStatus) hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
parser, err := NewProxiesParser(schema.Filter, schema.ExcludeFilter, schema.ExcludeType, schema.DialerProxy, schema.Override) path := C.Path.Resolve(schema.Path)
if err != nil {
return nil, err
}
var vehicle types.Vehicle var vehicle types.Vehicle
switch schema.Type { switch schema.Type {
case "file": case "file":
path := C.Path.Resolve(schema.Path) vehicle = NewFileVehicle(path)
vehicle = resource.NewFileVehicle(path)
case "http": case "http":
path := C.Path.GetPathByHash("proxies", schema.URL) vehicle = NewHTTPVehicle(schema.URL, path)
if schema.Path != "" {
path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path)
}
}
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)
case "inline":
return NewInlineProvider(name, schema.Payload, parser, hc)
default: default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type) return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
} }
interval := time.Duration(uint(schema.Interval)) * time.Second interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter
return NewProxySetProvider(name, interval, schema.Payload, parser, vehicle, hc) return NewProxySetProvider(name, interval, filter, vehicle, hc)
} }

View File

@@ -4,21 +4,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "math"
"reflect"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/metacubex/mihomo/adapter" "github.com/Dreamacro/clash/adapter"
"github.com/metacubex/mihomo/common/convert" "github.com/Dreamacro/clash/common/convert"
"github.com/metacubex/mihomo/common/utils" C "github.com/Dreamacro/clash/constant"
"github.com/metacubex/mihomo/component/profile/cachefile" types "github.com/Dreamacro/clash/constant/provider"
"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" "github.com/dlclark/regexp2"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -31,280 +24,143 @@ type ProxySchema struct {
Proxies []map[string]any `yaml:"proxies"` Proxies []map[string]any `yaml:"proxies"`
} }
type providerForApi struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicleType"`
Proxies []C.Proxy `json:"proxies"`
TestUrl string `json:"testUrl"`
ExpectedStatus string `json:"expectedStatus"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
SubscriptionInfo *SubscriptionInfo `json:"subscriptionInfo,omitempty"`
}
type baseProvider struct {
name string
proxies []C.Proxy
healthCheck *HealthCheck
version uint32
}
func (bp *baseProvider) Name() string {
return bp.name
}
func (bp *baseProvider) Version() uint32 {
return bp.version
}
func (bp *baseProvider) HealthCheck() {
bp.healthCheck.check()
}
func (bp *baseProvider) Type() types.ProviderType {
return types.Proxy
}
func (bp *baseProvider) Proxies() []C.Proxy {
return bp.proxies
}
func (bp *baseProvider) Count() int {
return len(bp.proxies)
}
func (bp *baseProvider) Touch() {
bp.healthCheck.touch()
}
func (bp *baseProvider) HealthCheckURL() string {
return bp.healthCheck.url
}
func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
bp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (bp *baseProvider) setProxies(proxies []C.Proxy) {
bp.proxies = proxies
bp.version += 1
bp.healthCheck.setProxy(proxies)
if bp.healthCheck.auto() {
go bp.healthCheck.check()
}
}
func (bp *baseProvider) Close() error {
bp.healthCheck.close()
return nil
}
// ProxySetProvider for auto gc // ProxySetProvider for auto gc
type ProxySetProvider struct { type ProxySetProvider struct {
*proxySetProvider *proxySetProvider
} }
type proxySetProvider struct { type proxySetProvider struct {
baseProvider *fetcher[[]C.Proxy]
*resource.Fetcher[[]C.Proxy] proxies []C.Proxy
subscriptionInfo *SubscriptionInfo healthCheck *HealthCheck
version uint
} }
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) { func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(providerForApi{ return json.Marshal(map[string]any{
Name: pp.Name(), "name": pp.Name(),
Type: pp.Type().String(), "type": pp.Type().String(),
VehicleType: pp.VehicleType().String(), "vehicleType": pp.VehicleType().String(),
Proxies: pp.Proxies(), "proxies": pp.Proxies(),
TestUrl: pp.healthCheck.url, "updatedAt": pp.updatedAt,
ExpectedStatus: pp.healthCheck.expectedStatus.String(),
UpdatedAt: pp.UpdatedAt(),
SubscriptionInfo: pp.subscriptionInfo,
}) })
} }
func (pp *proxySetProvider) Version() uint {
return pp.version
}
func (pp *proxySetProvider) Name() string { func (pp *proxySetProvider) Name() string {
return pp.Fetcher.Name() return pp.name
}
func (pp *proxySetProvider) HealthCheck() {
pp.healthCheck.check()
} }
func (pp *proxySetProvider) Update() error { func (pp *proxySetProvider) Update() error {
_, _, err := pp.Fetcher.Update() elm, same, err := pp.fetcher.Update()
if err == nil && !same {
pp.onUpdate(elm)
}
return err return err
} }
func (pp *proxySetProvider) Initial() error { func (pp *proxySetProvider) Initial() error {
_, err := pp.Fetcher.Initial() elm, err := pp.fetcher.Initial()
if err != nil { if err != nil {
return err return err
} }
if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { pp.onUpdate(elm)
pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo)
}
pp.closeAllConnections()
return nil return nil
} }
func (pp *proxySetProvider) closeAllConnections() { func (pp *proxySetProvider) Type() types.ProviderType {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool { return types.Proxy
for _, chain := range c.Chains() {
if chain == pp.Name() {
_ = c.Close()
break
}
}
return true
})
} }
func (pp *proxySetProvider) Close() error { func (pp *proxySetProvider) Proxies() []C.Proxy {
_ = pp.baseProvider.Close() return pp.proxies
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) { func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
defer func() { go pp.healthCheck.check() }()
}
}
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
_ = pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, filter string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
filterReg, err := regexp2.Compile(filter, 0)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
if hc.auto() { if hc.auto() {
go hc.process() go hc.process()
} }
pd := &proxySetProvider{ pd := &proxySetProvider{
baseProvider: baseProvider{ proxies: []C.Proxy{},
name: name, healthCheck: hc,
proxies: []C.Proxy{},
healthCheck: hc,
},
} }
if len(payload) > 0 { // using as fallback proxies fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg), proxiesOnUpdate(pd))
ps := ProxySchema{Proxies: payload} pd.fetcher = fetcher
buf, err := yaml.Marshal(ps)
if err != nil {
return nil, err
}
proxies, err := parser(buf)
if err != nil {
return nil, err
}
pd.proxies = 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} wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, (*ProxySetProvider).Close) runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil return wrapper, nil
} }
func (pp *ProxySetProvider) Close() error {
runtime.SetFinalizer(pp, nil)
return pp.proxySetProvider.Close()
}
// InlineProvider for auto gc
type InlineProvider struct {
*inlineProvider
}
type inlineProvider struct {
baseProvider
updateAt time.Time
}
func (ip *inlineProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(providerForApi{
Name: ip.Name(),
Type: ip.Type().String(),
VehicleType: ip.VehicleType().String(),
Proxies: ip.Proxies(),
TestUrl: ip.healthCheck.url,
ExpectedStatus: ip.healthCheck.expectedStatus.String(),
UpdatedAt: ip.updateAt,
})
}
func (ip *inlineProvider) VehicleType() types.VehicleType {
return types.Inline
}
func (ip *inlineProvider) Initial() error {
return nil
}
func (ip *inlineProvider) Update() error {
// make api update happy
ip.updateAt = time.Now()
return nil
}
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
if hc.auto() {
go hc.process()
}
ps := ProxySchema{Proxies: payload}
buf, err := yaml.Marshal(ps)
if err != nil {
return nil, err
}
proxies, err := parser(buf)
if err != nil {
return nil, err
}
ip := &inlineProvider{
baseProvider: baseProvider{
name: name,
proxies: proxies,
healthCheck: hc,
},
updateAt: time.Now(),
}
wrapper := &InlineProvider{ip}
runtime.SetFinalizer(wrapper, (*InlineProvider).Close)
return wrapper, nil
}
func (ip *InlineProvider) Close() error {
runtime.SetFinalizer(ip, nil)
return ip.baseProvider.Close()
}
// CompatibleProvider for auto gc // CompatibleProvider for auto gc
type CompatibleProvider struct { type CompatibleProvider struct {
*compatibleProvider *compatibleProvider
} }
type compatibleProvider struct { type compatibleProvider struct {
baseProvider name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint
} }
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) { func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(providerForApi{ return json.Marshal(map[string]any{
Name: cp.Name(), "name": cp.Name(),
Type: cp.Type().String(), "type": cp.Type().String(),
VehicleType: cp.VehicleType().String(), "vehicleType": cp.VehicleType().String(),
Proxies: cp.Proxies(), "proxies": cp.Proxies(),
TestUrl: cp.healthCheck.url,
ExpectedStatus: cp.healthCheck.expectedStatus.String(),
}) })
} }
func (cp *compatibleProvider) Version() uint {
return cp.version
}
func (cp *compatibleProvider) Name() string {
return cp.name
}
func (cp *compatibleProvider) HealthCheck() {
cp.healthCheck.check()
}
func (cp *compatibleProvider) Update() error { func (cp *compatibleProvider) Update() error {
return nil return nil
} }
func (cp *compatibleProvider) Initial() error { func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
cp.HealthCheck()
}
return nil return nil
} }
@@ -312,6 +168,22 @@ func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible return types.Compatible
} }
func (cp *compatibleProvider) Type() types.ProviderType {
return types.Proxy
}
func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) { func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("provider need one proxy at least") return nil, errors.New("provider need one proxy at least")
@@ -322,49 +194,35 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
} }
pd := &compatibleProvider{ pd := &compatibleProvider{
baseProvider: baseProvider{ name: name,
name: name, proxies: proxies,
proxies: proxies, healthCheck: hc,
healthCheck: hc,
},
} }
wrapper := &CompatibleProvider{pd} wrapper := &CompatibleProvider{pd}
runtime.SetFinalizer(wrapper, (*CompatibleProvider).Close) runtime.SetFinalizer(wrapper, stopCompatibleProvider)
return wrapper, nil return wrapper, nil
} }
func (cp *CompatibleProvider) Close() error { func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
runtime.SetFinalizer(cp, nil) return func(elm []C.Proxy) {
return cp.compatibleProvider.Close() pd.setProxies(elm)
if pd.version == math.MaxUint {
pd.version = 0
} else {
pd.version++
}
}
} }
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { func proxiesParseAndFilter(filter string, filterReg *regexp2.Regexp) parser[[]C.Proxy] {
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) { return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil { if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf) proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil { if err1 != nil {
return nil, fmt.Errorf("%w, %w", err, err1) return nil, fmt.Errorf("%s, %w", err.Error(), err1)
} }
schema.Proxies = proxies schema.Proxies = proxies
} }
@@ -374,94 +232,17 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
} }
proxies := []C.Proxy{} proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{} for idx, mapping := range schema.Proxies {
for _, filterReg := range filterRegs { name, ok := mapping["name"]
for idx, mapping := range schema.Proxies { mat, _ := filterReg.FindStringMatch(name.(string))
if nil != excludeTypeArray && len(excludeTypeArray) > 0 { if ok && len(filter) > 0 && mat == nil {
mType, ok := mapping["type"] continue
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)
} }
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
} }
if len(proxies) == 0 { if len(proxies) == 0 {
@@ -472,5 +253,5 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
} }
return proxies, nil return proxies, nil
}, nil }
} }

View File

@@ -1,58 +0,0 @@
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)
}

View File

@@ -0,0 +1,65 @@
package provider
import (
"context"
"io"
"net/http"
"os"
"time"
netHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
)
type FileVehicle struct {
path string
}
func (f *FileVehicle) Type() types.VehicleType {
return types.File
}
func (f *FileVehicle) Path() string {
return f.path
}
func (f *FileVehicle) Read() ([]byte, error) {
return os.ReadFile(f.path)
}
func NewFileVehicle(path string) *FileVehicle {
return &FileVehicle{path: path}
}
type HTTPVehicle struct {
url string
path string
}
func (h *HTTPVehicle) Type() types.VehicleType {
return types.HTTP
}
func (h *HTTPVehicle) Path() string {
return h.path
}
func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return buf, nil
}
func NewHTTPVehicle(url string, path string) *HTTPVehicle {
return &HTTPVehicle{url, path}
}

View File

@@ -1,21 +0,0 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89
package main
// #include <time.h>
import "C"
import "time"
func init() {
var currentT C.time_t
var currentTM C.struct_tm
C.time(&currentT)
C.localtime_r(&currentT, &currentTM)
tzOffset := int(currentTM.tm_gmtoff)
tz := C.GoString(currentTM.tm_zone)
time.Local = time.FixedZone(tz, tzOffset)
}

View File

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

View File

@@ -1,105 +0,0 @@
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)
}
}
}

View File

@@ -1,32 +0,0 @@
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)
}
}

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
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 (
Must = common.Must
Error = common.Error
)

106
common/cache/cache.go vendored Normal file
View File

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

72
common/cache/cache_test.go vendored Normal file
View File

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

View File

@@ -1,4 +1,4 @@
package lru package cache
// Modified by https://github.com/die-net/lrucache // Modified by https://github.com/die-net/lrucache
@@ -6,8 +6,7 @@ import (
"sync" "sync"
"time" "time"
list "github.com/bahlo/generic-list-go" "github.com/Dreamacro/clash/common/generics/list"
"github.com/samber/lo"
) )
// Option is part of Functional Options Pattern // Option is part of Functional Options Pattern
@@ -66,10 +65,12 @@ type LruCache[K comparable, V any] struct {
onEvict EvictCallback[K, V] onEvict EvictCallback[K, V]
} }
// New creates an LruCache // NewLRUCache creates an LruCache
func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] { func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{} lc := &LruCache[K, V]{
lc.Clear() lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]),
}
for _, option := range options { for _, option := range options {
option(lc) option(lc)
@@ -78,55 +79,26 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
return lc return lc
} }
func (c *LruCache[K, V]) Clear() { // Get returns the any representation of a cached response and a bool
c.mu.Lock()
defer c.mu.Unlock()
c.lru = list.New[*entry[K, V]]()
c.cache = make(map[K]*list.Element[*entry[K, V]])
}
// Get returns any representation of a cached response and a bool
// set to true if the key was found. // set to true if the key was found.
func (c *LruCache[K, V]) Get(key K) (V, bool) { func (c *LruCache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { if el == nil {
return lo.Empty[V](), false return getZero[V](), false
} }
value := el.value value := el.value
return value, true return value, true
} }
func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) { // GetWithExpire returns the any representation of a cached response,
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
value := constructor()
c.set(key, value)
return value, false
}
value := el.value
return value, true
}
// GetWithExpire returns any representation of a cached response,
// a time.Time Give expected expires, // a time.Time Give expected expires,
// and a bool set to true if the key was found. // and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires. // This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) { func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key) el := c.get(key)
if el == nil { if el == nil {
return lo.Empty[V](), time.Time{}, false return getZero[V](), time.Time{}, false
} }
return el.value, time.Unix(el.expires, 0), true return el.value, time.Unix(el.expires, 0), true
@@ -141,32 +113,21 @@ func (c *LruCache[K, V]) Exist(key K) bool {
return ok return ok
} }
// Set stores any representation of a response for a given key. // Set stores the any representation of a response for a given key.
func (c *LruCache[K, V]) Set(key K, value V) { func (c *LruCache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.set(key, value)
}
func (c *LruCache[K, V]) set(key K, value V) {
expires := int64(0) expires := int64(0)
if c.maxAge > 0 { if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge expires = time.Now().Unix() + c.maxAge
} }
c.setWithExpire(key, value, time.Unix(expires, 0)) c.SetWithExpire(key, value, time.Unix(expires, 0))
} }
// SetWithExpire stores any representation of a response for a given key and given expires. // SetWithExpire stores the any representation of a response for a given key and given expires.
// The expires time will round to second. // The expires time will round to second.
func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) { func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
c.setWithExpire(key, value, expires)
}
func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le) c.lru.MoveToBack(le)
e := le.Value e := le.Value
@@ -204,6 +165,9 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[K, V]) {
} }
func (c *LruCache[K, V]) get(key K) *entry[K, V] { func (c *LruCache[K, V]) get(key K) *entry[K, V] {
c.mu.Lock()
defer c.mu.Unlock()
le, ok := c.cache[key] le, ok := c.cache[key]
if !ok { if !ok {
return nil return nil
@@ -227,15 +191,12 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
// Delete removes the value associated with a key. // Delete removes the value associated with a key.
func (c *LruCache[K, V]) Delete(key K) { func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
c.delete(key)
}
func (c *LruCache[K, V]) delete(key K) {
if le, ok := c.cache[key]; ok { if le, ok := c.cache[key]; ok {
c.deleteElement(le) c.deleteElement(le)
} }
c.mu.Unlock()
} }
func (c *LruCache[K, V]) maybeDeleteOldest() { func (c *LruCache[K, V]) maybeDeleteOldest() {
@@ -256,32 +217,13 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
} }
} }
// Compute either sets the computed new value for the key or deletes func (c *LruCache[K, V]) Clear() error {
// the value for the key. When the delete result of the valueFn function
// is set to true, the value will be deleted, if it exists. When delete
// is set to false, the value is updated to the newValue.
// The ok result indicates whether value was computed and stored, thus, is
// present in the map. The actual result contains the new value in cases where
// the value was computed and stored.
func (c *LruCache[K, V]) Compute(
key K,
valueFn func(oldValue V, loaded bool) (newValue V, delete bool),
) (actual V, ok bool) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock()
if el := c.get(key); el != nil { c.cache = make(map[K]*list.Element[*entry[K, V]])
actual, ok = el.value, true
} c.mu.Unlock()
if newValue, del := valueFn(actual, ok); del { return nil
if ok { // data not in cache, so needn't delete
c.delete(key)
}
return lo.Empty[V](), false
} else {
c.set(key, newValue)
return newValue, true
}
} }
type entry[K comparable, V any] struct { type entry[K comparable, V any] struct {
@@ -289,3 +231,8 @@ type entry[K comparable, V any] struct {
value V value V
expires int64 expires int64
} }
func getZero[T any]() T {
var result T
return result
}

View File

@@ -1,4 +1,4 @@
package lru package cache
import ( import (
"testing" "testing"
@@ -19,7 +19,7 @@ var entries = []struct {
} }
func TestLRUCache(t *testing.T) { func TestLRUCache(t *testing.T) {
c := New[string, string]() c := NewLRUCache[string, string]()
for _, e := range entries { for _, e := range entries {
c.Set(e.key, e.value) c.Set(e.key, e.value)
@@ -45,7 +45,7 @@ func TestLRUCache(t *testing.T) {
} }
func TestLRUMaxAge(t *testing.T) { func TestLRUMaxAge(t *testing.T) {
c := New[string, string](WithAge[string, string](86400)) c := NewLRUCache[string, string](WithAge[string, string](86400))
now := time.Now().Unix() now := time.Now().Unix()
expected := now + 86400 expected := now + 86400
@@ -88,7 +88,7 @@ func TestLRUMaxAge(t *testing.T) {
} }
func TestLRUpdateOnGet(t *testing.T) { func TestLRUpdateOnGet(t *testing.T) {
c := New[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]()) c := NewLRUCache[string, string](WithAge[string, string](86400), WithUpdateAgeOnGet[string, string]())
now := time.Now().Unix() now := time.Now().Unix()
expires := now + 86400/2 expires := now + 86400/2
@@ -103,7 +103,7 @@ func TestLRUpdateOnGet(t *testing.T) {
} }
func TestMaxSize(t *testing.T) { func TestMaxSize(t *testing.T) {
c := New[string, string](WithSize[string, string](2)) c := NewLRUCache[string, string](WithSize[string, string](2))
// Add one expired entry // Add one expired entry
c.Set("foo", "bar") c.Set("foo", "bar")
_, ok := c.Get("foo") _, ok := c.Get("foo")
@@ -117,7 +117,7 @@ func TestMaxSize(t *testing.T) {
} }
func TestExist(t *testing.T) { func TestExist(t *testing.T) {
c := New[int, int](WithSize[int, int](1)) c := NewLRUCache[int, int](WithSize[int, int](1))
c.Set(1, 2) c.Set(1, 2)
assert.True(t, c.Exist(1)) assert.True(t, c.Exist(1))
c.Set(2, 3) c.Set(2, 3)
@@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
temp = key + value temp = key + value
} }
c := New[int, int](WithEvict[int, int](evict), WithSize[int, int](1)) c := NewLRUCache[int, int](WithEvict[int, int](evict), WithSize[int, int](1))
c.Set(1, 2) c.Set(1, 2)
c.Set(2, 3) c.Set(2, 3)
@@ -138,7 +138,7 @@ func TestEvict(t *testing.T) {
} }
func TestSetWithExpire(t *testing.T) { func TestSetWithExpire(t *testing.T) {
c := New[int, *struct{}](WithAge[int, *struct{}](1)) c := NewLRUCache[int, *struct{}](WithAge[int, *struct{}](1))
now := time.Now().Unix() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) tenSecBefore := time.Unix(now-10, 0)
@@ -153,7 +153,7 @@ func TestSetWithExpire(t *testing.T) {
} }
func TestStale(t *testing.T) { func TestStale(t *testing.T) {
c := New[int, int](WithAge[int, int](1), WithStale[int, int](true)) c := NewLRUCache[int, int](WithAge[int, int](1), WithStale[int, int](true))
now := time.Now().Unix() now := time.Now().Unix()
tenSecBefore := time.Unix(now-10, 0) tenSecBefore := time.Unix(now-10, 0)
@@ -166,11 +166,11 @@ func TestStale(t *testing.T) {
} }
func TestCloneTo(t *testing.T) { func TestCloneTo(t *testing.T) {
o := New[string, int](WithSize[string, int](10)) o := NewLRUCache[string, int](WithSize[string, int](10))
o.Set("1", 1) o.Set("1", 1)
o.Set("2", 2) o.Set("2", 2)
n := New[string, int](WithSize[string, int](2)) n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3) n.Set("3", 3)
n.Set("4", 4) n.Set("4", 4)

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ func ExecCmd(cmdStr string) (string, error) {
cmd = exec.Command(args[0]) cmd = exec.Command(args[0])
} else { } else {
cmd = exec.Command(args[0], args[1:]...) cmd = exec.Command(args[0], args[1:]...)
} }
prepareBackgroundCommand(cmd) prepareBackgroundCommand(cmd)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()

View File

@@ -7,5 +7,4 @@ import (
) )
func prepareBackgroundCommand(cmd *exec.Cmd) { func prepareBackgroundCommand(cmd *exec.Cmd) {
} }

View File

@@ -21,7 +21,7 @@ func TestSplitArgs(t *testing.T) {
func TestExecCmd(t *testing.T) { func TestExecCmd(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
_, err := ExecCmd("cmd -c 'dir'") _, err := ExecCmd("dir")
assert.Nil(t, err) assert.Nil(t, err)
return return
} }

View File

@@ -0,0 +1,56 @@
package collections
import "sync"
type (
stack struct {
top *node
length int
lock *sync.RWMutex
}
node struct {
value interface{}
prev *node
}
)
// NewStack Create a new stack
func NewStack() *stack {
return &stack{nil, 0, &sync.RWMutex{}}
}
// Len Return the number of items in the stack
func (this *stack) Len() int {
return this.length
}
// Peek View the top item on the stack
func (this *stack) Peek() interface{} {
if this.length == 0 {
return nil
}
return this.top.value
}
// Pop the top item of the stack and return it
func (this *stack) Pop() interface{} {
this.lock.Lock()
defer this.lock.Unlock()
if this.length == 0 {
return nil
}
n := this.top
this.top = n.prev
this.length--
return n.value
}
// Push a value onto the top of the stack
func (this *stack) Push(value interface{}) {
this.lock.Lock()
defer this.lock.Unlock()
n := &node{value, this.top}
this.top = n
this.length++
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,13 +8,36 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"github.com/metacubex/mihomo/log"
) )
// ConvertsV2Ray convert V2Ray subscribe proxies data to mihomo proxies config var enc = base64.StdEncoding
func DecodeBase64(buf []byte) ([]byte, error) {
dBuf := make([]byte, enc.DecodedLen(len(buf)))
n, err := enc.Decode(dBuf, buf)
if err != nil {
return nil, err
}
return dBuf[:n], nil
}
// DecodeBase64StringToString decode base64 string to string
func DecodeBase64StringToString(s string) (string, error) {
dBuf, err := enc.DecodeString(s)
if err != nil {
return "", err
}
return string(dBuf), nil
}
// ConvertsV2Ray convert V2Ray subscribe proxies data to clash proxies config
func ConvertsV2Ray(buf []byte) ([]map[string]any, error) { func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
data := DecodeBase64(buf) data, err := DecodeBase64(buf)
if err != nil {
data = buf
}
arr := strings.Split(string(data), "\n") arr := strings.Split(string(data), "\n")
@@ -50,102 +73,15 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port() hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer") hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs") hysteria["obfs"] = query.Get("obfs")
if alpn := query.Get("alpn"); alpn != "" { hysteria["alpn"] = query.Get("alpn")
hysteria["alpn"] = strings.Split(alpn, ",")
}
hysteria["auth_str"] = query.Get("auth") hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol") hysteria["protocol"] = query.Get("protocol")
up := query.Get("up") hysteria["down_mbps"], _ = strconv.Atoi(query.Get("downmbps"))
down := query.Get("down") hysteria["up_mbps"], _ = strconv.Atoi(query.Get("upmbps"))
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")) hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria) 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": case "trojan":
urlTrojan, err := url.Parse(line) urlTrojan, err := url.Parse(line)
if err != nil { if err != nil {
@@ -163,14 +99,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["port"] = urlTrojan.Port() trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username() trojan["password"] = urlTrojan.User.Username()
trojan["udp"] = true trojan["udp"] = true
trojan["skip-cert-verify"], _ = strconv.ParseBool(query.Get("allowInsecure")) trojan["skip-cert-verify"] = false
if sni := query.Get("sni"); sni != "" { sni := query.Get("sni")
if sni != "" {
trojan["sni"] = sni trojan["sni"] = sni
} }
if alpn := query.Get("alpn"); alpn != "" {
trojan["alpn"] = strings.Split(alpn, ",")
}
network := strings.ToLower(query.Get("type")) network := strings.ToLower(query.Get("type"))
if network != "" { if network != "" {
@@ -182,6 +116,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent() headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path") wsOpts["path"] = query.Get("path")
@@ -195,54 +130,97 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["grpc-opts"] = grpcOpts trojan["grpc-opts"] = grpcOpts
} }
if fingerprint := query.Get("fp"); fingerprint == "" {
trojan["client-fingerprint"] = "chrome"
} else {
trojan["client-fingerprint"] = fingerprint
}
proxies = append(proxies, trojan) proxies = append(proxies, trojan)
case "vless": case "vless":
urlVLess, err := url.Parse(line) urlVless, err := url.Parse(line)
if err != nil { if err != nil {
continue continue
} }
query := urlVLess.Query()
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20) vless := make(map[string]any, 20)
err = handleVShareLink(names, urlVLess, scheme, vless)
if err != nil { vless["name"] = name
log.Warnln("error:%s line:%s", err.Error(), line) vless["type"] = scheme
continue vless["server"] = urlVless.Hostname()
vless["port"] = urlVless.Port()
vless["uuid"] = urlVless.User.Username()
vless["udp"] = true
vless["skip-cert-verify"] = false
sni := query.Get("sni")
if sni != "" {
vless["servername"] = sni
} }
if flow := query.Get("flow"); flow != "" {
vless["flow"] = strings.ToLower(flow) flow := strings.ToLower(query.Get("flow"))
if flow != "" {
vless["flow"] = flow
} }
network := strings.ToLower(query.Get("type"))
if network != "" {
fakeType := strings.ToLower(query.Get("headerType"))
if network == "tcp" && fakeType == "http" {
network = "http"
}
if network == "http" {
network = "h2"
}
vless["network"] = network
}
switch network {
case "http":
headers := make(map[string]any)
httpOpts := make(map[string]any)
if query.Get("method") != "" {
httpOpts["method"] = query.Get("method")
}
if query.Get("path") != "" {
httpOpts["path"] = query.Get("path")
}
headers["User-Agent"] = RandUserAgent()
httpOpts["headers"] = headers
vless["http-opts"] = httpOpts
case "h2":
headers := make(map[string]any)
h2Opts := make(map[string]any)
headers["User-Agent"] = RandUserAgent()
h2Opts["path"] = query.Get("path")
h2Opts["headers"] = headers
vless["h2-opts"] = h2Opts
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
wsOpts["headers"] = headers
vless["ws-opts"] = wsOpts
case "grpc":
grpcOpts := make(map[string]any)
grpcOpts["grpc-service-name"] = query.Get("serviceName")
vless["grpc-opts"] = grpcOpts
}
proxies = append(proxies, vless) proxies = append(proxies, vless)
case "vmess": case "vmess":
// V2RayN-styled share link dcBuf, err := enc.DecodeString(body)
// 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 { 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 continue
} }
@@ -252,11 +230,8 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if jsonDc.Decode(&values) != nil { if jsonDc.Decode(&values) != nil {
continue continue
} }
tempName, ok := values["ps"].(string)
if !ok { name := uniqueName(names, values["ps"].(string))
continue
}
name := uniqueName(names, tempName)
vmess := make(map[string]any, 20) vmess := make(map[string]any, 20)
vmess["name"] = name vmess["name"] = name
@@ -264,58 +239,38 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
vmess["server"] = values["add"] vmess["server"] = values["add"]
vmess["port"] = values["port"] vmess["port"] = values["port"]
vmess["uuid"] = values["id"] vmess["uuid"] = values["id"]
if alterId, ok := values["aid"]; ok { vmess["alterId"] = values["aid"]
vmess["alterId"] = alterId vmess["cipher"] = "auto"
} else {
vmess["alterId"] = 0
}
vmess["udp"] = true vmess["udp"] = true
vmess["xudp"] = true
vmess["tls"] = false
vmess["skip-cert-verify"] = false vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto" sni := values["sni"]
if cipher, ok := values["scy"].(string); ok && cipher != "" { if sni != "" {
vmess["cipher"] = cipher vmess["sni"] = sni
} }
if sni, ok := values["sni"].(string); ok && sni != "" { host := values["host"]
vmess["servername"] = sni network := strings.ToLower(values["net"].(string))
}
network, ok := values["net"].(string) vmess["network"] = network
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) tls := strings.ToLower(values["tls"].(string))
if ok { if tls != "" && tls != "0" && tls != "null" {
tls = strings.ToLower(tls) if host != nil {
if strings.HasSuffix(tls, "tls") { vmess["servername"] = host
vmess["tls"] = true
}
if alpn, ok := values["alpn"].(string); ok {
vmess["alpn"] = strings.Split(alpn, ",")
} }
vmess["tls"] = true
} }
switch network { switch network {
case "http": case "http":
headers := make(map[string]any) headers := make(map[string]any)
httpOpts := make(map[string]any) httpOpts := make(map[string]any)
if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host} // headers["Host"] = RandHost()
} headers["User-Agent"] = RandUserAgent()
httpOpts["path"] = []string{"/"} httpOpts["method"] = values["method"]
if path, ok := values["path"].(string); ok && path != "" { httpOpts["path"] = values["path"]
httpOpts["path"] = []string{path}
}
httpOpts["headers"] = headers httpOpts["headers"] = headers
vmess["http-opts"] = httpOpts vmess["http-opts"] = httpOpts
@@ -323,49 +278,26 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "h2": case "h2":
headers := make(map[string]any) headers := make(map[string]any)
h2Opts := make(map[string]any) h2Opts := make(map[string]any)
if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host}
}
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
h2Opts["path"] = values["path"] h2Opts["path"] = values["path"]
h2Opts["headers"] = headers h2Opts["headers"] = headers
vmess["h2-opts"] = h2Opts vmess["h2-opts"] = h2Opts
case "ws", "httpupgrade": case "ws":
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
wsOpts["path"] = "/"
if host, ok := values["host"].(string); ok && host != "" { headers["Host"] = RandHost()
headers["Host"] = host headers["User-Agent"] = RandUserAgent()
}
if path, ok := values["path"].(string); ok && path != "" { if values["path"] != nil {
path := path wsOpts["path"] = values["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 wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts vmess["ws-opts"] = wsOpts
case "grpc": case "grpc":
@@ -386,7 +318,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
port := urlSS.Port() port := urlSS.Port()
if port == "" { if port == "" {
dcBuf, err := encRaw.DecodeString(urlSS.Host) dcBuf, err := enc.DecodeString(urlSS.Host)
if err != nil { if err != nil {
continue continue
} }
@@ -398,28 +330,23 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
} }
var ( var (
cipherRaw = urlSS.User.Username() cipher = urlSS.User.Username()
cipher string password string
password string
) )
cipher = cipherRaw
if password, found = urlSS.User.Password(); !found { if password, found = urlSS.User.Password(); !found {
dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw) dcBuf, err := enc.DecodeString(cipher)
if err != nil { if err != nil {
dcBuf, _ = enc.DecodeString(cipherRaw) continue
} }
cipher, password, found = strings.Cut(string(dcBuf), ":") cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found { if !found {
continue 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 := make(map[string]any, 20)
ss["name"] = name ss["name"] = name
ss["type"] = scheme ss["type"] = scheme
@@ -427,36 +354,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
ss["port"] = urlSS.Port() ss["port"] = urlSS.Port()
ss["cipher"] = cipher ss["cipher"] = cipher
ss["password"] = password ss["password"] = password
query := urlSS.Query()
ss["udp"] = true 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) proxies = append(proxies, ss)
case "ssr": case "ssr":
dcBuf, err := encRaw.DecodeString(body) dcBuf, err := enc.DecodeString(body)
if err != nil { if err != nil {
continue continue
} }
@@ -523,6 +425,18 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
return proxies, nil return proxies, nil
} }
func urlSafe(data string) string {
return strings.ReplaceAll(strings.ReplaceAll(data, "+", "-"), "/", "_")
}
func decodeUrlSafe(data string) string {
dcBuf, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return ""
}
return string(dcBuf)
}
func uniqueName(names map[string]int, name string) string { func uniqueName(names map[string]int, name string) string {
if index, ok := names[name]; ok { if index, ok := names[name]; ok {
index++ index++

View File

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