Compare commits
2 Commits
v1.14.3
...
dev-refact
Author | SHA1 | Date | |
---|---|---|---|
637a8b6ed5 | |||
cd466f05d3 |
26
.github/release.sh
vendored
26
.github/release.sh
vendored
@ -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
35
.github/rename-cgo.sh
vendored
@ -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
|
16
.github/workflows/Delete.yml
vendored
16
.github/workflows/Delete.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: Delete old workflow runs
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
# Run monthly, at 00:00 on the 1st day of month.
|
||||
|
||||
jobs:
|
||||
del_runs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete workflow runs
|
||||
uses: GitRML/delete-workflow-runs@main
|
||||
with:
|
||||
token: ${{ secrets.AUTH_PAT }}
|
||||
repository: ${{ github.repository }}
|
||||
retain_days: 30
|
20
.github/workflows/build.yaml
vendored
Normal file
20
.github/workflows/build.yaml
vendored
Normal 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
|
357
.github/workflows/build.yml
vendored
357
.github/workflows/build.yml
vendored
@ -1,357 +0,0 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "README.md"
|
||||
branches:
|
||||
- Alpha
|
||||
- Beta
|
||||
- Meta
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request_target:
|
||||
branches:
|
||||
- Alpha
|
||||
- Beta
|
||||
- Meta
|
||||
|
||||
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=='Beta'}}
|
||||
run: echo "VERSION=beta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
if: ${{github.ref_name=='Meta'}}
|
||||
run: echo "VERSION=meta-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set variables
|
||||
if: ${{github.ref_name=='' || github.ref_type=='tag'}}
|
||||
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set ENV
|
||||
run: |
|
||||
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@v3
|
||||
with:
|
||||
go-version: "1.20"
|
||||
check-latest: true
|
||||
|
||||
- name: Test
|
||||
if: ${{ matrix.job.id=='1' && matrix.job.type=='WithoutCGO' }}
|
||||
run: |
|
||||
go test ./...
|
||||
|
||||
- name: Build WithoutCGO
|
||||
if: ${{ matrix.job.type=='WithoutCGO' }}
|
||||
env:
|
||||
NAME: Clash.Meta
|
||||
BINDIR: bin
|
||||
run: make -j$(($(nproc) + 1)) ${{ matrix.job.target }}
|
||||
|
||||
- uses: nttld/setup-ndk@v1
|
||||
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r25b
|
||||
add-to-path: false
|
||||
local-cache: true
|
||||
|
||||
- name: Build Android
|
||||
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target=='android' }}
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
mkdir bin
|
||||
CC=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android33-clang
|
||||
CGO_ENABLED=1 CC=${CC} GOARCH=arm64 GOOS=android go build -tags ${TAGS} -trimpath -ldflags "${LDFLAGS}" -o bin/${NAME}-android-arm64
|
||||
|
||||
- name: Set up xgo
|
||||
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
|
||||
run: |
|
||||
docker pull techknowlogick/xgo:latest
|
||||
go install src.techknowlogick.com/xgo@latest
|
||||
|
||||
- name: Build by xgo
|
||||
if: ${{ matrix.job.type=='WithCGO' && matrix.job.target!='android' }}
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
mkdir bin
|
||||
xgo --targets="${{ matrix.job.target }}" --tags="${TAGS}" -ldflags="${LDFLAGS}" --out bin/${NAME} ./
|
||||
|
||||
- name: Rename
|
||||
if: ${{ matrix.job.type=='WithCGO' }}
|
||||
run: |
|
||||
cd bin
|
||||
ls -la
|
||||
cp ../.github/rename-cgo.sh ./
|
||||
bash ./rename-cgo.sh
|
||||
rm ./rename-cgo.sh
|
||||
ls -la
|
||||
cd ..
|
||||
|
||||
- name: Zip
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
cd bin
|
||||
ls -la
|
||||
chmod +x *
|
||||
cp ../.github/release.sh ./
|
||||
bash ./release.sh
|
||||
rm ./release.sh
|
||||
ls -la
|
||||
cd ..
|
||||
|
||||
- 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' }}
|
||||
needs: [Build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: bin/
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
working-directory: bin
|
||||
|
||||
- name: Delete current release assets
|
||||
uses: andreaswilli/delete-release-assets-action@v2.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: Prerelease-${{ github.ref_name }}
|
||||
deleteOnlyFromDrafts: false
|
||||
|
||||
- name: 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>
|
||||
### release version
|
||||
`default(not specified in file name)`: compiled with GOAMD64=v3
|
||||
`cgo`: support lwip tun stack, compiled with GOAMD64=v1
|
||||
`compatible`: compiled with GOAMD64=v1
|
||||
Check details between different architectural levels [here](https://github.com/golang/go/wiki/MinimumRequirements#amd64).
|
||||
EOF
|
||||
|
||||
- name: Upload Prerelease
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
tag_name: Prerelease-${{ github.ref_name }}
|
||||
files: |
|
||||
bin/*
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
body_path: release.txt
|
||||
|
||||
Upload-Release:
|
||||
permissions: write-all
|
||||
if: ${{ github.ref_type=='tag' }}
|
||||
needs: [Build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: bin/
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
working-directory: bin
|
||||
|
||||
- name: Upload Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ success() }}
|
||||
with:
|
||||
tag: ${{ github.ref_name }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
files: bin/*
|
||||
generate_release_notes: true
|
||||
|
||||
Docker:
|
||||
permissions: write-all
|
||||
needs: [Build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: artifact
|
||||
path: bin/
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
working-directory: bin
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_ACCOUNT }}/${{secrets.DOCKERHUB_REPO}}
|
||||
- name: Show files
|
||||
run: |
|
||||
ls .
|
||||
ls bin/
|
||||
- name: Log into registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: |
|
||||
linux/386
|
||||
linux/amd64
|
||||
linux/arm64/v8
|
||||
linux/arm/v7
|
||||
# linux/riscv64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
16
.github/workflows/delete.yml
vendored
16
.github/workflows/delete.yml
vendored
@ -1,16 +0,0 @@
|
||||
name: Delete old workflow runs
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
# Run monthly, at 00:00 on the 1st day of month.
|
||||
|
||||
jobs:
|
||||
del_runs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete workflow runs
|
||||
uses: GitRML/delete-workflow-runs@main
|
||||
with:
|
||||
token: ${{ secrets.AUTH_PAT }}
|
||||
repository: ${{ github.repository }}
|
||||
retain_days: 30
|
61
.github/workflows/docker.yaml
vendored
Normal file
61
.github/workflows/docker.yaml
vendored
Normal 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
70
.github/workflows/prerelease.yml
vendored
Normal 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
44
.github/workflows/release.yaml
vendored
Normal 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
3
.gitignore
vendored
@ -24,5 +24,4 @@ vendor
|
||||
# test suite
|
||||
test/config/cache*
|
||||
/output
|
||||
.vscode/
|
||||
.fleet/
|
||||
/.vscode
|
@ -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'
|
||||
|
25
Dockerfile
25
Dockerfile
@ -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/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 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
|
||||
|
||||
COPY . /clash-src
|
||||
WORKDIR /clash-src
|
||||
RUN go mod download &&\
|
||||
make docker &&\
|
||||
mv ./bin/Clash.Meta-docker /clash
|
||||
|
||||
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" ]
|
26
Makefile
26
Makefile
@ -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='
|
||||
|
||||
@ -47,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)-$@
|
||||
@ -69,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)-$@
|
||||
@ -101,9 +98,6 @@ linux-mips64:
|
||||
linux-mips64le:
|
||||
GOARCH=mips64le GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
linux-riscv64:
|
||||
GOARCH=riscv64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
android-arm64:
|
||||
GOARCH=arm64 GOOS=android $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
||||
|
||||
@ -123,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
|
||||
@ -153,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 ./...
|
||||
|
155
README.md
155
README.md
@ -29,41 +29,12 @@
|
||||
- Netfilter TCP redirecting. Deploy Clash on your Internet gateway with `iptables`.
|
||||
- Comprehensive HTTP RESTful API controller
|
||||
|
||||
## 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).
|
||||
## Getting Started
|
||||
Documentations are now moved to [GitHub Wiki](https://github.com/Dreamacro/clash/wiki).
|
||||
|
||||
## Build
|
||||
## Advanced usage for this branch
|
||||
|
||||
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`.
|
||||
|
||||
@ -73,6 +44,7 @@ Support resolve ip with a `Proxy Tunnel`.
|
||||
|
||||
```yaml
|
||||
proxy-groups:
|
||||
|
||||
- name: DNS
|
||||
type: url-test
|
||||
use:
|
||||
@ -81,7 +53,6 @@ proxy-groups:
|
||||
interval: 180
|
||||
lazy: true
|
||||
```
|
||||
|
||||
```yaml
|
||||
dns:
|
||||
enable: true
|
||||
@ -97,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:
|
||||
@ -119,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
|
||||
@ -153,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)
|
||||
@ -172,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:
|
||||
@ -194,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"
|
||||
@ -210,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
|
||||
@ -235,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
|
||||
@ -289,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.
|
||||
|
||||
@ -314,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
|
||||
@ -323,13 +260,10 @@ ExecStart=/usr/local/bin/Clash-Meta -d /etc/Clash-Meta
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Launch clashd on system startup with:
|
||||
|
||||
```shell
|
||||
$ systemctl enable Clash-Meta
|
||||
```
|
||||
|
||||
Launch clashd immediately with:
|
||||
|
||||
```shell
|
||||
@ -340,29 +274,22 @@ $ systemctl start Clash-Meta
|
||||
|
||||
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 [Razord-meta](https://github.com/MetaCubeX/Razord-meta).
|
||||
To display process name in GUI please use [Dashboard For Meta](https://github.com/Clash-Mini/Dashboard).
|
||||
|
||||
### Dashboard
|
||||
|
||||
We also made a custom fork of yacd provide better support for this project, check it out at [Yacd-meta](https://github.com/MetaCubeX/Yacd-meta)
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
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
|
||||
|
||||
|
@ -4,15 +4,16 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
@ -39,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
|
||||
}
|
||||
|
||||
@ -52,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
|
||||
}
|
||||
|
||||
@ -92,8 +99,6 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||
mapping["history"] = p.DelayHistory()
|
||||
mapping["name"] = p.Name()
|
||||
mapping["udp"] = p.SupportUDP()
|
||||
mapping["xudp"] = p.SupportXUDP()
|
||||
mapping["tfo"] = p.SupportTFO()
|
||||
return json.Marshal(mapping)
|
||||
}
|
||||
|
||||
@ -146,32 +151,25 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
t = uint16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
@ -200,9 +198,10 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||
}
|
||||
|
||||
addr = C.Metadata{
|
||||
Host: u.Hostname(),
|
||||
DstIP: netip.Addr{},
|
||||
DstPort: port,
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: u.Hostname(),
|
||||
DstIP: netip.Addr{},
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -1,29 +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 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -1,26 +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 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)
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -10,21 +10,17 @@ 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)
|
||||
@ -34,16 +30,19 @@ 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(dst); err == nil {
|
||||
metadata.DstPort = port
|
||||
if host == "" {
|
||||
if ip, err := netip.ParseAddr(h); err == nil {
|
||||
metadata.DstIP = ip
|
||||
} else {
|
||||
metadata.Host = h
|
||||
metadata.AddrType = C.AtypIPv4
|
||||
if ip.Is6() {
|
||||
metadata.AddrType = C.AtypIPv6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@ -11,10 +10,30 @@ 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:
|
||||
@ -25,8 +44,7 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len]))
|
||||
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.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]))
|
||||
}
|
||||
|
||||
@ -44,33 +62,29 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
|
||||
host = strings.TrimRight(host, ".")
|
||||
|
||||
metadata := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
Host: host,
|
||||
DstIP: netip.Addr{},
|
||||
DstPort: port,
|
||||
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, string, error) {
|
||||
// Filter when net.Addr interface is nil
|
||||
if addr == nil {
|
||||
return netip.Addr{}, "", errors.New("nil addr")
|
||||
}
|
||||
if rawAddr, ok := addr.(interface{ RawAddr() net.Addr }); ok {
|
||||
ip, port, err := parseAddr(rawAddr.RawAddr())
|
||||
if err == nil {
|
||||
return ip, port, err
|
||||
}
|
||||
}
|
||||
addrStr := addr.String()
|
||||
host, port, err := net.SplitHostPort(addrStr)
|
||||
func parseAddr(addr string) (netip.Addr, string, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return netip.Addr{}, "", err
|
||||
}
|
||||
|
@ -7,23 +7,23 @@ import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
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
|
||||
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
|
||||
@ -34,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
|
||||
@ -54,26 +59,16 @@ func (b *Base) DialContext(ctx context.Context, metadata *C.Metadata, opts ...di
|
||||
return nil, errors.New("no support")
|
||||
}
|
||||
|
||||
// DialContextWithDialer implements C.ProxyAdapter
|
||||
func (b *Base) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
return nil, errors.New("no support")
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (b *Base) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||
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) {
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (b *Base) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
return nil, errors.New("no support")
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (b *Base) SupportWithDialer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SupportUOT implements C.ProxyAdapter
|
||||
func (b *Base) SupportUOT() bool {
|
||||
return false
|
||||
@ -84,16 +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
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
@ -108,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
|
||||
}
|
||||
|
||||
@ -122,30 +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))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
type BasicOption struct {
|
||||
TFO bool `proxy:"tfo,omitempty" group:"tfo,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"`
|
||||
}
|
||||
|
||||
type BaseOption struct {
|
||||
@ -153,29 +120,23 @@ type BaseOption struct {
|
||||
Addr string
|
||||
Type C.AdapterType
|
||||
UDP bool
|
||||
XUDP bool
|
||||
TFO 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,
|
||||
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
|
||||
}
|
||||
@ -194,19 +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 NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
||||
return &conn{c, []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
||||
}
|
||||
|
||||
type packetConn struct {
|
||||
net.PacketConn
|
||||
nc N.PacketConn
|
||||
chain C.Chain
|
||||
adapterName string
|
||||
connID string
|
||||
actualRemoteDestination string
|
||||
}
|
||||
|
||||
@ -224,13 +180,22 @@ func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
||||
c.chain = append(c.chain, a.Name())
|
||||
}
|
||||
|
||||
func (c *packetConn) LocalAddr() net.Addr {
|
||||
lAddr := c.PacketConn.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) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error {
|
||||
return c.nc.WritePacket(buffer, addr)
|
||||
}
|
||||
|
||||
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||
return &packetConn{pc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())}
|
||||
var nc N.PacketConn
|
||||
if n, isNc := pc.(N.PacketConn); isNc {
|
||||
nc = n
|
||||
} else {
|
||||
nc = &bufio.PacketConnWrapper{PacketConn: pc}
|
||||
}
|
||||
return &packetConn{pc, nc, []string{a.Name()}, parseRemoteDestination(a.Addr())}
|
||||
}
|
||||
|
||||
func parseRemoteDestination(addr string) string {
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
@ -15,7 +14,7 @@ type Direct struct {
|
||||
|
||||
// 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
|
||||
@ -26,8 +25,8 @@ func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
|
||||
opts = append(opts, dialer.WithResolver(resolver.DefaultResolver))
|
||||
pc, err := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", metadata.DstIP), "", d.Base.DialOptions(opts...)...)
|
||||
opts = append(opts, dialer.WithDirect())
|
||||
pc, err := dialer.ListenPacket(ctx, "udp", "", d.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -41,10 +40,9 @@ type directPacketConn struct {
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{
|
||||
Base: &Base{
|
||||
name: "DIRECT",
|
||||
tp: C.Direct,
|
||||
udp: true,
|
||||
prefer: C.DualStack,
|
||||
name: "DIRECT",
|
||||
tp: C.Direct,
|
||||
udp: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -52,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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
@ -36,7 +35,6 @@ 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"`
|
||||
}
|
||||
|
||||
@ -44,9 +42,7 @@ type HttpOption struct {
|
||||
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
if h.tlsConfig != nil {
|
||||
cc := tls.Client(c, h.tlsConfig)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
err := cc.HandshakeContext(ctx)
|
||||
err := cc.Handshake()
|
||||
c = cc
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
@ -61,20 +57,13 @@ func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = h.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
@ -84,11 +73,6 @@ func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metad
|
||||
return NewConn(c, h), nil
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (h *Http) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
addr := metadata.RemoteAddress()
|
||||
req := &http.Request{
|
||||
@ -102,7 +86,7 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
},
|
||||
}
|
||||
|
||||
//增加headers
|
||||
// 增加headers
|
||||
if len(h.option.Headers) != 0 {
|
||||
for key, value := range h.option.Headers {
|
||||
req.Header.Add(key, value)
|
||||
@ -142,42 +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
|
||||
}
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
})
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(&tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
}, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ServerName: sni,
|
||||
}
|
||||
}
|
||||
|
||||
return &Http{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Http,
|
||||
tfo: option.TFO,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -2,43 +2,40 @@ package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/quic-go"
|
||||
"github.com/metacubex/quic-go/congestion"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
hyCongestion "github.com/Dreamacro/clash/transport/hysteria/congestion"
|
||||
"github.com/Dreamacro/clash/transport/hysteria/core"
|
||||
"github.com/Dreamacro/clash/transport/hysteria/obfs"
|
||||
"github.com/Dreamacro/clash/transport/hysteria/pmtud_fix"
|
||||
"github.com/Dreamacro/clash/transport/hysteria/transport"
|
||||
"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$`)
|
||||
@ -46,39 +43,24 @@ var rateStringRegexp = regexp.MustCompile(`^(\d+)\s*([KMGT]?)([Bb])ps$`)
|
||||
type Hysteria struct {
|
||||
*Base
|
||||
|
||||
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) {
|
||||
hdc := hyDialerWithContext{
|
||||
ctx: context.Background(),
|
||||
hyDialer: func(network string) (net.PacketConn, error) {
|
||||
return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...)
|
||||
},
|
||||
remoteAddr: func(addr string) (net.Addr, error) {
|
||||
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
|
||||
},
|
||||
}
|
||||
|
||||
tcpConn, err := h.client.DialTCP(metadata.RemoteAddress(), &hdc)
|
||||
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) {
|
||||
hdc := hyDialerWithContext{
|
||||
ctx: context.Background(),
|
||||
hyDialer: func(network string) (net.PacketConn, error) {
|
||||
return dialer.ListenPacket(ctx, network, "", h.Base.DialOptions(opts...)...)
|
||||
},
|
||||
remoteAddr: func(addr string) (net.Addr, error) {
|
||||
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer)
|
||||
},
|
||||
}
|
||||
udpConn, err := h.client.DialUDP(&hdc)
|
||||
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
|
||||
}
|
||||
@ -87,44 +69,45 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -134,131 +117,96 @@ 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 bs []byte
|
||||
var err error
|
||||
if len(option.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = []string{option.ALPN}
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||
}
|
||||
if len(option.CustomCA) > 0 {
|
||||
bs, err = os.ReadFile(option.CustomCA)
|
||||
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 != "" {
|
||||
bs = []byte(option.CustomCAString)
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
block, _ := pem.Decode(bs)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("CA cert is not PEM")
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM([]byte(option.CustomCAString)) {
|
||||
return nil, fmt.Errorf("hysteria %s failed to parse ca_str", addr)
|
||||
}
|
||||
|
||||
fpBytes := sha256.Sum256(block.Bytes)
|
||||
if len(option.Fingerprint) == 0 {
|
||||
option.Fingerprint = hex.EncodeToString(fpBytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) != 0 {
|
||||
var err error
|
||||
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
if len(option.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = option.ALPN
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{DefaultALPN}
|
||||
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,
|
||||
},
|
||||
client: client,
|
||||
client: client,
|
||||
clientTransport: clientTransport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -266,12 +214,6 @@ 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
|
||||
@ -321,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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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,47 +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) 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 }
|
||||
|
@ -6,52 +6,39 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
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-shadowsocks"
|
||||
"github.com/metacubex/sing-shadowsocks/shadowimpl"
|
||||
"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 {
|
||||
@ -64,37 +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"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
// fix tls handshake not timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
return ss.StreamConnContext(ctx, c, metadata)
|
||||
}
|
||||
|
||||
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
useEarly := false
|
||||
switch ss.obfsMode {
|
||||
case "tls":
|
||||
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
||||
@ -107,124 +70,49 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
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 ss.option.UDPOverTCP {
|
||||
tcpConn, err := ss.DialContextWithDialer(ctx, dialer, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
destination := M.ParseSocksaddr(metadata.RemoteAddress())
|
||||
if ss.option.UDPOverTCPVersion == 1 {
|
||||
return newPacketConn(uot.NewConn(tcpConn, uot.Request{Destination: destination}), ss), nil
|
||||
} else {
|
||||
return newPacketConn(uot.NewLazyConn(tcpConn, uot.Request{Destination: destination}), ss), nil
|
||||
}
|
||||
}
|
||||
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(&bufio.BindPacketConn{PacketConn: pc, Addr: addr})
|
||||
return newPacketConn(pc, ss), nil
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if ss.option.UDPOverTCP {
|
||||
destination := M.ParseSocksaddr(metadata.RemoteAddress())
|
||||
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, errors.New("no support")
|
||||
}
|
||||
|
||||
// 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 := shadowimpl.FetchMethod(option.Cipher, option.Password, time.Now)
|
||||
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})
|
||||
@ -260,64 +148,22 @@ 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)
|
||||
restlsConfig.SessionTicketsDisabled = true
|
||||
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.Version
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -60,20 +60,13 @@ func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn,
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = ssr.StreamConn(c, metadata)
|
||||
return NewConn(c, ssr), err
|
||||
@ -81,18 +74,14 @@ func (ssr *ShadowSocksR) DialContextWithDialer(ctx context.Context, dialer C.Dia
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -101,11 +90,6 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
|
||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (ssr *ShadowSocksR) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||
// SSR protocol compatibility
|
||||
// https://github.com/Dreamacro/clash/pull/2056
|
||||
@ -159,14 +143,12 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||
|
||||
return &ShadowSocksR{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.ShadowsocksR,
|
||||
udp: option.UDP,
|
||||
tfo: option.TFO,
|
||||
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,
|
||||
},
|
||||
cipher: coreCiph,
|
||||
obfs: obfs,
|
||||
|
@ -78,20 +78,13 @@ func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = s.StreamConn(c, metadata)
|
||||
return NewConn(c, s), err
|
||||
@ -99,12 +92,7 @@ func (s *Snell) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
@ -120,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() bool {
|
||||
return true
|
||||
// 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
|
||||
@ -163,14 +152,12 @@ func NewSnell(option SnellOption) (*Snell, error) {
|
||||
|
||||
s := &Snell{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Snell,
|
||||
udp: option.UDP,
|
||||
tfo: option.TFO,
|
||||
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,
|
||||
},
|
||||
psk: psk,
|
||||
obfsOption: obfsOption,
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
@ -34,16 +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"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTLSTimeout)
|
||||
defer cancel()
|
||||
err := cc.HandshakeContext(ctx)
|
||||
err := cc.Handshake()
|
||||
c = cc
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
@ -65,20 +61,13 @@ func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = ss.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
@ -88,11 +77,6 @@ func (ss *Socks5) DialContextWithDialer(ctx context.Context, dialer C.Dialer, me
|
||||
return NewConn(c, ss), nil
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (ss *Socks5) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr, ss.Base.DialOptions(opts...)...)
|
||||
@ -103,15 +87,11 @@ 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)
|
||||
|
||||
tcpKeepAlive(c)
|
||||
var user *socks5.User
|
||||
@ -128,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 := dialer.ListenPacket(ctx, dialer.ParseNetwork("udp", bindUDPAddr.AddrPort().Addr()), "", ss.Base.DialOptions(opts...)...)
|
||||
pc, err := dialer.ListenPacket(ctx, "udp", "", ss.Base.DialOptions(opts...)...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -155,44 +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,
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Socks5{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Socks5,
|
||||
udp: option.UDP,
|
||||
tfo: option.TFO,
|
||||
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,
|
||||
},
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
tls: option.TLS,
|
||||
skipCertVerify: option.SkipCertVerify,
|
||||
tlsConfig: tlsConfig,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type socksPacketConn struct {
|
||||
|
@ -8,9 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
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"
|
||||
@ -26,28 +24,23 @@ 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"`
|
||||
Flow string `proxy:"flow,omitempty"`
|
||||
FlowShow bool `proxy:"flow-show,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(c net.Conn) (net.Conn, error) {
|
||||
@ -80,13 +73,8 @@ func (t *Trojan) plainStream(c net.Conn) (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(c)
|
||||
}
|
||||
@ -105,7 +93,7 @@ func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error)
|
||||
return c, err
|
||||
}
|
||||
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
||||
return N.NewExtendedConn(c), err
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@ -130,20 +118,14 @@ 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) {
|
||||
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)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = t.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
@ -163,33 +145,18 @@ 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)
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
tcpKeepAlive(c)
|
||||
c, err = t.plainStream(c)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
tcpKeepAlive(c)
|
||||
c, err = t.plainStream(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
|
||||
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
||||
@ -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() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
pc := t.instance.PacketConn(c)
|
||||
@ -221,25 +183,20 @@ 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,
|
||||
FlowShow: option.FlowShow,
|
||||
Fingerprint: option.Fingerprint,
|
||||
ClientFingerprint: option.ClientFingerprint,
|
||||
Password: option.Password,
|
||||
ALPN: option.ALPN,
|
||||
ServerName: option.Server,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
FlowShow: option.FlowShow,
|
||||
}
|
||||
|
||||
switch option.Network {
|
||||
case "", "tcp":
|
||||
if 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.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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,26 +206,17 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
|
||||
t := &Trojan{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Trojan,
|
||||
udp: option.UDP,
|
||||
tfo: option.TFO,
|
||||
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) {
|
||||
c, err := dialer.DialContext(context.Background(), "tcp", t.addr, t.Base.DialOptions()...)
|
||||
@ -286,17 +234,12 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
ServerName: tOption.ServerName,
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) == 0 {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
if t.option.Flow != "" {
|
||||
t.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig)
|
||||
} else {
|
||||
var err error
|
||||
if tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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,
|
||||
|
@ -1,262 +0,0 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/quic-go"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/tuic"
|
||||
)
|
||||
|
||||
type Tuic struct {
|
||||
*Base
|
||||
client *tuic.PoolClient
|
||||
}
|
||||
|
||||
type TuicOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Token string `proxy:"token"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
HeartbeatInterval int `proxy:"heartbeat-interval,omitempty"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
ReduceRtt bool `proxy:"reduce-rtt,omitempty"`
|
||||
RequestTimeout int `proxy:"request-timeout,omitempty"`
|
||||
UdpRelayMode string `proxy:"udp-relay-mode,omitempty"`
|
||||
CongestionController string `proxy:"congestion-controller,omitempty"`
|
||||
DisableSni bool `proxy:"disable-sni,omitempty"`
|
||||
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
|
||||
|
||||
FastOpen bool `proxy:"fast-open,omitempty"`
|
||||
MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||
CustomCA string `proxy:"ca,omitempty"`
|
||||
CustomCAString string `proxy:"ca-str,omitempty"`
|
||||
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `proxy:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
|
||||
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (t *Tuic) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
return t.DialContextWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
||||
}
|
||||
|
||||
// DialContextWithDialer implements C.ProxyAdapter
|
||||
func (t *Tuic) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
|
||||
conn, err := t.client.DialContextWithDialer(ctx, metadata, dialer, t.dialWithDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(conn, t), err
|
||||
}
|
||||
|
||||
// ListenPacketContext implements C.ProxyAdapter
|
||||
func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
|
||||
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
|
||||
}
|
||||
|
||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
pc, err := t.client.ListenPacketWithDialer(ctx, metadata, dialer, t.dialWithDialer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPacketConn(pc, t), nil
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (t *Tuic) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tuic) dial(ctx context.Context, opts ...dialer.Option) (pc net.PacketConn, addr net.Addr, err error) {
|
||||
return t.dialWithDialer(ctx, dialer.NewDialer(opts...))
|
||||
}
|
||||
|
||||
func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (pc net.PacketConn, addr net.Addr, err error) {
|
||||
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
addr = udpAddr
|
||||
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewTuic(option TuicOption) (*Tuic, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
serverName := option.Server
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: serverName,
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
if option.SNI != "" {
|
||||
tlsConfig.ServerName = option.SNI
|
||||
}
|
||||
|
||||
var bs []byte
|
||||
var err error
|
||||
if len(option.CustomCA) > 0 {
|
||||
bs, err = os.ReadFile(option.CustomCA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tuic %s load ca error: %w", addr, err)
|
||||
}
|
||||
} else if option.CustomCAString != "" {
|
||||
bs = []byte(option.CustomCAString)
|
||||
}
|
||||
|
||||
if len(bs) > 0 {
|
||||
block, _ := pem.Decode(bs)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("CA cert is not PEM")
|
||||
}
|
||||
|
||||
fpBytes := sha256.Sum256(block.Bytes)
|
||||
if len(option.Fingerprint) == 0 {
|
||||
option.Fingerprint = hex.EncodeToString(fpBytes[:])
|
||||
}
|
||||
}
|
||||
|
||||
if len(option.Fingerprint) != 0 {
|
||||
var err error
|
||||
tlsConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
tlsConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
if len(option.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = option.ALPN
|
||||
} else {
|
||||
tlsConfig.NextProtos = []string{"h3"}
|
||||
}
|
||||
|
||||
if option.RequestTimeout == 0 {
|
||||
option.RequestTimeout = 8000
|
||||
}
|
||||
|
||||
if option.HeartbeatInterval <= 0 {
|
||||
option.HeartbeatInterval = 10000
|
||||
}
|
||||
|
||||
if option.UdpRelayMode != "quic" {
|
||||
option.UdpRelayMode = "native"
|
||||
}
|
||||
|
||||
if option.MaxUdpRelayPacketSize == 0 {
|
||||
option.MaxUdpRelayPacketSize = 1252
|
||||
}
|
||||
|
||||
if option.MaxOpenStreams == 0 {
|
||||
option.MaxOpenStreams = 100
|
||||
}
|
||||
|
||||
if option.MaxDatagramFrameSize == 0 {
|
||||
option.MaxDatagramFrameSize = option.MaxUdpRelayPacketSize + tuic.PacketOverHead
|
||||
}
|
||||
|
||||
if option.MaxDatagramFrameSize > 1400 {
|
||||
option.MaxDatagramFrameSize = 1400
|
||||
}
|
||||
option.MaxUdpRelayPacketSize = option.MaxDatagramFrameSize - tuic.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))
|
||||
}
|
||||
host := option.Server
|
||||
if option.DisableSni {
|
||||
host = ""
|
||||
tlsConfig.ServerName = ""
|
||||
}
|
||||
tkn := tuic.GenTKN(option.Token)
|
||||
|
||||
t := &Tuic{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Tuic,
|
||||
udp: true,
|
||||
tfo: option.FastOpen,
|
||||
iface: option.Interface,
|
||||
rmark: option.RoutingMark,
|
||||
prefer: C.NewDNSPrefer(option.IPVersion),
|
||||
},
|
||||
}
|
||||
|
||||
clientMaxOpenStreams := int64(option.MaxOpenStreams)
|
||||
|
||||
// to avoid tuic's "too many open streams", decrease to 0.9x
|
||||
if clientMaxOpenStreams == 100 {
|
||||
clientMaxOpenStreams = clientMaxOpenStreams - int64(math.Ceil(float64(clientMaxOpenStreams)/10.0))
|
||||
}
|
||||
|
||||
if clientMaxOpenStreams < 1 {
|
||||
clientMaxOpenStreams = 1
|
||||
}
|
||||
clientOption := &tuic.ClientOption{
|
||||
TlsConfig: tlsConfig,
|
||||
QuicConfig: quicConfig,
|
||||
Host: host,
|
||||
Token: tkn,
|
||||
UdpRelayMode: option.UdpRelayMode,
|
||||
CongestionController: option.CongestionController,
|
||||
ReduceRtt: option.ReduceRtt,
|
||||
RequestTimeout: time.Duration(option.RequestTimeout) * time.Millisecond,
|
||||
MaxUdpRelayPacketSize: option.MaxUdpRelayPacketSize,
|
||||
FastOpen: option.FastOpen,
|
||||
MaxOpenStreams: clientMaxOpenStreams,
|
||||
}
|
||||
|
||||
t.client = tuic.NewPoolClient(clientOption)
|
||||
|
||||
return t, nil
|
||||
}
|
@ -2,11 +2,8 @@ package outbound
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
xtls "github.com/xtls/go"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@ -14,6 +11,7 @@ import (
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
xtls "github.com/xtls/go"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -45,11 +43,10 @@ func getClientXSessionCache() xtls.ClientSessionCache {
|
||||
|
||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||
var buf [][]byte
|
||||
addrType := metadata.AddrType()
|
||||
aType := uint8(addrType)
|
||||
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)
|
||||
@ -64,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
|
||||
}
|
||||
@ -134,7 +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()
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,10 @@ import (
|
||||
"github.com/Dreamacro/clash/common/convert"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"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/sagernet/sing-vmess"
|
||||
"github.com/sagernet/sing-vmess/packetaddr"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -42,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"`
|
||||
FlowShow bool `proxy:"flow-show,omitempty"`
|
||||
TLS bool `proxy:"tls,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) 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,
|
||||
@ -89,43 +70,30 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
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"},
|
||||
}
|
||||
|
||||
if len(v.option.Fingerprint) == 0 {
|
||||
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
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(c, wsOpts)
|
||||
case "http":
|
||||
@ -157,7 +125,11 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
|
||||
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 And XTLS
|
||||
@ -168,17 +140,20 @@ func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
|
||||
return v.client.StreamConn(c, parseVlessAddr(metadata))
|
||||
}
|
||||
|
||||
func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
|
||||
if v.isLegacyXTLSEnabled() && !isH2 {
|
||||
if v.isXTLSEnabled() {
|
||||
xtlsOpts := vless.XTLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
Fingerprint: v.option.Fingerprint,
|
||||
}
|
||||
|
||||
if isH2 {
|
||||
xtlsOpts.NextProtos = []string{"h2"}
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
@ -189,11 +164,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
|
||||
|
||||
} 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,
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
}
|
||||
|
||||
if isH2 {
|
||||
@ -210,8 +182,8 @@ func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (v *Vless) isLegacyXTLSEnabled() bool {
|
||||
return v.client.Addons != nil && v.client.Addons.Flow != vless.XRV
|
||||
func (v *Vless) isXTLSEnabled() bool {
|
||||
return v.client.Addons != nil
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@ -222,43 +194,32 @@ 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) {
|
||||
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())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
return NewConn(c, v), err
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
@ -272,55 +233,17 @@ 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)
|
||||
|
||||
if v.option.PacketAddr {
|
||||
packetAddrMetadata := *metadata // make a copy
|
||||
packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress
|
||||
packetAddrMetadata.DstPort = "443"
|
||||
|
||||
c, err = v.client.StreamConn(c, parseVlessAddr(&packetAddrMetadata, false))
|
||||
} else {
|
||||
c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new vless client error: %v", err)
|
||||
}
|
||||
|
||||
return v.ListenPacketOnStreamConn(c, metadata)
|
||||
}
|
||||
return v.ListenPacketWithDialer(ctx, dialer.NewDialer(v.Base.DialOptions(opts...)...), metadata)
|
||||
}
|
||||
|
||||
// ListenPacketWithDialer implements C.ProxyAdapter
|
||||
func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr
|
||||
if !metadata.Resolved() {
|
||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
||||
if err != nil {
|
||||
return nil, errors.New("can't resolve ip")
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
if v.option.PacketAddr {
|
||||
packetAddrMetadata := *metadata // make a copy
|
||||
packetAddrMetadata.Host = packetaddr.SeqPacketMagicAddress
|
||||
packetAddrMetadata.DstPort = "443"
|
||||
|
||||
c, err = v.StreamConn(c, &packetAddrMetadata)
|
||||
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("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
}
|
||||
|
||||
@ -331,24 +254,8 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
|
||||
return v.ListenPacketOnStreamConn(c, metadata)
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (v *Vless) SupportWithDialer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (v *Vless) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if v.option.XUDP {
|
||||
return newPacketConn(&threadSafePacketConn{
|
||||
PacketConn: vmessSing.NewXUDPConn(c, M.ParseSocksaddr(metadata.RemoteAddress())),
|
||||
}, v), nil
|
||||
} else if v.option.PacketAddr {
|
||||
return newPacketConn(&threadSafePacketConn{
|
||||
PacketConn: packetaddr.NewConn(&vlessPacketConn{
|
||||
Conn: c, rAddr: metadata.UDPAddr(),
|
||||
}, M.ParseSocksaddr(metadata.RemoteAddress())),
|
||||
}, v), nil
|
||||
}
|
||||
return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||
}
|
||||
|
||||
@ -357,19 +264,19 @@ 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))
|
||||
@ -381,8 +288,7 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
|
||||
UDP: metadata.NetWork == C.UDP,
|
||||
AddrType: addrType,
|
||||
Addr: addr,
|
||||
Port: uint16(port),
|
||||
Mux: metadata.NetWork == C.UDP && xudp,
|
||||
Port: uint(port),
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,9 +389,6 @@ 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)
|
||||
fallthrough
|
||||
case vless.XRO, vless.XRD, vless.XRS:
|
||||
addons = &vless.Addons{
|
||||
Flow: option.Flow,
|
||||
@ -495,16 +398,6 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
}
|
||||
}
|
||||
|
||||
switch option.PacketEncoding {
|
||||
case "packetaddr", "packet":
|
||||
option.PacketAddr = true
|
||||
option.XUDP = false
|
||||
default: // https://github.com/XTLS/Xray-core/pull/1567#issuecomment-1407305458
|
||||
if !option.PacketAddr {
|
||||
option.XUDP = true
|
||||
}
|
||||
}
|
||||
|
||||
client, err := vless.NewClient(option.UUID, addons, option.FlowShow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -512,25 +405,16 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
|
||||
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,
|
||||
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 {
|
||||
@ -547,14 +431,13 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
tlsConfig := tlsC.GetGlobalTLSConfig(&tls.Config{
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
})
|
||||
}
|
||||
|
||||
if v.option.ServerName == "" {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
@ -564,8 +447,11 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
|
||||
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
|
||||
|
@ -9,23 +9,15 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/common/convert"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
tlsC "github.com/Dreamacro/clash/component/tls"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/gun"
|
||||
clashVMess "github.com/Dreamacro/clash/transport/vmess"
|
||||
|
||||
vmess "github.com/sagernet/sing-vmess"
|
||||
"github.com/sagernet/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
|
||||
@ -35,35 +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"`
|
||||
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 {
|
||||
@ -91,22 +77,17 @@ type WSOptions struct {
|
||||
// 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 {
|
||||
@ -117,126 +98,99 @@ func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
if len(v.option.Fingerprint) == 0 {
|
||||
wsOpts.TLSConfig = tlsC.GetGlobalTLSConfig(tlsConfig)
|
||||
} else {
|
||||
if wsOpts.TLSConfig, err = tlsC.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
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(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,
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
c, err = clashVMess.StreamTLSConn(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(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,
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = clashVMess.StreamTLSConn(c, tlsOpts)
|
||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if metadata.NetWork == C.UDP {
|
||||
if v.option.XUDP {
|
||||
if N.NeedHandshake(c) {
|
||||
return v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
|
||||
} else {
|
||||
return v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
} else {
|
||||
if N.NeedHandshake(c) {
|
||||
return v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
|
||||
} else {
|
||||
return v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if N.NeedHandshake(c) {
|
||||
return v.client.DialEarlyConn(c, M.ParseSocksaddr(metadata.RemoteAddress())), nil
|
||||
} else {
|
||||
return v.client.DialConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
}
|
||||
|
||||
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
@ -247,30 +201,22 @@ 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) {
|
||||
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())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
return NewConn(c, v), err
|
||||
@ -278,22 +224,15 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if v.option.PacketAddr {
|
||||
_metadata := *metadata // make a copy
|
||||
metadata = &_metadata
|
||||
metadata.Host = packetaddr.SeqPacketMagicAddress
|
||||
metadata.DstPort = "443"
|
||||
}
|
||||
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil && len(opts) == 0 {
|
||||
@ -301,71 +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)
|
||||
|
||||
if v.option.XUDP {
|
||||
if N.NeedHandshake(c) {
|
||||
c = v.client.DialEarlyXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
} else {
|
||||
c, err = v.client.DialXUDPPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
} else {
|
||||
if N.NeedHandshake(c) {
|
||||
c = v.client.DialEarlyPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
} else {
|
||||
c, err = v.client.DialPacketConn(c, M.ParseSocksaddr(metadata.RemoteAddress()))
|
||||
}
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
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(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) {
|
||||
// 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 = v.StreamConn(c, metadata)
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer func(c net.Conn) {
|
||||
safeConnClose(c, err)
|
||||
}(c)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||
}
|
||||
return v.ListenPacketOnStreamConn(c, metadata)
|
||||
}
|
||||
|
||||
// SupportWithDialer implements C.ProxyAdapter
|
||||
func (v *Vmess) SupportWithDialer() bool {
|
||||
return true
|
||||
return v.ListenPacketOnStreamConn(c, metadata)
|
||||
}
|
||||
|
||||
// ListenPacketOnStreamConn implements C.ProxyAdapter
|
||||
func (v *Vmess) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
if v.option.PacketAddr {
|
||||
return newPacketConn(&threadSafePacketConn{PacketConn: packetaddr.NewBindConn(c)}, v), nil
|
||||
} else if pc, ok := c.(net.PacketConn); ok {
|
||||
return newPacketConn(&threadSafePacketConn{PacketConn: pc}, v), nil
|
||||
}
|
||||
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||
}
|
||||
|
||||
@ -376,46 +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())
|
||||
}
|
||||
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 {
|
||||
option.TLS = true
|
||||
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,
|
||||
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,
|
||||
@ -437,9 +321,8 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
@ -454,45 +337,46 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
|
||||
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 threadSafePacketConn struct {
|
||||
net.PacketConn
|
||||
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))
|
||||
}
|
||||
|
||||
func (c *threadSafePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
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
|
||||
access sync.Mutex
|
||||
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.(*net.UDPAddr)
|
||||
destAddr := addr.(*net.UDPAddr)
|
||||
if !(allowedAddr.IP.Equal(destAddr.IP) && allowedAddr.Port == destAddr.Port) {
|
||||
return 0, ErrUDPRemoteAddrMismatch
|
||||
}
|
||||
uc.access.Lock()
|
||||
defer uc.access.Unlock()
|
||||
return uc.Conn.Write(b)
|
||||
}
|
||||
|
||||
|
@ -1,269 +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/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
|
||||
wireguard "github.com/metacubex/sing-wireguard"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/wireguard-go/device"
|
||||
)
|
||||
|
||||
type WireGuard struct {
|
||||
*Base
|
||||
bind *wireguard.ClientBind
|
||||
device *device.Device
|
||||
tunDevice wireguard.Device
|
||||
dialer *wgSingDialer
|
||||
startOnce sync.Once
|
||||
startErr error
|
||||
}
|
||||
|
||||
type WireGuardOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
Ipv6 string `proxy:"ipv6,omitempty"`
|
||||
PrivateKey string `proxy:"private-key"`
|
||||
PublicKey string `proxy:"public-key"`
|
||||
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
|
||||
Reserved []uint8 `proxy:"reserved,omitempty"`
|
||||
Workers int `proxy:"workers,omitempty"`
|
||||
MTU int `proxy:"mtu,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"`
|
||||
}
|
||||
|
||||
type wgSingDialer struct {
|
||||
dialer dialer.Dialer
|
||||
}
|
||||
|
||||
var _ N.Dialer = &wgSingDialer{}
|
||||
|
||||
func (d *wgSingDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
return d.dialer.DialContext(ctx, network, destination.String())
|
||||
}
|
||||
|
||||
func (d *wgSingDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
return d.dialer.ListenPacket(ctx, "udp", "", destination.AddrPort())
|
||||
}
|
||||
|
||||
type wgNetDialer struct {
|
||||
tunDevice wireguard.Device
|
||||
}
|
||||
|
||||
var _ dialer.NetDialer = &wgNetDialer{}
|
||||
|
||||
func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap())
|
||||
}
|
||||
|
||||
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: &wgSingDialer{dialer: 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))
|
||||
}
|
||||
reserved[0] = uint8(option.Reserved[0])
|
||||
reserved[1] = uint8(option.Reserved[1])
|
||||
reserved[2] = uint8(option.Reserved[2])
|
||||
}
|
||||
peerAddr := M.ParseSocksaddrHostPort(option.Server, uint16(option.Port))
|
||||
outbound.bind = wireguard.NewClientBind(context.Background(), outbound.dialer, peerAddr, reserved)
|
||||
localPrefixes := make([]netip.Prefix, 0, 2)
|
||||
if len(option.Ip) > 0 {
|
||||
if !strings.Contains(option.Ip, "/") {
|
||||
option.Ip = option.Ip + "/32"
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(option.Ip); err == nil {
|
||||
localPrefixes = append(localPrefixes, prefix)
|
||||
} else {
|
||||
return nil, E.Cause(err, "ip address parse error")
|
||||
}
|
||||
}
|
||||
if len(option.Ipv6) > 0 {
|
||||
if !strings.Contains(option.Ipv6, "/") {
|
||||
option.Ipv6 = option.Ipv6 + "/128"
|
||||
}
|
||||
if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil {
|
||||
localPrefixes = append(localPrefixes, prefix)
|
||||
} else {
|
||||
return nil, E.Cause(err, "ipv6 address parse error")
|
||||
}
|
||||
}
|
||||
if len(localPrefixes) == 0 {
|
||||
return nil, E.New("missing local address")
|
||||
}
|
||||
var privateKey, peerPublicKey, preSharedKey string
|
||||
{
|
||||
bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode private key")
|
||||
}
|
||||
privateKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
{
|
||||
bytes, err := base64.StdEncoding.DecodeString(option.PublicKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode peer public key")
|
||||
}
|
||||
peerPublicKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
if option.PreSharedKey != "" {
|
||||
bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode pre shared key")
|
||||
}
|
||||
preSharedKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
ipcConf := "private_key=" + privateKey
|
||||
ipcConf += "\npublic_key=" + peerPublicKey
|
||||
ipcConf += "\nendpoint=" + peerAddr.String()
|
||||
if preSharedKey != "" {
|
||||
ipcConf += "\npreshared_key=" + preSharedKey
|
||||
}
|
||||
var has4, has6 bool
|
||||
for _, address := range localPrefixes {
|
||||
if address.Addr().Is4() {
|
||||
has4 = true
|
||||
} else {
|
||||
has6 = true
|
||||
}
|
||||
}
|
||||
if has4 {
|
||||
ipcConf += "\nallowed_ip=0.0.0.0/0"
|
||||
}
|
||||
if has6 {
|
||||
ipcConf += "\nallowed_ip=::/0"
|
||||
}
|
||||
if option.PersistentKeepalive != 0 {
|
||||
ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive)
|
||||
}
|
||||
mtu := option.MTU
|
||||
if mtu == 0 {
|
||||
mtu = 1408
|
||||
}
|
||||
var err error
|
||||
outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create WireGuard device")
|
||||
}
|
||||
outbound.device = device.NewDevice(outbound.tunDevice, outbound.bind, &device.Logger{
|
||||
Verbosef: func(format string, args ...interface{}) {
|
||||
log.SingLogger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
|
||||
},
|
||||
Errorf: func(format string, args ...interface{}) {
|
||||
log.SingLogger.Error(fmt.Sprintf(strings.ToLower(format), args...))
|
||||
},
|
||||
}, option.Workers)
|
||||
if debug.Enabled {
|
||||
log.SingLogger.Trace("created wireguard ipc conf: \n", ipcConf)
|
||||
}
|
||||
err = outbound.device.IpcSet(ipcConf)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "setup wireguard")
|
||||
}
|
||||
//err = outbound.tunDevice.Start()
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func closeWireGuard(w *WireGuard) {
|
||||
if w.device != nil {
|
||||
w.device.Close()
|
||||
}
|
||||
_ = common.Close(w.tunDevice)
|
||||
}
|
||||
|
||||
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
|
||||
options := w.Base.DialOptions(opts...)
|
||||
w.dialer.dialer = 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() {
|
||||
options = append(options, dialer.WithResolver(resolver.DefaultResolver))
|
||||
options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
|
||||
conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
|
||||
} else {
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, uint16(port)).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.dialer = 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() {
|
||||
ip, err := resolver.ResolveIP(ctx, metadata.Host)
|
||||
if err != nil {
|
||||
return nil, errors.New("can't resolve ip")
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, uint16(port)).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
|
||||
}
|
@ -7,8 +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/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/constant/provider"
|
||||
@ -32,21 +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.FirstWriteCallBackConn{
|
||||
Conn: c,
|
||||
Callback: func(err error) {
|
||||
if err == nil {
|
||||
f.onDialSuccess()
|
||||
} else {
|
||||
f.onDialFailed(proxy.Type(), err)
|
||||
}
|
||||
},
|
||||
}
|
||||
f.onDialFailed()
|
||||
}
|
||||
|
||||
return c, err
|
||||
@ -87,30 +73,24 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
// 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() {
|
||||
return proxy
|
||||
}
|
||||
} else {
|
||||
if proxy.Name() == f.selected {
|
||||
if proxy.Alive() {
|
||||
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 {
|
||||
@ -146,8 +126,6 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
|
||||
RoutingMark: option.RoutingMark,
|
||||
},
|
||||
option.Filter,
|
||||
option.ExcludeFilter,
|
||||
option.ExcludeType,
|
||||
providers,
|
||||
}),
|
||||
disableUDP: option.DisableUDP,
|
||||
|
@ -3,192 +3,102 @@ package outboundgroup
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/constant/provider"
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
"github.com/Dreamacro/clash/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
|
||||
}
|
||||
|
||||
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 {
|
||||
if mat, _ := gb.filter.FindStringMatch(p.Name()); mat != nil {
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
}
|
||||
|
||||
gb.proxies.Store(pd.Name(), newProxies)
|
||||
gb.versions.Store(pd.Name(), pd.Version())
|
||||
}
|
||||
}
|
||||
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"])
|
||||
}
|
||||
|
||||
if len(gb.providers) > 1 && len(gb.filterRegs) > 1 {
|
||||
var newProxies []C.Proxy
|
||||
proxiesSet := map[string]struct{}{}
|
||||
for _, filterReg := range gb.filterRegs {
|
||||
for _, p := range proxies {
|
||||
name := p.Name()
|
||||
if mat, _ := filterReg.FindStringMatch(name); mat != nil {
|
||||
if _, ok := proxiesSet[name]; !ok {
|
||||
proxiesSet[name] = struct{}{}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range proxies { // add not matched proxies at the end
|
||||
name := p.Name()
|
||||
if _, ok := proxiesSet[name]; !ok {
|
||||
proxiesSet[name] = struct{}{}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
}
|
||||
proxies = newProxies
|
||||
}
|
||||
if gb.excludeTypeArray != nil {
|
||||
var newProxies []C.Proxy
|
||||
for _, p := range proxies {
|
||||
mType := p.Type().String()
|
||||
flag := false
|
||||
for i := range gb.excludeTypeArray {
|
||||
if strings.EqualFold(mType, gb.excludeTypeArray[i]) {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
if flag {
|
||||
continue
|
||||
}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
proxies = newProxies
|
||||
}
|
||||
|
||||
if gb.excludeFilterReg != nil {
|
||||
var newProxies []C.Proxy
|
||||
for _, p := range proxies {
|
||||
name := p.Name()
|
||||
if mat, _ := gb.excludeFilterReg.FindStringMatch(name); mat != nil {
|
||||
continue
|
||||
}
|
||||
newProxies = append(newProxies, p)
|
||||
}
|
||||
proxies = newProxies
|
||||
}
|
||||
|
||||
return proxies
|
||||
}
|
||||
|
||||
@ -202,11 +112,11 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
delay, err := proxy.URLTest(ctx, url)
|
||||
lock.Lock()
|
||||
if err == nil {
|
||||
lock.Lock()
|
||||
mp[proxy.Name()] = delay
|
||||
lock.Unlock()
|
||||
}
|
||||
lock.Unlock()
|
||||
|
||||
wg.Done()
|
||||
}()
|
||||
@ -220,13 +130,8 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -240,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()
|
||||
}
|
||||
|
@ -6,22 +6,18 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/common/cache"
|
||||
"github.com/Dreamacro/clash/common/callback"
|
||||
"github.com/Dreamacro/clash/common/murmur3"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"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
|
||||
@ -32,8 +28,10 @@ type LoadBalance struct {
|
||||
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"
|
||||
}
|
||||
@ -85,28 +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)
|
||||
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.FirstWriteCallBackConn{
|
||||
Conn: c,
|
||||
Callback: func(err error) {
|
||||
if err == nil {
|
||||
lb.onDialSuccess()
|
||||
} else {
|
||||
lb.onDialFailed(proxy.Type(), err)
|
||||
}
|
||||
},
|
||||
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...)...)
|
||||
return
|
||||
}
|
||||
|
||||
@ -118,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...)...)
|
||||
}
|
||||
|
||||
@ -129,25 +117,12 @@ func (lb *LoadBalance) SupportUDP() bool {
|
||||
|
||||
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]
|
||||
for i := 0; i < length; i++ {
|
||||
idx = (idx + 1) % length
|
||||
proxy := proxies[idx]
|
||||
if proxy.Alive() {
|
||||
i++
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
@ -158,7 +133,7 @@ func strategyRoundRobin() strategyFn {
|
||||
|
||||
func strategyConsistentHashing() strategyFn {
|
||||
maxRetry := 5
|
||||
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
|
||||
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||
buckets := int32(len(proxies))
|
||||
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
||||
@ -169,13 +144,6 @@ func strategyConsistentHashing() strategyFn {
|
||||
}
|
||||
}
|
||||
|
||||
// when availability is poor, traverse the entire list to get the available nodes
|
||||
for _, proxy := range proxies {
|
||||
if proxy.Alive() {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
return proxies[0]
|
||||
}
|
||||
}
|
||||
@ -183,10 +151,10 @@ func strategyConsistentHashing() 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 {
|
||||
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)
|
||||
@ -216,9 +184,9 @@ func strategyStickySessions() 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
|
||||
@ -254,8 +222,6 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
|
||||
RoutingMark: option.RoutingMark,
|
||||
},
|
||||
option.Filter,
|
||||
option.ExcludeFilter,
|
||||
option.ExcludeType,
|
||||
providers,
|
||||
}),
|
||||
strategyFn: strategyFn,
|
||||
|
@ -21,17 +21,15 @@ var (
|
||||
|
||||
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"`
|
||||
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) {
|
||||
@ -78,7 +76,7 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
||||
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 {
|
||||
|
@ -3,12 +3,9 @@ package outboundgroup
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
N "github.com/Dreamacro/clash/common/net"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/constant/provider"
|
||||
@ -18,36 +15,6 @@ type Relay struct {
|
||||
*GroupBase
|
||||
}
|
||||
|
||||
type proxyDialer struct {
|
||||
proxy C.Proxy
|
||||
dialer C.Dialer
|
||||
}
|
||||
|
||||
func (p proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
currentMeta, err := addrToMetadata(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.Contains(network, "udp") { // should not support this operation
|
||||
currentMeta.NetWork = C.UDP
|
||||
pc, err := p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
|
||||
}
|
||||
return p.proxy.DialContextWithDialer(ctx, p.dialer, currentMeta)
|
||||
}
|
||||
|
||||
func (p proxyDialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
|
||||
currentMeta, err := addrToMetadata(rAddrPort.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentMeta.NetWork = C.UDP
|
||||
return p.proxy.ListenPacketWithDialer(ctx, p.dialer, currentMeta)
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
|
||||
proxies, chainProxies := r.proxies(metadata, true)
|
||||
@ -58,19 +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{
|
||||
proxy: proxy,
|
||||
dialer: d,
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
@ -92,18 +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{
|
||||
proxy: proxy,
|
||||
dialer: d,
|
||||
}
|
||||
}
|
||||
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-- {
|
||||
@ -121,19 +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
|
||||
}
|
||||
if !proxy.SupportWithDialer() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
last := proxies[len(proxies)-1]
|
||||
return last.SupportUDP() && last.SupportUOT()
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
@ -158,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,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()
|
||||
}
|
||||
|
||||
@ -190,8 +185,6 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re
|
||||
RoutingMark: option.RoutingMark,
|
||||
},
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
providers,
|
||||
}),
|
||||
}
|
||||
|
@ -74,8 +74,8 @@ func (s *Selector) Set(name string) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@ -99,8 +99,6 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider)
|
||||
RoutingMark: option.RoutingMark,
|
||||
},
|
||||
option.Filter,
|
||||
option.ExcludeFilter,
|
||||
option.ExcludeType,
|
||||
providers,
|
||||
}),
|
||||
selected: "COMPATIBLE",
|
||||
|
@ -6,8 +6,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/singledo"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
@ -36,27 +34,13 @@ func (u *URLTest) Now() string {
|
||||
|
||||
// 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.FirstWriteCallBackConn{
|
||||
Conn: c,
|
||||
Callback: func(err error) {
|
||||
if err == nil {
|
||||
u.onDialSuccess()
|
||||
} else {
|
||||
u.onDialFailed(proxy.Type(), err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return c, err
|
||||
}
|
||||
|
||||
@ -71,12 +55,12 @@ 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 {
|
||||
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()
|
||||
@ -105,9 +89,6 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
||||
|
||||
return u.fastNode, nil
|
||||
})
|
||||
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
|
||||
u.Touch()
|
||||
}
|
||||
|
||||
return elm
|
||||
}
|
||||
@ -158,8 +139,6 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
|
||||
},
|
||||
|
||||
option.Filter,
|
||||
option.ExcludeFilter,
|
||||
option.ExcludeType,
|
||||
providers,
|
||||
}),
|
||||
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
||||
|
@ -16,19 +16,32 @@ func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if ip, err := netip.ParseAddr(host); err != nil {
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
addr = &C.Metadata{
|
||||
Host: host,
|
||||
DstPort: port,
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
DstIP: netip.Addr{},
|
||||
DstPort: port,
|
||||
}
|
||||
} else {
|
||||
err = nil
|
||||
return
|
||||
} else if ip.Is4() {
|
||||
addr = &C.Metadata{
|
||||
Host: "",
|
||||
DstIP: ip.Unmap(),
|
||||
DstPort: port,
|
||||
AddrType: C.AtypIPv4,
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypIPv6,
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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,20 +88,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewHysteria(*hyOption)
|
||||
case "wireguard":
|
||||
wgOption := &outbound.WireGuardOption{}
|
||||
err = decoder.Decode(mapping, wgOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewWireGuard(*wgOption)
|
||||
case "tuic":
|
||||
tuicOption := &outbound.TuicOption{}
|
||||
err = decoder.Decode(mapping, tuicOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewTuic(*tuicOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package resource
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -16,49 +16,45 @@ var (
|
||||
dirMode os.FileMode = 0o755
|
||||
)
|
||||
|
||||
type Parser[V any] func([]byte) (V, error)
|
||||
type parser[V any] func([]byte) (V, error)
|
||||
|
||||
type Fetcher[V any] struct {
|
||||
resourceType string
|
||||
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)
|
||||
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 {
|
||||
func (f *fetcher[V]) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) Vehicle() types.Vehicle {
|
||||
return f.vehicle
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) VehicleType() types.VehicleType {
|
||||
func (f *fetcher[V]) VehicleType() types.VehicleType {
|
||||
return f.vehicle.Type()
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) Initial() (V, error) {
|
||||
func (f *fetcher[V]) Initial() (V, error) {
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
isLocal bool
|
||||
forceUpdate bool
|
||||
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
|
||||
f.updatedAt = &modTime
|
||||
isLocal = true
|
||||
if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
|
||||
log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name())
|
||||
forceUpdate = true
|
||||
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()
|
||||
@ -68,21 +64,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
var contents V
|
||||
if forceUpdate {
|
||||
var forceBuf []byte
|
||||
if forceBuf, err = f.vehicle.Read(); err == nil {
|
||||
if contents, err = f.parser(forceBuf); err == nil {
|
||||
isLocal = false
|
||||
buf = forceBuf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !forceUpdate {
|
||||
contents, err = f.parser(buf)
|
||||
}
|
||||
|
||||
proxies, err := f.parser(buf)
|
||||
if err != nil {
|
||||
if !isLocal {
|
||||
return getZero[V](), err
|
||||
@ -94,7 +76,7 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
return getZero[V](), err
|
||||
}
|
||||
|
||||
contents, err = f.parser(buf)
|
||||
proxies, err = f.parser(buf)
|
||||
if err != nil {
|
||||
return getZero[V](), err
|
||||
}
|
||||
@ -110,15 +92,15 @@ func (f *Fetcher[V]) Initial() (V, error) {
|
||||
|
||||
f.hash = md5.Sum(buf)
|
||||
|
||||
// pull contents automatically
|
||||
// pull proxies automatically
|
||||
if f.ticker != nil {
|
||||
go f.pullLoop()
|
||||
}
|
||||
|
||||
return contents, nil
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||
func (f *fetcher[V]) Update() (V, bool, error) {
|
||||
buf, err := f.vehicle.Read()
|
||||
if err != nil {
|
||||
return getZero[V](), false, err
|
||||
@ -127,12 +109,12 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||
now := time.Now()
|
||||
hash := md5.Sum(buf)
|
||||
if bytes.Equal(f.hash[:], hash[:]) {
|
||||
f.UpdatedAt = &now
|
||||
_ = os.Chtimes(f.vehicle.Path(), now, now)
|
||||
f.updatedAt = &now
|
||||
os.Chtimes(f.vehicle.Path(), now, now)
|
||||
return getZero[V](), true, nil
|
||||
}
|
||||
|
||||
contents, err := f.parser(buf)
|
||||
proxies, err := f.parser(buf)
|
||||
if err != nil {
|
||||
return getZero[V](), false, err
|
||||
}
|
||||
@ -143,37 +125,37 @@ func (f *Fetcher[V]) Update() (V, bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
f.UpdatedAt = &now
|
||||
f.updatedAt = &now
|
||||
f.hash = hash
|
||||
|
||||
return contents, false, nil
|
||||
return proxies, false, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) Destroy() error {
|
||||
func (f *fetcher[V]) Destroy() error {
|
||||
if f.ticker != nil {
|
||||
f.done <- struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher[V]) pullLoop() {
|
||||
func (f *fetcher[V]) pullLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-f.ticker.C:
|
||||
elm, same, err := f.Update()
|
||||
if err != nil {
|
||||
log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if same {
|
||||
log.Debugln("[Provider] %s's content doesn't change", f.Name())
|
||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infoln("[Provider] %s's content update", f.Name())
|
||||
if f.OnUpdate != nil {
|
||||
f.OnUpdate(elm)
|
||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||
if f.onUpdate != nil {
|
||||
f.onUpdate(elm)
|
||||
}
|
||||
case <-f.done:
|
||||
f.ticker.Stop()
|
||||
@ -194,20 +176,19 @@ func safeWrite(path string, buf []byte) error {
|
||||
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] {
|
||||
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]{
|
||||
return &fetcher[V]{
|
||||
name: name,
|
||||
ticker: ticker,
|
||||
vehicle: vehicle,
|
||||
parser: parser,
|
||||
done: make(chan struct{}, 1),
|
||||
OnUpdate: onUpdate,
|
||||
interval: interval,
|
||||
onUpdate: onUpdate,
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
@ -29,19 +25,22 @@ type HealthCheck struct {
|
||||
lazy bool
|
||||
lastTouch *atomic.Int64
|
||||
done chan struct{}
|
||||
singleDo *singledo.Single[struct{}]
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) process() {
|
||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||
|
||||
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()
|
||||
@ -63,26 +62,17 @@ func (hc *HealthCheck) touch() {
|
||||
}
|
||||
|
||||
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))
|
||||
for _, proxy := range hc.proxies {
|
||||
p := proxy
|
||||
b.Go(p.Name(), func() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||
defer cancel()
|
||||
log.Debugln("Health Checking %s {%s}", p.Name(), id)
|
||||
_, _ = p.URLTest(ctx, hc.url)
|
||||
log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id)
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
b.Wait()
|
||||
log.Debugln("Finish A Health Checking {%s}", id)
|
||||
return struct{}{}, nil
|
||||
})
|
||||
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
|
||||
for _, proxy := range hc.proxies {
|
||||
p := proxy
|
||||
b.Go(p.Name(), func() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||
defer cancel()
|
||||
_, _ = p.URLTest(ctx, hc.url)
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
b.Wait()
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) close() {
|
||||
@ -97,6 +87,5 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
|
||||
lazy: lazy,
|
||||
lastTouch: atomic.NewInt64(0),
|
||||
done: make(chan struct{}, 1),
|
||||
singleDo: singledo.NewSingle[struct{}](time.Second),
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/resource"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
)
|
||||
@ -21,14 +20,12 @@ type healthCheckSchema struct {
|
||||
}
|
||||
|
||||
type proxyProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Path string `provider:"path"`
|
||||
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"`
|
||||
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) {
|
||||
@ -54,17 +51,14 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
|
||||
var vehicle types.Vehicle
|
||||
switch schema.Type {
|
||||
case "file":
|
||||
vehicle = resource.NewFileVehicle(path)
|
||||
vehicle = NewFileVehicle(path)
|
||||
case "http":
|
||||
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
|
||||
|
||||
return NewProxySetProvider(name, interval, filter, excludeFilter, excludeType, vehicle, hc)
|
||||
return NewProxySetProvider(name, interval, filter, vehicle, hc)
|
||||
}
|
||||
|
@ -1,23 +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"
|
||||
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/dlclark/regexp2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
@ -36,30 +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(),
|
||||
"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() {
|
||||
@ -67,20 +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.onUpdate(elm)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -100,66 +91,19 @@ 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 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, 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() {
|
||||
@ -171,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), 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
|
||||
@ -187,7 +132,7 @@ type compatibleProvider struct {
|
||||
name string
|
||||
healthCheck *HealthCheck
|
||||
proxies []C.Proxy
|
||||
version uint32
|
||||
version uint
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||
@ -199,7 +144,7 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Version() uint32 {
|
||||
func (cp *compatibleProvider) Version() uint {
|
||||
return cp.version
|
||||
}
|
||||
|
||||
@ -262,12 +207,15 @@ 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) resource.Parser[[]C.Proxy] {
|
||||
func proxiesParseAndFilter(filter string, filterReg *regexp2.Regexp) parser[[]C.Proxy] {
|
||||
return func(buf []byte) ([]C.Proxy, error) {
|
||||
schema := &ProxySchema{}
|
||||
|
||||
@ -284,59 +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
|
||||
}
|
||||
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 {
|
||||
|
@ -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
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
package resource
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
netHttp "github.com/Dreamacro/clash/component/http"
|
||||
types "github.com/Dreamacro/clash/constant/provider"
|
||||
)
|
||||
|
||||
type FileVehicle struct {
|
||||
@ -35,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
|
||||
}
|
||||
@ -50,7 +47,7 @@ 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
|
||||
}
|
@ -1,25 +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 StackNew = buf.StackNew
|
||||
var StackNewSize = buf.StackNewSize
|
||||
var With = buf.With
|
||||
|
||||
var KeepAlive = common.KeepAlive
|
||||
|
||||
//go:norace
|
||||
func Dup[T any](obj T) T {
|
||||
return common.Dup(obj)
|
||||
}
|
||||
|
||||
var Must = common.Must
|
||||
var Error = common.Error
|
106
common/cache/cache.go
vendored
Normal file
106
common/cache/cache.go
vendored
Normal 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
72
common/cache/cache_test.go
vendored
Normal 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()
|
||||
}
|
4
common/cache/lrucache.go
vendored
4
common/cache/lrucache.go
vendored
@ -65,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]]),
|
||||
|
20
common/cache/lrucache_test.go
vendored
20
common/cache/lrucache_test.go
vendored
@ -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)
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
package callback
|
||||
|
||||
import (
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type FirstWriteCallBackConn struct {
|
||||
C.Conn
|
||||
Callback func(error)
|
||||
written bool
|
||||
}
|
||||
|
||||
func (c *FirstWriteCallBackConn) Write(b []byte) (n int, err error) {
|
||||
defer func() {
|
||||
if !c.written {
|
||||
c.written = true
|
||||
c.Callback(err)
|
||||
}
|
||||
}()
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *FirstWriteCallBackConn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
@ -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()
|
||||
|
@ -7,5 +7,4 @@ import (
|
||||
)
|
||||
|
||||
func prepareBackgroundCommand(cmd *exec.Cmd) {
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -5,15 +5,39 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/Dreamacro/clash/log"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
@ -49,19 +73,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
hysteria["port"] = urlHysteria.Port()
|
||||
hysteria["sni"] = query.Get("peer")
|
||||
hysteria["obfs"] = query.Get("obfs")
|
||||
hysteria["alpn"] = []string{query.Get("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)
|
||||
@ -83,7 +99,7 @@ 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
|
||||
|
||||
sni := query.Get("sni")
|
||||
if sni != "" {
|
||||
@ -100,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")
|
||||
@ -113,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
|
||||
}
|
||||
|
||||
@ -170,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
|
||||
@ -182,35 +239,26 @@ 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
|
||||
}
|
||||
|
||||
if sni, ok := values["sni"]; ok && sni != "" {
|
||||
vmess["servername"] = sni
|
||||
sni := values["sni"]
|
||||
if sni != "" {
|
||||
vmess["sni"] = sni
|
||||
}
|
||||
|
||||
host := values["host"]
|
||||
network := strings.ToLower(values["net"].(string))
|
||||
if values["type"] == "http" {
|
||||
network = "http"
|
||||
} else if network == "http" {
|
||||
network = "h2"
|
||||
}
|
||||
|
||||
vmess["network"] = network
|
||||
|
||||
tls := strings.ToLower(values["tls"].(string))
|
||||
if strings.HasSuffix(tls, "tls") {
|
||||
if tls != "" && tls != "0" && tls != "null" {
|
||||
if host != nil {
|
||||
vmess["servername"] = host
|
||||
}
|
||||
vmess["tls"] = true
|
||||
}
|
||||
|
||||
@ -218,13 +266,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
|
||||
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
|
||||
@ -232,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
|
||||
|
||||
@ -244,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":
|
||||
@ -272,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
|
||||
}
|
||||
@ -284,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
|
||||
@ -313,22 +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)
|
||||
case "ssr":
|
||||
dcBuf, err := encRaw.DecodeString(body)
|
||||
dcBuf, err := enc.DecodeString(body)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@ -395,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++
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,129 +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
|
||||
proxy["skip-cert-verify"] = false
|
||||
proxy["tls"] = false
|
||||
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 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
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package net
|
||||
|
||||
import "net"
|
||||
|
||||
type bindPacketConn struct {
|
||||
net.PacketConn
|
||||
rAddr net.Addr
|
||||
}
|
||||
|
||||
func (wpc *bindPacketConn) Read(b []byte) (n int, err error) {
|
||||
n, _, err = wpc.PacketConn.ReadFrom(b)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (wpc *bindPacketConn) Write(b []byte) (n int, err error) {
|
||||
return wpc.PacketConn.WriteTo(b, wpc.rAddr)
|
||||
}
|
||||
|
||||
func (wpc *bindPacketConn) RemoteAddr() net.Addr {
|
||||
return wpc.rAddr
|
||||
}
|
||||
|
||||
func (wpc *bindPacketConn) LocalAddr() net.Addr {
|
||||
if wpc.PacketConn.LocalAddr() == nil {
|
||||
return &net.UDPAddr{IP: net.IPv4zero, Port: 0}
|
||||
} else {
|
||||
return wpc.PacketConn.LocalAddr()
|
||||
}
|
||||
}
|
||||
|
||||
func NewBindPacketConn(pc net.PacketConn, rAddr net.Addr) net.Conn {
|
||||
return &bindPacketConn{
|
||||
PacketConn: pc,
|
||||
rAddr: rAddr,
|
||||
}
|
||||
}
|
@ -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,22 +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.Buffered() > 0 {
|
||||
_, err = buffer.ReadOnceFrom(c.r)
|
||||
return
|
||||
}
|
||||
return c.ExtendedConn.ReadBuffer(buffer)
|
||||
}
|
||||
|
||||
func (c *BufferedConn) Upstream() any {
|
||||
return c.ExtendedConn
|
||||
}
|
||||
|
||||
func (c *BufferedConn) ReaderReplaceable() bool {
|
||||
if c.r.Buffered() > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1,104 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type refConn struct {
|
||||
conn net.Conn
|
||||
ref any
|
||||
}
|
||||
|
||||
func (c *refConn) Read(b []byte) (n int, err error) {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *refConn) Write(b []byte) (n int, err error) {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *refConn) Close() error {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *refConn) LocalAddr() net.Addr {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *refConn) RemoteAddr() net.Addr {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *refConn) SetDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *refConn) SetReadDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *refConn) SetWriteDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(c.ref)
|
||||
return c.conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *refConn) Upstream() any {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func NewRefConn(conn net.Conn, ref any) net.Conn {
|
||||
return &refConn{conn: conn, ref: ref}
|
||||
}
|
||||
|
||||
type refPacketConn struct {
|
||||
pc net.PacketConn
|
||||
ref any
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.ReadFrom(p)
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.WriteTo(p, addr)
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) Close() error {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.Close()
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) LocalAddr() net.Addr {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.LocalAddr()
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) SetDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) SetReadDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (pc *refPacketConn) SetWriteDeadline(t time.Time) error {
|
||||
defer runtime.KeepAlive(pc.ref)
|
||||
return pc.pc.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func NewRefPacketConn(pc net.PacketConn, ref any) net.PacketConn {
|
||||
return &refPacketConn{pc: pc, ref: ref}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -1,30 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
var NewExtendedConn = bufio.NewExtendedConn
|
||||
var NewExtendedWriter = bufio.NewExtendedWriter
|
||||
var NewExtendedReader = bufio.NewExtendedReader
|
||||
|
||||
type ExtendedConn = network.ExtendedConn
|
||||
type ExtendedWriter = network.ExtendedWriter
|
||||
type ExtendedReader = network.ExtendedReader
|
||||
|
||||
func NeedHandshake(conn any) bool {
|
||||
if earlyConn, isEarlyConn := common.Cast[network.EarlyConn](conn); isEarlyConn && earlyConn.NeedHandshake() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Relay copies between left and right bidirectionally.
|
||||
func Relay(leftConn, rightConn net.Conn) {
|
||||
_ = bufio.CopyConn(context.TODO(), leftConn, rightConn)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -6,10 +6,16 @@ import (
|
||||
"errors"
|
||||
"math/bits"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
)
|
||||
|
||||
var defaultAllocator = NewAllocator()
|
||||
|
||||
func init() {
|
||||
buf.DefaultAllocator = defaultAllocator
|
||||
}
|
||||
|
||||
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
|
||||
type Allocator struct {
|
||||
buffers []sync.Pool
|
||||
|
@ -1,10 +1,10 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zhangyunhao116/fastrand"
|
||||
)
|
||||
|
||||
func TestAllocGet(t *testing.T) {
|
||||
@ -43,6 +43,6 @@ func TestAllocPutThenGet(t *testing.T) {
|
||||
|
||||
func BenchmarkMSB(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
msb(fastrand.Int())
|
||||
msb(rand.Int())
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
package pool
|
||||
|
||||
import "github.com/sagernet/sing/common/buf"
|
||||
|
||||
func init() {
|
||||
buf.DefaultAllocator = defaultAllocator
|
||||
}
|
@ -3,7 +3,6 @@ package structure
|
||||
// references: https://github.com/mitchellh/mapstructure
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@ -14,11 +13,8 @@ import (
|
||||
type Option struct {
|
||||
TagName string
|
||||
WeaklyTypedInput bool
|
||||
KeyReplacer *strings.Replacer
|
||||
}
|
||||
|
||||
var DefaultKeyReplacer = strings.NewReplacer("_", "-")
|
||||
|
||||
// Decoder is the core of structure
|
||||
type Decoder struct {
|
||||
option *Option
|
||||
@ -53,23 +49,6 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||
omitempty := found && omitKey == "omitempty"
|
||||
|
||||
value, ok := src[key]
|
||||
if !ok {
|
||||
if d.option.KeyReplacer != nil {
|
||||
key = d.option.KeyReplacer.Replace(key)
|
||||
}
|
||||
|
||||
for _strKey := range src {
|
||||
strKey := _strKey
|
||||
if d.option.KeyReplacer != nil {
|
||||
strKey = d.option.KeyReplacer.Replace(strKey)
|
||||
}
|
||||
if strings.EqualFold(key, strKey) {
|
||||
value = src[_strKey]
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok || value == nil {
|
||||
if omitempty {
|
||||
continue
|
||||
@ -86,16 +65,9 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
|
||||
}
|
||||
|
||||
func (d *Decoder) decode(name string, data any, val reflect.Value) error {
|
||||
kind := val.Kind()
|
||||
switch {
|
||||
case isInt(kind):
|
||||
switch val.Kind() {
|
||||
case reflect.Int:
|
||||
return d.decodeInt(name, data, val)
|
||||
case isUint(kind):
|
||||
return d.decodeUint(name, data, val)
|
||||
case isFloat(kind):
|
||||
return d.decodeFloat(name, data, val)
|
||||
}
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
return d.decodeString(name, data, val)
|
||||
case reflect.Bool:
|
||||
@ -113,42 +85,13 @@ func (d *Decoder) decode(name string, data any, val reflect.Value) error {
|
||||
}
|
||||
}
|
||||
|
||||
func isInt(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isUint(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isFloat(kind reflect.Kind) bool {
|
||||
switch kind {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case isInt(kind):
|
||||
case kind == reflect.Int:
|
||||
val.SetInt(dataVal.Int())
|
||||
case isUint(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetInt(int64(dataVal.Uint()))
|
||||
case isFloat(kind) && d.option.WeaklyTypedInput:
|
||||
case kind == reflect.Float64 && d.option.WeaklyTypedInput:
|
||||
val.SetInt(int64(dataVal.Float()))
|
||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||
var i int64
|
||||
@ -167,72 +110,14 @@ func (d *Decoder) decodeInt(name string, data any, val reflect.Value) (err error
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeUint(name string, data any, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case isUint(kind):
|
||||
val.SetUint(dataVal.Uint())
|
||||
case isInt(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetUint(uint64(dataVal.Int()))
|
||||
case isFloat(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetUint(uint64(dataVal.Float()))
|
||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||
var i uint64
|
||||
i, err = strconv.ParseUint(dataVal.String(), 0, val.Type().Bits())
|
||||
if err == nil {
|
||||
val.SetUint(i)
|
||||
} else {
|
||||
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
name, val.Type(), dataVal.Type(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeFloat(name string, data any, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case isFloat(kind):
|
||||
val.SetFloat(dataVal.Float())
|
||||
case isUint(kind):
|
||||
val.SetFloat(float64(dataVal.Uint()))
|
||||
case isInt(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetFloat(float64(dataVal.Int()))
|
||||
case kind == reflect.String && d.option.WeaklyTypedInput:
|
||||
var i float64
|
||||
i, err = strconv.ParseFloat(dataVal.String(), val.Type().Bits())
|
||||
if err == nil {
|
||||
val.SetFloat(i)
|
||||
} else {
|
||||
err = fmt.Errorf("cannot parse '%s' as int: %s", name, err)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
name, val.Type(), dataVal.Type(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Decoder) decodeString(name string, data any, val reflect.Value) (err error) {
|
||||
dataVal := reflect.ValueOf(data)
|
||||
kind := dataVal.Kind()
|
||||
switch {
|
||||
case kind == reflect.String:
|
||||
val.SetString(dataVal.String())
|
||||
case isInt(kind) && d.option.WeaklyTypedInput:
|
||||
case kind == reflect.Int && d.option.WeaklyTypedInput:
|
||||
val.SetString(strconv.FormatInt(dataVal.Int(), 10))
|
||||
case isUint(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
|
||||
case isFloat(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetString(strconv.FormatFloat(dataVal.Float(), 'E', -1, dataVal.Type().Bits()))
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
@ -248,10 +133,8 @@ func (d *Decoder) decodeBool(name string, data any, val reflect.Value) (err erro
|
||||
switch {
|
||||
case kind == reflect.Bool:
|
||||
val.SetBool(dataVal.Bool())
|
||||
case isInt(kind) && d.option.WeaklyTypedInput:
|
||||
case kind == reflect.Int && d.option.WeaklyTypedInput:
|
||||
val.SetBool(dataVal.Int() != 0)
|
||||
case isUint(kind) && d.option.WeaklyTypedInput:
|
||||
val.SetString(strconv.FormatUint(dataVal.Uint(), 10))
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"'%s' expected type '%s', got unconvertible type '%s'",
|
||||
@ -266,17 +149,6 @@ func (d *Decoder) decodeSlice(name string, data any, val reflect.Value) error {
|
||||
valType := val.Type()
|
||||
valElemType := valType.Elem()
|
||||
|
||||
if dataVal.Kind() == reflect.String && valElemType.Kind() == reflect.Uint8 { // from encoding/json
|
||||
s := []byte(dataVal.String())
|
||||
b := make([]byte, base64.StdEncoding.DecodedLen(len(s)))
|
||||
n, err := base64.StdEncoding.Decode(b, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("try decode '%s' by base64 error: %w", name, err)
|
||||
}
|
||||
val.SetBytes(b[:n])
|
||||
return nil
|
||||
}
|
||||
|
||||
if dataVal.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("'%s' is not a slice", name)
|
||||
}
|
||||
@ -481,18 +353,12 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
||||
if !rawMapVal.IsValid() {
|
||||
// Do a slower search by iterating over each key and
|
||||
// doing case-insensitive search.
|
||||
if d.option.KeyReplacer != nil {
|
||||
fieldName = d.option.KeyReplacer.Replace(fieldName)
|
||||
}
|
||||
for dataValKey := range dataValKeys {
|
||||
mK, ok := dataValKey.Interface().(string)
|
||||
if !ok {
|
||||
// Not a string key
|
||||
continue
|
||||
}
|
||||
if d.option.KeyReplacer != nil {
|
||||
mK = d.option.KeyReplacer.Replace(mK)
|
||||
}
|
||||
|
||||
if strings.EqualFold(mK, fieldName) {
|
||||
rawMapKey = dataValKey
|
||||
|
@ -137,45 +137,3 @@ func TestStructure_Nest(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, s.BazOptional, goal)
|
||||
}
|
||||
|
||||
func TestStructure_SliceNilValue(t *testing.T) {
|
||||
rawMap := map[string]any{
|
||||
"foo": 1,
|
||||
"bar": []any{"bar", nil},
|
||||
}
|
||||
|
||||
goal := &BazSlice{
|
||||
Foo: 1,
|
||||
Bar: []string{"bar", ""},
|
||||
}
|
||||
|
||||
s := &BazSlice{}
|
||||
err := weakTypeDecoder.Decode(rawMap, s)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, goal.Bar, s.Bar)
|
||||
|
||||
s = &BazSlice{}
|
||||
err = decoder.Decode(rawMap, s)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestStructure_SliceNilValueComplex(t *testing.T) {
|
||||
rawMap := map[string]any{
|
||||
"bar": []any{map[string]any{"bar": "foo"}, nil},
|
||||
}
|
||||
|
||||
s := &struct {
|
||||
Bar []map[string]any `test:"bar"`
|
||||
}{}
|
||||
|
||||
err := decoder.Decode(rawMap, s)
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, s.Bar[1])
|
||||
|
||||
ss := &struct {
|
||||
Bar []Baz `test:"bar"`
|
||||
}{}
|
||||
|
||||
err = decoder.Decode(rawMap, ss)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package utils
|
||||
|
||||
func MustOK[T any](result T, ok bool) T {
|
||||
if ok {
|
||||
return result
|
||||
}
|
||||
panic("operation failed")
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Filter[T comparable](tSlice []T, filter func(t T) bool) []T {
|
||||
result := make([]T, 0)
|
||||
for _, t := range tSlice {
|
||||
if filter(t) {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToStringSlice(value any) ([]string, error) {
|
||||
strArr := make([]string, 0)
|
||||
switch reflect.TypeOf(value).Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
origin := reflect.ValueOf(value)
|
||||
for i := 0; i < origin.Len(); i++ {
|
||||
item := fmt.Sprintf("%v", origin.Index(i))
|
||||
strArr = append(strArr, item)
|
||||
}
|
||||
case reflect.String:
|
||||
strArr = append(strArr, fmt.Sprintf("%v", value))
|
||||
default:
|
||||
return nil, errors.New("value format error, must be string or array")
|
||||
}
|
||||
return strArr, nil
|
||||
}
|
@ -2,50 +2,15 @@ package utils
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/zhangyunhao116/fastrand"
|
||||
)
|
||||
|
||||
type fastRandReader struct{}
|
||||
|
||||
func (r fastRandReader) Read(p []byte) (int, error) {
|
||||
return fastrand.Read(p)
|
||||
}
|
||||
|
||||
var UnsafeUUIDGenerator = uuid.NewGenWithOptions(uuid.WithRandomReader(fastRandReader{}))
|
||||
|
||||
func NewUUIDV1() uuid.UUID {
|
||||
u, _ := UnsafeUUIDGenerator.NewV1() // fastrand.Read wouldn't cause error, so ignore err is safe
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUUIDV3(ns uuid.UUID, name string) uuid.UUID {
|
||||
return UnsafeUUIDGenerator.NewV3(ns, name)
|
||||
}
|
||||
|
||||
func NewUUIDV4() uuid.UUID {
|
||||
u, _ := UnsafeUUIDGenerator.NewV4() // fastrand.Read wouldn't cause error, so ignore err is safe
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUUIDV5(ns uuid.UUID, name string) uuid.UUID {
|
||||
return UnsafeUUIDGenerator.NewV5(ns, name)
|
||||
}
|
||||
|
||||
func NewUUIDV6() uuid.UUID {
|
||||
u, _ := UnsafeUUIDGenerator.NewV6() // fastrand.Read wouldn't cause error, so ignore err is safe
|
||||
return u
|
||||
}
|
||||
|
||||
func NewUUIDV7() uuid.UUID {
|
||||
u, _ := UnsafeUUIDGenerator.NewV7() // fastrand.Read wouldn't cause error, so ignore err is safe
|
||||
return u
|
||||
}
|
||||
var uuidNamespace, _ = uuid.FromString("00000000-0000-0000-0000-000000000000")
|
||||
|
||||
// UUIDMap https://github.com/XTLS/Xray-core/issues/158#issue-783294090
|
||||
func UUIDMap(str string) (uuid.UUID, error) {
|
||||
u, err := uuid.FromString(str)
|
||||
if err != nil {
|
||||
return NewUUIDV5(uuid.Nil, str), nil
|
||||
return uuid.NewV5(uuidNamespace, str), nil
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
func TestUUIDMap(t *testing.T) {
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/Dreamacro/clash/common/nnip"
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
|
@ -1,18 +1,24 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func bindControl(ifaceIdx int) controlFn {
|
||||
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
|
||||
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||
|
||||
func bindControl(ifaceIdx int, chain controlFn) controlFn {
|
||||
return func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(address)
|
||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
||||
return
|
||||
@ -42,7 +48,7 @@ func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.A
|
||||
return err
|
||||
}
|
||||
|
||||
addControlToDialer(dialer, bindControl(ifaceObj.Index))
|
||||
dialer.Control = bindControl(ifaceObj.Index, dialer.Control)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -52,10 +58,6 @@ func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address
|
||||
return "", err
|
||||
}
|
||||
|
||||
addControlToListenConfig(lc, bindControl(ifaceObj.Index))
|
||||
lc.Control = bindControl(ifaceObj.Index, lc.Control)
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func ParseNetwork(network string, addr netip.Addr) string {
|
||||
return network
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
@ -9,8 +8,16 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func bindControl(ifaceName string) controlFn {
|
||||
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
|
||||
type controlFn = func(network, address string, c syscall.RawConn) error
|
||||
|
||||
func bindControl(ifaceName string, chain controlFn) controlFn {
|
||||
return func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(address)
|
||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
||||
return
|
||||
@ -30,17 +37,13 @@ func bindControl(ifaceName string) controlFn {
|
||||
}
|
||||
|
||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
|
||||
addControlToDialer(dialer, bindControl(ifaceName))
|
||||
dialer.Control = bindControl(ifaceName, dialer.Control)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||
addControlToListenConfig(lc, bindControl(ifaceName))
|
||||
lc.Control = bindControl(ifaceName, lc.Control)
|
||||
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func ParseNetwork(network string, addr netip.Addr) string {
|
||||
return network
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:build !linux && !darwin && !windows
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package dialer
|
||||
|
||||
@ -91,13 +91,3 @@ func bindIfaceToListenConfig(ifaceName string, _ *net.ListenConfig, network, add
|
||||
|
||||
return addr.String(), nil
|
||||
}
|
||||
|
||||
func ParseNetwork(network string, addr netip.Addr) string {
|
||||
// fix bindIfaceToListenConfig() force bind to an ipv4 address
|
||||
if !strings.HasSuffix(network, "4") &&
|
||||
!strings.HasSuffix(network, "6") &&
|
||||
addr.Unmap().Is6() {
|
||||
network += "6"
|
||||
}
|
||||
return network
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Dreamacro/clash/component/iface"
|
||||
)
|
||||
|
||||
const (
|
||||
IP_UNICAST_IF = 31
|
||||
IPV6_UNICAST_IF = 31
|
||||
)
|
||||
|
||||
func bind4(handle syscall.Handle, ifaceIdx int) error {
|
||||
var bytes [4]byte
|
||||
binary.BigEndian.PutUint32(bytes[:], uint32(ifaceIdx))
|
||||
idx := *(*uint32)(unsafe.Pointer(&bytes[0]))
|
||||
return syscall.SetsockoptInt(handle, syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx))
|
||||
}
|
||||
|
||||
func bind6(handle syscall.Handle, ifaceIdx int) error {
|
||||
return syscall.SetsockoptInt(handle, syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, ifaceIdx)
|
||||
}
|
||||
|
||||
func bindControl(ifaceIdx int) controlFn {
|
||||
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
|
||||
addrPort, err := netip.ParseAddrPort(address)
|
||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
||||
return
|
||||
}
|
||||
|
||||
var innerErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
handle := syscall.Handle(fd)
|
||||
bind6err := bind6(handle, ifaceIdx)
|
||||
bind4err := bind4(handle, ifaceIdx)
|
||||
switch network {
|
||||
case "ip6", "tcp6":
|
||||
innerErr = bind6err
|
||||
case "ip4", "tcp4", "udp4":
|
||||
innerErr = bind4err
|
||||
case "udp6":
|
||||
// golang will set network to udp6 when listenUDP on wildcard ip (eg: ":0", "")
|
||||
if (!addrPort.Addr().IsValid() || addrPort.Addr().IsUnspecified()) && bind6err != nil {
|
||||
// try bind ipv6, if failed, ignore. it's a workaround for windows disable interface ipv6
|
||||
if bind4err != nil {
|
||||
innerErr = bind6err
|
||||
} else {
|
||||
innerErr = bind4err
|
||||
}
|
||||
} else {
|
||||
innerErr = bind6err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func bindIfaceToDialer(ifaceName string, dialer *net.Dialer, _ string, _ netip.Addr) error {
|
||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addControlToDialer(dialer, bindControl(ifaceObj.Index))
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindIfaceToListenConfig(ifaceName string, lc *net.ListenConfig, _, address string) (string, error) {
|
||||
ifaceObj, err := iface.ResolveInterface(ifaceName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
addControlToListenConfig(lc, bindControl(ifaceObj.Index))
|
||||
return address, nil
|
||||
}
|
||||
|
||||
func ParseNetwork(network string, addr netip.Addr) string {
|
||||
return network
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type controlFn = func(ctx context.Context, network, address string, c syscall.RawConn) error
|
||||
|
||||
func addControlToListenConfig(lc *net.ListenConfig, fn controlFn) {
|
||||
llc := *lc
|
||||
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
switch {
|
||||
case llc.Control != nil:
|
||||
if err = llc.Control(network, address, c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return fn(context.Background(), network, address, c)
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
//go:build !go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func addControlToDialer(d *net.Dialer, fn controlFn) {
|
||||
ld := *d
|
||||
d.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
switch {
|
||||
case ld.Control != nil:
|
||||
if err = ld.Control(network, address, c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return fn(context.Background(), network, address, c)
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
//go:build go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func addControlToDialer(d *net.Dialer, fn controlFn) {
|
||||
ld := *d
|
||||
d.ControlContext = func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
|
||||
switch {
|
||||
case ld.ControlContext != nil:
|
||||
if err = ld.ControlContext(ctx, network, address, c); err != nil {
|
||||
return
|
||||
}
|
||||
case ld.Control != nil:
|
||||
if err = ld.Control(network, address, c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return fn(ctx, network, address, c)
|
||||
}
|
||||
}
|
@ -2,28 +2,24 @@ package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
)
|
||||
|
||||
type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error)
|
||||
|
||||
var (
|
||||
dialMux sync.Mutex
|
||||
actualSingleStackDialContext = serialSingleStackDialContext
|
||||
actualDualStackDialContext = serialDualStackDialContext
|
||||
tcpConcurrent = false
|
||||
fallbackTimeout = 300 * time.Millisecond
|
||||
dialMux sync.Mutex
|
||||
actualSingleDialContext = singleDialContext
|
||||
actualDualStackDialContext = dualStackDialContext
|
||||
tcpConcurrent = false
|
||||
DisableIPv6 = false
|
||||
)
|
||||
|
||||
func applyOptions(options ...Option) *option {
|
||||
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
|
||||
opt := &option{
|
||||
interfaceName: DefaultInterface.Load(),
|
||||
routingMark: int(DefaultRoutingMark.Load()),
|
||||
@ -37,39 +33,29 @@ func applyOptions(options ...Option) *option {
|
||||
o(opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
|
||||
opt := applyOptions(options...)
|
||||
|
||||
if opt.network == 4 || opt.network == 6 {
|
||||
if strings.Contains(network, "tcp") {
|
||||
network = "tcp"
|
||||
} else {
|
||||
network = "udp"
|
||||
}
|
||||
|
||||
network = fmt.Sprintf("%s%d", network, opt.network)
|
||||
}
|
||||
|
||||
ips, port, err := parseAddr(ctx, network, address, opt.resolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch network {
|
||||
case "tcp4", "tcp6", "udp4", "udp6":
|
||||
return actualSingleStackDialContext(ctx, network, ips, port, opt)
|
||||
return actualSingleDialContext(ctx, network, address, opt)
|
||||
case "tcp", "udp":
|
||||
return actualDualStackDialContext(ctx, network, ips, port, opt)
|
||||
return actualDualStackDialContext(ctx, network, address, opt)
|
||||
default:
|
||||
return nil, ErrorInvalidedNetworkStack
|
||||
return nil, errors.New("network invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func ListenPacket(ctx context.Context, network, address string, options ...Option) (net.PacketConn, error) {
|
||||
cfg := applyOptions(options...)
|
||||
cfg := &option{
|
||||
interfaceName: DefaultInterface.Load(),
|
||||
routingMark: int(DefaultRoutingMark.Load()),
|
||||
}
|
||||
|
||||
for _, o := range DefaultOptions {
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(cfg)
|
||||
}
|
||||
|
||||
lc := &net.ListenConfig{}
|
||||
if cfg.interfaceName != "" {
|
||||
@ -89,40 +75,26 @@ func ListenPacket(ctx context.Context, network, address string, options ...Optio
|
||||
return lc.ListenPacket(ctx, network, address)
|
||||
}
|
||||
|
||||
func SetTcpConcurrent(concurrent bool) {
|
||||
func SetDial(concurrent bool) {
|
||||
dialMux.Lock()
|
||||
defer dialMux.Unlock()
|
||||
tcpConcurrent = concurrent
|
||||
if concurrent {
|
||||
actualSingleStackDialContext = concurrentSingleStackDialContext
|
||||
actualSingleDialContext = concurrentSingleDialContext
|
||||
actualDualStackDialContext = concurrentDualStackDialContext
|
||||
} else {
|
||||
actualSingleStackDialContext = serialSingleStackDialContext
|
||||
actualDualStackDialContext = serialDualStackDialContext
|
||||
actualSingleDialContext = singleDialContext
|
||||
actualDualStackDialContext = dualStackDialContext
|
||||
}
|
||||
|
||||
dialMux.Unlock()
|
||||
}
|
||||
|
||||
func GetTcpConcurrent() bool {
|
||||
dialMux.Lock()
|
||||
defer dialMux.Unlock()
|
||||
func GetDial() bool {
|
||||
return tcpConcurrent
|
||||
}
|
||||
|
||||
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
address := net.JoinHostPort(destination.String(), port)
|
||||
|
||||
netDialer := opt.netDialer
|
||||
switch netDialer.(type) {
|
||||
case nil:
|
||||
netDialer = &net.Dialer{}
|
||||
case *net.Dialer:
|
||||
_netDialer := *netDialer.(*net.Dialer)
|
||||
netDialer = &_netDialer // make a copy
|
||||
default:
|
||||
return netDialer.DialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
dialer := netDialer.(*net.Dialer)
|
||||
dialer := &net.Dialer{}
|
||||
if opt.interfaceName != "" {
|
||||
if err := bindIfaceToDialer(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
@ -131,213 +103,218 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
if opt.routingMark != 0 {
|
||||
bindMarkToDialer(opt.routingMark, dialer, network, destination)
|
||||
}
|
||||
if opt.tfo {
|
||||
return dialTFO(ctx, *dialer, network, address)
|
||||
|
||||
if DisableIPv6 && destination.Is6() {
|
||||
return nil, fmt.Errorf("IPv6 is diabled, dialer cancel")
|
||||
}
|
||||
return dialer.DialContext(ctx, network, address)
|
||||
|
||||
return dialer.DialContext(ctx, network, net.JoinHostPort(destination.String(), port))
|
||||
}
|
||||
|
||||
func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
return serialDialContext(ctx, network, ips, port, opt)
|
||||
}
|
||||
|
||||
func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt)
|
||||
}
|
||||
|
||||
func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
return parallelDialContext(ctx, network, ips, port, opt)
|
||||
}
|
||||
|
||||
func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
if opt.prefer != 4 && opt.prefer != 6 {
|
||||
return parallelDialContext(ctx, network, ips, port, opt)
|
||||
func dualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt)
|
||||
}
|
||||
|
||||
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
ipv4s, ipv6s := sortationAddr(ips)
|
||||
preferIPVersion := opt.prefer
|
||||
|
||||
fallbackTicker := time.NewTicker(fallbackTimeout)
|
||||
defer fallbackTicker.Stop()
|
||||
results := make(chan dialResult)
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
racer := func(ips []netip.Addr, isPrimary bool) {
|
||||
result := dialResult{isPrimary: isPrimary}
|
||||
|
||||
type dialResult struct {
|
||||
net.Conn
|
||||
error
|
||||
resolved bool
|
||||
ipv6 bool
|
||||
done bool
|
||||
}
|
||||
results := make(chan dialResult)
|
||||
var primary, fallback dialResult
|
||||
|
||||
startRacer := func(ctx context.Context, network, host string, direct bool, ipv6 bool) {
|
||||
result := dialResult{ipv6: ipv6, done: true}
|
||||
defer func() {
|
||||
select {
|
||||
case results <- result:
|
||||
case <-returned:
|
||||
if result.Conn != nil && result.error == nil {
|
||||
if result.Conn != nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
result.Conn, result.error = dialFn(ctx, network, ips, port, opt)
|
||||
}
|
||||
go racer(ipv4s, preferIPVersion != 6)
|
||||
go racer(ipv6s, preferIPVersion != 4)
|
||||
var fallback dialResult
|
||||
var errs []error
|
||||
for i := 0; i < 2; {
|
||||
select {
|
||||
case <-fallbackTicker.C:
|
||||
if fallback.error == nil && fallback.Conn != nil {
|
||||
return fallback.Conn, nil
|
||||
}
|
||||
case res := <-results:
|
||||
i++
|
||||
if res.error == nil {
|
||||
if res.isPrimary {
|
||||
return res.Conn, nil
|
||||
}
|
||||
fallback = res
|
||||
|
||||
var ip netip.Addr
|
||||
if ipv6 {
|
||||
if !direct {
|
||||
ip, result.error = resolver.ResolveIPv6ProxyServerHost(host)
|
||||
} else {
|
||||
if res.isPrimary {
|
||||
errs = append([]error{fmt.Errorf("connect failed: %w", res.error)}, errs...)
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("connect failed: %w", res.error))
|
||||
}
|
||||
ip, result.error = resolver.ResolveIPv6(host)
|
||||
}
|
||||
} else {
|
||||
if !direct {
|
||||
ip, result.error = resolver.ResolveIPv4ProxyServerHost(host)
|
||||
} else {
|
||||
ip, result.error = resolver.ResolveIPv4(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
if fallback.error == nil && fallback.Conn != nil {
|
||||
return fallback.Conn, nil
|
||||
}
|
||||
return nil, errorsJoin(errs...)
|
||||
}
|
||||
if result.error != nil {
|
||||
return
|
||||
}
|
||||
result.resolved = true
|
||||
|
||||
func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
if len(ips) == 0 {
|
||||
return nil, ErrorNoIpAddress
|
||||
}
|
||||
results := make(chan dialResult)
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
racer := func(ctx context.Context, ip netip.Addr) {
|
||||
result := dialResult{isPrimary: true, ip: ip}
|
||||
defer func() {
|
||||
select {
|
||||
case results <- result:
|
||||
case <-returned:
|
||||
if result.Conn != nil && result.error == nil {
|
||||
_ = result.Conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
result.Conn, result.error = dialContext(ctx, network, ip, port, opt)
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
go racer(ctx, ip)
|
||||
}
|
||||
var errs []error
|
||||
for i := 0; i < len(ips); i++ {
|
||||
res := <-results
|
||||
go startRacer(ctx, network+"4", host, opt.direct, false)
|
||||
go startRacer(ctx, network+"6", host, opt.direct, true)
|
||||
|
||||
for res := range results {
|
||||
if res.error == nil {
|
||||
return res.Conn, nil
|
||||
}
|
||||
errs = append(errs, res.error)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errorsJoin(errs...)
|
||||
}
|
||||
return nil, os.ErrDeadlineExceeded
|
||||
}
|
||||
|
||||
func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
if len(ips) == 0 {
|
||||
return nil, ErrorNoIpAddress
|
||||
}
|
||||
var errs []error
|
||||
for _, ip := range ips {
|
||||
if conn, err := dialContext(ctx, network, ip, port, opt); err == nil {
|
||||
return conn, nil
|
||||
if !res.ipv6 {
|
||||
primary = res
|
||||
} else {
|
||||
errs = append(errs, err)
|
||||
fallback = res
|
||||
}
|
||||
|
||||
if primary.done && fallback.done {
|
||||
if primary.resolved {
|
||||
return nil, primary.error
|
||||
} else if fallback.resolved {
|
||||
return nil, fallback.error
|
||||
} else {
|
||||
return nil, primary.error
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errorsJoin(errs...)
|
||||
|
||||
return nil, errors.New("never touched")
|
||||
}
|
||||
|
||||
type dialResult struct {
|
||||
ip netip.Addr
|
||||
net.Conn
|
||||
error
|
||||
isPrimary bool
|
||||
}
|
||||
|
||||
func parseAddr(ctx context.Context, network, address string, preferResolver resolver.Resolver) ([]netip.Addr, string, error) {
|
||||
func concurrentDualStackDialContext(ctx context.Context, network, address string, opt *option) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, "-1", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []netip.Addr
|
||||
|
||||
if opt.direct {
|
||||
ips, err = resolver.ResolveAllIP(host)
|
||||
} else {
|
||||
ips, err = resolver.ResolveAllIPProxyServerHost(host)
|
||||
}
|
||||
|
||||
return concurrentDialContext(ctx, network, ips, port, opt)
|
||||
}
|
||||
|
||||
func concurrentDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) {
|
||||
returned := make(chan struct{})
|
||||
defer close(returned)
|
||||
|
||||
type dialResult struct {
|
||||
ip netip.Addr
|
||||
net.Conn
|
||||
error
|
||||
resolved bool
|
||||
}
|
||||
|
||||
results := make(chan dialResult)
|
||||
|
||||
tcpRacer := func(ctx context.Context, ip netip.Addr) {
|
||||
result := dialResult{ip: ip}
|
||||
|
||||
defer func() {
|
||||
select {
|
||||
case results <- result:
|
||||
case <-returned:
|
||||
if result.Conn != nil {
|
||||
result.Conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
v := "4"
|
||||
if ip.Is6() {
|
||||
v = "6"
|
||||
}
|
||||
|
||||
result.Conn, result.error = dialContext(ctx, network+v, ip, port, opt)
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
go tcpRacer(ctx, ip)
|
||||
}
|
||||
|
||||
connCount := len(ips)
|
||||
for res := range results {
|
||||
connCount--
|
||||
if res.error == nil {
|
||||
return res.Conn, nil
|
||||
}
|
||||
|
||||
if connCount == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all ips %v tcp shake hands failed", ips)
|
||||
}
|
||||
|
||||
func singleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ip netip.Addr
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
if !opt.direct {
|
||||
ip, err = resolver.ResolveIPv4ProxyServerHost(host)
|
||||
} else {
|
||||
ip, err = resolver.ResolveIPv4(host)
|
||||
}
|
||||
default:
|
||||
if !opt.direct {
|
||||
ip, err = resolver.ResolveIPv6ProxyServerHost(host)
|
||||
} else {
|
||||
ip, err = resolver.ResolveIPv6(host)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dialContext(ctx, network, ip, port, opt)
|
||||
}
|
||||
|
||||
func concurrentSingleDialContext(ctx context.Context, network string, address string, opt *option) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []netip.Addr
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
if preferResolver == nil {
|
||||
ips, err = resolver.LookupIPv4ProxyServerHost(ctx, host)
|
||||
if !opt.direct {
|
||||
ips, err = resolver.ResolveAllIPv4ProxyServerHost(host)
|
||||
} else {
|
||||
ips, err = resolver.LookupIPv4WithResolver(ctx, host, preferResolver)
|
||||
}
|
||||
case "tcp6", "udp6":
|
||||
if preferResolver == nil {
|
||||
ips, err = resolver.LookupIPv6ProxyServerHost(ctx, host)
|
||||
} else {
|
||||
ips, err = resolver.LookupIPv6WithResolver(ctx, host, preferResolver)
|
||||
ips, err = resolver.ResolveAllIPv4(host)
|
||||
}
|
||||
default:
|
||||
if preferResolver == nil {
|
||||
ips, err = resolver.LookupIPProxyServerHost(ctx, host)
|
||||
if !opt.direct {
|
||||
ips, err = resolver.ResolveAllIPv6ProxyServerHost(host)
|
||||
} else {
|
||||
ips, err = resolver.LookupIPWithResolver(ctx, host, preferResolver)
|
||||
ips, err = resolver.ResolveAllIPv6(host)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, "-1", fmt.Errorf("dns resolve failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
for i, ip := range ips {
|
||||
if ip.Is4In6() {
|
||||
ips[i] = ip.Unmap()
|
||||
}
|
||||
}
|
||||
return ips, port, nil
|
||||
}
|
||||
|
||||
func sortationAddr(ips []netip.Addr) (ipv4s, ipv6s []netip.Addr) {
|
||||
for _, v := range ips {
|
||||
if v.Is4() { // 4in6 parse was in parseAddr
|
||||
ipv4s = append(ipv4s, v)
|
||||
} else {
|
||||
ipv6s = append(ipv6s, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Dialer struct {
|
||||
opt option
|
||||
}
|
||||
|
||||
func (d Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return DialContext(ctx, network, address, WithOption(d.opt))
|
||||
}
|
||||
|
||||
func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort) (net.PacketConn, error) {
|
||||
opt := WithOption(d.opt)
|
||||
if rAddrPort.Addr().Unmap().IsLoopback() {
|
||||
// avoid "The requested address is not valid in its context."
|
||||
opt = WithInterface("")
|
||||
}
|
||||
return ListenPacket(ctx, ParseNetwork(network, rAddrPort.Addr()), address, opt)
|
||||
}
|
||||
|
||||
func NewDialer(options ...Option) Dialer {
|
||||
opt := applyOptions(options...)
|
||||
return Dialer{opt: *opt}
|
||||
return concurrentDialContext(ctx, network, ips, port, opt)
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorNoIpAddress = errors.New("no ip address")
|
||||
ErrorInvalidedNetworkStack = errors.New("invalided network stack")
|
||||
)
|
||||
|
||||
func errorsJoin(errs ...error) error {
|
||||
// compatibility with golang<1.20
|
||||
// maybe use errors.Join(errs...) is better after we drop the old version's support
|
||||
return E.Errors(errs...)
|
||||
}
|
@ -3,35 +3,39 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func bindMarkToDialer(mark int, dialer *net.Dialer, _ string, _ netip.Addr) {
|
||||
addControlToDialer(dialer, bindMarkToControl(mark))
|
||||
dialer.Control = bindMarkToControl(mark, dialer.Control)
|
||||
}
|
||||
|
||||
func bindMarkToListenConfig(mark int, lc *net.ListenConfig, _, _ string) {
|
||||
addControlToListenConfig(lc, bindMarkToControl(mark))
|
||||
lc.Control = bindMarkToControl(mark, lc.Control)
|
||||
}
|
||||
|
||||
func bindMarkToControl(mark int) controlFn {
|
||||
return func(ctx context.Context, network, address string, c syscall.RawConn) (err error) {
|
||||
func bindMarkToControl(mark int, chain controlFn) controlFn {
|
||||
return func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(address)
|
||||
if err == nil && !addrPort.Addr().IsGlobalUnicast() {
|
||||
return
|
||||
}
|
||||
|
||||
var innerErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
innerErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||
return c.Control(func(fd uintptr) {
|
||||
switch network {
|
||||
case "tcp4", "udp4":
|
||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||
case "tcp6", "udp6":
|
||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, mark)
|
||||
}
|
||||
})
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,6 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
import "go.uber.org/atomic"
|
||||
|
||||
var (
|
||||
DefaultOptions []Option
|
||||
@ -15,19 +8,11 @@ var (
|
||||
DefaultRoutingMark = atomic.NewInt32(0)
|
||||
)
|
||||
|
||||
type NetDialer interface {
|
||||
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type option struct {
|
||||
interfaceName string
|
||||
addrReuse bool
|
||||
routingMark int
|
||||
network int
|
||||
prefer int
|
||||
tfo bool
|
||||
resolver resolver.Resolver
|
||||
netDialer NetDialer
|
||||
direct bool
|
||||
}
|
||||
|
||||
type Option func(opt *option)
|
||||
@ -50,48 +35,8 @@ func WithRoutingMark(mark int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithResolver(r resolver.Resolver) Option {
|
||||
func WithDirect() Option {
|
||||
return func(opt *option) {
|
||||
opt.resolver = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithPreferIPv4() Option {
|
||||
return func(opt *option) {
|
||||
opt.prefer = 4
|
||||
}
|
||||
}
|
||||
|
||||
func WithPreferIPv6() Option {
|
||||
return func(opt *option) {
|
||||
opt.prefer = 6
|
||||
}
|
||||
}
|
||||
|
||||
func WithOnlySingleStack(isIPv4 bool) Option {
|
||||
return func(opt *option) {
|
||||
if isIPv4 {
|
||||
opt.network = 4
|
||||
} else {
|
||||
opt.network = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithTFO(tfo bool) Option {
|
||||
return func(opt *option) {
|
||||
opt.tfo = tfo
|
||||
}
|
||||
}
|
||||
|
||||
func WithNetDialer(netDialer NetDialer) Option {
|
||||
return func(opt *option) {
|
||||
opt.netDialer = netDialer
|
||||
}
|
||||
}
|
||||
|
||||
func WithOption(o option) Option {
|
||||
return func(opt *option) {
|
||||
*opt = o
|
||||
opt.direct = true
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
@ -11,10 +10,18 @@ import (
|
||||
)
|
||||
|
||||
func addrReuseToListenConfig(lc *net.ListenConfig) {
|
||||
addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error {
|
||||
chain := lc.Control
|
||||
|
||||
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
@ -9,9 +8,17 @@ import (
|
||||
)
|
||||
|
||||
func addrReuseToListenConfig(lc *net.ListenConfig) {
|
||||
addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error {
|
||||
chain := lc.Control
|
||||
|
||||
lc.Control = func(network, address string, c syscall.RawConn) (err error) {
|
||||
defer func() {
|
||||
if err == nil && chain != nil {
|
||||
err = chain(network, address, c)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.Control(func(fd uintptr) {
|
||||
windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,123 +0,0 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/sagernet/tfo-go"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tfoConn struct {
|
||||
net.Conn
|
||||
closed bool
|
||||
dialed chan bool
|
||||
cancel context.CancelFunc
|
||||
ctx context.Context
|
||||
dialFn func(ctx context.Context, earlyData []byte) (net.Conn, error)
|
||||
}
|
||||
|
||||
func (c *tfoConn) Dial(earlyData []byte) (err error) {
|
||||
c.Conn, err = c.dialFn(c.ctx, earlyData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.dialed <- true
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *tfoConn) Read(b []byte) (n int, err error) {
|
||||
if c.closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if c.Conn == nil {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
case <-c.dialed:
|
||||
}
|
||||
}
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (c *tfoConn) Write(b []byte) (n int, err error) {
|
||||
if c.closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
if c.Conn == nil {
|
||||
if err := c.Dial(b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (c *tfoConn) Close() error {
|
||||
c.closed = true
|
||||
c.cancel()
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
func (c *tfoConn) LocalAddr() net.Addr {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *tfoConn) RemoteAddr() net.Addr {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *tfoConn) SetDeadline(t time.Time) error {
|
||||
if err := c.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *tfoConn) SetReadDeadline(t time.Time) error {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *tfoConn) SetWriteDeadline(t time.Time) error {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (c *tfoConn) Upstream() any {
|
||||
if c.Conn == nil { // ensure return a nil interface not an interface with nil value
|
||||
return nil
|
||||
}
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *tfoConn) NeedHandshake() bool {
|
||||
return c.Conn == nil
|
||||
}
|
||||
|
||||
func dialTFO(ctx context.Context, netDialer net.Dialer, network, address string) (net.Conn, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
dialer := tfo.Dialer{Dialer: netDialer, DisableTFO: false}
|
||||
return &tfoConn{
|
||||
dialed: make(chan bool, 1),
|
||||
cancel: cancel,
|
||||
ctx: ctx,
|
||||
dialFn: func(ctx context.Context, earlyData []byte) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, address, earlyData)
|
||||
},
|
||||
}, nil
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
#ifndef __BPF_ENDIAN__
|
||||
#define __BPF_ENDIAN__
|
||||
|
||||
/*
|
||||
* Isolate byte #n and put it into byte #m, for __u##b type.
|
||||
* E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64:
|
||||
* 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx
|
||||
* 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000
|
||||
* 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn
|
||||
* 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000
|
||||
*/
|
||||
#define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8))
|
||||
|
||||
#define ___bpf_swab16(x) ((__u16)( \
|
||||
___bpf_mvb(x, 16, 0, 1) | \
|
||||
___bpf_mvb(x, 16, 1, 0)))
|
||||
|
||||
#define ___bpf_swab32(x) ((__u32)( \
|
||||
___bpf_mvb(x, 32, 0, 3) | \
|
||||
___bpf_mvb(x, 32, 1, 2) | \
|
||||
___bpf_mvb(x, 32, 2, 1) | \
|
||||
___bpf_mvb(x, 32, 3, 0)))
|
||||
|
||||
#define ___bpf_swab64(x) ((__u64)( \
|
||||
___bpf_mvb(x, 64, 0, 7) | \
|
||||
___bpf_mvb(x, 64, 1, 6) | \
|
||||
___bpf_mvb(x, 64, 2, 5) | \
|
||||
___bpf_mvb(x, 64, 3, 4) | \
|
||||
___bpf_mvb(x, 64, 4, 3) | \
|
||||
___bpf_mvb(x, 64, 5, 2) | \
|
||||
___bpf_mvb(x, 64, 6, 1) | \
|
||||
___bpf_mvb(x, 64, 7, 0)))
|
||||
|
||||
/* LLVM's BPF target selects the endianness of the CPU
|
||||
* it compiles on, or the user specifies (bpfel/bpfeb),
|
||||
* respectively. The used __BYTE_ORDER__ is defined by
|
||||
* the compiler, we cannot rely on __BYTE_ORDER from
|
||||
* libc headers, since it doesn't reflect the actual
|
||||
* requested byte order.
|
||||
*
|
||||
* Note, LLVM's BPF target has different __builtin_bswapX()
|
||||
* semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE
|
||||
* in bpfel and bpfeb case, which means below, that we map
|
||||
* to cpu_to_be16(). We could use it unconditionally in BPF
|
||||
* case, but better not rely on it, so that this header here
|
||||
* can be used from application and BPF program side, which
|
||||
* use different targets.
|
||||
*/
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
# define __bpf_ntohs(x) __builtin_bswap16(x)
|
||||
# define __bpf_htons(x) __builtin_bswap16(x)
|
||||
# define __bpf_constant_ntohs(x) ___bpf_swab16(x)
|
||||
# define __bpf_constant_htons(x) ___bpf_swab16(x)
|
||||
# define __bpf_ntohl(x) __builtin_bswap32(x)
|
||||
# define __bpf_htonl(x) __builtin_bswap32(x)
|
||||
# define __bpf_constant_ntohl(x) ___bpf_swab32(x)
|
||||
# define __bpf_constant_htonl(x) ___bpf_swab32(x)
|
||||
# define __bpf_be64_to_cpu(x) __builtin_bswap64(x)
|
||||
# define __bpf_cpu_to_be64(x) __builtin_bswap64(x)
|
||||
# define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x)
|
||||
# define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x)
|
||||
#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
|
||||
# define __bpf_ntohs(x) (x)
|
||||
# define __bpf_htons(x) (x)
|
||||
# define __bpf_constant_ntohs(x) (x)
|
||||
# define __bpf_constant_htons(x) (x)
|
||||
# define __bpf_ntohl(x) (x)
|
||||
# define __bpf_htonl(x) (x)
|
||||
# define __bpf_constant_ntohl(x) (x)
|
||||
# define __bpf_constant_htonl(x) (x)
|
||||
# define __bpf_be64_to_cpu(x) (x)
|
||||
# define __bpf_cpu_to_be64(x) (x)
|
||||
# define __bpf_constant_be64_to_cpu(x) (x)
|
||||
# define __bpf_constant_cpu_to_be64(x) (x)
|
||||
#else
|
||||
# error "Fix your compiler's __BYTE_ORDER__?!"
|
||||
#endif
|
||||
|
||||
#define bpf_htons(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_htons(x) : __bpf_htons(x))
|
||||
#define bpf_ntohs(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_ntohs(x) : __bpf_ntohs(x))
|
||||
#define bpf_htonl(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_htonl(x) : __bpf_htonl(x))
|
||||
#define bpf_ntohl(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_ntohl(x) : __bpf_ntohl(x))
|
||||
#define bpf_cpu_to_be64(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x))
|
||||
#define bpf_be64_to_cpu(x) \
|
||||
(__builtin_constant_p(x) ? \
|
||||
__bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x))
|
||||
|
||||
#endif /* __BPF_ENDIAN__ */
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user