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
552 changed files with 12961 additions and 40773 deletions

View File

@ -1,82 +0,0 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug] "
body:
- type: checkboxes
id: ensure
attributes:
label: Verify steps
description: "
在提交之前,请确认
Please verify that you've followed these steps
"
options:
- label: "
确保你使用的是**本仓库**最新的的 clash 或 clash Alpha 版本
Ensure you are using the latest version of Clash or Clash Premium 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: "
这是 Clash 核心的问题,并非我所使用的 Clash 衍生版本(如 OpenClash、KoolClash 等)的特定问题
This is an issue of the Clash core *per se*, not to the derivatives of Clash, like OpenClash or KoolClash.
"
required: true
- type: input
attributes:
label: Clash version
description: "use `clash -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: "Clash config"
description: "
在下方附上 Clash core 配置文件,请确保配置文件中没有敏感信息(比如:服务器地址,密码,端口等)
Paste the Clash 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: Clash log
description: "
在下方附上 Clash Core 的日志log level 使用 DEBUG
Paste the Clash core log below with the log level set to `DEBUG`.
"
- type: textarea
attributes:
label: Description
validations:
required: true

View File

@ -1,36 +0,0 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature] "
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: 请详细、清晰地表达你要提出的论述,例如这个问题如何影响到你?你想实现什么功能?目前 Clash 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:"* %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:"* %s by @%an" --grep="^fix" -i $version_range | sort -f | uniq >> release.md
echo "" >> release.md
echo "## Maintenance" >> release.md
git log --pretty=format:"* %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/Clash.Meta/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

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 clash.meta-darwin-arm64-cgo
elif [[ $FILENAME =~ "darwin-10.16-amd64" ]];then
echo "rename darwin-10.16-amd64 $FILENAME"
mv $FILENAME clash.meta-darwin-amd64-cgo
elif [[ $FILENAME =~ "windows-4.0-386" ]];then
echo "rename windows 386 $FILENAME"
mv $FILENAME clash.meta-windows-386-cgo.exe
elif [[ $FILENAME =~ "windows-4.0-amd64" ]];then
echo "rename windows amd64 $FILENAME"
mv $FILENAME clash.meta-windows-amd64-cgo.exe
elif [[ $FILENAME =~ "clash.meta-linux-arm-5" ]];then
echo "rename clash.meta-linux-arm-5 $FILENAME"
mv $FILENAME clash.meta-linux-armv5-cgo
elif [[ $FILENAME =~ "clash.meta-linux-arm-6" ]];then
echo "rename clash.meta-linux-arm-6 $FILENAME"
mv $FILENAME clash.meta-linux-armv6-cgo
elif [[ $FILENAME =~ "clash.meta-linux-arm-7" ]];then
echo "rename clash.meta-linux-arm-7 $FILENAME"
mv $FILENAME clash.meta-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

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,363 +0,0 @@
name: Build
on:
workflow_dispatch:
push:
paths-ignore:
- "docs/**"
- "README.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- Alpha
tags:
- "v*"
pull_request_target:
branches:
- Alpha
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
env:
REGISTRY: docker.io
jobs:
Build:
permissions: write-all
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job:
- {
type: "WithoutCGO",
target: "linux-amd64 linux-amd64-compatible",
id: "1",
}
- {
type: "WithoutCGO",
target: "linux-armv5 linux-armv6 linux-armv7",
id: "2",
}
- {
type: "WithoutCGO",
target: "linux-arm64 linux-mips64 linux-mips64le",
id: "3",
}
- {
type: "WithoutCGO",
target: "linux-mips-softfloat linux-mips-hardfloat linux-mipsle-softfloat linux-mipsle-hardfloat",
id: "4",
}
- { type: "WithoutCGO", target: "linux-386 linux-riscv64", id: "5" }
- {
type: "WithoutCGO",
target: "freebsd-386 freebsd-amd64 freebsd-arm64",
id: "6",
}
- {
type: "WithoutCGO",
target: "windows-amd64-compatible windows-amd64 windows-386",
id: "7",
}
- {
type: "WithoutCGO",
target: "windows-arm64 windows-arm32v7",
id: "8",
}
- {
type: "WithoutCGO",
target: "darwin-amd64 darwin-arm64 android-arm64",
id: "9",
}
- { type: "WithCGO", target: "windows/*", id: "1" }
- { type: "WithCGO", target: "linux/386", id: "2" }
- { type: "WithCGO", target: "linux/amd64", id: "3" }
- { type: "WithCGO", target: "linux/arm64,linux/riscv64", id: "4" }
- { type: "WithCGO", target: "linux/arm,", id: "5" }
- { type: "WithCGO", target: "linux/arm-6,linux/arm-7", id: "6" }
- { type: "WithCGO", target: "linux/mips,linux/mipsle", id: "7" }
- { type: "WithCGO", target: "linux/mips64", id: "8" }
- { type: "WithCGO", target: "linux/mips64le", id: "9" }
- { type: "WithCGO", target: "darwin-10.16/*", id: "10" }
- { type: "WithCGO", target: "android", id: "11" }
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Set variables
run: echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Alpha'}}
run: echo "VERSION=alpha-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='Meta'}}
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set variables
if: ${{github.ref_name=='' || github.ref_type=='tag'}}
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV
shell: bash
- name: Set ENV
run: |
sudo timedatectl set-timezone "Asia/Shanghai"
echo "NAME=clash.meta" >> $GITHUB_ENV
echo "REPO=${{ github.repository }}" >> $GITHUB_ENV
echo "ShortSHA=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV
echo "BUILDTIME=$(date)" >> $GITHUB_ENV
echo "BRANCH=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_ENV
shell: bash
- name: Set ENV
run: |
echo "TAGS=with_gvisor,with_lwip" >> $GITHUB_ENV
echo "LDFLAGS=-X 'github.com/Dreamacro/clash/constant.Version=${VERSION}' -X 'github.com/Dreamacro/clash/constant.BuildTime=${BUILDTIME}' -w -s -buildid=" >> $GITHUB_ENV
shell: bash
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: "1.21"
check-latest: true
- name: Test
if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }}
run: |
go test ./...
- name: Build WithoutCGO
if: ${{ matrix.job.type=='WithoutCGO' }}
env:
NAME: Clash.Meta
BINDIR: bin
run: make -j$(($(nproc) + 1)) ${{ matrix.job.target }}
- uses: nttld/setup-ndk@v1
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
id: setup-ndk
with:
ndk-version: r26
add-to-path: false
local-cache: true
- name: Build Android
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir bin
CC=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang
CGO_ENABLED=1 CC=${CC} GOARCH=arm64 GOOS=android go build -tags ${TAGS} -trimpath -ldflags "${LDFLAGS}" -o bin/${NAME}-android-arm64
- name: Set up xgo
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
run: |
docker pull techknowlogick/xgo:latest
go install src.techknowlogick.com/xgo@latest
- name: Build by xgo
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
mkdir bin
xgo --targets="${{ matrix.job.target }}" --tags="${TAGS}" -ldflags="${LDFLAGS}" --out bin/${NAME} ./
- name: Rename
if: ${{ matrix.job.type=='WithCGO' }}
run: |
cd bin
ls -la
cp ../.github/rename-cgo.sh ./
bash ./rename-cgo.sh
rm ./rename-cgo.sh
ls -la
cd ..
- name: Zip
if: ${{ success() }}
run: |
cd bin
ls -la
chmod +x *
cp ../.github/release.sh ./
bash ./release.sh
rm ./release.sh
ls -la
cd ..
- name: Save version
run: echo ${VERSION} > bin/version.txt
shell: bash
- uses: actions/upload-artifact@v3
if: ${{ success() }}
with:
name: artifact
path: bin/
Upload-Prerelease:
permissions: write-all
if: ${{ github.ref_type=='branch' && github.event_name != 'pull_request' }}
needs: [Build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Delete current release assets
uses: 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.0.6
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/Clash.Meta/wiki/FAQ)
[查看文档 / 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.ref_type=='tag' }}
needs: [Build]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get tags
run: |
echo "CURRENTVERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
git fetch --tags
echo "PREVERSION=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_ENV
- name: Generate release notes
run: |
cp ./.github/genReleaseNote.sh ./
bash ./genReleaseNote.sh -v ${PREVERSION}...${CURRENTVERSION}
rm ./genReleaseNote.sh
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Upload Release
uses: softprops/action-gh-release@v1
if: ${{ success() }}
with:
tag_name: ${{ github.ref_name }}
files: bin/*
generate_release_notes: true
body_path: release.md
Docker:
if: ${{ github.event_name != 'pull_request' }}
permissions: write-all
needs: [Build]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/download-artifact@v3
with:
name: artifact
path: bin/
- name: Display structure of downloaded files
run: ls -R
working-directory: bin
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
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@v4
with:
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
- name: Show files
run: |
ls .
ls bin/
- name: Log into registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
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@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name != 'pull_request' }}
platforms: |
linux/386
linux/amd64
linux/arm64/v8
linux/arm/v7
# linux/riscv64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,15 +0,0 @@
name: Delete old workflow runs
on:
schedule:
- cron: "0 0 * * SUN"
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: GitRML/delete-workflow-runs@main
with:
token: ${{ secrets.AUTH_PAT }}
repository: ${{ github.repository }}
retain_days: 30

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

3
.gitignore vendored
View File

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

View File

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

View File

@ -1,27 +1,26 @@
FROM alpine:latest as builder
ARG TARGETPLATFORM
RUN echo "I'm building for $TARGETPLATFORM"
FROM golang:alpine as builder
RUN apk add --no-cache gzip && \
RUN apk add --no-cache make git && \
mkdir /clash-config && \
wget -O /clash-config/geoip.metadb https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb && \
wget -O /clash-config/geosite.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat && \
wget -O /clash-config/geoip.dat https://fastly.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat
wget -O /clash-config/Country.mmdb https://raw.githubusercontent.com/Loyalsoldier/geoip/release/Country.mmdb && \
wget -O /clash-config/geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.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 /clash/file-name.sh
WORKDIR /clash
COPY bin/ bin/
RUN FILE_NAME=`sh file-name.sh` && echo $FILE_NAME && \
FILE_NAME=`ls bin/ | egrep "$FILE_NAME.*"|awk NR==1` && echo $FILE_NAME && \
mv bin/$FILE_NAME clash.gz && gzip -d clash.gz && echo "$FILE_NAME" > /clash-config/test
FROM alpine:latest
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/clash/"]
COPY --from=builder /clash-config/ /root/.config/clash/
COPY --from=builder /clash/clash /clash
COPY --from=builder /clash /clash
RUN chmod +x /clash
ENTRYPOINT [ "/clash" ]
ENTRYPOINT [ "/clash" ]

View File

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

177
README.md
View File

@ -24,55 +24,17 @@
- 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.
- Rules based off domains, GEOIP, IPCIDR or Process to forward packets to different nodes
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node
based off latency
- Remote providers, allowing users to get node lists remotely instead of hard-coding in config
- Remote groups allow users to implement powerful rules. Supports automatic fallback, load balancing or auto select node based off latency
- Remote providers, allowing users to get node lists remotely instead of hardcoding in config
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
- Comprehensive HTTP RESTful API controller
## Dashboard
## Getting Started
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
We made an official web dashboard providing first class support for this project, check it out
at [metacubexd](https://github.com/MetaCubeX/metacubexd)
## Advanced usage for this branch
## Wiki
Configuration examples can be found
at [/docs/config.yaml](https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml), while documentation can be
found [Clash.Meta Wiki](https://clash-meta.wiki).
## Build
You should install [golang](https://go.dev) first.
Then get the source code of Clash.Meta:
```shell
git clone https://github.com/MetaCubeX/Clash.Meta.git
cd Clash.Meta && go mod download
```
If you can't visit GitHub, you should set proxy first:
```shell
go env -w GOPROXY=https://goproxy.io,direct
```
Now you can build it:
```shell
go build
```
If you need gvisor for tun stack, build with:
```shell
go build -tags with_gvisor
```
<!-- ## Advanced usage of this fork -->
<!-- ### DNS configuration
### DNS configuration
Support `geosite` with `fallback-filter`.
@ -82,6 +44,7 @@ Support resolve ip with a `Proxy Tunnel`.
```yaml
proxy-groups:
- name: DNS
type: url-test
use:
@ -90,7 +53,6 @@ proxy-groups:
interval: 180
lazy: true
```
```yaml
dns:
enable: true
@ -106,12 +68,12 @@ dns:
- 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"
- '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.
- gfw # `geosite` filter only use fallback server to resolve ip, prevent DNS leaks to unsafe DNS providers.
domain:
- +.example.com
ipcidr:
@ -128,30 +90,28 @@ Built-in [Wintun](https://www.wintun.net) driver.
# Enable the TUN listener
tun:
enable: true
stack: system # system/gvisor
dns-hijack:
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
@ -162,17 +122,18 @@ rules:
- 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
```
### Proxies configuration
Active health detection `urltest / fallback` (based on tcp handshake, multiple failures within a limited time will actively trigger health detection to use the node)
@ -181,17 +142,18 @@ Support `Policy Group Filter`
```yaml
proxy-groups:
- name: 🚀 HK Group
type: select
use:
- ALL
filter: "HK"
filter: 'HK'
- name: 🚀 US Group
type: select
use:
- ALL
filter: "US"
filter: 'US'
proxy-providers:
ALL:
@ -203,12 +165,14 @@ proxy-providers:
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"
@ -219,7 +183,7 @@ proxies:
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
@ -244,50 +208,12 @@ proxies:
network: grpc
servername: example.com # priority over wss host
# skip-cert-verify: true
grpc-opts:
grpc-opts:
grpc-service-name: grpcname
```
Support outbound transport protocol `Wireguard`
```yaml
proxies:
- name: "wg"
type: wireguard
server: 162.159.192.1
port: 2480
ip: 172.16.0.2
ipv6: fd01:5ca1:ab1e:80fa:ab85:6eea:213f:f4a5
private-key: eCtXsJZ27+4PbhDkHnB923tkUn2Gj59wZw5wFA75MnU=
public-key: Cr8hWlKvtDt7nrvf+f0brNQQzabAqrjfBvas9pmowjo=
udp: true
```
Support outbound transport protocol `Tuic`
```yaml
proxies:
- name: "tuic"
server: www.example.com
port: 10443
type: tuic
token: TOKEN
# ip: 127.0.0.1 # for overwriting the DNS lookup result of the server address set in option 'server'
# heartbeat-interval: 10000
# alpn: [h3]
# disable-sni: true
reduce-rtt: true
# request-timeout: 8000
udp-relay-mode: native # Available: "native", "quic". Default: "native"
# congestion-controller: bbr # Available: "cubic", "new_reno", "bbr". Default: "cubic"
# max-udp-relay-packet-size: 1500
# fast-open: true
# skip-cert-verify: true
``` -->
### IPTABLES configuration
Work on Linux OS which supported `iptables`
Work on Linux OS who's supported `iptables`
```yaml
# Enable the TPROXY listener
@ -298,15 +224,17 @@ iptables:
inbound-interface: eth0 # detect the inbound interface, default is 'lo'
```
### General installation guide for Linux
- Create user given name `clash-meta`
### General installation guide for Linux
+ Create user given name `clash-meta`
- Download and decompress pre-built binaries from [releases](https://github.com/MetaCubeX/Clash.Meta/releases)
+ 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
- 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.
@ -323,8 +251,8 @@ User=clash-meta
Group=clash-meta
LimitNPROC=500
LimitNOFILE=1000000
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
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
@ -332,37 +260,36 @@ ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
[Install]
WantedBy=multi-user.target
```
Launch clash-meta daemon on system startup with:
Launch clashd on system startup with:
```shell
$ systemctl enable Clash-Meta
```
Launch clash-meta daemon immediately with:
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
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)
## Debugging
Check [wiki](https://github.com/MetaCubeX/Clash.Meta/wiki/How-to-use-debug-api) to get an instruction on using debug
API.
## Credits
- [Dreamacro/clash](https://github.com/Dreamacro/clash)
- [SagerNet/sing-box](https://github.com/SagerNet/sing-box)
- [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
- [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
- [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
- [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
* [Dreamacro/clash](https://github.com/Dreamacro/clash)
* [riobard/go-shadowsocks2](https://github.com/riobard/go-shadowsocks2)
* [v2ray/v2ray-core](https://github.com/v2ray/v2ray-core)
* [WireGuard/wireguard-go](https://github.com/WireGuard/wireguard-go)
* [yaling888/clash-plus-pro](https://github.com/yaling888/clash)
## License

View File

@ -3,42 +3,26 @@ package adapter
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
"strings"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/queue"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/puzpuzpuz/xsync/v2"
"go.uber.org/atomic"
)
var UnifiedDelay = atomic.NewBool(false)
const (
defaultHistoriesNum = 10
)
type extraProxyState struct {
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
}
type Proxy struct {
C.ProxyAdapter
history *queue.Queue[C.DelayHistory]
alive *atomic.Bool
url string
extra *xsync.MapOf[string, *extraProxyState]
}
// Alive implements C.Proxy
@ -46,15 +30,6 @@ func (p *Proxy) Alive() bool {
return p.alive.Load()
}
// AliveForTestUrl implements C.Proxy
func (p *Proxy) AliveForTestUrl(url string) bool {
if state, ok := p.extra.Load(url); ok {
return state.alive.Load()
}
return p.alive.Load()
}
// Dial implements C.Proxy
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
@ -65,6 +40,11 @@ func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
// DialContext implements C.ProxyAdapter
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
conn, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
wasCancel := false
if err != nil {
wasCancel = strings.Contains(err.Error(), "operation was canceled")
}
p.alive.Store(err == nil || wasCancel)
return conn, err
}
@ -78,6 +58,7 @@ func (p *Proxy) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
// ListenPacketContext implements C.ProxyAdapter
func (p *Proxy) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
p.alive.Store(err == nil)
return pc, err
}
@ -88,51 +69,9 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
// DelayHistoryForTestUrl implements C.Proxy
func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
var queueM []C.DelayHistory
if state, ok := p.extra.Load(url); ok {
queueM = state.history.Copy()
}
if queueM == nil {
queueM = p.history.Copy()
}
histories := []C.DelayHistory{}
for _, item := range queueM {
histories = append(histories, item)
}
return histories
}
func (p *Proxy) ExtraDelayHistory() map[string][]C.DelayHistory {
extraHistory := map[string][]C.DelayHistory{}
p.extra.Range(func(k string, v *extraProxyState) bool {
testUrl := k
state := v
histories := []C.DelayHistory{}
queueM := state.history.Copy()
for _, item := range queueM {
histories = append(histories, item)
}
extraHistory[testUrl] = histories
return true
})
return extraHistory
}
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
// implements C.Proxy
func (p *Proxy) LastDelay() (delay uint16) {
@ -148,28 +87,6 @@ func (p *Proxy) LastDelay() (delay uint16) {
return history.Delay
}
// LastDelayForTestUrl implements C.Proxy
func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
var max uint16 = 0xffff
alive := p.alive.Load()
history := p.history.Last()
if state, ok := p.extra.Load(url); ok {
alive = state.alive.Load()
history = state.history.Last()
}
if !alive {
return max
}
if history.Delay == 0 {
return max
}
return history.Delay
}
// MarshalJSON implements C.ProxyAdapter
func (p *Proxy) MarshalJSON() ([]byte, error) {
inner, err := p.ProxyAdapter.MarshalJSON()
@ -180,64 +97,23 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
mapping := map[string]any{}
_ = json.Unmarshal(inner, &mapping)
mapping["history"] = p.DelayHistory()
mapping["extra"] = p.ExtraDelayHistory()
mapping["alive"] = p.Alive()
mapping["name"] = p.Name()
mapping["udp"] = p.SupportUDP()
mapping["xudp"] = p.SupportXUDP()
mapping["tfo"] = p.SupportTFO()
return json.Marshal(mapping)
}
// URLTest get the delay for the specified URL
// implements C.Proxy
func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store C.DelayHistoryStoreType) (t uint16, err error) {
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
defer func() {
alive := err == nil
store = p.determineFinalStoreType(store, url)
switch store {
case C.OriginalHistory:
p.alive.Store(alive)
record := C.DelayHistory{Time: time.Now()}
if alive {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum {
p.history.Pop()
}
// test URL configured by the proxy provider
if len(p.url) == 0 {
p.url = url
}
case C.ExtraHistory:
record := C.DelayHistory{Time: time.Now()}
if alive {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > defaultHistoriesNum {
p.history.Pop()
}
state, ok := p.extra.Load(url)
if !ok {
state = &extraProxyState{
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
}
p.extra.Store(url, state)
}
state.alive.Store(alive)
state.history.Put(record)
if state.history.Len() > defaultHistoriesNum {
state.history.Pop()
}
default:
log.Debugln("health check result will be discarded, url: %s alive: %t, delay: %d", url, alive, t)
p.alive.Store(err == nil)
record := C.DelayHistory{Time: time.Now()}
if err == nil {
record.Delay = t
}
p.history.Put(record)
if p.history.Len() > 10 {
p.history.Pop()
}
}()
@ -275,48 +151,31 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
}
client := http.Client{
Timeout: 30 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return
}
_ = resp.Body.Close()
if unifiedDelay {
second := time.Now()
start = time.Now()
resp, err = client.Do(req)
if err == nil {
_ = resp.Body.Close()
start = second
if err != nil {
return
}
}
if expectedStatus != nil && !expectedStatus.Check(uint16(resp.StatusCode)) {
// maybe another value should be returned for differentiation
err = errors.New("response status is inconsistent with the expected status")
}
_ = resp.Body.Close()
t = uint16(time.Since(start) / time.Millisecond)
return
}
func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{
ProxyAdapter: adapter,
history: queue.New[C.DelayHistory](defaultHistoriesNum),
alive: atomic.NewBool(true),
url: "",
extra: xsync.NewMapOf[*extraProxyState]()}
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
}
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
@ -337,36 +196,12 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return
}
}
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: uint16(uintPort),
AddrType: C.AtypDomainName,
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: port,
}
return
}
func (p *Proxy) determineFinalStoreType(store C.DelayHistoryStoreType, url string) C.DelayHistoryStoreType {
if store != C.DropHistory {
return store
}
if len(p.url) == 0 || url == p.url {
return C.OriginalHistory
}
if p.extra.Size() < 2*C.DefaultMaxHealthCheckUrlNum {
return C.ExtraHistory
}
_, ok := p.extra.Load(url)
if ok {
return C.ExtraHistory
}
return store
}

View File

@ -1,35 +0,0 @@
package inbound
import (
C "github.com/Dreamacro/clash/constant"
)
type Addition func(metadata *C.Metadata)
func (a Addition) Apply(metadata *C.Metadata) {
a(metadata)
}
func WithInName(name string) Addition {
return func(metadata *C.Metadata) {
metadata.InName = name
}
}
func 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package inbound
import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
M "github.com/sagernet/sing/common/metadata"
)
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
@ -17,26 +17,17 @@ func (s *PacketAdapter) Metadata() *C.Metadata {
}
// NewPacket is PacketAdapter generator
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type, additions ...Addition) C.PacketAdapter {
metadata := parseSocksAddr(target)
func NewPacket(target M.Socksaddr, packet C.UDPPacket, source C.Type) *PacketAdapter {
metadata := socksAddrToMetadata(target)
metadata.NetWork = C.UDP
metadata.Type = source
for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(packet.LocalAddr()); err == nil {
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if p, ok := packet.(C.UDPPacketInAddr); ok {
if ip, port, err := parseAddr(p.InAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
}
}
return &PacketAdapter{
packet,
metadata,
UDPPacket: packet,
metadata: metadata,
}
}

View File

@ -3,7 +3,6 @@ package inbound
import (
"net"
"net/netip"
"strconv"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
@ -11,40 +10,40 @@ import (
)
// NewSocket receive TCP inbound and return ConnContext
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type, additions ...Addition) *context.ConnContext {
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
metadata := parseSocksAddr(target)
metadata.NetWork = C.TCP
metadata.Type = source
for _, addition := range additions {
addition.Apply(metadata)
}
if ip, port, err := parseAddr(conn.RemoteAddr()); err == nil {
metadata.SrcIP = ip
metadata.SrcPort = port
}
if ip, port, err := parseAddr(conn.LocalAddr()); err == nil {
metadata.InIP = ip
metadata.InPort = port
remoteAddr := conn.RemoteAddr()
// Filter when net.Addr interface is nil
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, address string) *context.ConnContext {
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.DNSNormal
metadata.DNSMode = C.DNSMapping
metadata.Host = host
metadata.AddrType = C.AtypDomainName
metadata.Process = C.ClashName
if h, port, err := net.SplitHostPort(address); err == nil {
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
metadata.DstPort = uint16(port)
}
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
} else {
metadata.Host = h
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
}
}
}
}

View File

@ -1,7 +1,6 @@
package inbound
import (
"errors"
"net"
"net/http"
"net/netip"
@ -11,23 +10,42 @@ import (
"github.com/Dreamacro/clash/common/nnip"
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{}
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] {
case socks5.AtypDomainName:
// trim for FQDN
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4:
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip6.Unmap()
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv6len]))
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
}
return metadata
@ -43,48 +61,34 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737)
host = strings.TrimRight(host, ".")
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
}
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: netip.Addr{},
DstPort: uint16Port,
NetWork: C.TCP,
AddrType: C.AtypDomainName,
Host: host,
DstIP: netip.Addr{},
DstPort: port,
}
ip, err := netip.ParseAddr(host)
if err == nil {
switch {
case ip.Is6():
metadata.AddrType = C.AtypIPv6
default:
metadata.AddrType = C.AtypIPv4
}
metadata.DstIP = ip
}
return metadata
}
func parseAddr(addr net.Addr) (netip.Addr, uint16, error) {
// Filter when net.Addr interface is nil
if addr == nil {
return netip.Addr{}, 0, errors.New("nil addr")
}
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
ip, port, err := parseAddr(rawAddr.RawAddr())
if err == nil {
return ip, port, err
}
}
addrStr := addr.String()
host, port, err := net.SplitHostPort(addrStr)
func parseAddr(addr string) (netip.Addr, string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return netip.Addr{}, 0, err
}
var uint16Port uint16
if port, err := strconv.ParseUint(port, 10, 16); err == nil {
uint16Port = uint16(port)
return netip.Addr{}, "", err
}
ip, err := netip.ParseAddr(host)
return ip, uint16Port, err
return ip, port, err
}

View File

@ -3,28 +3,27 @@ package outbound
import (
"context"
"encoding/json"
"errors"
"net"
"strings"
"syscall"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/gofrs/uuid"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Base struct {
name string
addr string
iface string
tp C.AdapterType
udp bool
xudp bool
tfo bool
mpTcp bool
rmark int
id string
prefer C.DNSPrefer
name string
addr string
iface string
tp C.AdapterType
udp bool
rmark int
id string
}
// Name implements C.ProxyAdapter
@ -35,7 +34,12 @@ func (b *Base) Name() string {
// Id implements C.ProxyAdapter
func (b *Base) Id() string {
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
@ -46,33 +50,23 @@ func (b *Base) Type() C.AdapterType {
return b.tp
}
// StreamConnContext implements C.ProxyAdapter
func (b *Base) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, C.ErrNotSupport
// StreamConn implements C.ProxyAdapter
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
return c, errors.New("no support")
}
func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return nil, C.ErrNotSupport
}
// DialContextWithDialer implements C.ProxyAdapter
func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
return nil, C.ErrNotSupport
return nil, errors.New("no support")
}
// ListenPacketContext implements C.ProxyAdapter
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
func (b *Base) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, C.ErrNotSupport
}
// SupportWithDialer implements C.ProxyAdapter
func (b *Base) SupportWithDialer() C.NetWork {
return C.InvalidNet
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return nil, errors.New("no support")
}
// SupportUOT implements C.ProxyAdapter
@ -85,21 +79,6 @@ func (b *Base) SupportUDP() bool {
return b.udp
}
// SupportXUDP implements C.ProxyAdapter
func (b *Base) SupportXUDP() bool {
return b.xudp
}
// SupportTFO implements C.ProxyAdapter
func (b *Base) SupportTFO() bool {
return b.tfo
}
// IsL3Protocol implements C.ProxyAdapter
func (b *Base) IsL3Protocol(metadata *C.Metadata) bool {
return false
}
// MarshalJSON implements C.ProxyAdapter
func (b *Base) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{
@ -114,7 +93,7 @@ func (b *Base) Addr() string {
}
// 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
}
@ -128,36 +107,12 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
opts = append(opts, dialer.WithRoutingMark(b.rmark))
}
switch b.prefer {
case C.IPv4Only:
opts = append(opts, dialer.WithOnlySingleStack(true))
case C.IPv6Only:
opts = append(opts, dialer.WithOnlySingleStack(false))
case C.IPv4Prefer:
opts = append(opts, dialer.WithPreferIPv4())
case C.IPv6Prefer:
opts = append(opts, dialer.WithPreferIPv6())
default:
}
if b.tfo {
opts = append(opts, dialer.WithTFO(true))
}
if b.mpTcp {
opts = append(opts, dialer.WithMPTCP(true))
}
return opts
}
type BasicOption struct {
TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"`
Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"`
RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"`
IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"`
DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy
}
type BaseOption struct {
@ -165,31 +120,23 @@ type BaseOption struct {
Addr string
Type C.AdapterType
UDP bool
XUDP bool
TFO bool
MPTCP bool
Interface string
RoutingMark int
Prefer C.DNSPrefer
}
func NewBase(opt BaseOption) *Base {
return &Base{
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
xudp: opt.XUDP,
tfo: opt.TFO,
mpTcp: opt.MPTCP,
iface: opt.Interface,
rmark: opt.RoutingMark,
prefer: opt.Prefer,
name: opt.Name,
addr: opt.Addr,
tp: opt.Type,
udp: opt.UDP,
iface: opt.Interface,
rmark: opt.RoutingMark,
}
}
type conn struct {
N.ExtendedConn
net.Conn
chain C.Chain
actualRemoteDestination string
}
@ -208,30 +155,14 @@ func (c *conn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func (c *conn) Upstream() any {
return c.ExtendedConn
}
func (c *conn) WriterReplaceable() bool {
return true
}
func (c *conn) ReaderReplaceable() bool {
return true
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
}
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
return &conn{c, []string{a.Name()}, parseRemoteDestination(a.Addr())}
}
type packetConn struct {
N.EnhancePacketConn
net.PacketConn
nc N.PacketConn
chain C.Chain
adapterName string
connID string
actualRemoteDestination string
}
@ -249,29 +180,22 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
c.chain = append(c.chain, a.Name())
}
func (c *packetConn) LocalAddr() net.Addr {
lAddr := c.EnhancePacketConn.LocalAddr()
return N.NewCustomAddr(c.adapterName, c.connID, lAddr) // make quic-go's connMultiplexer happy
func (c *packetConn) ReadPacket(buffer *buf.Buffer) (addr M.Socksaddr, err error) {
return c.nc.ReadPacket(buffer)
}
func (c *packetConn) Upstream() any {
return c.EnhancePacketConn
}
func (c *packetConn) WriterReplaceable() bool {
return true
}
func (c *packetConn) ReaderReplaceable() bool {
return true
func (c *packetConn) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
return c.nc.WritePacket(buffer, addr)
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
var nc N.PacketConn
if n, isNc := pc.(N.PacketConn); isNc {
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 {

View File

@ -2,12 +2,9 @@ package outbound
import (
"context"
"errors"
"net/netip"
"net"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
)
@ -15,61 +12,37 @@ type Direct struct {
*Base
}
type DirectOption struct {
BasicOption
Name string `proxy:"name"`
}
// DialContext implements C.ProxyAdapter
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
opts = append(opts, dialer.WithDirect())
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress(), d.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
return NewConn(c, d), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DefaultResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
pc, err := dialer.NewDialer(d.Base.DialOptions(opts...)...).ListenPacket(ctx, "udp", "", netip.AddrPortFrom(metadata.DstIP, metadata.DstPort))
opts = append(opts, dialer.WithDirect())
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
return newPacketConn(pc, d), nil
return newPacketConn(&directPacketConn{pc}, d), nil
}
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),
},
}
type directPacketConn struct {
net.PacketConn
}
func NewDirect() *Direct {
return &Direct{
Base: &Base{
name: "DIRECT",
tp: C.Direct,
udp: true,
prefer: C.DualStack,
name: "DIRECT",
tp: C.Direct,
udp: true,
},
}
}
@ -77,10 +50,9 @@ func NewDirect() *Direct {
func NewCompatible() *Direct {
return &Direct{
Base: &Base{
name: "COMPATIBLE",
tp: C.Compatible,
udp: true,
prefer: C.DualStack,
name: "COMPATIBLE",
tp: C.Compatible,
udp: true,
},
}
}

View File

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

View File

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

View File

@ -1,157 +0,0 @@
package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"runtime"
"strconv"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant"
tuicCommon "github.com/Dreamacro/clash/transport/tuic/common"
"github.com/metacubex/sing-quic/hysteria2"
M "github.com/sagernet/sing/common/metadata"
)
func init() {
hysteria2.SetCongestionController = tuicCommon.SetCongestionController
}
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"`
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"`
}
func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := h.Base.DialOptions(opts...)
h.dialer.SetDialer(dialer.NewDialer(options...))
c, err := h.client.DialConn(ctx, M.ParseSocksaddr(metadata.RemoteAddress()))
if err != nil {
return nil, err
}
return NewConn(CN.NewRefConn(c, h), h), nil
}
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := h.Base.DialOptions(opts...)
h.dialer.SetDialer(dialer.NewDialer(options...))
pc, err := h.client.ListenPacket(ctx)
if err != nil {
return nil, err
}
if pc == nil {
return nil, errors.New("packetConn is nil")
}
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil
}
func closeHysteria2(h *Hysteria2) {
if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed"))
}
}
func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
var salamanderPassword string
if len(option.Obfs) > 0 {
if option.ObfsPassword == "" {
return nil, errors.New("missing obfs password")
}
switch option.Obfs {
case hysteria2.ObfsTypeSalamander:
salamanderPassword = option.ObfsPassword
default:
return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs)
}
}
serverName := option.Server
if option.SNI != "" {
serverName = option.SNI
}
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if len(option.ALPN) > 0 {
tlsConfig.NextProtos = option.ALPN
}
singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer())
clientOptions := hysteria2.ClientOptions{
Context: context.TODO(),
Dialer: singDialer,
ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)),
SendBPS: StringToBps(option.Up),
ReceiveBPS: StringToBps(option.Down),
SalamanderPassword: salamanderPassword,
Password: option.Password,
TLSConfig: tlsConfig,
UDPDisabled: false,
CWND: option.CWND,
}
client, err := hysteria2.NewClient(clientOptions)
if err != nil {
return nil, err
}
outbound := &Hysteria2{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Hysteria2,
udp: true,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
client: client,
dialer: singDialer,
}
runtime.SetFinalizer(outbound, closeHysteria2)
return outbound, nil
}

View File

@ -1,35 +0,0 @@
package outbound
import (
"encoding/base64"
"encoding/hex"
"errors"
tlsC "github.com/Dreamacro/clash/component/tls"
"golang.org/x/crypto/curve25519"
)
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)
n, err := base64.RawURLEncoding.Decode(config.PublicKey[:], []byte(o.PublicKey))
if err != nil || n != curve25519.ScalarSize {
return nil, errors.New("invalid REALITY public key")
}
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,7 +6,6 @@ import (
"net"
"time"
"github.com/Dreamacro/clash/common/buf"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
)
@ -17,21 +16,20 @@ type Reject struct {
// DialContext implements C.ProxyAdapter
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return NewConn(nopConn{}, r), nil
return NewConn(&nopConn{}, r), nil
}
// ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return newPacketConn(nopPacketConn{}, r), nil
return newPacketConn(&nopPacketConn{}, r), nil
}
func NewReject() *Reject {
return &Reject{
Base: &Base{
name: "REJECT",
tp: C.Reject,
udp: true,
prefer: C.DualStack,
name: "REJECT",
tp: C.Reject,
udp: true,
},
}
}
@ -39,50 +37,36 @@ func NewReject() *Reject {
func NewPass() *Reject {
return &Reject{
Base: &Base{
name: "PASS",
tp: C.Pass,
udp: true,
prefer: C.DualStack,
name: "PASS",
tp: C.Pass,
udp: true,
},
}
}
type nopConn struct{}
func (rw nopConn) Read(b []byte) (int, error) {
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) {
func (rw *nopConn) Write(b []byte) (int, error) {
return 0, io.EOF
}
func (rw nopConn) WriteBuffer(buffer *buf.Buffer) error {
return io.EOF
}
func (rw nopConn) Close() error { return nil }
func (rw nopConn) LocalAddr() net.Addr { return nil }
func (rw nopConn) RemoteAddr() net.Addr { return nil }
func (rw nopConn) SetDeadline(time.Time) error { return nil }
func (rw nopConn) SetReadDeadline(time.Time) error { return nil }
func (rw nopConn) SetWriteDeadline(time.Time) error { return nil }
var udpAddrIPv4Unspecified = &net.UDPAddr{IP: net.IPv4zero, Port: 0}
func (rw *nopConn) Close() error { return nil }
func (rw *nopConn) LocalAddr() net.Addr { return nil }
func (rw *nopConn) RemoteAddr() net.Addr { return nil }
func (rw *nopConn) SetDeadline(time.Time) error { return nil }
func (rw *nopConn) SetReadDeadline(time.Time) error { return nil }
func (rw *nopConn) SetWriteDeadline(time.Time) error { return nil }
type nopPacketConn struct{}
func (npc nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc nopPacketConn) WaitReadFrom() ([]byte, func(), net.Addr, error) {
return nil, nil, nil, io.EOF
}
func (npc nopPacketConn) Close() error { return nil }
func (npc nopPacketConn) LocalAddr() net.Addr { return udpAddrIPv4Unspecified }
func (npc nopPacketConn) SetDeadline(time.Time) error { return nil }
func (npc nopPacketConn) SetReadDeadline(time.Time) error { return nil }
func (npc nopPacketConn) SetWriteDeadline(time.Time) error { return nil }
func (npc *nopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { return len(b), nil }
func (npc *nopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, io.EOF }
func (npc *nopPacketConn) Close() error { return nil }
func (npc *nopPacketConn) LocalAddr() net.Addr { return &net.UDPAddr{IP: net.IPv4zero, Port: 0} }
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 }

View File

@ -7,49 +7,38 @@ import (
"net"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/restls"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
shadowtls "github.com/Dreamacro/clash/transport/sing-shadowtls"
"github.com/Dreamacro/clash/transport/socks5"
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
restlsC "github.com/3andne/restls-client-go"
shadowsocks "github.com/metacubex/sing-shadowsocks2"
"github.com/sagernet/sing-shadowsocks"
"github.com/sagernet/sing-shadowsocks/shadowimpl"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
)
type ShadowSocks struct {
*Base
method shadowsocks.Method
option *ShadowSocksOption
// obfs
obfsMode string
obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option
shadowTLSOption *shadowtls.ShadowTLSOption
restlsConfig *restlsC.Config
obfsMode string
obfsOption *simpleObfsOption
v2rayOption *v2rayObfs.Option
}
type ShadowSocksOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
UDPOverTCP bool `proxy:"udp-over-tcp,omitempty"`
UDPOverTCPVersion int `proxy:"udp-over-tcp-version,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Plugin string `proxy:"plugin,omitempty"`
PluginOpts map[string]any `proxy:"plugin-opts,omitempty"`
}
type simpleObfsOption struct {
@ -62,30 +51,13 @@ type v2rayObfsOption struct {
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"`
}
type shadowTLSOption struct {
Password string `obfs:"password"`
Host string `obfs:"host"`
Fingerprint string `obfs:"fingerprint,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
Version int `obfs:"version,omitempty"`
}
type restlsOption struct {
Password string `obfs:"password"`
Host string `obfs:"host"`
VersionHint string `obfs:"version-hint"`
RestlsScript string `obfs:"restls-script,omitempty"`
}
// StreamConnContext implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
useEarly := false
// StreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
switch ss.obfsMode {
case "tls":
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
@ -94,146 +66,53 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket":
var err error
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
case shadowtls.Mode:
var err error
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
if err != nil {
return nil, err
}
useEarly = true
case restls.Mode:
var err error
c, err = restls.NewRestls(ctx, c, ss.restlsConfig)
if err != nil {
return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err)
}
useEarly = true
}
useEarly = useEarly || N.NeedHandshake(c)
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion))
if useEarly {
return ss.method.DialEarlyConn(c, uotDestination), nil
} else {
return ss.method.DialConn(c, uotDestination)
}
}
if useEarly {
return ss.method.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
} else {
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
}
return ss.method.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
}
// DialContext implements C.ProxyAdapter
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ss.DialContextWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(ss.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = ss.StreamConnContext(ctx, c, metadata)
c, err = ss.StreamConn(c, metadata)
return NewConn(c, ss), err
}
// ListenPacketContext implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return ss.ListenPacketWithDialer(ctx, dialer.NewDialer(ss.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(ss.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ss.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
if ss.option.UDPOverTCP {
tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
return ss.ListenPacketOnStreamConn(ctx, tcpConn, metadata)
}
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer)
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
addr, err := resolveUDPAddr("udp", ss.addr)
if err != nil {
pc.Close()
return nil, err
}
pc = ss.method.DialPacketConn(N.NewBindPacketConn(pc, addr))
pc = ss.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
return newPacketConn(pc, ss), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (ss *ShadowSocks) SupportWithDialer() C.NetWork {
return C.ALLNet
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP {
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), ss), nil
} else {
return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), ss), nil
}
}
return nil, C.ErrNotSupport
}
// SupportUOT implements C.ProxyAdapter
func (ss *ShadowSocks) SupportUOT() bool {
return ss.option.UDPOverTCP
}
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{
Password: option.Password,
})
method, err := shadowimpl.FetchMethod(option.Cipher, option.Password)
if err != nil {
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
}
var v2rayOption *v2rayObfs.Option
var obfsOption *simpleObfsOption
var shadowTLSOpt *shadowtls.ShadowTLSOption
var restlsConfig *restlsC.Config
obfsMode := ""
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
@ -269,63 +148,54 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify
}
} else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode
opt := &shadowTLSOption{
Version: 2,
}
if err := decoder.Decode(option.PluginOpts, opt); err != nil {
return nil, fmt.Errorf("ss %s initialize shadow-tls-plugin error: %w", addr, err)
}
shadowTLSOpt = &shadowtls.ShadowTLSOption{
Password: opt.Password,
Host: opt.Host,
Fingerprint: opt.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
SkipCertVerify: opt.SkipCertVerify,
Version: opt.Version,
}
} else if option.Plugin == restls.Mode {
obfsMode = restls.Mode
restlsOpt := &restlsOption{}
if err := decoder.Decode(option.PluginOpts, restlsOpt); err != nil {
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
}
restlsConfig, err = restlsC.NewRestlsConfig(restlsOpt.Host, restlsOpt.Password, restlsOpt.VersionHint, restlsOpt.RestlsScript, option.ClientFingerprint)
if err != nil {
return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err)
}
}
switch option.UDPOverTCPVersion {
case uot.Version, uot.LegacyVersion:
case 0:
option.UDPOverTCPVersion = uot.LegacyVersion
default:
return nil, fmt.Errorf("ss %s unknown udp over tcp protocol version: %d", addr, option.UDPOverTCPVersion)
}
return &ShadowSocks{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: addr,
tp: C.Shadowsocks,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
method: method,
option: &option,
obfsMode: obfsMode,
v2rayOption: v2rayOption,
obfsOption: obfsOption,
shadowTLSOption: shadowTLSOpt,
restlsConfig: restlsConfig,
obfsMode: obfsMode,
v2rayOption: v2rayOption,
obfsOption: obfsOption,
}, 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 (
"context"
"errors"
"fmt"
"net"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/shadowsocks/core"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowaead"
"github.com/Dreamacro/clash/transport/shadowsocks/shadowstream"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/ssr/obfs"
"github.com/Dreamacro/clash/transport/ssr/protocol"
)
type ShadowSocksR struct {
*Base
option *ShadowSocksROption
cipher core.Cipher
obfs obfs.Obfs
protocol protocol.Protocol
@ -41,8 +36,8 @@ type ShadowSocksROption struct {
UDP bool `proxy:"udp,omitempty"`
}
// StreamConnContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// StreamConn implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c)
var (
@ -65,62 +60,34 @@ func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, meta
// DialContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
return ssr.DialContextWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(ssr.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
c, err := dialer.DialContext(ctx, "tcp", ssr.addr, ssr.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = ssr.StreamConnContext(ctx, c, metadata)
c, err = ssr.StreamConn(c, metadata)
return NewConn(c, ssr), err
}
// ListenPacketContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return ssr.ListenPacketWithDialer(ctx, dialer.NewDialer(ssr.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(ssr.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(ssr.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer)
pc, err := dialer.ListenPacket(ctx, "udp", "", ssr.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort())
addr, err := resolveUDPAddr("udp", ssr.addr)
if err != nil {
pc.Close()
return nil, err
}
epc := ssr.cipher.PacketConn(N.NewEnhancePacketConn(pc))
epc = ssr.protocol.PacketConn(epc)
return newPacketConn(&ssrPacketConn{EnhancePacketConn: epc, rAddr: addr}, ssr), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (ssr *ShadowSocksR) SupportWithDialer() C.NetWork {
return C.ALLNet
pc = ssr.cipher.PacketConn(pc)
pc = ssr.protocol.PacketConn(pc)
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
}
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
@ -176,78 +143,15 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
return &ShadowSocksR{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: addr,
tp: C.ShadowsocksR,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
option: &option,
cipher: coreCiph,
obfs: obfs,
protocol: protocol,
}, nil
}
type ssrPacketConn struct {
N.EnhancePacketConn
rAddr net.Addr
}
func (spc *ssrPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
if err != nil {
return
}
return spc.EnhancePacketConn.WriteTo(packet[3:], spc.rAddr)
}
func (spc *ssrPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
n, _, e := spc.EnhancePacketConn.ReadFrom(b)
if e != nil {
return 0, nil, e
}
addr := socks5.SplitAddr(b[:n])
if addr == nil {
return 0, nil, errors.New("parse addr error")
}
udpAddr := addr.UDPAddr()
if udpAddr == nil {
return 0, nil, errors.New("parse addr error")
}
copy(b, b[len(addr):])
return n - len(addr), udpAddr, e
}
func (spc *ssrPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
data, put, _, err = spc.EnhancePacketConn.WaitReadFrom()
if err != nil {
return nil, nil, nil, err
}
_addr := socks5.SplitAddr(data)
if _addr == nil {
if put != nil {
put()
}
return nil, nil, nil, errors.New("parse addr error")
}
addr = _addr.UDPAddr()
if addr == nil {
if put != nil {
put()
}
return nil, nil, nil, errors.New("parse addr error")
}
data = data[len(_addr):]
return
}

View File

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

View File

@ -6,10 +6,8 @@ import (
"net"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant"
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
"github.com/Dreamacro/clash/transport/snell"
@ -17,7 +15,6 @@ import (
type Snell struct {
*Base
option *SnellOption
psk []byte
pool *snell.Pool
obfsOption *simpleObfsOption
@ -53,14 +50,15 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
return snell.StreamConn(c, option.psk, option.version)
}
// StreamConnContext implements C.ProxyAdapter
func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// StreamConn implements C.ProxyAdapter
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP {
err := snell.WriteUDPHeader(c, s.version)
return c, err
}
err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
return c, err
}
@ -72,57 +70,33 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
return nil, err
}
if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil {
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
c.Close()
return nil, err
}
return NewConn(c, s), err
}
return s.DialContextWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(s.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = s.StreamConnContext(ctx, c, metadata)
c, err = s.StreamConn(c, metadata)
return NewConn(c, s), err
}
// ListenPacketContext implements C.ProxyAdapter
func (s *Snell) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
return s.ListenPacketWithDialer(ctx, dialer.NewDialer(s.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
var err error
if len(s.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(s.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", s.addr)
c, err := dialer.DialContext(ctx, "tcp", s.addr, s.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version)
@ -134,9 +108,10 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return newPacketConn(pc, s), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (s *Snell) SupportWithDialer() C.NetWork {
return C.ALLNet
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (s *Snell) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil
}
// SupportUOT implements C.ProxyAdapter
@ -177,17 +152,13 @@ func NewSnell(option SnellOption) (*Snell, error) {
s := &Snell{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Snell,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: addr,
tp: C.Snell,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
option: &option,
psk: psk,
obfsOption: obfsOption,
version: option.Version,
@ -195,20 +166,12 @@ func NewSnell(option SnellOption) (*Snell, error) {
if option.Version == snell.Version2 {
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
var err error
var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions()...)
if len(s.option.DialerProxy) > 0 {
cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer)
if err != nil {
return nil, err
}
}
c, err := cDialer.DialContext(ctx, "tcp", addr)
c, err := dialer.DialContext(ctx, "tcp", addr, s.Base.DialOptions()...)
if err != nil {
return nil, err
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
})
}

View File

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

View File

@ -8,14 +8,11 @@ import (
"net/http"
"strconv"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/trojan"
"github.com/Dreamacro/clash/transport/vless"
)
type Trojan struct {
@ -27,29 +24,26 @@ type Trojan struct {
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
}
type TrojanOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
}
func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) {
func (t *Trojan) plainStream(c net.Conn) (net.Conn, error) {
if t.option.Network == "ws" {
host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
@ -70,30 +64,30 @@ func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error)
wsOpts.Headers = header
}
return t.instance.StreamWebsocketConn(ctx, c, wsOpts)
return t.instance.StreamWebsocketConn(c, wsOpts)
}
return t.instance.StreamConn(ctx, c)
return t.instance.StreamConn(c)
}
// StreamConnContext implements C.ProxyAdapter
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// StreamConn implements C.ProxyAdapter
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
t.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
} else {
c, err = t.plainStream(ctx, c)
c, err = t.plainStream(c)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
c, err = t.instance.PresetXTLSConn(c)
if err != nil {
return nil, err
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
@ -111,6 +105,12 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return nil, err
}
c, err = t.instance.PresetXTLSConn(c)
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
@ -118,28 +118,16 @@ func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
return NewConn(c, t), nil
}
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", t.addr)
c, err := dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = t.StreamConnContext(ctx, c, metadata)
c, err = t.StreamConn(c, metadata)
if err != nil {
return nil, err
}
@ -157,40 +145,19 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
defer safeConnClose(c, err)
} else {
c, err = dialer.DialContext(ctx, "tcp", t.addr, t.Base.DialOptions(opts...)...)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
pc := t.instance.PacketConn(c)
return newPacketConn(pc, t), err
}
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
defer safeConnClose(c, err)
tcpKeepAlive(c)
c, err = t.plainStream(c)
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 {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
N.TCPKeepAlive(c)
c, err = t.plainStream(ctx, c)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil {
@ -201,11 +168,6 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
return newPacketConn(pc, t), err
}
// SupportWithDialer implements C.ProxyAdapter
func (t *Trojan) SupportWithDialer() C.NetWork {
return C.ALLNet
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c)
@ -221,12 +183,21 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
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 != "" {
@ -235,42 +206,24 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
t := &Trojan{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: addr,
tp: C.Trojan,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
instance: trojan.New(tOption),
option: &option,
}
var err error
t.realityConfig, err = option.RealityOpts.Parse()
if err != nil {
return nil, err
}
tOption.Reality = t.realityConfig
if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) {
var err error
var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...)
if len(t.option.DialerProxy) > 0 {
cDialer, err = proxydialer.NewByName(t.option.DialerProxy, cDialer)
if err != nil {
return nil, err
}
}
c, err := cDialer.DialContext(context.Background(), "tcp", t.addr)
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
return c, nil
}
@ -281,14 +234,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
ServerName: tOption.ServerName,
}
var err error
tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
if err != nil {
return nil, err
if t.option.Flow != "" {
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else {
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig)
t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName,

View File

@ -1,316 +0,0 @@
package outbound
import (
"context"
"crypto/tls"
"errors"
"fmt"
"math"
"net"
"strconv"
"time"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/tuic"
"github.com/gofrs/uuid/v5"
"github.com/metacubex/quic-go"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/uot"
)
type Tuic struct {
*Base
option *TuicOption
client *tuic.PoolClient
}
type TuicOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Token string `proxy:"token,omitempty"`
UUID string `proxy:"uuid,omitempty"`
Password string `proxy:"password,omitempty"`
Ip string `proxy:"ip,omitempty"`
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
RequestTimeout int `proxy:"request-timeout,omitempty"`
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
CongestionController string `proxy:"congestion-controller,omitempty"`
DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
}
// DialContext implements C.ProxyAdapter
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
}
return NewConn(conn, t), err
}
// ListenPacketContext implements C.ProxyAdapter
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata
uotMetadata.Host = uotDestination.Fqdn
uotMetadata.DstPort = uotDestination.Port
c, err := t.DialContextWithDialer(ctx, dialer, &uotMetadata)
if err != nil {
return nil, err
}
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr())
if t.option.UDPOverStreamVersion == uot.LegacyVersion {
return newPacketConn(uot.NewConn(c, uot.Request{Destination: destination}), t), nil
} else {
return newPacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination}), t), nil
}
}
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
if err != nil {
return nil, err
}
return newPacketConn(pc, t), nil
}
// SupportWithDialer implements C.ProxyAdapter
func (t *Tuic) SupportWithDialer() C.NetWork {
return C.ALLNet
}
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *quic.Transport, addr net.Addr, err error) {
if len(t.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(t.option.DialerProxy, dialer)
if err != nil {
return nil, nil, err
}
}
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer)
if err != nil {
return nil, nil, err
}
addr = udpAddr
var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
if err != nil {
return nil, nil, err
}
transport = &quic.Transport{Conn: pc}
transport.SetCreatedConn(true) // auto close conn
transport.SetSingleUse(true) // auto close transport
return
}
func NewTuic(option TuicOption) (*Tuic, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
serverName := option.Server
tlsConfig := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: option.SkipCertVerify,
MinVersion: tls.VersionTLS13,
}
if option.SNI != "" {
tlsConfig.ServerName = option.SNI
}
var err error
tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString)
if err != nil {
return nil, err
}
if option.ALPN != nil { // structure's Decode will ensure value not nil when input has value even it was set an empty array
tlsConfig.NextProtos = option.ALPN
} else {
tlsConfig.NextProtos = []string{"h3"}
}
if option.RequestTimeout == 0 {
option.RequestTimeout = 8000
}
if option.HeartbeatInterval <= 0 {
option.HeartbeatInterval = 10000
}
udpRelayMode := tuic.QUIC
if option.UdpRelayMode != "quic" {
udpRelayMode = tuic.NATIVE
}
if option.MaxUdpRelayPacketSize == 0 {
option.MaxUdpRelayPacketSize = 1252
}
if option.MaxOpenStreams == 0 {
option.MaxOpenStreams = 100
}
if option.CWND == 0 {
option.CWND = 32
}
packetOverHead := tuic.PacketOverHeadV4
if len(option.Token) == 0 {
packetOverHead = tuic.PacketOverHeadV5
}
if option.MaxDatagramFrameSize == 0 {
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + packetOverHead
}
if option.MaxDatagramFrameSize > 1400 {
option.MaxDatagramFrameSize = 1400
}
option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - packetOverHead
// ensure server's incoming stream can handle correctly, increase to 1.1x
quicMaxOpenStreams := int64(option.MaxOpenStreams)
quicMaxOpenStreams = quicMaxOpenStreams + int64(math.Ceil(float64(quicMaxOpenStreams)/10.0))
quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
InitialConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxConnectionReceiveWindow: uint64(option.ReceiveWindow),
MaxIncomingStreams: quicMaxOpenStreams,
MaxIncomingUniStreams: quicMaxOpenStreams,
KeepAlivePeriod: time.Duration(option.HeartbeatInterval) * time.Millisecond,
DisablePathMTUDiscovery: option.DisableMTUDiscovery,
MaxDatagramFrameSize: int64(option.MaxDatagramFrameSize),
EnableDatagrams: true,
}
if option.ReceiveWindowConn == 0 {
quicConfig.InitialStreamReceiveWindow = tuic.DefaultStreamReceiveWindow / 10
quicConfig.MaxStreamReceiveWindow = tuic.DefaultStreamReceiveWindow
}
if option.ReceiveWindow == 0 {
quicConfig.InitialConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow / 10
quicConfig.MaxConnectionReceiveWindow = tuic.DefaultConnectionReceiveWindow
}
if len(option.Ip) > 0 {
addr = net.JoinHostPort(option.Ip, strconv.Itoa(option.Port))
}
if option.DisableSni {
tlsConfig.ServerName = ""
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
}
switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion:
case 0:
option.UDPOverStreamVersion = uot.LegacyVersion
default:
return nil, fmt.Errorf("tuic %s unknown udp over stream protocol version: %d", addr, option.UDPOverStreamVersion)
}
t := &Tuic{
Base: &Base{
name: option.Name,
addr: addr,
tp: C.Tuic,
udp: true,
tfo: option.FastOpen,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
option: &option,
}
clientMaxOpenStreams := int64(option.MaxOpenStreams)
// to avoid tuic's "too many open streams", decrease to 0.9x
if clientMaxOpenStreams == 100 {
clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0))
}
if clientMaxOpenStreams < 1 {
clientMaxOpenStreams = 1
}
if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Token: tkn,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
FastOpen: option.FastOpen,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV4(clientOption)
} else {
maxUdpRelayPacketSize := option.MaxUdpRelayPacketSize
if maxUdpRelayPacketSize > tuic.MaxFragSizeV5 {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5
}
clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsConfig,
QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password,
UdpRelayMode: udpRelayMode,
CongestionController: option.CongestionController,
ReduceRtt: option.ReduceRtt,
MaxUdpRelayPacketSize: maxUdpRelayPacketSize,
MaxOpenStreams: clientMaxOpenStreams,
CWND: option.CWND,
}
t.client = tuic.NewPoolClientV5(clientOption)
}
return t, nil
}

View File

@ -2,25 +2,31 @@ package outbound
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"net"
"net/netip"
"regexp"
"strconv"
"sync"
"time"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/transport/socks5"
xtls "github.com/xtls/go"
)
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
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)
@ -28,13 +34,19 @@ func getClientSessionCache() tls.ClientSessionCache {
return globalClientSessionCache
}
func getClientXSessionCache() xtls.ClientSessionCache {
once.Do(func() {
globalClientXSessionCache = xtls.NewLRUClientSessionCache(128)
})
return globalClientXSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte
addrType := metadata.AddrType()
aType := uint8(addrType)
p := uint(metadata.DstPort)
aType := uint8(metadata.AddrType)
p, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType {
switch metadata.AddrType {
case socks5.AtypDomainName:
lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host)
@ -49,69 +61,13 @@ func serializesSocksAddr(metadata *C.Metadata) []byte {
return bytes.Join(buf, nil)
}
func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) {
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := resolver.ResolveProxyServerHost(ctx, host)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
var ip netip.Addr
var fallback netip.Addr
switch prefer {
case C.IPv4Only:
ip, err = resolver.ResolveIPv4ProxyServerHost(ctx, host)
case C.IPv6Only:
ip, err = resolver.ResolveIPv6ProxyServerHost(ctx, host)
case C.IPv6Prefer:
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is6() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
default:
// C.IPv4Prefer, C.DualStack and other
var ips []netip.Addr
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
if err == nil {
for _, addr := range ips {
if addr.Is4() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
}
if !ip.IsValid() && fallback.IsValid() {
ip = fallback
}
ip, err := resolver.ResolveProxyServerHost(host)
if err != nil {
return nil, err
}
@ -119,45 +75,7 @@ func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, pref
}
func safeConnClose(c net.Conn, err error) {
if err != nil && c != nil {
if err != nil {
_ = c.Close()
}
}
var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
func StringToBps(s string) uint64 {
if s == "" {
return 0
}
// when have not unit, use Mbps
if v, err := strconv.Atoi(s); err == nil {
return StringToBps(fmt.Sprintf("%d Mbps", v))
}
m := rateStringRegexp.FindStringSubmatch(s)
if m == nil {
return 0
}
var n uint64
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
}

View File

@ -13,23 +13,12 @@ import (
"sync"
"github.com/Dreamacro/clash/common/convert"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/transport/gun"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/Dreamacro/clash/transport/vless"
"github.com/Dreamacro/clash/transport/vmess"
vmessSing "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
)
const (
@ -46,46 +35,34 @@ type Vless struct {
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
}
type VlessOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
UDP bool `proxy:"udp,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
Network string `proxy:"network,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
Flow string `proxy:"flow,omitempty"`
FlowShow bool `proxy:"flow-show,omitempty"`
TLS bool `proxy:"tls,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
}
func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &vmess.WebsocketConfig{
Host: host,
@ -93,44 +70,35 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
}
if len(v.option.WSOpts.Headers) != 0 {
header := http.Header{}
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)
if err != nil {
return nil, err
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
wsOpts.TLS = true
wsOpts.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
} else {
if host := wsOpts.Headers.Get("Host"); host == "" {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
c, err = v.streamTLSConn(ctx, c, false)
c, err = v.streamTLSOrXTLSConn(c, false)
if err != nil {
return nil, err
}
@ -145,7 +113,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c = vmess.StreamHTTPConn(c, httpOpts)
case "h2":
c, err = v.streamTLSConn(ctx, c, true)
c, err = v.streamTLSOrXTLSConn(c, true)
if err != nil {
return nil, err
}
@ -157,59 +125,47 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(c, h2Opts)
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 tcp network
// handle TLS
c, err = v.streamTLSConn(ctx, c, false)
// handle TLS And XTLS
c, err = v.streamTLSOrXTLSConn(c, false)
}
if err != nil {
return nil, err
}
return v.streamConn(c, metadata)
return v.client.StreamConn(c, parseVlessAddr(metadata))
}
func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if metadata.NetWork == C.UDP {
if v.option.PacketAddr {
metadata = &C.Metadata{
NetWork: C.UDP,
Host: packetaddr.SeqPacketMagicAddress,
DstPort: 443,
}
} else {
metadata = &C.Metadata{ // a clear metadata only contains ip
NetWork: C.UDP,
DstIP: metadata.DstIP,
DstPort: metadata.DstPort,
}
}
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
if v.option.PacketAddr {
conn = packetaddr.NewBindConn(conn)
}
} else {
conn, err = v.client.StreamConn(c, parseVlessAddr(metadata, false))
}
if err != nil {
conn = nil
}
return
}
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
host, _, _ := net.SplitHostPort(v.addr)
func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (net.Conn, error) {
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
if v.isXTLSEnabled() {
xtlsOpts := vless.XTLSConfig{
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{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
}
if isH2 {
@ -220,12 +176,16 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
tlsOpts.Host = v.option.ServerName
}
return vmess.StreamTLSConn(ctx, conn, &tlsOpts)
return vmess.StreamTLSConn(conn, &tlsOpts)
}
return conn, nil
}
func (v *Vless) isXTLSEnabled() bool {
return v.client.Addons != nil
}
// DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
@ -234,54 +194,38 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
c, err = v.client.StreamConn(c, parseVlessAddr(metadata))
if err != nil {
return nil, err
}
return NewConn(c, v), nil
}
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(v.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
// ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
// vless use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
@ -289,88 +233,29 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.streamConn(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 {
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)
}
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// ListenPacketWithDialer implements C.ProxyAdapter
func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(v.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
c, err = v.StreamConn(c, metadata)
}
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err)
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter
func (v *Vless) SupportWithDialer() C.NetWork {
return C.ALLNet
return v.ListenPacketOnStreamConn(c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
return newPacketConn(N.NewThreadSafePacketConn(
vmessSing.NewXUDPConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
} else if v.option.PacketAddr {
return newPacketConn(N.NewThreadSafePacketConn(
packetaddr.NewConn(&vlessPacketConn{
Conn: c, rAddr: metadata.UDPAddr(),
}, M.SocksaddrFromNet(metadata.UDPAddr())),
), v), nil
}
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
@ -379,31 +264,31 @@ func (v *Vless) SupportUOT() bool {
return true
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr {
var addrType byte
var addr []byte
switch metadata.AddrType() {
case socks5.AtypIPv4:
switch metadata.AddrType {
case C.AtypIPv4:
addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypIPv6:
case C.AtypIPv6:
addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypDomainName:
case C.AtypDomainName:
addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host))
copy(addr[1:], metadata.Host)
}
port, _ := strconv.ParseUint(metadata.DstPort, 10, 16)
return &vless.DstAddr{
UDP: metadata.NetWork == C.UDP,
AddrType: addrType,
Addr: addr,
Port: metadata.DstPort,
Mux: metadata.NetWork == C.UDP && xudp,
Port: uint(port),
}
}
@ -504,58 +389,32 @@ func NewVless(option VlessOption) (*Vless, error) {
if option.Network != "ws" && len(option.Flow) >= 16 {
option.Flow = option.Flow[:16]
switch option.Flow {
case vless.XRV:
log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV)
case vless.XRO, vless.XRD, vless.XRS:
addons = &vless.Addons{
Flow: option.Flow,
}
case vless.XRO, vless.XRD, vless.XRS:
log.Fatalln("Legacy XTLS protocol %s is deprecated and no longer supported", option.Flow)
default:
return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow)
}
}
switch option.PacketEncoding {
case "packetaddr", "packet":
option.PacketAddr = true
option.XUDP = false
default: // https://github.com/XTLS/Xray-core/pull/1567#issuecomment-1407305458
if !option.PacketAddr {
option.XUDP = true
}
}
if option.XUDP {
option.PacketAddr = false
}
client, err := vless.NewClient(option.UUID, addons)
client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
if err != nil {
return nil, err
}
v := &Vless{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vless,
udp: option.UDP,
iface: option.Interface,
},
client: client,
option: &option,
}
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network {
case "h2":
if len(option.HTTP2Opts.Host) == 0 {
@ -563,46 +422,36 @@ func NewVless(option VlessOption) (*Vless, error) {
}
case "grpc":
dialFn := func(network, addr string) (net.Conn, error) {
var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 {
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
if err != nil {
return nil, err
}
}
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr)
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
return c, nil
}
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName,
}
if option.ServerName == "" {
gunConfig.Host = v.addr
tlsConfig := &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
})
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
}
if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
gunConfig.Host = host
}
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
if v.isXTLSEnabled() {
v.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
} else {
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
}
return v, nil

View File

@ -9,27 +9,15 @@ import (
"net/http"
"strconv"
"strings"
"sync"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/ca"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
tlsC "github.com/Dreamacro/clash/component/tls"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/ntp"
"github.com/Dreamacro/clash/transport/gun"
clashVMess "github.com/Dreamacro/clash/transport/vmess"
vmess "github.com/metacubex/sing-vmess"
"github.com/metacubex/sing-vmess/packetaddr"
M "github.com/sagernet/sing/common/metadata"
"github.com/Dreamacro/clash/transport/vmess"
)
var ErrUDPRemoteAddrMismatch = errors.New("udp packet dropped due to mismatched remote address")
type Vmess struct {
*Base
client *vmess.Client
@ -39,36 +27,29 @@ type Vmess struct {
gunTLSConfig *tls.Config
gunConfig *gun.Config
transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig
}
type VmessOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
ALPN []string `proxy:"alpn,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
PacketAddr bool `proxy:"packet-addr,omitempty"`
XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"`
GlobalPadding bool `proxy:"global-padding,omitempty"`
AuthenticatedLength bool `proxy:"authenticated-length,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
UUID string `proxy:"uuid"`
AlterID int `proxy:"alterId"`
Cipher string `proxy:"cipher"`
UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"`
TLS bool `proxy:"tls,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ServerName string `proxy:"servername,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"`
// TODO: compatible with VMESS WS older version configurations
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
WSPath string `proxy:"ws-path,omitempty"`
}
type HTTPOptions struct {
@ -93,25 +74,20 @@ type WSOptions struct {
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
}
// StreamConnContext implements C.ProxyAdapter
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
// StreamConn implements C.ProxyAdapter
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(v.addr)
wsOpts := &clashVMess.WebsocketConfig{
wsOpts := &vmess.WebsocketConfig{
Host: host,
Port: port,
Path: v.option.WSOpts.Path,
MaxEarlyData: v.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName,
ClientFingerprint: v.option.ClientFingerprint,
Headers: http.Header{},
Headers: make(http.Header),
}
if len(v.option.WSOpts.Headers) != 0 {
@ -122,154 +98,99 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
if v.option.TLS {
wsOpts.TLS = true
tlsConfig := &tls.Config{
wsOpts.TLSConfig = &tls.Config{
ServerName: host,
InsecureSkipVerify: v.option.SkipCertVerify,
NextProtos: []string{"http/1.1"},
}
wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
if err != nil {
return nil, err
}
if v.option.ServerName != "" {
wsOpts.TLSConfig.ServerName = v.option.ServerName
} else if host := wsOpts.Headers.Get("Host"); host != "" {
wsOpts.TLSConfig.ServerName = host
}
} else {
wsOpts.Headers.Set("Host", convert.RandHost())
convert.SetUserAgent(wsOpts.Headers)
}
c, err = clashVMess.StreamWebsocketConn(ctx, c, wsOpts)
c, err = vmess.StreamWebsocketConn(c, wsOpts)
case "http":
// readability first, so just copy default TLS logic
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &clashVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = clashVMess.StreamTLSConn(ctx, c, tlsOpts)
c, err = vmess.StreamTLSConn(c, tlsOpts)
if err != nil {
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)
httpOpts := &clashVMess.HTTPConfig{
httpOpts := &vmess.HTTPConfig{
Host: host,
Method: v.option.HTTPOpts.Method,
Path: v.option.HTTPOpts.Path,
Headers: v.option.HTTPOpts.Headers,
}
c = clashVMess.StreamHTTPConn(c, httpOpts)
c = vmess.StreamHTTPConn(c, httpOpts)
case "h2":
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := clashVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"},
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
tlsOpts := vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
NextProtos: []string{"h2"},
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = clashVMess.StreamTLSConn(ctx, c, &tlsOpts)
c, err = vmess.StreamTLSConn(c, &tlsOpts)
if err != nil {
return nil, err
}
h2Opts := &clashVMess.H2Config{
h2Opts := &vmess.H2Config{
Hosts: v.option.HTTP2Opts.Host,
Path: v.option.HTTP2Opts.Path,
}
c, err = clashVMess.StreamH2Conn(c, h2Opts)
c, err = vmess.StreamH2Conn(c, h2Opts)
case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
default:
// handle TLS
if v.option.TLS {
host, _, _ := net.SplitHostPort(v.addr)
tlsOpts := &clashVMess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint,
Reality: v.realityConfig,
NextProtos: v.option.ALPN,
tlsOpts := &vmess.TLSConfig{
Host: host,
SkipCertVerify: v.option.SkipCertVerify,
}
if v.option.ServerName != "" {
tlsOpts.Host = v.option.ServerName
}
c, err = clashVMess.StreamTLSConn(ctx, c, tlsOpts)
c, err = vmess.StreamTLSConn(c, tlsOpts)
}
}
if err != nil {
return nil, err
}
return v.streamConn(c, metadata)
}
func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if metadata.NetWork == C.UDP {
if v.option.XUDP {
var globalID [8]byte
if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress())
}
if N.NeedHandshake(c) {
conn = v.client.DialEarlyXUDPPacketConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else {
conn, err = v.client.DialXUDPPacketConn(c,
globalID,
M.SocksaddrFromNet(metadata.UDPAddr()))
}
} else if v.option.PacketAddr {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
} else {
conn, err = v.client.DialPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
}
conn = packetaddr.NewBindConn(conn)
} else {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
} else {
conn, err = v.client.DialPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr()))
}
}
} else {
if N.NeedHandshake(c) {
conn = v.client.DialEarlyConn(c,
M.ParseSocksaddr(metadata.RemoteAddress()))
} else {
conn, err = v.client.DialConn(c,
M.ParseSocksaddr(metadata.RemoteAddress()))
}
}
if err != nil {
conn = nil
}
return
return v.client.StreamConn(c, parseVmessAddr(metadata))
}
// DialContext implements C.ProxyAdapter
@ -280,51 +201,38 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
if err != nil {
return nil, err
}
return NewConn(c, v), nil
}
return v.DialContextWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
// DialContextWithDialer implements C.ProxyAdapter
func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
if len(v.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
tcpKeepAlive(c)
defer safeConnClose(c, err)
c, err = v.StreamConnContext(ctx, c, metadata)
c, err = v.StreamConn(c, metadata)
return NewConn(c, v), err
}
// ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
ip, err := resolver.ResolveIP(metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
var c net.Conn
// gun transport
if v.transport != nil && len(opts) == 0 {
@ -332,72 +240,29 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
defer safeConnClose(c, err)
c, err = v.streamConn(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 {
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)
}
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
}
tcpKeepAlive(c)
defer safeConnClose(c, err)
// ListenPacketWithDialer implements C.ProxyAdapter
func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if len(v.option.DialerProxy) > 0 {
dialer, err = proxydialer.NewByName(v.option.DialerProxy, dialer)
if err != nil {
return nil, err
}
c, err = v.StreamConn(c, metadata)
}
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
c, err := dialer.DialContext(ctx, "tcp", v.addr)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = v.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err)
}
return v.ListenPacketOnStreamConn(ctx, c, metadata)
}
// SupportWithDialer implements C.ProxyAdapter
func (v *Vmess) SupportWithDialer() C.NetWork {
return C.ALLNet
return v.ListenPacketOnStreamConn(c, metadata)
}
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
if pc, ok := c.(net.PacketConn); ok {
return newPacketConn(N.NewThreadSafePacketConn(pc), v), nil
}
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
}
@ -408,41 +273,33 @@ func (v *Vmess) SupportUOT() bool {
func NewVmess(option VmessOption) (*Vmess, error) {
security := strings.ToLower(option.Cipher)
var options []vmess.ClientOption
if option.GlobalPadding {
options = append(options, vmess.ClientWithGlobalPadding())
}
if option.AuthenticatedLength {
options = append(options, vmess.ClientWithAuthenticatedLength())
}
options = append(options, vmess.ClientWithTimeFunc(ntp.Now))
client, err := vmess.NewClient(option.UUID, security, option.AlterID, options...)
client, err := vmess.NewClient(vmess.Config{
UUID: option.UUID,
AlterID: uint16(option.AlterID),
Security: security,
HostName: option.Server,
Port: strconv.Itoa(option.Port),
IsAead: option.AlterID == 0,
})
if err != nil {
return nil, err
}
switch option.PacketEncoding {
case "packetaddr", "packet":
option.PacketAddr = true
case "xudp":
option.XUDP = true
}
if option.XUDP {
option.PacketAddr = false
switch option.Network {
case "h2", "grpc":
if !option.TLS {
return nil, fmt.Errorf("TLS must be true with h2/grpc network")
}
}
v := &Vmess{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
xudp: option.XUDP,
tfo: option.TFO,
mpTcp: option.MPTCP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.Vmess,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
},
client: client,
option: &option,
@ -455,72 +312,71 @@ func NewVmess(option VmessOption) (*Vmess, error) {
}
case "grpc":
dialFn := func(network, addr string) (net.Conn, error) {
var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 {
cDialer, err = proxydialer.NewByName(v.option.DialerProxy, cDialer)
if err != nil {
return nil, err
}
}
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr)
c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...)
if err != nil {
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
}
N.TCPKeepAlive(c)
tcpKeepAlive(c)
return c, nil
}
gunConfig := &gun.Config{
ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName,
ClientFingerprint: v.option.ClientFingerprint,
ServiceName: v.option.GrpcOpts.GrpcServiceName,
Host: v.option.ServerName,
}
if option.ServerName == "" {
gunConfig.Host = v.addr
tlsConfig := &tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
}
var tlsConfig *tls.Config
if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName,
})
if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
}
if v.option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host
gunConfig.Host = host
}
v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig)
}
v.realityConfig, err = v.option.RealityOpts.Parse()
if err != nil {
return nil, err
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
}
return v, nil
}
type vmessPacketConn struct {
net.Conn
rAddr net.Addr
access sync.Mutex
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
var addrType byte
var addr []byte
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) {
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)
}

View File

@ -1,531 +0,0 @@
package outbound
import (
"context"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"net"
"net/netip"
"runtime"
"strconv"
"strings"
"sync"
CN "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
"github.com/Dreamacro/clash/component/resolver"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/dns"
"github.com/Dreamacro/clash/log"
wireguard "github.com/metacubex/sing-wireguard"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/wireguard-go/device"
)
type WireGuard struct {
*Base
bind *wireguard.ClientBind
device *device.Device
tunDevice wireguard.Device
dialer proxydialer.SingDialer
startOnce sync.Once
startErr error
resolver *dns.Resolver
refP *refProxyAdapter
}
type WireGuardOption struct {
BasicOption
WireGuardPeerOption
Name string `proxy:"name"`
PrivateKey string `proxy:"private-key"`
Workers int `proxy:"workers,omitempty"`
MTU int `proxy:"mtu,omitempty"`
UDP bool `proxy:"udp,omitempty"`
PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"`
Peers []WireGuardPeerOption `proxy:"peers,omitempty"`
RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"`
Dns []string `proxy:"dns,omitempty"`
}
type WireGuardPeerOption struct {
Server string `proxy:"server"`
Port int `proxy:"port"`
Ip string `proxy:"ip,omitempty"`
Ipv6 string `proxy:"ipv6,omitempty"`
PublicKey string `proxy:"public-key,omitempty"`
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
Reserved []uint8 `proxy:"reserved,omitempty"`
AllowedIPs []string `proxy:"allowed-ips,omitempty"`
}
type wgSingErrorHandler struct {
name string
}
var _ E.Handler = (*wgSingErrorHandler)(nil)
func (w wgSingErrorHandler) NewError(ctx context.Context, err error) {
if E.IsClosedOrCanceled(err) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) connection closed: %s", w.name, err))
return
}
log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err))
}
type wgNetDialer struct {
tunDevice wireguard.Device
}
var _ dialer.NetDialer = (*wgNetDialer)(nil)
func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap())
}
func (option WireGuardPeerOption) Addr() M.Socksaddr {
return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port))
}
func (option WireGuardPeerOption) Prefixes() ([]netip.Prefix, error) {
localPrefixes := make([]netip.Prefix, 0, 2)
if len(option.Ip) > 0 {
if !strings.Contains(option.Ip, "/") {
option.Ip = option.Ip + "/32"
}
if prefix, err := netip.ParsePrefix(option.Ip); err == nil {
localPrefixes = append(localPrefixes, prefix)
} else {
return nil, E.Cause(err, "ip address parse error")
}
}
if len(option.Ipv6) > 0 {
if !strings.Contains(option.Ipv6, "/") {
option.Ipv6 = option.Ipv6 + "/128"
}
if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil {
localPrefixes = append(localPrefixes, prefix)
} else {
return nil, E.Cause(err, "ipv6 address parse error")
}
}
if len(localPrefixes) == 0 {
return nil, E.New("missing local address")
}
return localPrefixes, nil
}
func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
outbound := &WireGuard{
Base: &Base{
name: option.Name,
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
tp: C.WireGuard,
udp: option.UDP,
iface: option.Interface,
rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion),
},
dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()),
}
runtime.SetFinalizer(outbound, closeWireGuard)
var reserved [3]uint8
if len(option.Reserved) > 0 {
if len(option.Reserved) != 3 {
return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved))
}
copy(reserved[:], option.Reserved)
}
var isConnect bool
var connectAddr M.Socksaddr
if len(option.Peers) < 2 {
isConnect = true
if len(option.Peers) == 1 {
connectAddr = option.Peers[0].Addr()
} else {
connectAddr = option.Addr()
}
}
outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr, reserved)
var localPrefixes []netip.Prefix
var privateKey string
{
bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey)
if err != nil {
return nil, E.Cause(err, "decode private key")
}
privateKey = hex.EncodeToString(bytes)
}
ipcConf := "private_key=" + privateKey
if peersLen := len(option.Peers); peersLen > 0 {
localPrefixes = make([]netip.Prefix, 0, peersLen*2)
for i, peer := range option.Peers {
var peerPublicKey, preSharedKey string
{
bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
if err != nil {
return nil, E.Cause(err, "decode public key for peer ", i)
}
peerPublicKey = 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)
}
preSharedKey = hex.EncodeToString(bytes)
}
destination := peer.Addr()
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + destination.String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
if len(peer.AllowedIPs) == 0 {
return nil, E.New("missing allowed_ips for peer ", i)
}
for _, allowedIP := range peer.AllowedIPs {
ipcConf += "\nallowed_ip=" + allowedIP
}
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))
}
copy(reserved[:], option.Reserved)
outbound.bind.SetReservedForEndpoint(destination, reserved)
}
prefixes, err := peer.Prefixes()
if err != nil {
return nil, err
}
localPrefixes = append(localPrefixes, prefixes...)
}
} else {
var peerPublicKey, preSharedKey string
{
bytes, err := base64.StdEncoding.DecodeString(option.PublicKey)
if err != nil {
return nil, E.Cause(err, "decode peer public key")
}
peerPublicKey = hex.EncodeToString(bytes)
}
if option.PreSharedKey != "" {
bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey)
if err != nil {
return nil, E.Cause(err, "decode pre shared key")
}
preSharedKey = hex.EncodeToString(bytes)
}
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + connectAddr.String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
var err error
localPrefixes, err = option.Prefixes()
if err != nil {
return nil, err
}
var has4, has6 bool
for _, address := range localPrefixes {
if address.Addr().Is4() {
has4 = true
} else {
has6 = true
}
}
if has4 {
ipcConf += "\nallowed_ip=0.0.0.0/0"
}
if has6 {
ipcConf += "\nallowed_ip=::/0"
}
}
if option.PersistentKeepalive != 0 {
ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive)
}
mtu := option.MTU
if mtu == 0 {
mtu = 1408
}
if len(localPrefixes) == 0 {
return nil, E.New("missing local address")
}
var err error
outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu))
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
},
Errorf: func(format string, args ...interface{}) {
log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
},
}, option.Workers)
if debug.Enabled {
log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", option.Name, ipcConf))
}
err = outbound.device.IpcSet(ipcConf)
if err != nil {
return nil, E.Cause(err, "setup wireguard")
}
//err = outbound.tunDevice.Start()
var has6 bool
for _, address := range localPrefixes {
if !address.Addr().Unmap().Is4() {
has6 = true
break
}
}
refP := &refProxyAdapter{}
outbound.refP = refP
if option.RemoteDnsResolve && len(option.Dns) > 0 {
nss, err := dns.ParseNameServer(option.Dns)
if err != nil {
return nil, err
}
for i := range nss {
nss[i].ProxyAdapter = refP
}
outbound.resolver = dns.NewResolver(dns.Config{
Main: nss,
IPv6: has6,
})
}
return outbound, nil
}
func closeWireGuard(w *WireGuard) {
if w.device != nil {
w.device.Close()
}
_ = common.Close(w.tunDevice)
}
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := w.Base.DialOptions(opts...)
w.dialer.SetDialer(dialer.NewDialer(options...))
var conn net.Conn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
})
if w.startErr != nil {
return nil, w.startErr
}
if !metadata.Resolved() || w.resolver != nil {
r := resolver.DefaultResolver
if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver
}
options = append(options, dialer.WithResolver(r))
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
} else {
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
}
if err != nil {
return nil, err
}
if conn == nil {
return nil, E.New("conn is nil")
}
return NewConn(CN.NewRefConn(conn, w), w), nil
}
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
options := w.Base.DialOptions(opts...)
w.dialer.SetDialer(dialer.NewDialer(options...))
var pc net.PacketConn
w.startOnce.Do(func() {
w.startErr = w.tunDevice.Start()
})
if w.startErr != nil {
return nil, w.startErr
}
if err != nil {
return nil, err
}
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
if err != nil {
return nil, err
}
if pc == nil {
return nil, E.New("packetConn is nil")
}
return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil
}
// IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true
}
type refProxyAdapter struct {
proxyAdapter C.ProxyAdapter
count int
mutex sync.Mutex
}
func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.proxyAdapter = proxyAdapter
r.count++
}
func (r *refProxyAdapter) ClearProxyAdapter() {
r.mutex.Lock()
defer r.mutex.Unlock()
r.count--
if r.count == 0 {
r.proxyAdapter = nil
}
}
func (r *refProxyAdapter) Name() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Name()
}
return ""
}
func (r *refProxyAdapter) Type() C.AdapterType {
if r.proxyAdapter != nil {
return r.proxyAdapter.Type()
}
return C.AdapterType(0)
}
func (r *refProxyAdapter) Addr() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Addr()
}
return ""
}
func (r *refProxyAdapter) SupportUDP() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUDP()
}
return false
}
func (r *refProxyAdapter) SupportXUDP() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportXUDP()
}
return false
}
func (r *refProxyAdapter) SupportTFO() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportTFO()
}
return false
}
func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.MarshalJSON()
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) SupportUOT() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUOT()
}
return false
}
func (r *refProxyAdapter) SupportWithDialer() C.NetWork {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportWithDialer()
}
return C.InvalidNet
}
func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.IsL3Protocol(metadata)
}
return false
}
func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
if r.proxyAdapter != nil {
return r.proxyAdapter.Unwrap(metadata, touch)
}
return nil
}
var _ C.ProxyAdapter = (*refProxyAdapter)(nil)

View File

@ -7,9 +7,6 @@ import (
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/callback"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
@ -17,10 +14,9 @@ import (
type Fallback struct {
*GroupBase
disableUDP bool
testUrl string
selected string
expectedStatus string
disableUDP bool
testUrl string
selected string
}
func (f *Fallback) Now() string {
@ -34,18 +30,9 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata, opts .
c, err := proxy.DialContext(ctx, metadata, f.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(f)
f.onDialSuccess()
} else {
f.onDialFailed(proxy.Type(), err)
}
if N.NeedHandshake(c) {
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
if err == nil {
f.onDialSuccess()
} else {
f.onDialFailed(proxy.Type(), err)
}
})
f.onDialFailed()
}
return c, err
@ -72,11 +59,6 @@ func (f *Fallback) SupportUDP() bool {
return proxy.SupportUDP()
}
// IsL3Protocol implements C.ProxyAdapter
func (f *Fallback) IsL3Protocol(metadata *C.Metadata) bool {
return f.findAliveProxy(false).IsL3Protocol(metadata)
}
// MarshalJSON implements C.ProxyAdapter
func (f *Fallback) MarshalJSON() ([]byte, error) {
all := []string{}
@ -84,41 +66,31 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": f.Type().String(),
"now": f.Now(),
"all": all,
"testUrl": f.testUrl,
"expected": f.expectedStatus,
"type": f.Type().String(),
"now": f.Now(),
"all": all,
})
}
// Unwrap implements C.ProxyAdapter
func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
proxy := f.findAliveProxy(touch)
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy(true)
return proxy
}
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch)
for _, proxy := range proxies {
if len(f.selected) == 0 {
// if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
}
} else {
if proxy.Name() == f.selected {
// if proxy.Alive() {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
} else {
f.selected = ""
}
}
al := proxies[0]
for i := len(proxies) - 1; i > -1; i-- {
proxy := proxies[i]
if proxy.Name() == f.selected && proxy.Alive() {
return proxy
}
if proxy.Alive() {
al = proxy
}
}
return proxies[0]
return al
}
func (f *Fallback) Set(name string) error {
@ -135,21 +107,15 @@ func (f *Fallback) Set(name string) error {
}
f.selected = name
// if !p.Alive() {
if !p.AliveForTestUrl(f.testUrl) {
if !p.Alive() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus)
_, _ = p.URLTest(ctx, f.testUrl, expectedStatus, C.ExtraHistory)
_, _ = p.URLTest(ctx, f.testUrl)
}
return nil
}
func (f *Fallback) ForceSet(name string) {
f.selected = name
}
func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
return &Fallback{
GroupBase: NewGroupBase(GroupBaseOption{
@ -160,12 +126,9 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}),
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
disableUDP: option.DisableUDP,
testUrl: option.URL,
}
}

View File

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

View File

@ -6,36 +6,32 @@ import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/cache"
"github.com/Dreamacro/clash/common/callback"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/common/murmur3"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
"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 {
*GroupBase
disableUDP bool
strategyFn strategyFn
testUrl string
expectedStatus string
disableUDP bool
strategyFn strategyFn
}
var errStrategy = errors.New("unsupported strategy")
func parseStrategy(config map[string]any) string {
if strategy, ok := config["strategy"].(string); ok {
return strategy
if elm, ok := config["strategy"]; ok {
if strategy, ok := elm.(string); ok {
return strategy
}
}
return "consistent-hashing"
}
@ -87,25 +83,18 @@ func jumpHash(key uint64, buckets int32) int32 {
// DialContext implements C.ProxyAdapter
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := lb.Unwrap(metadata, true)
defer func() {
if err == nil {
c.AppendToChains(lb)
lb.onDialSuccess()
} else {
lb.onDialFailed()
}
}()
proxy := lb.Unwrap(metadata)
c, err = proxy.DialContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(lb)
} else {
lb.onDialFailed(proxy.Type(), err)
}
if N.NeedHandshake(c) {
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
if err == nil {
lb.onDialSuccess()
} else {
lb.onDialFailed(proxy.Type(), err)
}
})
}
return
}
@ -117,7 +106,7 @@ func (lb *LoadBalance) ListenPacketContext(ctx context.Context, metadata *C.Meta
}
}()
proxy := lb.Unwrap(metadata, true)
proxy := lb.Unwrap(metadata)
return proxy.ListenPacketContext(ctx, metadata, lb.Base.DialOptions(opts...)...)
}
@ -126,33 +115,14 @@ func (lb *LoadBalance) SupportUDP() bool {
return !lb.disableUDP
}
// IsL3Protocol implements C.ProxyAdapter
func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool {
return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
}
func strategyRoundRobin(url string) strategyFn {
func strategyRoundRobin() strategyFn {
idx := 0
idxMutex := sync.Mutex{}
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
idxMutex.Lock()
defer idxMutex.Unlock()
i := 0
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
length := len(proxies)
if touch {
defer func() {
idx = (idx + i) % length
}()
}
for ; i < length; i++ {
id := (idx + i) % length
proxy := proxies[id]
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
i++
for i := 0; i < length; i++ {
idx = (idx + 1) % length
proxy := proxies[idx]
if proxy.Alive() {
return proxy
}
}
@ -161,24 +131,15 @@ func strategyRoundRobin(url string) strategyFn {
}
}
func strategyConsistentHashing(url string) strategyFn {
func strategyConsistentHashing() strategyFn {
maxRetry := 5
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
key := utils.MapHash(getKey(metadata))
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
buckets := int32(len(proxies))
for i := 0; i < maxRetry; i, key = i+1, key+1 {
idx := jumpHash(key, buckets)
proxy := proxies[idx]
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
return proxy
}
}
// when availability is poor, traverse the entire list to get the available nodes
for _, proxy := range proxies {
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
if proxy.Alive() {
return proxy
}
}
@ -187,14 +148,14 @@ func strategyConsistentHashing(url string) strategyFn {
}
}
func strategyStickySessions(url string) strategyFn {
func strategyStickySessions() strategyFn {
ttl := time.Minute * 10
maxRetry := 5
lruCache := cache.New[uint64, int](
lruCache := cache.NewLRUCache[uint64, int](
cache.WithAge[uint64, int](int64(ttl.Seconds())),
cache.WithSize[uint64, int](1000))
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
key := utils.MapHash(getKeyWithSrcAndDst(metadata))
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
key := uint64(murmur3.Sum32([]byte(getKeyWithSrcAndDst(metadata))))
length := len(proxies)
idx, has := lruCache.Get(key)
if !has {
@ -204,8 +165,7 @@ func strategyStickySessions(url string) strategyFn {
nowIdx := idx
for i := 1; i < maxRetry; i++ {
proxy := proxies[nowIdx]
// if proxy.Alive() {
if proxy.AliveForTestUrl(url) {
if proxy.Alive() {
if nowIdx != idx {
lruCache.Delete(key)
lruCache.Set(key, nowIdx)
@ -224,9 +184,9 @@ func strategyStickySessions(url string) strategyFn {
}
// Unwrap implements C.ProxyAdapter
func (lb *LoadBalance) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
proxies := lb.GetProxies(touch)
return lb.strategyFn(proxies, metadata, touch)
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.GetProxies(true)
return lb.strategyFn(proxies, metadata)
}
// MarshalJSON implements C.ProxyAdapter
@ -236,10 +196,8 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": lb.Type().String(),
"all": all,
"testUrl": lb.testUrl,
"expectedStatus": lb.expectedStatus,
"type": lb.Type().String(),
"all": all,
})
}
@ -247,11 +205,11 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
var strategyFn strategyFn
switch strategy {
case "consistent-hashing":
strategyFn = strategyConsistentHashing(option.URL)
strategyFn = strategyConsistentHashing()
case "round-robin":
strategyFn = strategyRoundRobin(option.URL)
strategyFn = strategyRoundRobin()
case "sticky-sessions":
strategyFn = strategyStickySessions(option.URL)
strategyFn = strategyStickySessions()
default:
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
}
@ -264,13 +222,9 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}),
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
strategyFn: strategyFn,
disableUDP: option.DisableUDP,
}, nil
}

View File

@ -3,37 +3,33 @@ package outboundgroup
import (
"errors"
"fmt"
"strings"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/adapter/provider"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
var (
errFormat = errors.New("format error")
errType = errors.New("unsupported type")
errType = errors.New("unsupport type")
errMissProxy = errors.New("`use` or `proxies` missing")
errMissHealthCheck = errors.New("`url` or `interval` missing")
errDuplicateProvider = errors.New("duplicate provider name")
)
type GroupCommonOption struct {
outbound.BasicOption
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
ExcludeFilter string `group:"exclude-filter,omitempty"`
ExcludeType string `group:"exclude-type,omitempty"`
ExpectedStatus string `group:"expected-status,omitempty"`
Name string `group:"name"`
Type string `group:"type"`
Proxies []string `group:"proxies,omitempty"`
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"`
Filter string `group:"filter,omitempty"`
}
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
@ -55,67 +51,54 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
providers := []types.ProxyProvider{}
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
return nil, fmt.Errorf("%s: %w", groupName, errMissProxy)
return nil, errMissProxy
}
expectedStatus, err := utils.NewIntRanges[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
testUrl := groupOption.URL
if len(groupOption.Proxies) != 0 {
ps, err := getProxies(proxyMap, groupOption.Proxies)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
return nil, err
}
if _, ok := providersMap[groupName]; ok {
return nil, fmt.Errorf("%s: %w", groupName, errDuplicateProvider)
return nil, errDuplicateProvider
}
var url string
var interval uint
// select don't need health check
if groupOption.Type != "select" && groupOption.Type != "relay" {
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 = "https://cp.cloudflare.com/generate_204"
groupOption.URL = "http://www.gstatic.com/generate_204"
}
if groupOption.Interval == 0 {
groupOption.Interval = 300
}
url = groupOption.URL
interval = uint(groupOption.Interval)
}
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
}
hc := provider.NewHealthCheck(ps, url, interval, true, expectedStatus)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
providers = append(providers, pd)
providersMap[groupName] = pd
}
providers = append(providers, pd)
providersMap[groupName] = pd
}
if len(groupOption.Use) != 0 {
list, err := getProviders(providersMap, groupOption.Use)
if err != nil {
return nil, fmt.Errorf("%s: %w", groupName, err)
return nil, err
}
// different proxy groups use different test URL
addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
providers = append(providers, list...)
} else {
groupOption.Filter = ""
@ -169,13 +152,3 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
}
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

@ -3,9 +3,10 @@ package outboundgroup
import (
"context"
"encoding/json"
"fmt"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/proxydialer"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/constant/provider"
)
@ -24,16 +25,37 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
case 1:
return proxies[0].DialContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var d C.Dialer
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
for _, proxy := range proxies[:len(proxies)-1] {
d = proxydialer.New(proxy, d, false)
}
first := proxies[0]
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 {
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-- {
conn.AppendToChains(chainProxies[i])
@ -55,15 +77,39 @@ func (r *Relay) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
return proxies[0].ListenPacketContext(ctx, metadata, r.Base.DialOptions(opts...)...)
}
var d C.Dialer
d = dialer.NewDialer(r.Base.DialOptions(opts...)...)
for _, proxy := range proxies[:len(proxies)-1] {
d = proxydialer.New(proxy, d, false)
}
first := proxies[0]
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 {
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-- {
@ -81,22 +127,8 @@ func (r *Relay) SupportUDP() bool {
if len(proxies) == 0 { // C.Direct
return true
}
for i := len(proxies) - 1; i >= 0; i-- {
proxy := proxies[i]
if !proxy.SupportUDP() {
return false
}
if proxy.SupportUOT() {
return true
}
switch proxy.SupportWithDialer() {
case C.ALLNet:
case C.UDP:
default: // C.TCP and C.InvalidNet
return false
}
}
return true
last := proxies[len(proxies)-1]
return last.SupportUDP() && last.SupportUOT()
}
// MarshalJSON implements C.ProxyAdapter
@ -121,11 +153,11 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy)
for n, proxy := range rawProxies {
proxies = append(proxies, proxy)
chainProxies = append(chainProxies, proxy)
subproxy := proxy.Unwrap(metadata, touch)
subproxy := proxy.Unwrap(metadata)
for subproxy != nil {
chainProxies = append(chainProxies, subproxy)
proxies[n] = subproxy
subproxy = subproxy.Unwrap(metadata, touch)
subproxy = subproxy.Unwrap(metadata)
}
}
@ -139,7 +171,7 @@ func (r *Relay) proxies(metadata *C.Metadata, touch bool) ([]C.Proxy, []C.Proxy)
}
func (r *Relay) Addr() string {
proxies, _ := r.proxies(nil, false)
proxies, _ := r.proxies(nil, true)
return proxies[len(proxies)-1].Addr()
}
@ -153,8 +185,6 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
RoutingMark: option.RoutingMark,
},
"",
"",
"",
providers,
}),
}

View File

@ -44,11 +44,6 @@ func (s *Selector) SupportUDP() bool {
return s.selectedProxy(false).SupportUDP()
}
// IsL3Protocol implements C.ProxyAdapter
func (s *Selector) IsL3Protocol(metadata *C.Metadata) bool {
return s.selectedProxy(false).IsL3Protocol(metadata)
}
// MarshalJSON implements C.ProxyAdapter
func (s *Selector) MarshalJSON() ([]byte, error) {
all := []string{}
@ -78,13 +73,9 @@ func (s *Selector) Set(name string) error {
return errors.New("proxy not exist")
}
func (s *Selector) ForceSet(name string) {
s.selected = name
}
// Unwrap implements C.ProxyAdapter
func (s *Selector) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return s.selectedProxy(touch)
func (s *Selector) Unwrap(*C.Metadata) C.Proxy {
return s.selectedProxy(true)
}
func (s *Selector) selectedProxy(touch bool) C.Proxy {
@ -108,8 +99,6 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider)
RoutingMark: option.RoutingMark,
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}),
selected: "COMPATIBLE",

View File

@ -3,12 +3,9 @@ package outboundgroup
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/callback"
N "github.com/Dreamacro/clash/common/net"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/component/dialer"
C "github.com/Dreamacro/clash/constant"
@ -25,59 +22,25 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {
type URLTest struct {
*GroupBase
selected string
testUrl string
expectedStatus string
tolerance uint16
disableUDP bool
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
tolerance uint16
disableUDP bool
fastNode C.Proxy
fastSingle *singledo.Single[C.Proxy]
}
func (u *URLTest) Now() string {
return u.fast(false).Name()
}
func (u *URLTest) Set(name string) error {
var p C.Proxy
for _, proxy := range u.GetProxies(false) {
if proxy.Name() == name {
p = proxy
break
}
}
if p == nil {
return errors.New("proxy not exist")
}
u.selected = name
u.fast(false)
return nil
}
func (u *URLTest) ForceSet(name string) {
u.selected = name
}
// DialContext implements C.ProxyAdapter
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (c C.Conn, err error) {
proxy := u.fast(true)
c, err = proxy.DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
c, err = u.fast(true).DialContext(ctx, metadata, u.Base.DialOptions(opts...)...)
if err == nil {
c.AppendToChains(u)
u.onDialSuccess()
} else {
u.onDialFailed(proxy.Type(), err)
u.onDialFailed()
}
if N.NeedHandshake(c) {
c = callback.NewFirstWriteCallBackConn(c, func(err error) {
if err == nil {
u.onDialSuccess()
} else {
u.onDialFailed(proxy.Type(), err)
}
})
}
return c, err
}
@ -92,29 +55,15 @@ func (u *URLTest) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
}
// Unwrap implements C.ProxyAdapter
func (u *URLTest) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
return u.fast(touch)
func (u *URLTest) Unwrap(*C.Metadata) C.Proxy {
return u.fast(true)
}
func (u *URLTest) fast(touch bool) C.Proxy {
proxies := u.GetProxies(touch)
if u.selected != "" {
for _, proxy := range proxies {
if !proxy.Alive() {
continue
}
if proxy.Name() == u.selected {
u.fastNode = proxy
return proxy
}
}
}
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
elm, _, _ := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch)
fast := proxies[0]
// min := fast.LastDelay()
min := fast.LastDelayForTestUrl(u.testUrl)
min := fast.LastDelay()
fastNotExist := true
for _, proxy := range proxies[1:] {
@ -122,29 +71,24 @@ func (u *URLTest) fast(touch bool) C.Proxy {
fastNotExist = false
}
// if !proxy.Alive() {
if !proxy.AliveForTestUrl(u.testUrl) {
if !proxy.Alive() {
continue
}
// delay := proxy.LastDelay()
delay := proxy.LastDelayForTestUrl(u.testUrl)
delay := proxy.LastDelay()
if delay < min {
fast = proxy
min = delay
}
}
// tolerance
// if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.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
}
return u.fastNode, nil
})
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
u.Touch()
}
return elm
}
@ -154,12 +98,8 @@ func (u *URLTest) SupportUDP() bool {
if u.disableUDP {
return false
}
return u.fast(false).SupportUDP()
}
// IsL3Protocol implements C.ProxyAdapter
func (u *URLTest) IsL3Protocol(metadata *C.Metadata) bool {
return u.fast(false).IsL3Protocol(metadata)
return u.fast(false).SupportUDP()
}
// MarshalJSON implements C.ProxyAdapter
@ -169,11 +109,9 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]any{
"type": u.Type().String(),
"now": u.Now(),
"all": all,
"testUrl": u.testUrl,
"expected": u.expectedStatus,
"type": u.Type().String(),
"now": u.Now(),
"all": all,
})
}
@ -201,14 +139,10 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
},
option.Filter,
option.ExcludeFilter,
option.ExcludeType,
providers,
}),
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP,
testUrl: option.URL,
expectedStatus: option.ExpectedStatus,
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
disableUDP: option.DisableUDP,
}
for _, option := range options {

View File

@ -1,6 +1,57 @@
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 {
Set(string) error
ForceSet(name string)
}

View File

@ -3,15 +3,13 @@ package adapter
import (
"fmt"
tlsC "github.com/Dreamacro/clash/component/tls"
"github.com/Dreamacro/clash/adapter/outbound"
"github.com/Dreamacro/clash/common/structure"
C "github.com/Dreamacro/clash/constant"
)
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)
if !existType {
return nil, fmt.Errorf("missing type")
@ -23,7 +21,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
)
switch proxyType {
case "ss":
ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption)
if err != nil {
break
@ -42,30 +40,28 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
if err != nil {
break
}
proxy, err = outbound.NewSocks5(*socksOption)
proxy = outbound.NewSocks5(*socksOption)
case "http":
httpOption := &outbound.HttpOption{}
err = decoder.Decode(mapping, httpOption)
if err != nil {
break
}
proxy, err = outbound.NewHttp(*httpOption)
proxy = outbound.NewHttp(*httpOption)
case "vmess":
vmessOption := &outbound.VmessOption{
HTTPOpts: outbound.HTTPOptions{
Method: "GET",
Path: []string{"/"},
},
ClientFingerprint: tlsC.GetGlobalFingerprint(),
}
err = decoder.Decode(mapping, vmessOption)
if err != nil {
break
}
proxy, err = outbound.NewVmess(*vmessOption)
case "vless":
vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
vlessOption := &outbound.VlessOption{}
err = decoder.Decode(mapping, vlessOption)
if err != nil {
break
@ -79,7 +75,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
}
proxy, err = outbound.NewSnell(*snellOption)
case "trojan":
trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()}
trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption)
if err != nil {
break
@ -92,34 +88,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
break
}
proxy, err = outbound.NewHysteria(*hyOption)
case "hysteria2":
hyOption := &outbound.Hysteria2Option{}
err = decoder.Decode(mapping, hyOption)
if err != nil {
break
}
proxy, err = outbound.NewHysteria2(*hyOption)
case "wireguard":
wgOption := &outbound.WireGuardOption{}
err = decoder.Decode(mapping, wgOption)
if err != nil {
break
}
proxy, err = outbound.NewWireGuard(*wgOption)
case "tuic":
tuicOption := &outbound.TuicOption{}
err = decoder.Decode(mapping, tuicOption)
if err != nil {
break
}
proxy, err = outbound.NewTuic(*tuicOption)
case "direct":
directOption := &outbound.DirectOption{}
err = decoder.Decode(mapping, directOption)
if err != nil {
break
}
proxy = outbound.NewDirectWithOption(*directOption)
default:
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
}
@ -128,19 +96,5 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
return nil, err
}
if muxMapping, muxExist := mapping["smux"].(map[string]any); muxExist {
muxOption := &outbound.SingMuxOption{}
err = decoder.Decode(muxMapping, muxOption)
if err != nil {
return nil, err
}
if muxOption.Enabled {
proxy, err = outbound.NewSingMux(*muxOption, proxy, proxy.(outbound.ProxyBase))
if err != nil {
return nil, err
}
}
}
return NewProxy(proxy), nil
}

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,11 @@ package provider
import (
"context"
"strings"
"sync"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/batch"
"github.com/Dreamacro/clash/common/singledo"
"github.com/Dreamacro/clash/common/utils"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/dlclark/regexp2"
"go.uber.org/atomic"
)
const (
@ -25,45 +18,32 @@ type HealthCheckOption struct {
Interval uint
}
type extraOption struct {
expectedStatus utils.IntRanges[uint16]
filters map[string]struct{}
}
type HealthCheck struct {
url string
extra map[string]*extraOption
mu sync.Mutex
started *atomic.Bool
proxies []C.Proxy
interval uint
lazy bool
expectedStatus utils.IntRanges[uint16]
lastTouch *atomic.Int64
done chan struct{}
singleDo *singledo.Single[struct{}]
url string
proxies []C.Proxy
interval uint
lazy bool
lastTouch *atomic.Int64
done chan struct{}
}
func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
hc.start()
go func() {
time.Sleep(30 * time.Second)
hc.check()
}()
for {
select {
case <-ticker.C:
now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
} else {
log.Debugln("Skip once health check because we are lazy")
}
case <-hc.done:
ticker.Stop()
hc.stop()
return
}
}
@ -73,63 +53,6 @@ func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
hc.proxies = proxies
}
func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
url = strings.TrimSpace(url)
if len(url) == 0 || url == hc.url {
log.Debugln("ignore invalid health check url: %s", url)
return
}
hc.mu.Lock()
defer hc.mu.Unlock()
// if the provider has not set up health checks, then modify it to be the same as the group's interval
if hc.interval == 0 {
hc.interval = interval
}
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
}
// due to the time-consuming nature of health checks, a maximum of defaultMaxTestURLNum URLs can be set for testing
if len(hc.extra) > C.DefaultMaxHealthCheckUrlNum {
log.Debugln("skip add url: %s to health check because it has reached the maximum limit: %d", url, C.DefaultMaxHealthCheckUrlNum)
return
}
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
}
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
filter = strings.TrimSpace(filter)
if len(filter) != 0 {
for _, regex := range strings.Split(filter, "`") {
regex = strings.TrimSpace(regex)
if len(regex) != 0 {
option.filters[regex] = struct{}{}
}
}
}
}
func (hc *HealthCheck) auto() bool {
return hc.interval != 0
}
@ -138,102 +61,31 @@ func (hc *HealthCheck) touch() {
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() {
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
id := utils.NewUUIDV4().String()
log.Debugln("Start New Health Checking {%s}", id)
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
// execute default health check
option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
hc.execute(b, hc.url, id, option)
// execute extra health check
if len(hc.extra) != 0 {
for url, option := range hc.extra {
hc.execute(b, url, id, option)
}
}
b.Wait()
log.Debugln("Finish A Health Checking {%s}", id)
return struct{}{}, nil
})
}
func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
url = strings.TrimSpace(url)
if len(url) == 0 {
log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
return
}
var filterReg *regexp2.Regexp
var store = C.OriginalHistory
var expectedStatus utils.IntRanges[uint16]
if option != nil {
if url != hc.url {
store = C.ExtraHistory
}
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, "|"), 0)
}
}
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
for _, proxy := range hc.proxies {
// skip proxies that do not require health check
if filterReg != nil {
if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil {
continue
}
}
p := proxy
b.Go(p.Name(), func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
_, _ = p.URLTest(ctx, url, expectedStatus, store)
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)
_, _ = p.URLTest(ctx, hc.url)
return false, nil
})
}
b.Wait()
}
func (hc *HealthCheck) close() {
hc.done <- struct{}{}
}
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck {
if len(url) == 0 {
interval = 0
expectedStatus = nil
}
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
return &HealthCheck{
proxies: proxies,
url: url,
extra: map[string]*extraOption{},
started: atomic.NewBool(false),
interval: interval,
lazy: lazy,
expectedStatus: expectedStatus,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
singleDo: singledo.NewSingle[struct{}](time.Second),
proxies: proxies,
url: url,
interval: interval,
lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
}
}

View File

@ -6,35 +6,26 @@ import (
"time"
"github.com/Dreamacro/clash/common/structure"
"github.com/Dreamacro/clash/common/utils"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
)
var (
errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
)
var errVehicleType = errors.New("unsupport vehicle type")
type healthCheckSchema struct {
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
ExpectedStatus string `provider:"expected-status,omitempty"`
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
}
type proxyProviderSchema struct {
Type string `provider:"type"`
Path string `provider:"path,omitempty"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
ExcludeFilter string `provider:"exclude-filter,omitempty"`
ExcludeType string `provider:"exclude-type,omitempty"`
DialerProxy string `provider:"dialer-proxy,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
Type string `provider:"type"`
Path string `provider:"path"`
URL string `provider:"url,omitempty"`
Interval int `provider:"interval,omitempty"`
Filter string `provider:"filter,omitempty"`
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
}
func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvider, error) {
@ -49,42 +40,25 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
return nil, err
}
expectedStatus, err := utils.NewIntRanges[uint16](schema.HealthCheck.ExpectedStatus)
if err != nil {
return nil, err
}
var hcInterval uint
if schema.HealthCheck.Enable {
hcInterval = uint(schema.HealthCheck.Interval)
}
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy, expectedStatus)
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
path := C.Path.Resolve(schema.Path)
var vehicle types.Vehicle
switch schema.Type {
case "file":
path := C.Path.Resolve(schema.Path)
vehicle = resource.NewFileVehicle(path)
vehicle = NewFileVehicle(path)
case "http":
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)
} else {
path := C.Path.GetPathByHash("proxies", schema.URL)
vehicle = resource.NewHTTPVehicle(schema.URL, path)
}
vehicle = NewHTTPVehicle(schema.URL, path)
default:
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
}
interval := time.Duration(uint(schema.Interval)) * time.Second
filter := schema.Filter
excludeFilter := schema.ExcludeFilter
excludeType := schema.ExcludeType
dialerProxy := schema.DialerProxy
return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, dialerProxy, vehicle, hc)
return NewProxySetProvider(name, interval, filter, vehicle, hc)
}

View File

@ -1,25 +1,17 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"math"
"runtime"
"strings"
"time"
"github.com/Dreamacro/clash/adapter"
"github.com/Dreamacro/clash/common/convert"
"github.com/Dreamacro/clash/common/utils"
clashHttp "github.com/Dreamacro/clash/component/http"
"github.com/Dreamacro/clash/component/resource"
C "github.com/Dreamacro/clash/constant"
types "github.com/Dreamacro/clash/constant/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel/statistic"
"github.com/dlclark/regexp2"
"gopkg.in/yaml.v3"
)
@ -38,31 +30,28 @@ type ProxySetProvider struct {
}
type proxySetProvider struct {
*resource.Fetcher[[]C.Proxy]
proxies []C.Proxy
healthCheck *HealthCheck
version uint32
subscriptionInfo *SubscriptionInfo
*fetcher[[]C.Proxy]
proxies []C.Proxy
healthCheck *HealthCheck
version uint
}
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"name": pp.Name(),
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
"testUrl": pp.healthCheck.url,
"updatedAt": pp.UpdatedAt,
"subscriptionInfo": pp.subscriptionInfo,
"name": pp.Name(),
"type": pp.Type().String(),
"vehicleType": pp.VehicleType().String(),
"proxies": pp.Proxies(),
"updatedAt": pp.updatedAt,
})
}
func (pp *proxySetProvider) Version() uint32 {
func (pp *proxySetProvider) Version() uint {
return pp.version
}
func (pp *proxySetProvider) Name() string {
return pp.Fetcher.Name()
return pp.name
}
func (pp *proxySetProvider) HealthCheck() {
@ -70,21 +59,19 @@ func (pp *proxySetProvider) HealthCheck() {
}
func (pp *proxySetProvider) Update() error {
elm, same, err := pp.Fetcher.Update()
elm, same, err := pp.fetcher.Update()
if err == nil && !same {
pp.OnUpdate(elm)
pp.onUpdate(elm)
}
return err
}
func (pp *proxySetProvider) Initial() error {
elm, err := pp.Fetcher.Initial()
elm, err := pp.fetcher.Initial()
if err != nil {
return err
}
pp.OnUpdate(elm)
pp.getSubscriptionInfo()
pp.closeAllConnections()
pp.onUpdate(elm)
return nil
}
@ -100,86 +87,23 @@ func (pp *proxySetProvider) Touch() {
pp.healthCheck.touch()
}
func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
pp.proxies = proxies
pp.healthCheck.setProxy(proxies)
if pp.healthCheck.auto() {
go pp.healthCheck.check()
defer func() { go pp.healthCheck.check() }()
}
}
func (pp *proxySetProvider) getSubscriptionInfo() {
if pp.VehicleType() != types.HTTP {
return
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*90)
defer cancel()
resp, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {"clash"}}, nil)
if err != nil {
return
}
defer resp.Body.Close()
userInfoStr := strings.TrimSpace(resp.Header.Get("subscription-userinfo"))
if userInfoStr == "" {
resp2, err := clashHttp.HttpRequest(ctx, pp.Vehicle().(*resource.HTTPVehicle).Url(),
http.MethodGet, http.Header{"User-Agent": {"Quantumultx"}}, nil)
if err != nil {
return
}
defer resp2.Body.Close()
userInfoStr = strings.TrimSpace(resp2.Header.Get("subscription-userinfo"))
if userInfoStr == "" {
return
}
}
pp.subscriptionInfo, err = NewSubscriptionInfo(userInfoStr)
if err != nil {
log.Warnln("[Provider] get subscription-userinfo: %e", err)
}
}()
}
func (pp *proxySetProvider) closeAllConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
for _, chain := range c.Chains() {
if chain == pp.Name() {
_ = c.Close()
break
}
}
return true
})
}
func stopProxyProvider(pd *ProxySetProvider) {
pd.healthCheck.close()
_ = pd.Fetcher.Destroy()
_ = pd.fetcher.Destroy()
}
func NewProxySetProvider(name string, interval time.Duration, filter string, excludeFilter string, excludeType string, dialerProxy string, vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
excludeFilterReg, err := regexp2.Compile(excludeFilter, 0)
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 excludeFilter regex: %w", err)
}
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
}
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, 0)
if err != nil {
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
filterRegs = append(filterRegs, filterReg)
return nil, fmt.Errorf("invalid filter regex: %w", err)
}
if hc.auto() {
@ -191,8 +115,9 @@ func NewProxySetProvider(name string, interval time.Duration, filter string, exc
healthCheck: hc,
}
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, excludeFilter, excludeTypeArray, filterRegs, excludeFilterReg, dialerProxy), proxiesOnUpdate(pd))
pd.Fetcher = fetcher
fetcher := newFetcher[[]C.Proxy](name, interval, vehicle, proxiesParseAndFilter(filter, filterReg), proxiesOnUpdate(pd))
pd.fetcher = fetcher
wrapper := &ProxySetProvider{pd}
runtime.SetFinalizer(wrapper, stopProxyProvider)
return wrapper, nil
@ -207,7 +132,7 @@ type compatibleProvider struct {
name string
healthCheck *HealthCheck
proxies []C.Proxy
version uint32
version uint
}
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
@ -216,11 +141,10 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
"type": cp.Type().String(),
"vehicleType": cp.VehicleType().String(),
"proxies": cp.Proxies(),
"testUrl": cp.healthCheck.url,
})
}
func (cp *compatibleProvider) Version() uint32 {
func (cp *compatibleProvider) Version() uint {
return cp.version
}
@ -256,10 +180,6 @@ func (cp *compatibleProvider) Touch() {
cp.healthCheck.touch()
}
func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}
@ -287,19 +207,22 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
return func(elm []C.Proxy) {
pd.setProxies(elm)
pd.version += 1
pd.getSubscriptionInfo()
if pd.version == math.MaxUint {
pd.version = 0
} else {
pd.version++
}
}
}
func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray []string, filterRegs []*regexp2.Regexp, excludeFilterReg *regexp2.Regexp, dialerProxy string) resource.Parser[[]C.Proxy] {
func proxiesParseAndFilter(filter string, filterReg *regexp2.Regexp) parser[[]C.Proxy] {
return func(buf []byte) ([]C.Proxy, error) {
schema := &ProxySchema{}
if err := yaml.Unmarshal(buf, schema); err != nil {
proxies, err1 := convert.ConvertsV2Ray(buf)
if err1 != nil {
return nil, fmt.Errorf("%w, %w", err, err1)
return nil, fmt.Errorf("%s, %w", err.Error(), err1)
}
schema.Proxies = proxies
}
@ -309,62 +232,17 @@ func proxiesParseAndFilter(filter string, excludeFilter string, excludeTypeArray
}
proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs {
for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 {
mType, ok := mapping["type"]
if !ok {
continue
}
pType, ok := mType.(string)
if !ok {
continue
}
flag := false
for i := range excludeTypeArray {
if strings.EqualFold(pType, excludeTypeArray[i]) {
flag = true
break
}
}
if flag {
continue
}
}
mName, ok := mapping["name"]
if !ok {
continue
}
name, ok := mName.(string)
if !ok {
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.FindStringMatch(name); mat != nil {
continue
}
}
if len(filter) > 0 {
if mat, _ := filterReg.FindStringMatch(name); mat == nil {
continue
}
}
if _, ok := proxiesSet[name]; ok {
continue
}
if len(dialerProxy) > 0 {
mapping["dialer-proxy"] = dialerProxy
}
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)
for idx, mapping := range schema.Proxies {
name, ok := mapping["name"]
mat, _ := filterReg.FindStringMatch(name.(string))
if ok && len(filter) > 0 && mat == nil {
continue
}
proxy, err := adapter.ParseProxy(mapping)
if err != nil {
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
}
proxies = append(proxies, proxy)
}
if len(proxies) == 0 {

View File

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

View File

@ -1,14 +1,13 @@
package resource
package provider
import (
"context"
"errors"
"io"
"net/http"
"os"
"time"
clashHttp "github.com/Dreamacro/clash/component/http"
netHttp "github.com/Dreamacro/clash/component/http"
types "github.com/Dreamacro/clash/constant/provider"
)
@ -37,10 +36,6 @@ type HTTPVehicle struct {
path string
}
func (h *HTTPVehicle) Url() string {
return h.url
}
func (h *HTTPVehicle) Type() types.VehicleType {
return types.HTTP
}
@ -52,14 +47,12 @@ func (h *HTTPVehicle) Path() string {
func (h *HTTPVehicle) Read() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
resp, err := clashHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
resp, err := netHttp.HttpRequest(ctx, h.url, http.MethodGet, nil, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, errors.New(resp.Status)
}
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err

View File

@ -1,205 +0,0 @@
package atomic
import (
"encoding/json"
"fmt"
"strconv"
"sync/atomic"
)
type Bool struct {
atomic.Bool
}
func NewBool(val bool) *Bool {
i := &Bool{}
i.Store(val)
return i
}
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) *Pointer[T] {
var p Pointer[T]
if v != nil {
p.Store(v)
}
return &p
}
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) *Int32 {
i := &Int32{}
i.Store(val)
return i
}
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) *Int64 {
i := &Int64{}
i.Store(val)
return i
}
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) *Uint32 {
i := &Uint32{}
i.Store(val)
return i
}
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) *Uint64 {
i := &Uint64{}
i.Store(val)
return i
}
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) *Uintptr {
i := &Uintptr{}
i.Store(val)
return i
}
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,58 +0,0 @@
package atomic
import (
"encoding/json"
"sync/atomic"
)
func DefaultValue[T any]() T {
var defaultValue T
return defaultValue
}
type TypedValue[T any] struct {
value atomic.Value
}
func (t *TypedValue[T]) Load() T {
value := t.value.Load()
if value == nil {
return DefaultValue[T]()
}
return value.(T)
}
func (t *TypedValue[T]) Store(value T) {
t.value.Store(value)
}
func (t *TypedValue[T]) Swap(new T) T {
old := t.value.Swap(new)
if old == nil {
return DefaultValue[T]()
}
return old.(T)
}
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
return t.value.CompareAndSwap(old, 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) *TypedValue[T] {
v := &TypedValue[T]{}
v.Store(t)
return v
}

View File

@ -1,20 +0,0 @@
package buf
import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
)
const BufferSize = buf.BufferSize
type Buffer = buf.Buffer
var New = buf.New
var 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

@ -7,8 +7,6 @@ import (
"time"
"github.com/Dreamacro/clash/common/generics/list"
"github.com/samber/lo"
)
// Option is part of Functional Options Pattern
@ -67,8 +65,8 @@ type LruCache[K comparable, V any] struct {
onEvict EvictCallback[K, V]
}
// New creates an LruCache
func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
// NewLRUCache creates an LruCache
func NewLRUCache[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
lc := &LruCache[K, V]{
lru: list.New[*entry[K, V]](),
cache: make(map[K]*list.Element[*entry[K, V]]),
@ -84,27 +82,9 @@ func New[K comparable, V any](options ...Option[K, V]) *LruCache[K, V] {
// Get returns the any representation of a cached response and a bool
// set to true if the key was found.
func (c *LruCache[K, V]) Get(key K) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
return lo.Empty[V](), false
}
value := el.value
return value, true
}
func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
value := constructor()
c.set(key, value)
return value, false
return getZero[V](), false
}
value := el.value
@ -116,12 +96,9 @@ func (c *LruCache[K, V]) GetOrStore(key K, constructor func() V) (V, bool) {
// and a bool set to true if the key was found.
// This method will NOT check the maxAge of element and will NOT update the expires.
func (c *LruCache[K, V]) GetWithExpire(key K) (V, time.Time, bool) {
c.mu.Lock()
defer c.mu.Unlock()
el := c.get(key)
if el == nil {
return lo.Empty[V](), time.Time{}, false
return getZero[V](), time.Time{}, false
}
return el.value, time.Unix(el.expires, 0), true
@ -138,18 +115,11 @@ func (c *LruCache[K, V]) Exist(key K) bool {
// Set stores the any representation of a response for a given key.
func (c *LruCache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.set(key, value)
}
func (c *LruCache[K, V]) set(key K, value V) {
expires := int64(0)
if c.maxAge > 0 {
expires = time.Now().Unix() + c.maxAge
}
c.setWithExpire(key, value, time.Unix(expires, 0))
c.SetWithExpire(key, value, time.Unix(expires, 0))
}
// SetWithExpire stores the any representation of a response for a given key and given expires.
@ -158,10 +128,6 @@ func (c *LruCache[K, V]) SetWithExpire(key K, value V, expires time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.setWithExpire(key, value, expires)
}
func (c *LruCache[K, V]) setWithExpire(key K, value V, expires time.Time) {
if le, ok := c.cache[key]; ok {
c.lru.MoveToBack(le)
e := le.Value
@ -199,6 +165,9 @@ func (c *LruCache[K, V]) CloneTo(n *LruCache[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]
if !ok {
return nil
@ -222,11 +191,12 @@ func (c *LruCache[K, V]) get(key K) *entry[K, V] {
// Delete removes the value associated with a key.
func (c *LruCache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
if le, ok := c.cache[key]; ok {
c.deleteElement(le)
}
c.mu.Unlock()
}
func (c *LruCache[K, V]) maybeDeleteOldest() {
@ -249,10 +219,10 @@ func (c *LruCache[K, V]) deleteElement(le *list.Element[*entry[K, V]]) {
func (c *LruCache[K, V]) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[K]*list.Element[*entry[K, V]])
c.mu.Unlock()
return nil
}
@ -261,3 +231,8 @@ type entry[K comparable, V any] struct {
value V
expires int64
}
func getZero[T any]() T {
var result T
return result
}

View File

@ -19,7 +19,7 @@ var entries = []struct {
}
func TestLRUCache(t *testing.T) {
c := New[string, string]()
c := NewLRUCache[string, string]()
for _, e := range entries {
c.Set(e.key, e.value)
@ -45,7 +45,7 @@ func TestLRUCache(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()
expected := now + 86400
@ -88,7 +88,7 @@ func TestLRUMaxAge(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()
expires := now + 86400/2
@ -103,7 +103,7 @@ func TestLRUpdateOnGet(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
c.Set("foo", "bar")
_, ok := c.Get("foo")
@ -117,7 +117,7 @@ func TestMaxSize(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)
assert.True(t, c.Exist(1))
c.Set(2, 3)
@ -130,7 +130,7 @@ func TestEvict(t *testing.T) {
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(2, 3)
@ -138,7 +138,7 @@ func TestEvict(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()
tenSecBefore := time.Unix(now-10, 0)
@ -153,7 +153,7 @@ func TestSetWithExpire(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()
tenSecBefore := time.Unix(now-10, 0)
@ -166,11 +166,11 @@ func TestStale(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("2", 2)
n := New[string, int](WithSize[string, int](2))
n := NewLRUCache[string, int](WithSize[string, int](2))
n.Set("3", 3)
n.Set("4", 4)

View File

@ -1,55 +0,0 @@
package callback
import (
"github.com/Dreamacro/clash/common/buf"
N "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant"
)
type firstWriteCallBackConn struct {
C.Conn
callback func(error)
written bool
}
func (c *firstWriteCallBackConn) Write(b []byte) (n int, err error) {
defer func() {
if !c.written {
c.written = true
c.callback(err)
}
}()
return c.Conn.Write(b)
}
func (c *firstWriteCallBackConn) 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

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

View File

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

View File

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

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"
"strconv"
"strings"
"github.com/Dreamacro/clash/log"
)
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) {
data := DecodeBase64(buf)
data, err := DecodeBase64(buf)
if err != nil {
data = buf
}
arr := strings.Split(string(data), "\n")
@ -50,97 +73,14 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
hysteria["port"] = urlHysteria.Port()
hysteria["sni"] = query.Get("peer")
hysteria["obfs"] = query.Get("obfs")
if alpn := query.Get("alpn"); alpn != "" {
hysteria["alpn"] = strings.Split(alpn, ",")
}
hysteria["alpn"] = query.Get("alpn")
hysteria["auth_str"] = query.Get("auth")
hysteria["protocol"] = query.Get("protocol")
up := query.Get("up")
down := query.Get("down")
if up == "" {
up = query.Get("upmbps")
}
if down == "" {
down = query.Get("downmbps")
}
hysteria["down"] = down
hysteria["up"] = up
hysteria["down_mbps"], _ = strconv.Atoi(query.Get("downmbps"))
hysteria["up_mbps"], _ = strconv.Atoi(query.Get("upmbps"))
hysteria["skip-cert-verify"], _ = strconv.ParseBool(query.Get("insecure"))
proxies = append(proxies, hysteria)
case "hysteria2":
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"] = scheme
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
}
case "trojan":
urlTrojan, err := url.Parse(line)
@ -159,14 +99,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["port"] = urlTrojan.Port()
trojan["password"] = urlTrojan.User.Username()
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
}
if alpn := query.Get("alpn"); alpn != "" {
trojan["alpn"] = strings.Split(alpn, ",")
}
network := strings.ToLower(query.Get("type"))
if network != "" {
@ -178,6 +116,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
headers := make(map[string]any)
wsOpts := make(map[string]any)
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
wsOpts["path"] = query.Get("path")
@ -191,54 +130,97 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
trojan["grpc-opts"] = grpcOpts
}
if fingerprint := query.Get("fp"); fingerprint == "" {
trojan["client-fingerprint"] = "chrome"
} else {
trojan["client-fingerprint"] = fingerprint
}
proxies = append(proxies, trojan)
case "vless":
urlVLess, err := url.Parse(line)
urlVless, err := url.Parse(line)
if err != nil {
continue
}
query := urlVLess.Query()
query := urlVless.Query()
name := uniqueName(names, urlVless.Fragment)
vless := make(map[string]any, 20)
err = handleVShareLink(names, urlVLess, scheme, vless)
if err != nil {
log.Warnln("error:%s line:%s", err.Error(), line)
continue
vless["name"] = name
vless["type"] = scheme
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)
case "vmess":
// V2RayN-styled share link
// https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2)
dcBuf, err := tryDecodeBase64([]byte(body))
dcBuf, err := enc.DecodeString(body)
if err != nil {
// Xray VMessAEAD share link
urlVMess, err := url.Parse(line)
if err != nil {
continue
}
query := urlVMess.Query()
vmess := make(map[string]any, 20)
err = handleVShareLink(names, urlVMess, scheme, vmess)
if err != nil {
log.Warnln("error:%s line:%s", err.Error(), line)
continue
}
vmess["alterId"] = 0
vmess["cipher"] = "auto"
if encryption := query.Get("encryption"); encryption != "" {
vmess["cipher"] = encryption
}
proxies = append(proxies, vmess)
continue
}
@ -248,11 +230,8 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
if jsonDc.Decode(&values) != nil {
continue
}
tempName, ok := values["ps"].(string)
if !ok {
continue
}
name := uniqueName(names, tempName)
name := uniqueName(names, values["ps"].(string))
vmess := make(map[string]any, 20)
vmess["name"] = name
@ -260,56 +239,38 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
vmess["server"] = values["add"]
vmess["port"] = values["port"]
vmess["uuid"] = values["id"]
if alterId, ok := values["aid"]; ok {
vmess["alterId"] = alterId
} else {
vmess["alterId"] = 0
}
vmess["alterId"] = values["aid"]
vmess["cipher"] = "auto"
vmess["udp"] = true
vmess["xudp"] = true
vmess["tls"] = false
vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto"
if cipher, ok := values["scy"]; ok && cipher != "" {
vmess["cipher"] = cipher
sni := values["sni"]
if sni != "" {
vmess["sni"] = sni
}
if sni, ok := values["sni"]; ok && sni != "" {
vmess["servername"] = sni
}
host := values["host"]
network := strings.ToLower(values["net"].(string))
network, _ := values["net"].(string)
network = strings.ToLower(network)
if values["type"] == "http" {
network = "http"
} else if network == "http" {
network = "h2"
}
vmess["network"] = network
tls, ok := values["tls"].(string)
if ok {
tls = strings.ToLower(tls)
if strings.HasSuffix(tls, "tls") {
vmess["tls"] = true
}
if alpn, ok := values["alpn"].(string); ok {
vmess["alpn"] = strings.Split(alpn, ",")
tls := strings.ToLower(values["tls"].(string))
if tls != "" && tls != "0" && tls != "null" {
if host != nil {
vmess["servername"] = host
}
vmess["tls"] = true
}
switch network {
case "http":
headers := make(map[string]any)
httpOpts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)}
}
httpOpts["path"] = []string{"/"}
if path, ok := values["path"]; ok && path != "" {
httpOpts["path"] = []string{path.(string)}
}
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
httpOpts["method"] = values["method"]
httpOpts["path"] = values["path"]
httpOpts["headers"] = headers
vmess["http-opts"] = httpOpts
@ -317,10 +278,9 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "h2":
headers := make(map[string]any)
h2Opts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = []string{host.(string)}
}
// headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
h2Opts["path"] = values["path"]
h2Opts["headers"] = headers
@ -329,14 +289,15 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "ws":
headers := make(map[string]any)
wsOpts := make(map[string]any)
wsOpts["path"] = []string{"/"}
if host, ok := values["host"]; ok && host != "" {
headers["Host"] = host.(string)
}
if path, ok := values["path"]; ok && path != "" {
wsOpts["path"] = path.(string)
headers["Host"] = RandHost()
headers["User-Agent"] = RandUserAgent()
if values["path"] != nil {
wsOpts["path"] = values["path"]
}
wsOpts["headers"] = headers
vmess["ws-opts"] = wsOpts
case "grpc":
@ -357,7 +318,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
port := urlSS.Port()
if port == "" {
dcBuf, err := encRaw.DecodeString(urlSS.Host)
dcBuf, err := enc.DecodeString(urlSS.Host)
if err != nil {
continue
}
@ -369,28 +330,23 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
}
var (
cipherRaw = urlSS.User.Username()
cipher string
password string
cipher = urlSS.User.Username()
password string
)
cipher = cipherRaw
if password, found = urlSS.User.Password(); !found {
dcBuf, err := base64.RawURLEncoding.DecodeString(cipherRaw)
dcBuf, err := enc.DecodeString(cipher)
if err != nil {
dcBuf, _ = enc.DecodeString(cipherRaw)
continue
}
cipher, password, found = strings.Cut(string(dcBuf), ":")
if !found {
continue
}
err = VerifyMethod(cipher, password)
if err != nil {
dcBuf, _ = encRaw.DecodeString(cipherRaw)
cipher, password, found = strings.Cut(string(dcBuf), ":")
}
}
ss := make(map[string]any, 10)
ss := make(map[string]any, 20)
ss["name"] = name
ss["type"] = scheme
@ -398,23 +354,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
ss["port"] = urlSS.Port()
ss["cipher"] = cipher
ss["password"] = password
query := urlSS.Query()
ss["udp"] = true
if query.Get("udp-over-tcp") == "true" || query.Get("uot") == "1" {
ss["udp-over-tcp"] = true
}
if strings.Contains(query.Get("plugin"), "obfs") {
obfsParams := strings.Split(query.Get("plugin"), ";")
ss["plugin"] = "obfs"
ss["plugin-opts"] = map[string]any{
"host": obfsParams[2][10:],
"mode": obfsParams[1][5:],
}
}
proxies = append(proxies, ss)
proxies = append(proxies, ss)
case "ssr":
dcBuf, err := encRaw.DecodeString(body)
dcBuf, err := enc.DecodeString(body)
if err != nil {
continue
}
@ -481,6 +425,18 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
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 {
if index, ok := names[name]; ok {
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)
}

View File

@ -2,14 +2,11 @@ package convert
import (
"encoding/base64"
"math/rand"
"net/http"
"strings"
"time"
"github.com/Dreamacro/clash/common/utils"
"github.com/metacubex/sing-shadowsocks/shadowimpl"
"github.com/zhangyunhao116/fastrand"
"github.com/gofrs/uuid"
)
var hostsSuffix = []string{
@ -294,7 +291,8 @@ var (
)
func RandHost() string {
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(utils.NewUUIDV4().Bytes()))
id, _ := uuid.NewV4()
base := strings.ToLower(base64.RawURLEncoding.EncodeToString(id.Bytes()))
base = strings.ReplaceAll(base, "-", "")
base = strings.ReplaceAll(base, "_", "")
buf := []byte(base)
@ -302,11 +300,11 @@ func RandHost() string {
prefix += string(buf[6:8]) + "-"
prefix += string(buf[len(buf)-8:])
return prefix + hostsSuffix[fastrand.Intn(hostsLen)]
return prefix + hostsSuffix[rand.Intn(hostsLen)]
}
func RandUserAgent() string {
return userAgents[fastrand.Intn(uaLen)]
return userAgents[rand.Intn(uaLen)]
}
func SetUserAgent(header http.Header) {
@ -316,8 +314,3 @@ func SetUserAgent(header http.Header) {
userAgent := RandUserAgent()
header.Set("User-Agent", userAgent)
}
func VerifyMethod(cipher, password string) (err error) {
_, err = shadowimpl.FetchMethod(cipher, password, time.Now)
return
}

View File

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

View File

@ -5,10 +5,10 @@
// Package list implements a doubly linked list.
//
// To iterate over a list (where l is a *List):
//
// for e := l.Front(); e != nil; e = e.Next() {
// // do something with e.Value
// }
//
package list
// Element is an element of a linked list.

View File

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

View File

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

View File

@ -3,23 +3,18 @@ package net
import (
"bufio"
"net"
"github.com/Dreamacro/clash/common/buf"
)
var _ ExtendedConn = (*BufferedConn)(nil)
type BufferedConn struct {
r *bufio.Reader
ExtendedConn
peeked bool
net.Conn
}
func NewBufferedConn(c net.Conn) *BufferedConn {
if bc, ok := c.(*BufferedConn); ok {
return bc
}
return &BufferedConn{bufio.NewReader(c), NewExtendedConn(c), false}
return &BufferedConn{bufio.NewReader(c), c}
}
// Reader returns the internal bufio.Reader.
@ -27,24 +22,11 @@ func (c *BufferedConn) Reader() *bufio.Reader {
return c.r
}
func (c *BufferedConn) ResetPeeked() {
c.peeked = false
}
func (c *BufferedConn) Peeked() bool {
return c.peeked
}
// Peek returns the next n bytes without advancing the reader.
func (c *BufferedConn) Peek(n int) ([]byte, error) {
c.peeked = true
return c.r.Peek(n)
}
func (c *BufferedConn) Discard(n int) (discarded int, err error) {
return c.r.Discard(n)
}
func (c *BufferedConn) Read(p []byte) (int, error) {
return c.r.Read(p)
}
@ -60,37 +42,3 @@ func (c *BufferedConn) UnreadByte() error {
func (c *BufferedConn) Buffered() int {
return c.r.Buffered()
}
func (c *BufferedConn) ReadBuffer(buffer *buf.Buffer) (err error) {
if c.r != nil && c.r.Buffered() > 0 {
_, err = buffer.ReadOnceFrom(c.r)
return
}
return c.ExtendedConn.ReadBuffer(buffer)
}
func (c *BufferedConn) ReadCached() *buf.Buffer { // call in sing/common/bufio.Copy
if c.r != nil && c.r.Buffered() > 0 {
length := c.r.Buffered()
b, _ := c.r.Peek(length)
_, _ = c.r.Discard(length)
c.r = nil // drop bufio.Reader to let gc can clean up its internal buf
return buf.As(b)
}
return nil
}
func (c *BufferedConn) Upstream() any {
return c.ExtendedConn
}
func (c *BufferedConn) ReaderReplaceable() bool {
if c.r != nil && c.r.Buffered() > 0 {
return false
}
return true
}
func (c *BufferedConn) WriterReplaceable() bool {
return true
}

View File

@ -1,154 +0,0 @@
package deadline
import (
"net"
"os"
"runtime"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/Dreamacro/clash/common/net/packet"
)
type readResult struct {
data []byte
addr net.Addr
err error
}
type NetPacketConn struct {
net.PacketConn
deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline
disablePipe atomic.Bool
inRead atomic.Bool
resultCh chan any
}
func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
npc := &NetPacketConn{
PacketConn: pc,
pipeDeadline: makePipeDeadline(),
resultCh: make(chan any, 1),
}
npc.resultCh <- nil
if enhancePC, isEnhance := pc.(packet.EnhancePacketConn); isEnhance {
epc := &EnhancePacketConn{
NetPacketConn: npc,
enhancePacketConn: enhancePacketConn{
netPacketConn: npc,
enhancePacketConn: enhancePC,
},
}
if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC {
return &EnhanceSingPacketConn{
EnhancePacketConn: epc,
singPacketConn: singPacketConn{
netPacketConn: npc,
singPacketConn: singPC,
},
}
}
return epc
}
if singPC, isSingPC := pc.(packet.SingPacketConn); isSingPC {
return &SingPacketConn{
NetPacketConn: npc,
singPacketConn: singPacketConn{
netPacketConn: npc,
singPacketConn: singPC,
},
}
}
return npc
}
func (c *NetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
FOR:
for {
select {
case result := <-c.resultCh:
if result != nil {
if result, ok := result.(*readResult); ok {
n = copy(p, result.data)
addr = result.addr
err = result.err
c.resultCh <- nil // finish cache read
return
}
c.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.resultCh <- nil
break FOR
}
case <-c.pipeDeadline.wait():
return 0, nil, os.ErrDeadlineExceeded
}
}
if c.disablePipe.Load() {
return c.PacketConn.ReadFrom(p)
} else if c.deadline.Load().IsZero() {
c.inRead.Store(true)
defer c.inRead.Store(false)
n, addr, err = c.PacketConn.ReadFrom(p)
return
}
<-c.resultCh
go c.pipeReadFrom(len(p))
return c.ReadFrom(p)
}
func (c *NetPacketConn) pipeReadFrom(size int) {
buffer := make([]byte, size)
n, addr, err := c.PacketConn.ReadFrom(buffer)
buffer = buffer[:n]
result := &readResult{}
result.data = buffer
result.addr = addr
result.err = err
c.resultCh <- result
}
func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
if c.disablePipe.Load() {
return c.PacketConn.SetReadDeadline(t)
} else if c.inRead.Load() {
c.disablePipe.Store(true)
return c.PacketConn.SetReadDeadline(t)
}
c.deadline.Store(t)
c.pipeDeadline.set(t)
return nil
}
func (c *NetPacketConn) ReaderReplaceable() bool {
select {
case result := <-c.resultCh:
c.resultCh <- result
if result != nil {
return false // cache reading
} else {
break
}
default:
return false // pipe reading
}
return c.disablePipe.Load() || c.deadline.Load().IsZero()
}
func (c *NetPacketConn) WriterReplaceable() bool {
return true
}
func (c *NetPacketConn) Upstream() any {
return c.PacketConn
}
func (c *NetPacketConn) NeedAdditionalReadDeadline() bool {
return false
}

View File

@ -1,83 +0,0 @@
package deadline
import (
"net"
"os"
"runtime"
"github.com/Dreamacro/clash/common/net/packet"
)
type EnhancePacketConn struct {
*NetPacketConn
enhancePacketConn
}
var _ packet.EnhancePacketConn = (*EnhancePacketConn)(nil)
func NewEnhancePacketConn(pc packet.EnhancePacketConn) packet.EnhancePacketConn {
return NewNetPacketConn(pc).(packet.EnhancePacketConn)
}
type enhanceReadResult struct {
data []byte
put func()
addr net.Addr
err error
}
type enhancePacketConn struct {
netPacketConn *NetPacketConn
enhancePacketConn packet.EnhancePacketConn
}
func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*enhanceReadResult); ok {
data = result.data
put = result.put
addr = result.addr
err = result.err
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return nil, nil, nil, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.enhancePacketConn.WaitReadFrom()
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
data, put, addr, err = c.enhancePacketConn.WaitReadFrom()
return
}
<-c.netPacketConn.resultCh
go c.pipeWaitReadFrom()
return c.WaitReadFrom()
}
func (c *enhancePacketConn) pipeWaitReadFrom() {
data, put, addr, err := c.enhancePacketConn.WaitReadFrom()
result := &enhanceReadResult{}
result.data = data
result.put = put
result.addr = addr
result.err = err
c.netPacketConn.resultCh <- result
}

View File

@ -1,177 +0,0 @@
package deadline
import (
"os"
"runtime"
"github.com/Dreamacro/clash/common/net/packet"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingPacketConn struct {
*NetPacketConn
singPacketConn
}
var _ packet.SingPacketConn = (*SingPacketConn)(nil)
func NewSingPacketConn(pc packet.SingPacketConn) packet.SingPacketConn {
return NewNetPacketConn(pc).(packet.SingPacketConn)
}
type EnhanceSingPacketConn struct {
*EnhancePacketConn
singPacketConn
}
func NewEnhanceSingPacketConn(pc packet.EnhanceSingPacketConn) packet.EnhanceSingPacketConn {
return NewNetPacketConn(pc).(packet.EnhanceSingPacketConn)
}
var _ packet.EnhanceSingPacketConn = (*EnhanceSingPacketConn)(nil)
type singReadResult struct {
buffer *buf.Buffer
destination M.Socksaddr
err error
}
type singPacketConn struct {
netPacketConn *NetPacketConn
singPacketConn packet.SingPacketConn
}
func (c *singPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*singReadResult); ok {
destination = result.destination
err = result.err
n, _ := buffer.Write(result.buffer.Bytes())
result.buffer.Advance(n)
if result.buffer.IsEmpty() {
result.buffer.Release()
}
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.singPacketConn.ReadPacket(buffer)
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
destination, err = c.singPacketConn.ReadPacket(buffer)
return
}
<-c.netPacketConn.resultCh
go c.pipeReadPacket(buffer.FreeLen())
return c.ReadPacket(buffer)
}
func (c *singPacketConn) pipeReadPacket(pLen int) {
buffer := buf.NewSize(pLen)
destination, err := c.singPacketConn.ReadPacket(buffer)
result := &singReadResult{}
result.destination = destination
result.err = err
c.netPacketConn.resultCh <- result
}
func (c *singPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *singPacketConn) CreateReadWaiter() (N.PacketReadWaiter, bool) {
prw, isReadWaiter := bufio.CreatePacketReadWaiter(c.singPacketConn)
if isReadWaiter {
return &singPacketReadWaiter{
netPacketConn: c.netPacketConn,
packetReadWaiter: prw,
}, true
}
return nil, false
}
var _ N.PacketReadWaiter = (*singPacketReadWaiter)(nil)
type singPacketReadWaiter struct {
netPacketConn *NetPacketConn
packetReadWaiter N.PacketReadWaiter
}
type singWaitReadResult singReadResult
func (c *singPacketReadWaiter) InitializeReadWaiter(newBuffer func() *buf.Buffer) {
c.packetReadWaiter.InitializeReadWaiter(newBuffer)
}
func (c *singPacketReadWaiter) WaitReadPacket() (destination M.Socksaddr, err error) {
FOR:
for {
select {
case result := <-c.netPacketConn.resultCh:
if result != nil {
if result, ok := result.(*singWaitReadResult); ok {
destination = result.destination
err = result.err
c.netPacketConn.resultCh <- nil // finish cache read
return
}
c.netPacketConn.resultCh <- result // another type of read
runtime.Gosched() // allowing other goroutines to run
continue FOR
} else {
c.netPacketConn.resultCh <- nil
break FOR
}
case <-c.netPacketConn.pipeDeadline.wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded
}
}
if c.netPacketConn.disablePipe.Load() {
return c.packetReadWaiter.WaitReadPacket()
} else if c.netPacketConn.deadline.Load().IsZero() {
c.netPacketConn.inRead.Store(true)
defer c.netPacketConn.inRead.Store(false)
destination, err = c.packetReadWaiter.WaitReadPacket()
return
}
<-c.netPacketConn.resultCh
go c.pipeWaitReadPacket()
return c.WaitReadPacket()
}
func (c *singPacketReadWaiter) pipeWaitReadPacket() {
destination, err := c.packetReadWaiter.WaitReadPacket()
result := &singWaitReadResult{}
result.destination = destination
result.err = err
c.netPacketConn.resultCh <- result
}
func (c *singPacketReadWaiter) Upstream() any {
return c.packetReadWaiter
}

View File

@ -1,84 +0,0 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package deadline
import (
"sync"
"time"
)
// pipeDeadline is an abstraction for handling timeouts.
type pipeDeadline struct {
mu sync.Mutex // Guards timer and cancel
timer *time.Timer
cancel chan struct{} // Must be non-nil
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
}
// set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future.
//
// A zero value for t prevents timeout.
func (d *pipeDeadline) set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil && !d.timer.Stop() {
<-d.cancel // Wait for the timer callback to finish and close cancel
}
d.timer = nil
// Time is zero, then there is no deadline.
closed := isClosedChan(d.cancel)
if t.IsZero() {
if closed {
d.cancel = make(chan struct{})
}
return
}
// Time in the future, setup a timer to cancel in the future.
if dur := time.Until(t); dur > 0 {
if closed {
d.cancel = make(chan struct{})
}
d.timer = time.AfterFunc(dur, func() {
close(d.cancel)
})
return
}
// Time in the past, so close immediately.
if !closed {
close(d.cancel)
}
}
// wait returns a channel that is closed when the deadline is exceeded.
func (d *pipeDeadline) wait() chan struct{} {
d.mu.Lock()
defer d.mu.Unlock()
return d.cancel
}
func isClosedChan(c <-chan struct{}) bool {
select {
case <-c:
return true
default:
return false
}
}
func makeFilledChan() chan struct{} {
ch := make(chan struct{}, 1)
ch <- struct{}{}
return ch
}

View File

@ -1,18 +0,0 @@
package net
import (
"github.com/Dreamacro/clash/common/net/deadline"
"github.com/Dreamacro/clash/common/net/packet"
)
type EnhancePacketConn = packet.EnhancePacketConn
type WaitReadFrom = packet.WaitReadFrom
var NewEnhancePacketConn = packet.NewEnhancePacketConn
var NewThreadSafePacketConn = packet.NewThreadSafePacketConn
var NewRefPacketConn = packet.NewRefPacketConn
var NewDeadlineNetPacketConn = deadline.NewNetPacketConn
var NewDeadlineEnhancePacketConn = deadline.NewEnhancePacketConn
var NewDeadlineSingPacketConn = deadline.NewSingPacketConn
var NewDeadlineEnhanceSingPacketConn = deadline.NewEnhanceSingPacketConn

View File

@ -1,77 +0,0 @@
package packet
import (
"net"
"github.com/Dreamacro/clash/common/pool"
)
type WaitReadFrom interface {
WaitReadFrom() (data []byte, put func(), addr net.Addr, err error)
}
type EnhancePacketConn interface {
net.PacketConn
WaitReadFrom
}
func NewEnhancePacketConn(pc net.PacketConn) EnhancePacketConn {
if udpConn, isUDPConn := pc.(*net.UDPConn); isUDPConn {
return &enhanceUDPConn{UDPConn: udpConn}
}
if enhancePC, isEnhancePC := pc.(EnhancePacketConn); isEnhancePC {
return enhancePC
}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return newEnhanceSingPacketConn(singPC)
}
return &enhancePacketConn{PacketConn: pc}
}
type enhancePacketConn struct {
net.PacketConn
}
func (c *enhancePacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
return waitReadFrom(c.PacketConn)
}
func (c *enhancePacketConn) Upstream() any {
return c.PacketConn
}
func (c *enhancePacketConn) WriterReplaceable() bool {
return true
}
func (c *enhancePacketConn) ReaderReplaceable() bool {
return true
}
func (c *enhanceUDPConn) Upstream() any {
return c.UDPConn
}
func (c *enhanceUDPConn) WriterReplaceable() bool {
return true
}
func (c *enhanceUDPConn) ReaderReplaceable() bool {
return true
}
func waitReadFrom(pc net.PacketConn) (data []byte, put func(), addr net.Addr, err error) {
readBuf := pool.Get(pool.UDPBufferSize)
put = func() {
_ = pool.Put(readBuf)
}
var readN int
readN, addr, err = pc.ReadFrom(readBuf)
if readN > 0 {
data = readBuf[:readN]
} else {
put()
put = nil
}
return
}

View File

@ -1,65 +0,0 @@
//go:build !windows
package packet
import (
"net"
"strconv"
"syscall"
"github.com/Dreamacro/clash/common/pool"
)
type enhanceUDPConn struct {
*net.UDPConn
rawConn syscall.RawConn
}
func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
if c.rawConn == nil {
c.rawConn, _ = c.UDPConn.SyscallConn()
}
var readErr error
err = c.rawConn.Read(func(fd uintptr) (done bool) {
readBuf := pool.Get(pool.UDPBufferSize)
put = func() {
_ = pool.Put(readBuf)
}
var readFrom syscall.Sockaddr
var readN int
readN, _, _, readFrom, readErr = syscall.Recvmsg(int(fd), readBuf, nil, 0)
if readN > 0 {
data = readBuf[:readN]
} else {
put()
put = nil
data = nil
}
if readErr == syscall.EAGAIN {
return false
}
if readFrom != nil {
switch from := readFrom.(type) {
case *syscall.SockaddrInet4:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 4 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
case *syscall.SockaddrInet6:
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
}
}
// udp should not convert readN == 0 to io.EOF
//if readN == 0 {
// readErr = io.EOF
//}
return true
})
if err != nil {
return
}
if readErr != nil {
err = readErr
return
}
return
}

View File

@ -1,79 +0,0 @@
package packet
import (
"net"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type SingPacketConn = N.NetPacketConn
type EnhanceSingPacketConn interface {
SingPacketConn
EnhancePacketConn
}
type enhanceSingPacketConn struct {
SingPacketConn
packetReadWaiter N.PacketReadWaiter
}
func (c *enhanceSingPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
var buff *buf.Buffer
var dest M.Socksaddr
newBuffer := func() *buf.Buffer {
buff = buf.NewPacket() // do not use stack buffer
return buff
}
if c.packetReadWaiter != nil {
c.packetReadWaiter.InitializeReadWaiter(newBuffer)
defer c.packetReadWaiter.InitializeReadWaiter(nil)
dest, err = c.packetReadWaiter.WaitReadPacket()
} else {
dest, err = c.SingPacketConn.ReadPacket(newBuffer())
}
if dest.IsFqdn() {
addr = dest
} else {
addr = dest.UDPAddr()
}
if err != nil {
if buff != nil {
buff.Release()
}
return
}
if buff == nil {
return
}
if buff.IsEmpty() {
buff.Release()
return
}
data = buff.Bytes()
put = buff.Release
return
}
func (c *enhanceSingPacketConn) Upstream() any {
return c.SingPacketConn
}
func (c *enhanceSingPacketConn) WriterReplaceable() bool {
return true
}
func (c *enhanceSingPacketConn) ReaderReplaceable() bool {
return true
}
func newEnhanceSingPacketConn(conn SingPacketConn) *enhanceSingPacketConn {
epc := &enhanceSingPacketConn{SingPacketConn: conn}
if readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn); isReadWaiter {
epc.packetReadWaiter = readWaiter
}
return epc
}

View File

@ -1,15 +0,0 @@
//go:build windows
package packet
import (
"net"
)
type enhanceUDPConn struct {
*net.UDPConn
}
func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
return waitReadFrom(c.UDPConn)
}

View File

@ -1,75 +0,0 @@
package packet
import (
"net"
"runtime"
"time"
)
type refPacketConn struct {
pc EnhancePacketConn
ref any
}
func (c *refPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.WaitReadFrom()
}
func (c *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.ReadFrom(p)
}
func (c *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.pc.WriteTo(p, addr)
}
func (c *refPacketConn) Close() error {
defer runtime.KeepAlive(c.ref)
return c.pc.Close()
}
func (c *refPacketConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.pc.LocalAddr()
}
func (c *refPacketConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetDeadline(t)
}
func (c *refPacketConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetReadDeadline(t)
}
func (c *refPacketConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.pc.SetWriteDeadline(t)
}
func (c *refPacketConn) Upstream() any {
return c.pc
}
func (c *refPacketConn) ReaderReplaceable() bool { // Relay() will handle reference
return true
}
func (c *refPacketConn) WriterReplaceable() bool { // Relay() will handle reference
return true
}
func NewRefPacketConn(pc net.PacketConn, ref any) EnhancePacketConn {
rPC := &refPacketConn{pc: NewEnhancePacketConn(pc), ref: ref}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return &refSingPacketConn{
refPacketConn: rPC,
singPacketConn: singPC,
}
}
return rPC
}

View File

@ -1,26 +0,0 @@
package packet
import (
"runtime"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type refSingPacketConn struct {
*refPacketConn
singPacketConn SingPacketConn
}
var _ N.NetPacketConn = (*refSingPacketConn)(nil)
func (c *refSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer runtime.KeepAlive(c.ref)
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *refSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
defer runtime.KeepAlive(c.ref)
return c.singPacketConn.ReadPacket(buffer)
}

View File

@ -1,36 +0,0 @@
package packet
import (
"net"
"sync"
)
type threadSafePacketConn struct {
EnhancePacketConn
access sync.Mutex
}
func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
c.access.Lock()
defer c.access.Unlock()
return c.EnhancePacketConn.WriteTo(b, addr)
}
func (c *threadSafePacketConn) Upstream() any {
return c.EnhancePacketConn
}
func (c *threadSafePacketConn) ReaderReplaceable() bool {
return true
}
func NewThreadSafePacketConn(pc net.PacketConn) EnhancePacketConn {
tsPC := &threadSafePacketConn{EnhancePacketConn: NewEnhancePacketConn(pc)}
if singPC, isSingPC := pc.(SingPacketConn); isSingPC {
return &threadSafeSingPacketConn{
threadSafePacketConn: tsPC,
singPacketConn: singPC,
}
}
return tsPC
}

View File

@ -1,24 +0,0 @@
package packet
import (
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type threadSafeSingPacketConn struct {
*threadSafePacketConn
singPacketConn SingPacketConn
}
var _ N.NetPacketConn = (*threadSafeSingPacketConn)(nil)
func (c *threadSafeSingPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
c.access.Lock()
defer c.access.Unlock()
return c.singPacketConn.WritePacket(buffer, destination)
}
func (c *threadSafeSingPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
return c.singPacketConn.ReadPacket(buffer)
}

View File

@ -1,82 +0,0 @@
package net
import (
"net"
"runtime"
"time"
"github.com/Dreamacro/clash/common/buf"
)
type refConn struct {
conn ExtendedConn
ref any
}
func (c *refConn) Read(b []byte) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.conn.Read(b)
}
func (c *refConn) Write(b []byte) (n int, err error) {
defer runtime.KeepAlive(c.ref)
return c.conn.Write(b)
}
func (c *refConn) Close() error {
defer runtime.KeepAlive(c.ref)
return c.conn.Close()
}
func (c *refConn) LocalAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.conn.LocalAddr()
}
func (c *refConn) RemoteAddr() net.Addr {
defer runtime.KeepAlive(c.ref)
return c.conn.RemoteAddr()
}
func (c *refConn) SetDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetDeadline(t)
}
func (c *refConn) SetReadDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetReadDeadline(t)
}
func (c *refConn) SetWriteDeadline(t time.Time) error {
defer runtime.KeepAlive(c.ref)
return c.conn.SetWriteDeadline(t)
}
func (c *refConn) Upstream() any {
return c.conn
}
func (c *refConn) ReadBuffer(buffer *buf.Buffer) error {
defer runtime.KeepAlive(c.ref)
return c.conn.ReadBuffer(buffer)
}
func (c *refConn) WriteBuffer(buffer *buf.Buffer) error {
defer runtime.KeepAlive(c.ref)
return c.conn.WriteBuffer(buffer)
}
func (c *refConn) ReaderReplaceable() bool { // Relay() will handle reference
return true
}
func (c *refConn) WriterReplaceable() bool { // Relay() will handle reference
return true
}
var _ ExtendedConn = (*refConn)(nil)
func NewRefConn(conn net.Conn, ref any) net.Conn {
return &refConn{conn: NewExtendedConn(conn), ref: ref}
}

View File

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

View File

@ -1,40 +0,0 @@
package net
import (
"context"
"net"
"runtime"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
"github.com/sagernet/sing/common/network"
)
var NewExtendedConn = bufio.NewExtendedConn
var NewExtendedWriter = bufio.NewExtendedWriter
var NewExtendedReader = bufio.NewExtendedReader
type ExtendedConn = network.ExtendedConn
type ExtendedWriter = network.ExtendedWriter
type ExtendedReader = network.ExtendedReader
func NewDeadlineConn(conn net.Conn) ExtendedConn {
return deadline.NewFallbackConn(conn)
}
func NeedHandshake(conn any) bool {
if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() {
return true
}
return false
}
type CountFunc = network.CountFunc
// Relay copies between left and right bidirectionally.
func Relay(leftConn, rightConn net.Conn) {
defer runtime.KeepAlive(leftConn)
defer runtime.KeepAlive(rightConn)
_ = bufio.CopyConn(context.TODO(), leftConn, rightConn)
}

View File

@ -4,11 +4,8 @@ import (
"fmt"
"net"
"strings"
"time"
)
var KeepAliveInterval = 15 * time.Second
func SplitNetworkType(s string) (string, string, error) {
var (
shecme string
@ -47,10 +44,3 @@ func SplitHostPort(s string) (host, port string, hasPort bool, err error) {
host, port, err = net.SplitHostPort(temp)
return
}
func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok {
_ = tcp.SetKeepAlive(true)
_ = tcp.SetKeepAlivePeriod(KeepAliveInterval)
}
}

View File

@ -1,52 +0,0 @@
package net
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"math/big"
)
func ParseCert(certificate, privateKey string) (tls.Certificate, error) {
if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair()
}
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
if painTextErr == nil {
return cert, nil
}
cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey)
if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
return cert, nil
}
func newRandomTLSKeyPair() (tls.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return tls.Certificate{}, err
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(
rand.Reader,
&template,
&template,
&key.PublicKey,
key)
if err != nil {
return tls.Certificate{}, err
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return tls.Certificate{}, err
}
return tlsCert, nil
}

View File

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

View File

@ -5,9 +5,8 @@ import (
"testing"
"time"
"github.com/Dreamacro/clash/common/atomic"
"github.com/stretchr/testify/assert"
"go.uber.org/atomic"
)
func iterator[T any](item []T) chan T {
@ -45,7 +44,7 @@ func TestObservable_MultiSubscribe(t *testing.T) {
wg.Add(2)
waitCh := func(ch <-chan int) {
for range ch {
count.Add(1)
count.Inc()
}
wg.Done()
}

View File

@ -47,7 +47,6 @@ func (p *Picker[T]) Wait() T {
p.wg.Wait()
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
return p.result
}
@ -70,7 +69,6 @@ func (p *Picker[T]) Go(f func() (T, error)) {
p.result = ret
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
})
} else {
@ -80,13 +78,3 @@ func (p *Picker[T]) Go(f func() (T, error)) {
}
}()
}
// Close cancels the picker context and releases resources associated with it.
// If Wait has been called, then there is no need to call Close.
func (p *Picker[T]) Close() error {
if p.cancel != nil {
p.cancel()
p.cancel = nil
}
return nil
}

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